Revision code

Changes Version 3.10.0
Released on January 23, 2025 with 298 commits

Unpoly 3.10 is a major feature relase, adding support for client-side templates, arbitrary loading state and optimistic rendering. It also contains many bug fixes and quality-of-life improvements, like Relaxed JSON.

This release contains some minor breaking changes, which are marked with the ❌ emoji in this CHANGELOG. All breaking changes are polyfilled by unpoly-migrate.js.

Arbitrary loading state with previews

Previews are temporary page changes while waiting for a network request. They signal that the app is working, or provide clues for how the page will ultimately look. Because previews immediately appear after a user interaction, their use increases the perceived responsiveness of your application.

When the request ends for any reason, all preview changes will be reverted before the server response is processed. This ensures a consistent screen state in cases when a request is aborted, or when we end up updating a different fragment.

You can use previews to implement arbitrary loading state. Two common applications of previews are placeholders and optimistic rendering.

Placeholders

Placeholders are temporary spinners or UI skeletons shown while a fragment is loading:

To show a placeholder while a link is loading, set an [up-placeholder] attribute with the placeholder's HTML as its value:

<a href="/path" up-follow up-placeholder="<p>Loading…</p>">Show story</a> <!-- mark-phrase "up-placeholder" -->

When the link is clicked, the targeted fragment's content is replaced by the placeholder markup temporarily. When the request ends for any reason, the placeholder is removed and the original page state restored.

Instead of passing the placeholder HTML directly, you can also refer to any template by its CSS selector:

<a href="/path" up-follow up-placeholder="#loading-template">Show story</a> <!-- mark-phrase "#loading-message" -->

<template id="loading-template">
  <p>
    Loading…
  </p>
</template>

Optimistic rendering

Unpoly 3.10 supports optimistic rendering as an application of previews and templates. This is a pattern where we update the page without waiting for the server. When the server eventually does respond, the optimistic change is reverted and replaced by the server-confirmed content.

For example, this is the Tasks tab in the official demo app running with 1000 ms latency. Note how the UI updates instantly, without waiting for the server:

Since optimistic rendering requires additional code, we recommend to use it for interactions where the duplication is low, or where the extra effort adds ignificant value for the user. Some suitable use cases include:

  • Forms with few or simple validations (e.g. adding a todo)
  • Forms where users would expect an immediate effect (e.g. submitting a chat message)
  • Re-ordering items with drag'n'drop (because most logic is already on the client)
  • High-value screens where every conversion matters

To limit the duplication of view logic, you may use templates. By embedding templates into your responses, the server stays in control of HTML rendering.

Client-side templating

While Unpoly apps render on the server primarily, having client-side templates can be useful for placeholders, small overlays, or optimistic rendering.

Unpoly 3.10 allows your server to embed templates into your responses. Your frontend can then clone new fragments from these templates, without making another server request.

To refer to a template, pass its CSS selector to any attribute or option that accepts HTML:

<a href="#" up-target=".target" up-document="#my-template">Click me</a> <!-- mark-phrase "#my-template" -->

<div class="target">
  Old content
</div>

<template id="my-template"> <!-- mark-phrase "my-template" -->
  <div class="target">
    New content
  </div>
</template>

Template variables

Sometimes we want to clone a template, but with variations. For example, we may want to change a piece of text, or vary the size of a component.

Unpoly 3.10 offers many methods to implement dynamic templates with variables. You can even integrate template engines like Mustache.js or Handlebars:

<script id="results-template" type="text/mustache"> <!-- mark-phrase "text/mustache" -->
  <div id="game-results">
    <h1>Results of game {{gameCount}}</h1>

    {{#players}}
      <p>{{name}} has scored {{score}} points.</p>
    {{/players}}
  </div>>
</script>

Relaxed JSON

Unpoly now supports relaxed JSON in all attributes and options where it also accepts JSON. Relaxed JSON is a JSON dialect that that aims to be easier to write by humans. It supports syntactic sugar that you enjoy with JavaScript object literals:

  • Unquoted property names
  • Single-quoted strings
  • Trailing commas

For example, you can now write [up-data] like this:

<span class="user" up-data="{ name: 'Bob', age: 18 }">Bob</span>

When Unpoly outputs HTML (e.g. for the X-Up-Context header) it always produces regular JSON.

Progress bar

Improvements have been made to the global progress bar, which appears when requests are tooking long to load:

  • The progress bar will no longer restart its animation when a request is immediately followed by another request.

    For example, when a user changes an [up-autosubmit] form that is already waiting for a request, a second request is sent after the first request has loaded. The progress bar will now show one uninterrupted animation until all requests have loaded.

  • Old versions have used a "bad response time" setting to define the delay until the progress bar is shown. This setting has been renamed to "late delay" everywhere:

    Old name New name
    [up-bad-response-time] [up-late-delay]
    { badResponseTime } { lateDelay }
    up.network.config.badResponseTime up.network.config.lateDelay
    up.Request.prototype.badResponseTime up.Request.prototype.lateDelay
  • Foreground requests can now opt out of the progress bar (and up:network:late events) by setting [up-late-delay="false"] or { lateDelay: false }.

Disabling form fields

Unpoly 3.0 has added the [up-disable] attribute to disable forms while working. This release adds the following features:

Tokens are separated by comma

When a string contains multiple tokens, Unpoly used to separate those tokens with a space character. While this is still possible, the new canonical way is to separate tokens with a comma:

Old form (still valid) New form
[up-layer="parent root"] [up-parent="parent, root"]
[up-show-for="value1 value2"] [up-show-for="value1, value2"]
[up-alias="/foo /bar"] [up-alias="/foo, /bar"]
up.on('event1 event2', fn) up.on('event1, event2', fn)

In some cases tokens used to be separated by an or operator. This is no longer supported. Use a comma instead:

Old form (now invalid) New form
[up-scroll="target or main"] [up-scroll="target, main"]
[up-focus="target or main"] [up-scroll="target, main"]

Feedback classes

Unpoly assigns the .up-active class to clicked links and submit buttons, and .up-loading to targeted fragments that are loading new content.

These feedback classes have been reworked to make it easier to select working elements from CSS and JavaScript:

  • .up-active and .up-loading are now always enabled by default (even when not navigating). They can still disabled explicitly with an [up-feedback=false] attribute or a { feedback: false } option.
  • When submitting a form, the <form> element now also receives the .up-active class (in addition to the submit button).
  • When submitting a form from a focused field, the default submit button now also receives the .up-active class (in addition to the field and the <form>).
  • Added a configuration up.status.config.activeClasses. This allows to set custom CSS classes for working links and forms.
  • Added a configuration up.status.config.loadingClasses. This allows to set custom CSS classes for targeted fragments that are loading new content.

Better selector parsing

Unpoly 3.10 is smarter when parsing values with structured grammar, and no longer relies on magic strings to split complex expressions.

For example, Unpoly can now process more complex target selectors. Selectors like .foo:has(.bar, .baz) or .foo[attr="one, two"] used to cause quirky behavior, but are now parsed correctly.

Native :has() selector

For almost 10 years Unpoly has polyfilled the :has() pseudo-selector for all browsers. Since then the selector has been standardized and has received great browser support.

Starting with this release, Unpoly will no longer include a polyfill and use the browser's native :has() support. Unpoly will no longer boot on old browsers that don't support :has() natively.

Watching fields for changes

Unpoly has several methods to detect and process changes in form fields, most notably [up-watch], [up-autosubmit] and [up-validate]. This release includes the following changes:

  • The [up-watch] callback can now use an options argument. It contains an object of all watch options parsed from that field, e.g. { disable, preview, placeholder }.
  • The watch options { event, delay } will no longer be passed to callbacks of up.watch() and [up-watch], as these options have already been processed by Unpoly.
  • up.watch() and [up-watch] will no longer process an [up-watch-disable] attribute. Instead the attribute is only parsed and passed to the callback as a { disable } option. It is up to the callback to forward the option it to any rendering function that supports { disable }.
  • Fix a bug where up.autosubmit() options did not override options parsed from [up-watch-...] prefixed attributes. It is convention in Unpoly that JavaScript options always take precedence over HTML attributes.

Target derivation

Target derivation is the process of finding a discriminating CSS selector for an element. This release includes the following changes:

  • up.fragment.toTarget() now supports a { strong: true } option. This produces a more unique selector by only considering the element's [id] and [up-id] attributes. Weaker derivation patterns, like the element's class, are not considered in strong mode. The element's tag name is only considered for singleton elements like <html> or <body>.
  • up.fragment.toTarget() can now skip target verifcation by passing a { verify: false } option.
  • When a validated field wants to update its form group, that form group is no longer targeted by its [class], which would often be ambigous. Instead the form group is only targeted by its [id] or [up-id] attribute. If the form group doesn't have an [id] or [up-id] attribute, it is targeted with a .has() selector referencing the changed field, e.g. fieldset:has(#changed-field).

Fragment API

  • You can now update element's inner HTML using the .element:content pseudo-selector. This swaps all children of .element, while preserving the element itself. This feature was previously documented, but didn't work yet.
  • New configuration up.fragment.config.renderOptions. This is an object of default render options to always apply, even when not navigating.
  • When calling up.fragment.get() with multiple search layers (e.g. { layer: "current, parent"}), Unpoly will now search those layers in the given order.
  • Calling up.fragment.get() with an Element, that element is returned without further lookups.
  • New [up-use-data] attribute allows to override data for the targeted fragment. The corresponding render options is { data }.
  • The render option { useHungry } has been renamed to { hungry }, but { useHungry } is still accepted as an alias. The corresponding HTML attribute remains [up-use-hungry] as to not conflict with a link's or form's own [up-hungry] modifier.
  • The render option { useKeep } has been renamed to { keep }, but { useKeep } is still accepted as an alias. The corresponding HTML attribute remains [up-use-keep] as to not conflict with a link's or form's own [up-keep] modifier.

Layers

  • Opening a new overlay with only a { mode } option has been deprecated. Always pass a { layer: 'new' } option in addition to { mode }.
  • The layer option { dismissAriaLabel } has been renamed to { dismissARIALabel }
  • When opening a new layer with [up-use-data] or { data }, that data is now applied to the topmost swappable element (instead of to the overlay container). For example, up.layer.open({ target: '#target', url: '/path', data: { ... }}) will apply the data object to the #target element.
  • When opening an overlay, the property Request#layer is now set to 'new' (instead of to the parent layer). Also the #fragments property is now set to [] (instead of to the parent layer's main element)

Working with node lists

Many Unpoly functions have traditionally expected content with a single DOM element at its root. Unpoly 3.10 makes it easier to render lists of mixed Text and Element nodes:

Bootstrap plugin

  • Clicked links and submit buttons now receive the .active class.

up.feedback is now up.status

The up.feedback package has been renamed to up.status. This package exposed no public JavaScript functions, but does have some configuration settings you need to rename:

Old name New name
up.feedback.config.currentClasses up.status.config.currentClasses
up.feedback.config.navSelectors up.status.config.navSelectors
up.feedback.config.noNavSelectors up.status.config.noNavSelectors

Other changes

  • You can now embed CSP nonces into the attribute callbacks [up-on-keep], [up-on-hungry] and [up-on-opened].
  • New property up.Request#ended indicates whether this request is no longer waiting for the network for any reason. It is true when the server has responded or when the request failed or was aborted.
  • The attribute [up-flashes] is now stable (discussion #679)
  • Clicking a link with a page-local #hash in the [href] will now honor fixed layout obstructions if the browser location is already on that #hash.
  • Fix a bug where a link with [up-confirm] would show the confirmation dialog before preloading.
  • Fix a crash with up.submit({ submitButton: false }).
  • Fix a bug where rendering with { focus: 'keep' } would sometimes re-focus elements that never lost focus.
  • Fix a bug where opening an overlay would stop infinite scrolling (discussion #694).
  • Fix a bug where Unpoly would create duplicate cache entries when the server redirects to a fully qualified URL (with protocol and hostname).
  • Fix a bug where when a link with [up-preload] renders expired content from the cache, unhovering the link would abort the revalidation request.

Upgrading

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.