Unpoly 3.11.0 released

Posted . Visible to the public. Auto-destruct in 58 days

Unpoly 3.11.0 Show archive.org snapshot is a big release, shipping many features and quality-of-life improvements requested by the community. Highlights include a complete overhaul of history handling Show archive.org snapshot , form state switching Show archive.org snapshot and the preservation of [up-keep] elements Show archive.org snapshot . 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 Show archive.org snapshot .

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 Show archive.org snapshot 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 Show archive.org snapshot remains available for free community support, and the maintainers will also remain active there.

Learn more about support options at unpoly.com/support Show archive.org snapshot .

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:

  • Unpoly will now restore history entries created by clicking an in-page link to another #hash. Going back to such an entry will now reveal a matching fragment, scrolling far enough to ignore any obstructing elements Show archive.org snapshot in the layout.
  • Unpoly will now restore history entries created by the user changing the #hash in the browser's address bar (without also changing the path or search query).
  • Unpoly will now restore its own history entries that were later replaced by external scripts (through history.replaceState() Show archive.org snapshot ).

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:changed Show archive.org snapshot can now inspect and control which history changes Unpoly should handle:

  • A new experimental property { willHandle } shows if Unpoly thinks it is responsible for restoring the new location state.
  • A new experimental property { alreadyHandled } shows if Unpoly thinks the change has already been handled (e.g. after calls to history.pushState()).

Unpoly now handles most clicks on a link to a #hash within the current page, taking great care to emulate the browser's native scrolling behavior:

  • Hash links will now honor the viewport's scroll-behavior: smooth Show archive.org snapshot style.
  • Hash links can now override their scroll behavior using an [up-scroll-behavior] attribute. Valid values are instant, smooth and auto (uses CSS behavior).
  • Hash links will now always scroll to a fragment in link's layer, ignoring matching fragments in other layers.
  • Hash links will no longer scroll when another script prevented the click event.
  • Hash links that are followable Show archive.org snapshot will now scroll the page without re-rendering.
  • Hash links will now reliably scroll far enough to ignore any obstructing elements Show archive.org snapshot in the layout.

Every location change is now tracked

up:location:changed Show archive.org snapshot (and up:layer:location:changed Show archive.org snapshot ) used to only be emitted when history changed during rendering.
⚠️ These events are now emitted when the URL changes for any reason, including:

Reacting to #hash changes usually involves scrolling, not rendering. To better signal this case, the { reason } property of up:location:changed Show archive.org snapshot can now be the string 'hash' if only the location #hash was changed from the previous location.

Other improvements to history handling

  • The log Show archive.org snapshot now shows a purple event badge when the user navigates within history. This helps to correlate e.g. a popstate event with the logging output from a subsequent history restoration.
  • When a fragment update closes an overlay and then navigates the parent layer to a new location, Unpoly will no longer push a redundant history entry of the parent layer's location before navigating.
  • Published an experimental function up.history.replace() Show archive.org snapshot to change the URL of the current history state.
  • The up:layer:location:changed Show archive.org snapshot event now has a { previousLocation } property.
  • Fix a bug where history wasn't updated when a response contains comments before the <!DOCTYPE> or <html> tag (fixes #726)

Watching fields for changes

When watching fields using [up-watch] Show archive.org snapshot , [up-autosubmit] Show archive.org snapshot , [up-switch] Show archive.org snapshot or [up-validate] Show archive.org snapshot , the following cases are now addressed:

  • Fixed all cases where a watched field with [up-keep] Show archive.org snapshot is transported to a new <form> element by a fragment update.
  • Fixed all cases where a watched field outside its form (with [form] Show archive.org snapshot attribute) is added or removed dynamically.
  • When a watched field runs a callback, a purple event badge is now logged Show archive.org snapshot to help correlating cause and effect.
  • If a watched field with [up-watch-delay] was detached by an external script during the delay, watchers will no longer fire callbacks or send requests.
  • ⚠️ Watching an individual radio button will now throw an error. Watch a container for the entire radio group instead.
  • Directly watching a field without a [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] Show archive.org snapshot attribute has been reworked to be more powerful and flexible.

Also see our new guide Switching form state Show archive.org snapshot .

Disabling or enabling fields

You can now disable dependent fields using the new [up-disable-for] Show archive.org snapshot and [up-enable-for] Show archive.org snapshot 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] Show archive.org snapshot attribute with an selector targeting the controlled fields:

<select name="role" up-switch=".role-dependent">
  <option value="trainee">Trainee</option>
  <option value="manager">Manager</option>
</select>

The target elements can use [up-enable-for] Show archive.org snapshot and [up-disable-for] Show archive.org snapshot
attributes to indicate for which values they should be shown or hidden:

<!-- The department field is only shown for managers -->
<input class="role-dependent" name="department" up-enable-for="manager">

<!-- The mentor field is only shown for trainees -->
<input class="role-dependent" name="mentor" up-disable-for="manager">

See Disabling or enabling fields Show archive.org snapshot .

Custom switching effects

You can now implement custom, client-side switching effects by listening to the up:form:switch Show archive.org snapshot event on any element targeted by [up-switch] Show archive.org snapshot .

For example, we want a custom [highlight-for] attribute. It draws a bright
outline around the department field when the manager role is selected:

<select name="role" up-switch=".role-dependent">
  <option value="trainee">Trainee</option>
  <option value="manager">Manager</option>
</select>

<input class="role-dependent" name="department" highlight-for="manager">

When the role select changes, an up:form:switch Show archive.org snapshot event is emitted on all elements matching .role-dependent.
We can use this event to implement our custom [highlight-for] effect:

up.on('up:form:switch', '[highlight-for]', (event) => {
  let highlightedValue = event.target.getAttribute('highlight-for')
  let isHighlighted = (event.field.value === highlightedValue)
  event.target.style.highlight = isHighlighted ? '2px solid orange' : ''
})

See Custom switching effects Show archive.org snapshot .

New switching modifiers

The [up-switch] Show archive.org snapshot attribute itself has been reworked with new modifiying attributes:

More [up-switch] Show archive.org snapshot changes

Form validation

The [up-validate] Show archive.org snapshot attribute has been reworked.

Validating against other URLs

By default Unpoly will submit validation requests to the form's [action] attribute, setting an additional X-Up-Validate Show archive.org snapshot header to allow the server distinguish a validation request from a regular form submission.

Unpoly can now validate forms against other URLs Show archive.org snapshot . You can do so with the new [up-validate-url] Show archive.org snapshot and [up-validate-method] Show archive.org snapshot attributes on individudal fields or on entire forms:

<form method="post" action="/order" up-validate-url="/validate-order">
  ...
</form>

To have individual fields validate against different URLs, you can also set [up-validate-url] on a field:

<form method="post" action="/register">
  <input name="email" up-validate-url="/validate-email">
  <input name="password" up-validate-url="/validate-password">
</form>

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 Show archive.org snapshot 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:

up.validate('.foo', { url: '/path1' })
up.validate('.bar', { url: '/path2' })
up.validate('.baz', { url: '/path1' })
up.validate('.qux', { url: '/path2' })

This will send a sequence of two requests:

  1. A request to /path targeting .foo, .baz. The other validations are queued.
  2. Once that request finishes, a second request to /path2 targeting .bar, .qux.

Other validation changes

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:

Content-Type: text/html
X-Up-Open-Layer: { target: '#menu', mode: 'drawer', animation: 'move-to-right' }

<div id="menu">
  Overlay content
</div>

See Opening overlays from the server Show archive.org snapshot .

Closing overlays from forms

Forms can now have an [up-dismiss] Show archive.org snapshot or [up-accept] Show archive.org snapshot attribute to close their overlay when submitted Show archive.org snapshot .
This will immediately close the overlay on submission, without making a network request:

<form up-accept>
  <input name="email" value="foo@bar.de">
  <input type="submit">
</form>

The form's field values become the overlay's result value Show archive.org snapshot , encoded as an up.Params Show archive.org snapshot instance:

up.layer.open({
  url: '/form',
  onAccepted: ({ value }) => {
    console.log(value.get('email'))
  }
})

See Closing when a form is submitted Show archive.org snapshot .

Detecting the origin layer

The server can now detect if an interaction (e.g. clicking a link or submitting a form) originated Show archive.org snapshot from an overlay, by using the X-Up-Origin-Mode Show archive.org snapshot request header. This is opposed to the targeted layer, which is still sent as an X-Up-Mode Show archive.org snapshot header.

For example, we have the following link in a modal overlay. The link targets the root layer:

<!-- label: Link within an overlay -->
<a href="/" up-follow up-layer="root">Click me</a>

When the link is clicked, the following request headers are sent:

X-Up-Mode: root
X-Up-Origin-Mode: modal

Other layer changes

  • Fix a bug where overlays allowed scrolling of a background layer.

Script security

This version revises mechanisms to prevent cross-site scripting and handle strict content security policies Show archive.org snapshot .

Scripts in fragments are no longer executed

⚠️ Unpoly no longer executes <script> elements in new fragments.
This default can by changed by configuring up.fragment.config.runScripts Show archive.org snapshot .

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 Show archive.org snapshot for techniques to remove inline <script> elements.

Mandatory nonces for script-dynamic CSP

A CSP with strict-dynamic Show archive.org snapshot allows 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 Show archive.org snapshot in any response with a strict-dynamic CSP, even with runScripts = true.

If you cannot use nonces for some reasons, you can configure up.fragment.config.runScripts Show archive.org snapshot to a function
that returns true for allowed scripts only:

up.fragment.config.runScripts = (script) => {
  return script.src.startsWith('https://myhost.com/')
}

See CSPs with strict-dynamic Show archive.org snapshot for details.

Other CSP changes

  • Unpoly now uses CSP nonces from a default-src directive if no script-src directive is found in the policy.
  • ⚠️ Unpoly now ignores CSP nonces from the script-src-elem directive. Since nonces are used to allow attribute callbacks Show archive.org snapshot , using script-src-elem is not appropriate.
  • Fix a bug where <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.
  • When up:assets:changed Show archive.org snapshot listeners inspect event.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:

Parameters organized into sections

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() Show archive.org snapshot , inheriting most available render options in the process. We used to document this with a note like "Other up.render() Show archive.org snapshot 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:

When there is a guide with more context, the documentation for attributes or functions now show a link to that guide:

Link to guide with more context

Caching

  • When a POST request redirects to a GET route, that final GET request is now cached.
  • up.reload() Show archive.org snapshot can now restore a fragment to a previously cached state using an { cache: true } option. This was possible before, but was never documented.
  • ⚠️ Any [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.
  • ⚠️ The server can no longer prevent expiration with an X-Up-Expire-Cache: false response header.
  • Requests now clear out their { bindLayer } property after loading, allowing layer objects to be garbage-collected while the request is cached.
  • Links with both [up-hungry] Show archive.org snapshot and [up-preload] Show archive.org snapshot no longer throw an error after rendering cached, but expired content.

Navigation bars

Navigational containers Show archive.org snapshot can now match the current location of other layers by setting an [up-layer] attribute.
The .up-current Show archive.org snapshot class 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:

<!-- label: Navigation bar in an overlay -->
<nav up-layer="any">
  <a href="/users" up-layer="root">Users</a>
  <a href="/posts" up-layer="root">Posts</a>
  <a href="/sitemap" up-layer="current">Full sitemap</a>
</nav>

See Matching the location of other layers Show archive.org snapshot .

Preserving elements

The [up-keep] Show archive.org snapshot element now gives you more control over how long an element is kept.

Also see our new guide Preserving elements Show archive.org snapshot .

Keeping an element until its HTML changes

To preserve an element as long as its outer HTML Show archive.org snapshot 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 Show archive.org snapshot . 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:

<fieldset id="department-group" up-keep="same-html">
  <label for="department">Department</label>
  <select id="department" name="department" value="IT">
    <option>IT</option>
    <option>Sales</option>
    <option>Production</option>
    <option>Accounting</option>
  </select>
  <!-- Eventual errors go here -->
</fieldset>

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 Show archive.org snapshot ) are ignored.

Keeping an element until its data changes

To preserve an element as long as its data Show archive.org snapshot remains the same, set an [up-keep="same-data"] attribute. Only when the element's [up-data] Show archive.org snapshot 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 Show archive.org snapshot to render an interactive map into elements with a .map class. The initial map location is passed as an [up-data] Show archive.org snapshot 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:

<div class="map" up-data="{ location: 'Hofbräuhaus Munich' }" up-keep="same-data"></div>

Instead of [up-data] Show archive.org snapshot we can also use HTML5 [data-*] attributes Show archive.org snapshot :

<div class="map" data-location="Hofbräuhaus Munich" up-keep="same-data"></div>

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 Show archive.org snapshot ) 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 Show archive.org snapshot by listening to the up:fragment:keep Show archive.org snapshot event or setting an [up-on-keep] Show archive.org snapshot attribute.

Form data handling

  • ⚠️ Submitting or validating a form with a { 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 (like name[]).
  • You can now configure which params are treated as an array with multiple values, by setting up.form.config.arrayParam Show archive.org snapshot . By default, only field names ending in "[]" are treated as arrays. (by @apollo13)
  • Calling up.network.loadPage() Show archive.org snapshot will now remove binary values (from file inputs) from a given { params } option. JavaScript cannot make a full page load with binary params.
  • Fix the method up.Params#getAll() Show archive.org snapshot not returning the correct results or causing a stack overflow.

Focus ring visibility

You can now override focus ring visibility Show archive.org snapshot 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.autoFocusVisible Show archive.org snapshot configuration.

Scrolling to the top or bottom

This release adds a new scroll option Show archive.org snapshot [up-scroll='bottom']. This scrolls viewports around the targeted fragment to the bottom.

⚠️ For symmetry, the option [up-scroll='reset'] was changed to [up-scroll='top'].

Long-pressing an [up-instant] Show archive.org snapshot 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:click Show archive.org snapshot event.

Utility functions

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] Show archive.org snapshot .

Polling

An [up-poll] Show archive.org snapshot fragment now stops polling if an external script detaches the element.

Animation

Fixed a bug where prepending or appending Show archive.org snapshot 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] Show archive.org snapshot attribute.

JavaScript rendering API

Network requests

Listeners to up:request:load Show archive.org snapshot can now inspect or mutate request options Show archive.org snapshot before it is sent:

up.on('up:request:load', (event) => {
  if (event.request.url === '/stocks') {
    event.request.headers['X-Client-Time'] = Date.now().toString()
    event.request.timeout = 10_000
  }
})

This was possible before, but was never documented.

Developer experience

We modernized the codebase so that contributing to Unpoly is now simpler:

  • You can now run headless tests (without a browser window) by running npm run test.
  • CI: Tests now run automatically Show archive.org snapshot for every pull request.
  • All remaining CoffeeScript has been hosed off the test suite.
  • All tests now use async / await instead of our legacy asyncSpec() helper.
  • The release process was migrated from Ruby to Node.js.
  • The CI setup now automatically runs tests against various integration styles, such as using an ES6 build, the migration polyfills or various CSP settings.

Stabilization of experimental features

Many experimental features have now been declared as stable:

Migration polyfills

Henning Koch
Last edit
Henning Koch
License
Source code in this card is licensed under the MIT License.
Posted by Henning Koch to makandra dev (2025-07-03 11:45)