This release delivers many requested features while filing off some long-standing sharp edges throughout the framework.
Breaking changes are marked with a ⚠️ emoji and polyfilled by unpoly-migrate.js.
Note
Our sponsor makandra funded this release ❤️
You can hire makandra for Unpoly support.
This version introduces settings to globally control whether Unpoly will execute JavaScript on your page.
The boolean up.fragment.config.runScripts has been replaced with a more flexible setting up.script.config.scriptElementPolicy. This lets you control whether to run scripts in new fragments:
<div id="fragment">
<script>
<!-- chip: ❓ Will this script run? -->
</script>
</div>
⚠️ By default Unpoly will now run any <script> that passes your CSP checks, but requires a nonce for viral CSPs with strict-dynamic.
You can configure up.script.config.scriptElementPolicy to block all script elements, or to only allow scripts with a valid [nonce] attribute:
scriptElementPolicy |
Runs without CSP? | Runs with CSP? | Runs with strict-dynamic CSP? |
|---|---|---|---|
auto (default) |
Always | If passes CSP | With allowed nonce |
pass |
Always | If passes CSP | 🔥 Always |
block |
Never | Never | Never |
nonce |
With allowed nonce | With allowed nonce | With allowed nonce |
See Security for script elements.
Added a new setting up.script.config.callbackPolicy. This lets you control whether Unpoly will execute string callbacks in attributes or response headers:
<a
href="/path"
up-follow
up-on-loaded="console.log('Will this callback run?')"> <!-- mark: up-on-loaded -->
Click link
</>
<form>
<input
type="text"
name="title"
up-watch="console.log('Will this callback run?')"> <!-- mark: up-watch -->
</form>
⚠️ By default Unpoly will parse and execute callbacks, but require a nonce once you set a <meta name="csp-nonce"> in your <head>.
You can configure up.script.config.callbackPolicy to block all callbacks, or to only allow callbacks with a valid nonce:
evalCallbackPolicy |
Runs without CSP? | Runs with CSP? | Runs with CSP and <meta name="csp-nonce">? |
|---|---|---|---|
auto (default) |
Always | 🔥 With unsafe-eval
|
With allowed nonce |
pass |
Always | 🔥 With unsafe-eval
|
🔥 With unsafe-eval
|
block |
Never | Never | Never |
nonce |
With allowed nonce | With allowed nonce | With allowed nonce |
Unpoly will now log warnings for configurations or CSP headers it considers overly permissive (marked with 🔥 above).
An 'unsafe-eval' CSP allows arbitrary [up-on...] callbacks. Consider setting up.script.config.callbackPolicy = 'nonce'.
You can disable these warnings with up.script.config.cspWarnings = false.
⚠️ Renamed up.protocol.config.cspNonce to up.script.config.cspNonce.
It still defaults to reading <meta name="csp-nonce">.
You can now auto-close an overlay once it reaches a fragment that matches a CSS selector. This is an alternative close condition similar to observing events or locations.
To wait for a fragment, set an [up-accept-fragment] attribute on the link that opens an overlay:
<a href="/users/new"
up-layer="new"
up-accept-fragment=".user-profile"
up-on-accepted="alert('Hello user #' + value.id)">
Add a user
</a>
When an element in the new overlay matches the .user-profile selector, the overlay is closed automatically. The fragment's data becomes the overlay's acceptance value:
<div class="user-profile" data-id="123">
...
</div>
See Closing when a fragment is detected.
When a link or form from an overlay targets a background layer, the overlay will dismiss when the parent layer is updated. This behavior is called peeling.
By default, peeled overlays will be dismissed. You can now choose to accept them instead, by setting an [up-peel="accept"] attribute
on the link or form that is targeting a background layer:
<form method="post" action="/users" up-layer="parent" up-peel="accept"> <!-- mark: up-peel="accept" -->
...
</form>
When rendering from JavaScript, pass an { peel: 'accept' } option for the same effect.
Servers can send an X-Up-Open-Layer response header to force its response to open a new overlay.
Callbacks like { onAccepted } or { onDismissed } can now be passed as a string of JavaScript:
Content-Type: text/html
X-Up-Open-Layer: { onAccepted: 'up.reload("#users-list")' }
With a strict CSP you can prefix your callback with a nonce:
Content-Type: text/html
X-Up-Open-Layer: { onAccepted: 'nonce-secret123 up.reload("#users-list")' }
The minified source files (like unpoly.min.js) are now shipped with source maps for easier debugging.
When rendering content from a stale cache entry, Unpoly automatically reloads the fragment to ensure that the user never sees expired content.
Unpoly will now assign revalidating fragments the .up-revalidating class while the revalidation request is in flight:
<div id="target" class="up-revalidating"> <!-- mark: class="up-revalidating" -->
Possibly stale content
</div>
You can style revalidating fragments to convey that content might be stale:
.up-revalidating {
filter: grayscale(80%);
opacity: 0.5;
}
Note that the .up-loading and .up-active classes are not set during cache revalidation.
You can configure custom revalidation classes in up.status.config.revalidatingClasses.
[up-nav] links can now set [up-alias] from a macro.
This can be useful to link nested navigation trees programmatically.
Links and forms can now use an [up-use-data-map] attribute or { dataMap } option to map selectors to data objects. When a selector matches any element within an updated fragment, the matching element is compiled with the mapped data:
<a
href="/score"
up-target="#stats"
up-use-data-map="{ '#score': { startScore: 1500 }, '#message': { max: 3 } }"> <!-- mark: up-use-data-map="{ '#score': { startScore: 1500 }, '#message': { max: 3 } }" -->
Load score
</a>
<div id="stats">
<div id="score">
<!-- chip: Will compile with data { startScore: 1500 } -->
</div>
<div id="message">
<!-- chip: Will compile with data { max: 3 } -->
</div>
</div>
⚠️ When rendering multiple fragments, any [up-use-data] attribute or { data } option will only apply to the first fragment.
To apply data to multiple fragments, use a data map as shown above.
Scroll positions will reset when you insert a new viewport element (as opposed to updating a child element). This is default browser behavior for newly inserted elements.
You can now ask Unpoly to preserve the scroll positions of all viewports around the updated fragment. To do so, set [up-scroll="keep"]:
<a href="/list" up-follow up-scroll="keep">Reload list</a> <!-- mark: up-scroll="keep" -->
Internally, Unpoly will measure scroll positions before the update, and restore the same positions after the update.
up.reload() now uses this feature to preserve scroll positions by default.
See Keeping current scroll positions.
You can now scroll multiple viewports with a single render pass, by using an [up-scroll-map] attribute or { scrollMap } option. Its value is an object mapping selectors to scroll options:
<a
href="/dashboard"
up-target="#viewport1, #viewport2"
up-scroll-map="{ '#viewport1': 'top', '#viewport2': 'bottom' }"
>
Update fragments
</a>
<div id="viewport1" up-viewport>
<!-- chip: ✔ Will be scrolled to the top -->
</div>
<div id="viewport2" up-viewport>
<!-- chip: ✔ Will be scrolled to the bottom -->
</div>
To scroll a specific pixel position from the top, you can now use a number value for the [up-scroll] attribute or { scroll } option:
<a href="/list" up-follow up-scroll="35">Back to list</a> <!-- mark: up-scroll="35" -->
To scroll to the bottom, but leave a margin of some pixels, set a negative number value:
<a href="/messages" up-follow up-scroll="-40">Latest messages</a> <!-- mark: up-scroll="-40" -->
See Scrolling to a pixel position.
Compilers now receive a meta.ok argument. It indicates if the fragment is being rendered from a successful response (200 OK).
up.compiler('#result', function(element, data, meta) { // mark: meta
if (meta.ok) { // mark: meta.ok
console.log("Rendering from successful response")
} else {
console.log("Rendering from failed response")
}
})
The up:fragment:inserted event now includes { layer, revalidating, ok } properties, matching what compilers receive as meta:
up.on('up:fragment:inserted', function(event) {
console.log(event.layer)
console.log(event.revalidating)
console.log(event.ok)
})
Rendering HTML from a string is always considered successful.
fade-out animation now starts from the element's current opacity, rather than always starting from 1.0.up.motion.isEnabled() has been deprecated. Use up.motion.config.enabled instead.up.fragment.config.reloadOptions.up.RenderResult#target property now reflects the actual resolved target selector used, rather than the originally requested one (e.g. resolving :main to the concrete selector).[up-keep] element is not targetable, Unpoly now prints a warning instead of crashing the render pass.#hash fragment from the address bar or a link, Unpoly now also focuses the matching element (#787).up.render() function. Unpoly will still be smart about setting focus when navigating. You can restore the old behavior by setting up.fragment.config.renderOptions.focus = 'keep'.<html> does not have overflow-x: hidden, particularly on Firefox (#795).{ dismissable } has been renamed to { dismissible }.[up-dismissable] has been renamed to [up-dismissible].[up-switch] now switches disabled fields. This is useful when you re-use your (disabled) forms as read-only views, but also rely on [up-switch] to control dependent form sections.[up-switch] effects are now consistently applied before [up-validate] requests.[up-validate][up-watch-event=input][up-keep] to validate a field while the user is typing in it.[form] attribute) are now supported consistently.[up-switch] or up.watch() when another compiler changed the initial value of the observed field.up.event.onClosest(). This runs a callback when an event is observed on an element or its ancestors.up.fragment.onKept(). This runs a callback when an element or its ancestors are kept during a render pass.up:assets:changed event now has a { response } property. This is the up.Response that contained new asset versions not found on the current page.up:location:changed event now has a { previousLocation } property.up.util.mapObject(). It creates an object from a given array and mapping function.up.util.reverse(). Use Array#toReversed() instead.up.util.parseNumber(). Parses a string as a number, supporting negative numbers, negative zero, underscores for digit grouping, and floats.up.Request#isSafe(). It returns whether the request uses a safe HTTP method like GET.up:fragment:offline showing "undefined" as the reason.up.viewport.root as a function, when it is really a property.up.Request#loadPage() (it was #navigate() that was deprecated)For a long time Unpoly has migrated links with Rails UJS attributes ([data-method], [data-confirm]) to their Unpoly counterparts.
This migration is now also applied to forms and submit buttons, not just links.
If you're upgrading from an older Unpoly version you should load unpoly-migrate.js
to polyfill deprecated APIs.
Changes handled by unpoly-migrate.js are not considered breaking changes.
See our upgrading guide for details.