Unpoly apps often want to. E.g. a <div class="map">
should automatically start a Google Map widget.
Unpoly offers compilers to call JavaScript snippets when an element is inserted the DOM, and that element is matching a CSS selectors.
Compiler functions run both at the initial page load and when a new fragment is inserted later. This makes them a great tool to activate JavaScript snippets in a single-page environment that can persist through many user navigations.
We want to insert the current time into elements with a .current-time
class:
<div class='current-time'>
<!-- chip: insert current time here -->
</div>
To achieve this, register a JavaScript function with up.compiler()
:
up.compiler('.current-time', function(element) {
var now = new Date()
element.textContent = now.toString()
})
The compiler function will be called once for each matching element when the page loads, or when a matching fragment is rendered later.
DOMContentLoaded
Old school web developers might have implemented the .current-time
compilers
by listening to a DOMContentLoaded
(or load
) events:
document.addEventListener('DOMContentLoaded', function() {
for (let element of document.querySelector('.current-time')) {
var now = new Date()
element.textContent = now.toString()
}
})
A big drawback to this strategy is that elements are only matched once, during the initial page load. Your JavaScript enhancements will not be applied to elements that enter the page later. Compiler functions run both at the initial page load and when a new fragment is inserted later.
When adding Unpoly to an existing application, we recommend to
convert your DOMContentLoaded
listeners to compilers.
up.compiler()
is a great way to integrate external JavaScript libraries, like
maps, date pickers or charts.
Let's say your JavaScript plugin wants you to call lightboxify()
on links that should open a lightbox. You decide to
do this for all links with an lightbox
class:
<a href="river.png" class="lightbox">River</a>
<a href="ocean.png" class="lightbox">Ocean</a>
We can register a compiler that calls lightboxify()
on all matching elements:
up.compiler('a.lightbox', function(element) {
lightboxify(element)
})
In Unpoly the JavaScript environment can persist through many page navigation. To prevent memory leaks, is important that any compiler effects can be garbage collected when the element is destroyed.
When a compiler binds an event listener to the compiling element (or its descendants), they can be garbage collected once the element leaves the DOM, no further steps required:
// label: ✔️ Garbage collectable
up.compiler('.click-to-hide', function(element) {
let hide = () => element.style.display = 'none'
element.addEventListener('click', hide)
})
When your compiler registers effects outside the compiling element's subtree, that effect is not cleaned up automatically.
For example, this compiler registers a global scroll
listener to the global window
object.
Every compilation will subscribe another listener that is never removed, causing a memory leak:
// label: ❌ Memory leak
up.compiler('.scroll-to-hide', function(element) {
let hide = () => element.style.display = 'none'
window.addEventListener('scroll', hide)
})
To address this, a compiler can return a destructor function that reverts its non-local effect. Unpoly will call this destructor when the element is destroyed:
// label: ✔️ Garbage collectable
up.compiler('.scroll-to-hide', function(element) {
let hide = () => element.style.display = 'none'
window.addEventListener('scroll', hide)
return () => window.removeEventListener('scroll', hide) // mark: return
})
This compiler function is now safe for garbage collection.
Important
The destructor function is not expected to remove the element from the DOM.
To run multiple functions when the element is destroyed, return an array of functions:
up.compiler('.auto-hide', function(element) {
let hide = () => element.style.display = 'none'
window.addEventListener('scroll', hide)
let offScroll = () => window.removeEventListener('scroll', hide))
window.addEventListener('load', hide)
let offLoad = () => window.removeEventListener('load', hide))
return [offScroll, offLoad]
})
Instead of returning a destructor function, you can register it with up.destructor()
.
This helps placing the clean-up logic close to the effect that it reverts:
up.compiler('.auto-hide', function(element) {
let hide = () => element.style.display = 'none'
window.addEventListener('scroll', hide)
up.destructor(element, () => window.removeEventListener('scroll', hide))
window.addEventListener('load', hide)
up.destructor(element, () => window.removeEventListener('load', hide))
})
Tip
Other than
addEventListener()
,up.on()
returns a function that unbinds the listener.
You may attach data to an element using HTML5 data attributes
or encoded as relaxed JSON in an [up-data]
attribute:
<span class="user" up-data="{ age: 31, name: 'Alice' }">Alice</span>
An object with the element's attached data will be passed to your compilers as a second argument:
up.compiler('.user', function(element, data) { // mark: data
console.log(data.age) // result: 31
console.log(data.name) // result: "Alice"
})
See attaching data to elements for more details and examples.
Compilers may accept a third argument with information about the current render pass:
up.compiler('.user', function(element, data, meta) { // mark: meta
console.log(meta.layer.mode) // result: "root"
console.log(meta.revalidating) // result: true
})
The following properties are available:
Property | Type | Description | |
---|---|---|---|
meta.layer |
up.Layer |
The layer of the fragment being compiled. This has the same value as up.layer.current . |
|
meta.revalidating |
boolean |
optional | Whether the element was reloaded for the purpose of cache revalidation. |