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
.
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 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>
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:
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.
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>
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>
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:
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.
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 }
.
Unpoly 3.0 has added the [up-disable]
attribute to disable forms while working. This release adds the following features:
button[type=button]
or input[type=button]
.up.form.config.genericButtonSelectors
.{ disable }
option now also accepts an Element
(or an array of elements) to disable.up.render({ disable })
would crash unless an { origin }
was also passed.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"]
|
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.<form>
element now also receives the .up-active
class (in addition to the submit button)..up-active
class (in addition to the field and the <form>
).up.status.config.activeClasses
. This allows to set custom CSS classes for working links and forms.up.status.config.loadingClasses
. This allows to set custom CSS classes for targeted fragments that are loading new content.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.
:has()
selectorFor 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.
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:
[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 }
.{ 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 }
.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 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.[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)
..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.up.fragment.config.renderOptions
. This is an object of default render options to always apply, even when not navigating.up.fragment.get()
with multiple search layers (e.g. { layer: "current, parent"}
), Unpoly will now search those layers in the given order.up.fragment.get()
with an Element
, that element is returned without further lookups.[up-use-data]
attribute allows to override data for the targeted fragment. The corresponding render options is { data }
.{ 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.{ 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.{ mode }
option has been deprecated. Always pass a { layer: 'new' }
option in addition to { mode }
.{ dismissAriaLabel }
has been renamed to { dismissARIALabel }
[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.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)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:
[up-content]
now also accepts multiple elements, or a mix of Text
and Element
siblings.{ content }
now accepts any List<Node>
, e.g. the NodeList
returned by querySelectorAll()
.up.element.createNodesFromHTML()
. This parses a list of nodes from a string of HTML. Unlike up.element.createFromHTML()
, this new function does not require a single root element in the HTML. It can parse Text
nodes, or a mixed list of Text
and Element
siblings..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
|
[up-on-keep]
, [up-on-hungry]
and [up-on-opened]
.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.[up-flashes]
is now stable (discussion #679)#hash
in the [href]
will now honor fixed layout obstructions if the browser location is already on that #hash
.[up-confirm]
would show the confirmation dialog before preloading.up.submit({ submitButton: false })
.{ focus: 'keep' }
would sometimes re-focus elements that never lost focus.[up-preload]
renders expired content from the cache, unhovering the link would abort the revalidation request.
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.