This release adds asynchronous compilers and many other features requested by the community.
We also fixed a number of performance regressions introduced by Unpoly 3.11.
Breaking changes are marked with a ⚠️ emoji and polyfilled by unpoly-migrate.js
.
Note
Our sponsor makandra funded this release ❤️
Please consider hiring makandra for Unpoly support.
Compiler functions can now be async
. This is useful when a compiler needs to fetch network resources, or when calling a library with an asynchronous API:
up.compiler('textarea.wysiwyg', async function(textarea) { // mark: async
let editor = await import('wysiwyg-editor') // mark: await
editor.init(textarea) // mark: await
})
You can also use this to split up expensive tasks, giving the browser a chance to render and process user input:
up.compiler('.element', async function(element) {
doRenderBlockingWork(element)
await scheduler.yield() // mark-line
doUserVisibleWork(element)
})
Like synchronous compilers, async compiler functions can return a destructor function:
up.compiler('textarea.wysiwyg', async function(textarea) {
let editor = await import('wysiwyg-editor')
editor.init(textarea) // mark: await
return () => editor.destroy(textarea) // mark-line
})
Unpoly guarantees that the destructor is called, even if the element gets destroyed before the compiler function terminates.
Unpoly will run the first task of every compiler function before allowing the browser to render DOM changes. If an async compiler function runs for multiple tasks, the browser will render between tasks. If you have render-blocking mutations that should be hidden from the user, these must happen in the first task.
Async compilers will not delay the promise returned by rendering functions up.render()
or up.layer.open()
.
Async compilers will delay the promise returned by up.render().finished
and up.hello()
.
up.hello()
is now async⚠️ The up.hello()
function now returns a promise that fulfills when all synchronous and asynchronous compilers have terminated:
let textarea = up.element.createFromHTML('<textarea class="wysiwyg"></textarea>')
await up.hello(textarea) // mark: await
// chip: WYISWYG editor is now initialized
The fulfillment value is the same element that was passed as an argument:
let html = '<textarea class="wysiwyg"></textarea>'
let textarea = await up.hello(up.element.createFromHTML(html))
Unpoly 3.11 introduced a number of performance regressions that would be very noticable on pages with many elements, or many forms. To address this, this release includes a number of performance fixes:
[up-validate]
fields for every form. Now fields are only tracked for forms that use [up-validate]
.⚠️ Unpoly now requires server responses with an HTML content-type, like text/html
or application/xhtml+xml
. Trying to render responses with a different type will throw an error, even if the response body contains HTML markup.
Restricting content types is a security precaution. It protects in a hypothetical scenario where an attacker can both upload a file and can use an existing XSS vulnerability to cause Unpoly to render that file. It doesn't affect applications that reliably escape user input.
You can configure which responses Unpoly will process by configuring a function in up.fragment.config.renderableResponse
. To render any response regardless of content-type, configure a function that always returns true`:
up.fragment.config.renderableResponse = (response) => true
The documentation has been extended with new guides:
Submit buttons can now supplement or override most Unpoly attributes from the form:
<form method="post" action="/proposal/accept" up-submit>
<button type="submit" up-target="#success">Accept</button>
<button type="submit" up-target="#failure" up-confirm="Really reject?">Reject</button> <!-- mark: up-confirm="Really reject?" -->
</form>
Individual submit buttons can now opt for a full page load, by setting an [up-submit="false"]
attribute:
<form method="post" action="/report/update" up-submit>
<button type="submit" name="command" value="save">Save report</button>
<button type="submit" name="command" value="download" up-submit="false">Download PDF</button> <!-- mark: up-submit="false" -->
</form>
See [up-submit]
for a list of overridable attributes
When scrolling to reveal a target element, Unpoly will ensure that layout elements with [up-fixed=top]
are not covering the revealed content.
You can now use [up-fixed]
on elements with position: sticky
. Unpoly will measure sticky element like permanently fixed elements. The current scroll position is not taken into account.
In the past Unpoly didn't allow a server to optimize its response when the result was a single table row (or cell) without an enclosing <table>
:
Content-type: text/html
<tr>
<td>...</td>
</tr>
Unpoly can now parse responses that only contain a <tr>
, <td>
or <th>
element, without an enclosing <table>
(issue #91).
Unpoly lets you enlarge a link's click area using the [up-expand]
attribute. This version addresses inconsistent (or impractical) assignment of the .up-active
feedback class when an expanded link is clicked.
When either the [up-expand]
container or the first link is clicked, the .up-active
class
is now assigned to both elements:
<div up-expand class="up-active"> <!-- mark: class="up-active" -->
<a href="/foo" class="up-active">Foo</a> <!-- mark: class="up-active" -->
<a href="/bar">Bar</a>
</div>
When a non-expanded link is clicked, now only that link becomes .up-active
:
<div up-expand>
<a href="/foo">Foo</a> <!-- chip: not active -->
<a href="/bar" class="up-active">Bar</a> <!-- mark: class="up-active" -->
</div>
Two changes were made to preserving elements using the [up-keep]
attribute:
up:fragment:kept
. This event is emitted after all keep conditions are evaluated and preservation can no longer be prevented. A listener can be sure that the element is going to be kept.[up-poll]
and [up-keep]
now continue polling when the element is kept (fixes #763)up.Layer#isOpen()
has been deprecated. Use up.Layer#isAlive() instead
.up.Layer#isClosed()
has been deprecated. Use !up.Layer#isAlive() instead
.AbortError
instead of doing nothing.[up-switch]
input without a containing form is placed in an overlay, and that overlay is closed.[up-boot=manual]
must now be set on the <html>
element instead of on the <script>
loading Unpoly.<script type="module">
.up.fragment.get()
function now has a { destroying: true }
option. This allows to find destroyed elements that are still playing out their exit animation. Note that all up.fragment
functions normally ignore elements in an exit animation.up.fragment.isAlive()
. It returns whether an element is both attached to the DOM and also not in an exit animation.up.util.task()
to again queue macrotasks using setTimeout()
instead of postMessage()
. Unpoly 3.11 only recently switched to postMessage()
because of its tighter scheduling. Unfortunately message order is erratic with postMessage()
in Safari, making it hard to reason about the sequence of asynchronous callbacks.up.Response#isHTML()
. It returns whether the response has a content-type of text/html
or application/xml+html
. It doesn't test if the response body actual contains a valid HTML document.up.render({ response })
is called while another request is in flight.{ failTarget }
was made while waiting for the network, and the first request responded with an error status and updates , the second request is now aborted.
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.