Unpoly 3.11.0 released #762
triskweline
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
This is a big release, shipping many features and quality-of-life improvements requested by the community. Highlights include a complete overhaul of history handling, form state switching and the preservation of
[up-keep]elements. We also reworked major parts of the documentation and stabilized 89 experimental features.We had to make some breaking changes, which are marked with a⚠️ emoji in this CHANGELOG.
Most incompatibilities are polyfilled by
unpoly-migrate.js.Note
Our sponsor makandra funded this release ❤️
Please take a minute to check out makandra's services for web development, DevOps or UI/UX.
Professional support options
We're introducing optional commercial support for businesses that depend on Unpoly. You can now sponsor bug fixes, commission new features, or get direct help from Unpoly’s core developers.
Support commissions will fund Unpoly’s ongoing development while keeping it fully open source for everyone.
The Discussions board remains available for free community support, and the maintainers will also remain active there.
Learn more about support options at unpoly.com/support.
History handling
Improved history restoration
When pressing the back button, Unpoly used to only restore history entries that it created itself. This sometimes caused the back button to do nothing when a state was pushed by a user interacting with the browser UI, or when an external script replaced an entry.
Starting with this version, Unpoly will handle restoration for most history entries:
#hash. Going back to such an entry will now reveal a matching fragment, scrolling far enough to ignore any obstructing elements in the layout.#hashin the browser's address bar (without also changing the path or search query).history.replaceState()).When an external script pushes a history entry with a new path unknown to Unpoly, that external script is still responsible for restoration.
Listeners to
up:location:changedcan now inspect and control which history changes Unpoly should handle:{ willHandle }shows if Unpoly thinks it is responsible for restoring the new location state.{ alreadyHandled }shows if Unpoly thinks the change has already been handled (e.g. after calls tohistory.pushState()).Complete handling of
#hashlinksUnpoly now handles most clicks on a link to a
#hashwithin the current page, taking great care to emulate the browser's native scrolling behavior:scroll-behavior: smoothstyle.[up-scroll-behavior]attribute. Valid values areinstant,smoothandauto(uses CSS behavior).clickevent.Every location change is now tracked
up:location:changed(andup:layer:location:changed) used to only be emitted when history changed during rendering.history.pushState()orup.history.push().history.replaceState()orup.history.replace().#hashin the browser's address bar.#hashlink.Reacting to
#hashchanges usually involves scrolling, not rendering. To better signal this case, the{ reason }property ofup:location:changedcan now be the string'hash'if only the location#hashwas changed from the previous location.Other improvements to history handling
popstateevent with the logging output from a subsequent history restoration.up.history.replace()to change the URL of the current history state.up:layer:location:changedevent now has a{ previousLocation }property.<!DOCTYPE>or<html>tag (fixes v3.8.0 broke automatic updates of<title>during navigation when response has non-whitespace before opening<html>or<DOCTYPE>tag. #726)Watching fields for changes
When watching fields using
[up-watch],[up-autosubmit],[up-switch]or[up-validate], the following cases are now addressed:[up-keep]is transported to a new<form>element by a fragment update.[form]attribute) is added or removed dynamically.[up-watch-delay]was detached by an external script during the delay, watchers will no longer fire callbacks or send requests.[name]will now throw an error explaining that this attribute is required. In earlier versions callbacks were simply never called.Switching form state
The
[up-switch]attribute has been reworked to be more powerful and flexible.Also see our new guide Switching form state.
Disabling or enabling fields
You can now disable dependent fields using the new
[up-disable-for]and[up-enable-for]attributes.Let's say you have a
<select>for a user role. Its selected value should enable or disable other. You begin by setting an[up-switch]attribute with an selector targeting the controlled fields:The target elements can use
[up-enable-for]and[up-disable-for]attributes to indicate for which values they should be shown or hidden:
See Disabling or enabling fields.
Custom switching effects
You can now implement custom, client-side switching effects by listening to the
up:form:switchevent on any element targeted by[up-switch].For example, we want a custom
[highlight-for]attribute. It draws a brightoutline around the department field when the manager role is selected:
When the role select changes, an
up:form:switchevent is emitted on all elements matching.role-dependent.We can use this event to implement our custom
[highlight-for]effect:See Custom switching effects.
New switching modifiers
The
[up-switch]attribute itself has been reworked with new modifiying attributes:[up-switch-region]attribute allows to expand or narrow the region where elements are switched.[up-switch]can now react to other events, by setting an[up-watch-event]attribute.[up-switch]can now debounce their switching effects with an[up-watch-delay]attribute.More
[up-switch]changes[up-switch]on a text field will now switch while the user is typing (as opposed to when the field is blurred).[up-switch]now works on a container for a radio button group.[up-switch]now works on a container of multiple checkboxes for a single array param, likecategory[].[up-switch]on an individual radio button will now throw an error. Watch a container for the entire radio group instead.[up-switch]now require a[name]attribute.[up-switch]when that element has neither[up-show-for]nor[up-hide-for]attributes. This was an undocumented side effect of the old implementation.Form validation
The
[up-validate]attribute has been reworked.Validating against other URLs
By default Unpoly will submit validation requests to the form's
[action]attribute, setting an additionalX-Up-Validateheader to allow the server distinguish a validation request from a regular form submission.Unpoly can now validate forms against other URLs. You can do so with the new
[up-validate-url]and[up-validate-method]attributes on individudal fields or on entire forms:To have individual fields validate against different URLs, you can also set
[up-validate-url]on a field:Even with multiple URLs, Unpoly still guarantees eventual consistency in a form with many concurrent validations. This is done by separating request batches by URL and ensuring that only a single validation request per form will be in flight at the same time.
For instance, let's assume the following four validations:
This will send a sequence of two requests:
/pathtargeting.foo, .baz. The other validations are queued./path2targeting.bar, .qux.Other validation changes
up.form.config.batchValidate = false, or for individual forms or fields with an[up-validate-batch="false"]attribute.[up-validate-params]attribute.[up-validate-headers]attribute.:origin. This was possible before, but was never documented.[up-validate]can now be set on any container of fields, not just an individual field or an entire form. This was possible before, but never documented.Layers
Opening overlays from the server
The server can now force its response to open an overlay using an
X-Up-Open-Layer: { ...options }response header:See Opening overlays from the server.
Closing overlays from forms
Forms can now have an
[up-dismiss]or[up-accept]attribute to close their overlay when submitted.This will immediately close the overlay on submission, without making a network request:
The form's field values become the overlay's result value, encoded as an
up.Paramsinstance:See Closing when a form is submitted.
Detecting the origin layer
The server can now detect if an interaction (e.g. clicking a link or submitting a form) originated from an overlay, by using the
X-Up-Origin-Moderequest header. This is opposed to the targeted layer, which is still sent as anX-Up-Modeheader.For example, we have the following link in a modal overlay. The link targets the root layer:
When the link is clicked, the following request headers are sent:
Other layer changes
Script security
This version revises mechanisms to prevent cross-site scripting and handle strict content security policies.
Scripts in fragments are no longer executed
<script>elements in new fragments.This default can by changed by configuring
up.fragment.config.runScripts.Unfortunately our the default for this setting has changed a few times now. It took us a while to find the right balance between secure defaults and compatibility with legacy apps.
We have finally decided to err on the side of caution here.
See Migrating legacy JavaScripts for techniques to remove inline
<script>elements.Mandatory nonces for
script-dynamicCSPA CSP with
strict-dynamicallows any allowed script to load additional scripts. Because Unpoly is already an allowed script, this would allow any Unpoly-rendered script to execute.To prevent this, Unpoly requires matching CSP nonces in any response with a
strict-dynamicCSP, even withrunScripts = true.If you cannot use nonces for some reasons, you can configure
up.fragment.config.runScriptsto a functionthat returns
truefor allowed scripts only:See CSPs with
strict-dynamicfor details.Other CSP changes
default-srcdirective if noscript-srcdirective is found in the policy.script-src-elemdirective. Since nonces are used to allow attribute callbacks, usingscript-src-elemis not appropriate.<script>elements in new fragments would lose their[nonce]attribute. That attribute is now rewritten to the current page's nonce if it matches a nonce from the response that inserted the fragment.up:assets:changedlisteners inspectevent.newAssets, any asset nonces are now already rewritten to the current page's nonce if they a nonce from the response that caused the event.Reworked documentation
Parameters are organized into sections
It was sometimes hard to find documentation for a given parameter (or attribute) for features with many options. To address this, options have now been organized in sections like Request or Animation:
Inherited parameters are documented
You may discover that functions and attributes have a lot more documented options now.
This is because most features end up calling
up.render(), inheriting most available render options in the process. We used to document this with a note like "Otherup.render()options may also be used", which was often overlooked.Now most inherited options are now explicitly documented with the inheriting feature.
New guides
A number of guides have been added or overhauled:
Guide links
When there is a guide with more context, the documentation for attributes or functions now show a link to that guide:
Caching
up.reload()can now restore a fragment to a previously cached state using an{ cache: true }option. This was possible before, but was never documented.[up-expire-cache]and[up-evict-cache]attributes are now executed before the request is sent. In previous version, the cache was only changed after a response was loaded. This change allows the combined use of[up-evict-cache]and[up-cache]to clear and re-populate the cache with a single render pass.X-Up-Expire-Cache: falseresponse header.{ bindLayer }property after loading, allowing layer objects to be garbage-collected while the request is cached.[up-hungry]and[up-preload]no longer throw an error after rendering cached, but expired content.Navigation bars
Navigational containers can now match the current location of other layers by setting an
[up-layer]attribute.The
.up-currentclass will be set when the matching layer is already at the link's[href].For example, this navigation bar in an overlay will highlight links whose URL matches the location of any layer:
See Matching the location of other layers.
Preserving elements
The
[up-keep]element now gives you more control over how long an element is kept.Also see our new guide Preserving elements.
Keeping an element until its HTML changes
To preserve an element as long as its outer HTML remains the same, set an
[up-keep="same-html"]attribute. Only when the element's attributes or children changes between versions, it is replaced by the new version.The example below uses a JavaScript-based
<select>replacement like Tom Select. Because initialization is expensive, we want to preserve the element as long is possible. We do want to update it when the server renders a different value, different options, or a validation error. We can achieve this by setting[up-keep="same-html"]on a container that contains the select and eventual error messages:Unpoly will compare the element's initial HTML as it is rendered by the server.
Client-side changes to the element (e.g. by a compiler) are ignored.
Keeping an element until its data changes
To preserve an element as long as its data remains the same, set an
[up-keep="same-data"]attribute. Only when the element's[up-data]attribute changes between versions, it is replaced by the new version. Changes in other attributes or its children are ignored.The example below uses a compiler to render an interactive map into elements with a
.mapclass. The initial map location is passed as an[up-data]attribute. Because we don't want to lose client-side state (like pan or zoom ettings), we want to keep the map widget as long as possible. Only when the map's initial location changes, we want to re-render the map centered around the new location. We can achieve this by setting an[up-keep="same-data"]attribute on the map container:Instead of
[up-data]we can also use HTML5[data-*]attributes:Unpoly will compare the element's initial data as it is rendered by the server.
Client-side changes to the data object (e.g. by a compiler) are ignored.
Custom keep conditions
We're providing
[up-keep="same-html"]and[up-keep="same-data"]as shortcuts for common keep constraints.You can still implement arbitrary keep conditions by listening to the
up:fragment:keepevent or setting an[up-on-keep]attribute.Form data handling
{ params }option now overrides existing params with the same name. Formerly, a new param with the same name was added. This made it impossible to override array fields (likename[]).up.form.config.arrayParam. By default, only field names ending in"[]"are treated as arrays. (by @apollo13)up.network.loadPage()will now remove binary values (from file inputs) from a given{ params }option. JavaScript cannot make a full page load with binary params.up.Params#getAll()not returning the correct results or causing a stack overflow.Focus ring visibility
You can now override focus ring visibility for individual links or forms, by setting an
[up-focus-visible]attribute or by passing a{ focusVisible }render option.For global visibility rules, use the existing
up.viewport.config.autoFocusVisibleconfiguration.Scrolling to the top or bottom
This release adds a new scroll option
[up-scroll='bottom']. This scrolls viewports around the targeted fragment to the bottom.[up-scroll='reset']was changed to[up-scroll='top'].Instant links on iOS
Long-pressing an
[up-instant]link (to open the context menu) will no longer follow the link on iOS (issue #271).Also long-pressing an instant link will no longer emit an
up:clickevent.Utility functions
"§1") (parseRelaxedJSON fails when called with a string containing a paragraph #752).up.util.task()implementation now usespostMessage()instead ofsetTimeout(). This causes the new task to be scheduled earlier, ideally before the browser renders the next frame. The task is still guaranteed to run after all microtasks, such as settled promise callbacks.unpoly-migratenow allows to disable all deprecation warnings withup.migrate.config.logLevel = 'none'. This allows to keep polyfills installed without noise in the console.up.util.pickBy()no longer passes the entire object as a third argument to the callback function.up.element.subtree()now prevents redundant array allocations.Accessibility
Unpoly now prevents interactions with elements that are being destroyed (and playing out their exit animation).
To achieve this, destroying elements are marked as
[inert].Polling
An
[up-poll]fragment now stops polling if an external script detaches the element.Animation
Fixed a bug where prepending or appending an insertion could not be animated using an
{ animate }option or[up-animate]option. In earlier versions, Unpoly wrongly expected animations in a{ transition }option or[up-transition]attribute.JavaScript rendering API
up.RenderResult#ok. It indicated whether the render pass has rendered a successful response.up.RenderResult#renderOptions. It contains the effective render options used to produce this result.up.RenderJob#optionstoup.RenderJob#renderOptionsup.hello()is called on an element that has been compiled before,up:fragment:insertedis no longer emitted a second time.up:fragment:insertedis emitted after compilation.Network requests
Listeners to
up:request:loadcan now inspect or mutate request options before it is sent:This was possible before, but was never documented.
Developer experience
We modernized the codebase so that contributing to Unpoly is now simpler:
npm run test.async/awaitinstead of our legacyasyncSpec()helper.Stabilization of experimental features
Many experimental features have now been declared as stable:
up.deferred.load()functionup:deferred:loadeventup.event.build()functionup.form.fields()functionup.fragment.config.skipResponseconfiguration[up-etag]attributeup.fragment.etag()function[up-time]attributeup.fragment.time()functionevent.skip()method forup:fragment:loadedeventup:fragment:offlineeventup.fragment.subtree()functionup.fragment.isTargetable()function:layerselectorup.fragment.matches()functionup.fragment.abort()functionup:fragment:abortedeventup.template.clone()functionup:template:cloneeventup.history.locationpropertyup.history.previousLocationpropertyup.history.isLocation()functionup:layer:location:changedeventevent.responseproperty forup:layer:accept,up:layer:dismiss,up:layer:acceptedandup:layer:dismissedevents[up-defer]attributeup.network.config.lateDelayconfiguration{ lateDelay }option forup.request()up.network.loadPage()functionup:request:abortedeventup:fragment:hungryevent{ ifLayer }option forup.radio.startPolling()[up-if-layer]modifier for[up-poll]up:fragment:polleventup.script.config.scriptSelectorsandup.script.config.noScriptSelectorsconfigurationevent.preventDefault()forup:assets:changedeventup.util.noop()functionup.util.normalizeURL()functionup.util.isBlank.keypropertyup.util.wrapList()functionup.util.copy.keypropertyup.util.findResult()functionup.util.every()functionup.util.evalOption()functionup.util.pluckKey()functionup.util.flatten()functionup.util.flatMap()functionup.util.isEqual()functionup.util.isEqual.keypropertyup.focus()functionup.viewport.get()functionup.viewport.rootpropertyup.viewport.saveScroll()functionup.viewport.restoreScroll()functionup.viewport.saveFocus()functionup.viewport.restoreFocus()functionnew up.Paramsconstructorup.Params#clear()methodup.Params#toFormData()methodup.Params#toQuery()methodup.Params#add()methodup.Params#addAll()methodup.Params#set()methodup.Params#delete()methodup.Params#get()methodup.Params.fromForm()static methodup.Params.fromURL()static methodup.RenderResult#nonepropertyup.RenderResult#okpropertyup.Request#layerpropertyup.Request#failLayerpropertyup.Request#originpropertyup.Request#backgroundpropertyup.Request#lateDelaypropertyup.Request#fragmentspropertyup.Request#fragmentpropertyup.Request#loadPage()functionup.Request#abort()functionup.Request#endedpropertyup.Response#contentTypepropertyup.Response#lastModifiedpropertyup.Response#etagpropertyup.Response#agepropertyup.Response#expiredproprty{ response }option forup.Layer#accept()method andup.layer.accept()funcitonup.Layer#asCurrent()up.layer.locationandup.Layer#locationproperties[up-abortable]attribute for[up-follow]links[up-late-delay]attribute for[up-follow]links{ lateDelay }option forup.render()Migration polyfills
unpoly-migrate.jswhere a{ style }string passed toup.element.affix()orup.element.createFromSelector()would sometimes be transformed incorrectly.Beta Was this translation helpful? Give feedback.
All reactions