Unpoly lets you preserve selected elements during rendering.
Preserved elements remain attached to their current position on the page, even when their parent element is replaced.
Unpoly apps rarely do fine-grained element updates, instead opting to update medium-sized fragments.
However, there are use cases for preserving individual elements:
<video>
, <audio>
) that should retain their playback state during updates.<select>
replacements.In the example below we want to preserve an <audio>
element so it keeps playing
as content around it is updated. We can achieve this by setting an [up-keep]
attribute
on the element we want to keep:
<!-- label: Initial page -->
<div id="article">
<p>Article 1</p>
<audio id="player" up-keep src="song1.mp3"></audio> <!-- mark: up-keep -->
</div>
<a href="/article2" up-target="#article">Go to article 2</a>
When the link is clicked, Unpoly will request /article2
and receives HTML like this:
<!-- label: Response from the server -->
<div id="article">
<p>Article 2</p>
<audio id="player" up-keep src="song2.mp3"></audio>
</div>
Before Unpoly renders the new HTML, it tries to correlate [up-keep]
elements within the current page
and the response.
Because the <audio>
element's derived target (#player
) matches in both the old and new content,
Unpoly can preserve it. Elements around it will be updated with new content, highlighted below:
<!-- label: Page after update -->
<article> <!-- mark-line -->
<p>Article 2</p> <!-- mark-line -->
<audio id="player" up-keep src="song1.mp3"></audio> <!-- chip: preserved -->
</article> <!-- mark-line -->
<a href="/article2" up-target="#article">Go to article 2</a> <!-- chip: not targeted -->
Important
The
[up-keep]
element must have a derivable target selector so its position within the old and new parents can be correlated. If Unpoly cannot uniquely identify the element within both the old and new content, the element cannot be preserved.
Sometimes we want more control over how long an element is preserved. For example, when we want to keep similar elements until a substantial change is detected.
To preserve an element as long as its outer HTML 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.
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"> <!-- mark: 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) are ignored.
Before the HTML is compared, light normalization is applied.
You can customize normalization with up.fragment.config.normalizeKeepHTML
.
To preserve an element as long as its data remains the same,
set an [up-keep="same-data"]
attribute. Only when the element's [up-data]
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 to render an interactive map into elements with a .map
class.
The initial map location is passed as an [up-data]
attribute.
Because we don't want to lose client-side state (like pan or zoom settings), 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> <!-- mark: same-data -->
Instead of [up-data]
we can also use HTML5 [data-*]
attributes:
<div class="map" data-location="Hofbräuhaus Munich" up-keep="same-data"></div> <!-- mark: data-location -->
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) are ignored.
Tip
Instead of re-rendering when data changes you can inspect data in the new version and update the existing element.
You can define arbitrary conditions for keeping an element to JavaScript.
Let's say we only want to update an <audio up-keep>
when its track changes.
In addition, we never want to update an audio element that is currently playing.
We can achieve this by listening to up:keep:fragment
and comparing the old and new elements.
When we prevent the event, the element is no longer kept and an update is forced:
up.on('up:fragment:keep', 'audio', function(event) {
let oldAudio = event.target
let newAudio = event.newElement
if (oldAudio.src !== newAudio.src && oldAudio.paused) {
// Preventing the event forces an update
event.preventDefault()
}
})
Short keep conditions can also be inlined as an [up-on-keep]
attribute:
<audio src="song.mp3" up-keep up-on-keep="if (!this.paused) event.preventDefault()"></audio> <!-- mark: up-on-keep -->
There are many ways to force an update of an [up-keep]
element:
[up-keep=false]
attribute.[id]
or [up-id]
attribute so its derived target no longer matches the existing element.up:fragment:keep
event that is emitted on the existing element.You can also choose to render without keeping elements:
[up-keep]
elements by setting an [up-use-keep=false]
attribute.[up-keep]
elements by passing an { keep: false }
option.Even when keeping elements, you may reconcile its data object with the data from the new element that was discarded.
Let's say you want to display a map within an element. The center of the map
is encoded using an [up-data]
attribute:
<div id="map" up-keep up-data="{ lat: 50.86, lng: 7.40 }"></div>
We can initialize the map using a compiler like this:
up.compiler('.map', function(element, data) {
var map = new google.maps.Map(element)
map.setCenter(data)
})
While we want to preserve the map during page loads, we do want to pick up
a new center coordinate when the containing fragment is updated. We can do so by
listening to an up:fragment:keep
event and observing event.newData
:
up.compiler('.map', function(element, data) {
var map = new google.maps.Map(element)
map.setCenter(data)
map.addEventListener('up:fragment:keep', function(event) { // mark-line
map.setCenter(event.newData) // mark-line
}) // mark-line
})
Tip
Instead of keeping an element and update its data you may also preserve an element's data through reloads.