An element with an [up-keep] attribute survives when the fragment around it is replaced.
A playing <video> or a map widget with its pan and zoom state stays where it is
while Unpoly updates everything around it.
Set an [up-keep] attribute on the element you want to keep.
In the example below, an <audio> element keeps playing while the article around it changes:
<!-- 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 requests /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 the new HTML is rendered, Unpoly looks for [up-keep] elements in the fragment that is about to be replaced.
For each one it derives a target selector (here #player) and looks for a match in the new content.
When it finds one, the existing element is moved into the new fragment and its counterpart from the response is discarded.
Everything else is updated as usual:
<!-- label: Page after update -->
<div id="article"> <!-- mark-line -->
<p>Article 2</p> <!-- mark-line -->
<audio id="player" up-keep src="song1.mp3"></audio> <!-- chip: kept -->
</div> <!-- mark-line -->
<a href="/article2" up-target="#article">Go to article 2</a> <!-- chip: not targeted -->
The kept element is the old element, so it still plays song1.mp3.
Its event listeners and any state set by a compiler survive the update.
Its compilers don't run again and its destructors are not called. Once an update no longer contains a
match, the element is destroyed like any other, and its destructors run.
When you target an [up-keep] element directly, it is kept as well, so the update changes nothing.
To replace it, force an update.
Important
The
[up-keep]element needs a derivable target selector, like an[id]. If Unpoly cannot uniquely identify the element within both the old and the new content, the element is replaced like any other.
Elements can only be kept within the <body>. See [up-keep] for a few more limitations with media elements.
By default an element is kept for as long as its target selector can be matched in the new content. Often you want an element to live through small updates, but to be replaced once something substantial changes.
To keep an element only as long as its outer HTML stays the same,
set [up-keep="same-html"]. The element is only replaced when its attributes or children
differ between versions.
The example below uses a JavaScript-based <select> replacement like Tom Select.
Because initialization is expensive, we want to keep the element as long as 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 holds both the select
and any error messages:
<fieldset id="department-group" up-keep="same-html"> <!-- mark: up-keep="same-html" -->
<label for="department">Department</label>
<select id="department" name="department">
<option selected>IT</option>
<option>Sales</option>
<option>Production</option>
<option>Accounting</option>
</select>
<!-- Eventual errors go here -->
</fieldset>
Unpoly compares the element's initial HTML as it was rendered by the server. Changes made on the client, like elements inserted by a compiler, are ignored.
Before the HTML is compared, light normalization is applied: indentation and fluctuating attributes like CSP nonces are ignored.
You can customize this with up.fragment.config.normalizeKeepHTML.
To keep an element only as long as its data stays the same, set [up-keep="same-data"].
The element is only replaced when its [up-data] attribute differs between versions.
Changes in other attributes or in 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, we keep the map widget as long as possible.
Only when the map's initial location changes, we re-render the map around the new location:
<div class="map" up-data="{ location: 'Hofbräuhaus Munich' }" up-keep="same-data"></div> <!-- mark: up-keep="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="Hofbräuhaus Munich" -->
Unpoly compares the element's initial data as it was rendered by the server. Changes to the data object made on the client, e.g. by a compiler, are ignored.
Tip
Instead of re-rendering when data changes you can inspect the data of the new version and update the existing element.
For conditions that HTML cannot express, listen to the up:fragment:keep event.
It is emitted on the existing element before it is kept.
The counterpart from the response is available as event.newFragment.
When you prevent the event, the element is not kept and the update is forced.
Let's say we only want to update an <audio up-keep> when its track changes,
and never while it is playing:
up.on('up:fragment:keep', 'audio', function(event) {
let oldAudio = event.target
let newAudio = event.newFragment // mark: event.newFragment
if (oldAudio.src !== newAudio.src && oldAudio.paused) {
// Preventing the event forces an update
event.preventDefault()
}
})
Short conditions can also be inlined as an [up-on-keep] attribute.
The snippet can access the old element as this, and the new element as newFragment:
<audio src="song.mp3" up-keep up-on-keep="if (!this.paused) event.preventDefault()"></audio> <!-- mark: up-on-keep="if (!this.paused) event.preventDefault()" -->
There are several ways to replace an [up-keep] element despite its attribute:
[up-keep="false"] attribute. It then replaces the existing element,
even if that element has [up-keep].[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 render without keeping any elements at all:
[up-keep] elements by setting an [up-use-keep="false"] attribute.{ keep: false } option.Even when keeping an element, you may want to pick up 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 as an [up-data] attribute:
<div class="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) {
let map = new google.maps.Map(element)
map.setCenter(data)
})
While we want to keep the map during updates, we do want to pick up a new center coordinate
when the containing fragment is updated. We can do so by listening to the up:fragment:keep event
and reading the new element's data from event.newData:
up.compiler('.map', function(element, data) {
let map = new google.maps.Map(element)
map.setCenter(data)
element.addEventListener('up:fragment:keep', function(event) { // mark-line
map.setCenter(event.newData) // mark-line
}) // mark-line
})
If you only want to be notified after the decision was made, listen to up:fragment:kept instead.
It is emitted after all keep conditions have been evaluated and can no longer prevent the keeping.
Tip
Instead of keeping an element and updating its data, you may also preserve an element's data through reloads.
Tip Your coding agent can search these docs locally with the Unpoly agent skill