You may hook into specific stages of the rendering process in order to change the result or handle error cases.
The techniques below apply to all functions that render, most notably up.render()
, up.follow()
, up.submit()
and up.reload()
. For brevity we only use up.render()
in examples. Most callbacks also have an equivalent HTML attribute for use in links or forms, e.g. [up-on-loaded]
for { onLoaded }
.
Render returns a promise that fulfills when fragments successfully were inserted and compiled. To run code after fragments were updated, await
that promise:
await up.render({ url: '/path', target: '.target' })
console.log("Updated fragment is", document.querySelector('.target'))
After the up.render()
promise fulfills the fragments may still change further through animation or revalidation.
To run code when animations have concluded and cached content was revalidated, use the up.render().finished
promise:
let result = await up.render({ target: '.target', url: '/path' }).finished
console.log("Final fragments: ", result.fragments)
The up.render().finished
promise resolves to the last up.RenderResult
that updated a fragment.
If revalidation re-rendered the fragment, it is the result from the
second render pass. If no revalidation was performed, or if revalidation yielded an empty response,
it is the result from the initial render pass.
The promise rejects when there is any error during the initial render pass or during revalidation.
Instead of awaiting a promise you may also pass an { onFinished }
callback.
In HTML you can set an [up-on-finished]
attribute on a link or form.
To run code after every render pass, use the { onRendered }
callback.
This callback may be called zero, one or two times:
{ onRendered }
is not called.{ onRendered }
is called with the result.{ onRendered }
is called again with the final result.In HTML you can set an [up-on-rendered]
attribute on a link or form:
<a
href="/foo"
up-target="/target"
up-on-rendered="console.log('Updated fragment is', document.querySelector('.target'))">
Click me
</a>
Both up.render()
and up.render().finished
promises resolve to an up.RenderResult
object. You may query this object for the effective results of each render pass:
let result = await up.render({ url: '/path', target: '.target', failTarget: '.errors' })
console.log("Updated layer: ", result.layer)
console.log("Updated fragments: ", result.fragments)
console.log("Effective option used: ", result.options)
Functions like up.render()
and up.follow()
offer numerous options and events that allow you to control the render process.
Intent | Hook | Type |
---|---|---|
Inspect response before rendering | { onLoaded } |
Callback |
Inspect response before rendering | up:fragment:loaded |
Event |
Modify new elements | up.compiler() |
Component registry |
Modify new elements | { onRendered } |
Callback |
Modify new elements | up:fragment:inserted |
Event |
Preserve elements within a fragment | [up-keep] |
HTML attribute |
Control scrolling | { scroll } |
Option |
Control focus | { focus } |
Option |
Control concurrency | { abort } |
Option |
Control concurrency | { disable } |
Option |
For a full list of available options see up.render() parameters
and the lifecycle diagram below.
The promises returned by up.render()
and up.render().finished
reject if any error is thrown during rendering, or if the server responds with an HTTP error code.
You may handle the following error cases:
Error case | Hook | Type |
---|---|---|
Server responds with non-200 HTTP status | fail-prefixed options | Options |
Server responds with non-200 HTTP status |
up.RenderResult (thrown) |
Error |
Disconnect or timeout | up.Offline |
Error |
Disconnect or timeout | { onOffline } |
Callback |
Disconnect or timeout | up:fragment:offline |
Event |
Target selector not found | up.CannotMatch |
Error |
Compiler throws error | error |
Error |
Fragment update was aborted | up.AbortError |
Error |
Fragment update was aborted | up:fragment:aborted |
Event |
Any error thrown while rendering | { onError } |
Callback |
Any error thrown while rendering | up.Error |
Error superclass |
Unpoly functions are generally not interrupted by errors in user code, such as compilers, transitions or callbacks.
When a user-provided function throws an exception, Unpoly instead an emits error
event on window
.
The operation then succeeds successfully:
up.compiler('.element', () => { throw new Error('broken compiler') })
let element = up.element.affix(document.body, '.element')
up.hello(element) // no error is thrown
This behavior is consistent with how the web platform handles errors in event listeners and custom elements.
Exceptions in user code are also logged to the browser's error console. This way you can still access the stack trace or detect JavaScript errors in E2E tests.
Some test runners like Jasmine already listen to the error
event and fail your test if any uncaught exception is observed.
In Jasmine you may use jasmine.spyOnGlobalErrorsAsync()
to make assertions on the unhandled error.
To demonstrate control flow in case of error, the code below handles many different error cases:
try {
window.addEventListener('error', function(event) {
console.log('Compiler threw error', event.error)
})
let result = await up.render({
url: '/path',
target: '.target', // selector to replace for 200 OK status
failTarget: '.errors' // selector to replace for non-200 status
})
} catch (error) {
if (error instanceof up.RenderResult) {
// Server sent HTML with a non-200 status code
console.log("Updated .errors with", error.fragments)
} else if (error instanceof up.CannotMatch) {
console.log("Could not find .target in current page or response")
} else if (error instanceof up.Aborted) {
console.log("Request to aborted")
} else if (error instanceof up.Offline) {
console.log("Connection loss or timeout")
} else {
console.log("Other error while rendering: ", error)
}
}
Note how we use a fail
-prefixed render option { failTarget }
to update a different fragment in case the server responds with an error code. See handling failed responses for more details on handling server responses with an error code.
The render lifecycle emits many events that you can prevent by calling event.preventDefault()
. When these events are prevented, the render process will abort and no elements will be changed. Focus and scroll positions will be kept. The up.render()
promise will reject with an up.AbortError
.
The most important preventable events are:
Tip
The last preventable event is
up:fragment:loaded
. It is emitted after a response is loaded but before any elements were changed.
Also see skipping unnecessary rendering.
Events like up:link:follow
, up:form:submit
and up:fragment:loaded
allow you to adjust render options by mutating event.renderOptions
.
The code below will open all form-contained links in an overlay, as to not lose the user's form data:
up.on('up:link:follow', 'form a', function(event, link) {
if (link.closest('form')) {
event.renderOptions.layer = 'new'
}
})
If you have compilers that only set default attributes, consider using a single event listener that manipulates event.renderOptions
. It's much leaner than a compiler, which needs to be called for every new fragment.
]()
Assume we give a new attribute [require-session]
to links that require a signed-in user:
<a href="/projects" require-session>My projects</a>
When clicking the link without a session, a login form should open in a modal overlay. When the user has signed in successfully, the overlay closes and the original link is followed.
We can implement this with the following handler:
up.on('up:link:follow', 'a[require-session]', async function(event) {
if (!isSignedIn()) {
// Abort the current render pass
event.preventDefault()
// Wait until the user has signed in in a modal
await up.layer.ask('/session/new', { acceptLocation: '/welcome' })
// Start a new render pass with the original render pass
up.render(event.renderOptions)
}
})
The diagram below attempts to visualize the sequence of render steps, edge cases and error states.