---
area: "Learn"
parent: "Scripting chapter"
url: "https://unpoly.com/learn/enhancing-elements"
---
<nav aria-label="Unpoly docs">[All docs](https://unpoly.com/index.md) · [Scripting chapter](https://unpoly.com/learn/scripting.md)</nav>

# Enhancing elements with JavaScript

Unpoly lets you enhance server-rendered HTML with JavaScript behavior. For example, every `<div class="map">` could automatically start a map widget.

For this you register _compilers_: functions that are called whenever an element matching a CSS selector enters the DOM. Compilers run at the initial page load, and again whenever a new fragment is inserted later. This makes them a reliable home for all your custom JavaScript, on a page that persists through many navigations.

<nav aria-label="Contents">

#### Contents

- [Registering compilers](#registration)
- [Integrating JavaScript libraries](#integrating-libraries)
- [Passing data to a compiler](#data)
- [Cleaning up after yourself](#destructor)
- [Accessing information about the render pass](#meta)
- [Defining new attributes with macros](#macros)
- [Compiling elements inserted by other code](#hello)

</nav>

## Registering compilers {#registration}

We want to insert the current time into elements with a `.current-time` class:

```html
<div class="current-time">
  <!-- insert current time here -->
</div>
```

To achieve this, register a JavaScript function with [`up.compiler()`](https://unpoly.com/api/up.compiler.md):

```js
up.compiler('.current-time', function(element) {
  let now = new Date()
  element.textContent = now.toString()
})
```

The compiler function is called once for each matching element, when the page first loads and when a matching fragment is rendered later.

Compilers run in the order they were registered, each for all its matches in the new fragment. To change the order, pass a [`{ priority }`](https://unpoly.com/api/up.compiler.md#order).

### Compilers see the DOM after the change {#dom-after-change}

When a compiler runs, the render pass has already changed the DOM:

- All new elements are inserted. When a render pass [updates multiple fragments](https://unpoly.com/learn/targeting-fragments.md#multiple), Unpoly inserts all of them before it runs the first compiler, including the layer's [hungry elements](https://unpoly.com/learn/hungry-elements.md).
- Old elements are removed. When the render pass plays a [transition](https://unpoly.com/api/up.morph.md#named-transitions), an old element stays on the screen until its exit animation ends. Unpoly marks it as being destroyed, and lookups like [`up.fragment.get()`](https://unpoly.com/api/up.fragment.get.md) and [`up.fragment.all()`](https://unpoly.com/api/up.fragment.all.md) skip it.

So a compiler can look at other parts of the page and see their new versions:

```html
<a href="/folders/5" up-target="#files, #sidebar">Open folder</a>
```

```js
up.compiler('#sidebar', function(sidebar) {
  let count = up.fragment.all('#files .file').length // counts the new files
  sidebar.querySelector('.file-count').textContent = count
})
```

Compilers run in the order of the target selector, followed by the layer's hungry elements. Focus and scroll position are updated after all compilers have run, and animations start last.

Hungry elements on [other layers](https://unpoly.com/learn/hungry-elements.md#layers) are rendered as a separate group, after the current layer (or before it closes, when the render pass closes an overlay). Don't rely on fragments in other layers being updated when your compiler runs.

### Avoid `DOMContentLoaded` {#no-load-event}

Without Unpoly, the `.current-time` enhancement might have been implemented by listening to a `DOMContentLoaded` (or `load`) event:

```js
document.addEventListener('DOMContentLoaded', function() {
  for (let element of document.querySelectorAll('.current-time')) {
    let now = new Date()
    element.textContent = now.toString()
  }
})
```

A big drawback of this strategy is that elements are only matched once, during the initial page load. Since Unpoly updates fragments without a new page load, elements that enter the page later are never enhanced. Compilers close this gap: they run for the initial page and for every new fragment.

When adding Unpoly to an existing application, we recommend to [convert your `DOMContentLoaded` listeners to compilers](https://unpoly.com/learn/legacy-scripts.md#migrate-to-compiler).

## Integrating JavaScript libraries {#integrating-libraries}

[`up.compiler()`](https://unpoly.com/api/up.compiler.md) is a great way to integrate external JavaScript libraries, like maps, date pickers or charts.

Let's say a date picker library wants you to call `new DatePicker()` on every input that should show a calendar. You decide to do this for all inputs with a `date-picker` class:

```html
<input name="birthday" class="date-picker">
```

We can register a compiler that creates a date picker for every matching input:

```js
up.compiler('input.date-picker', function(input) {
  new DatePicker(input)
})
```

If the library adds elements or listeners outside the input, like a calendar popup in the `<body>`, return a [destructor](#destructor) that calls the library's clean-up method.

To mount components from a frontend framework like React or Vue, see [Framework islands](https://unpoly.com/learn/islands.md).

## Passing data to a compiler {#data}

You may attach data to an element using HTML5 data attributes or encoded as [relaxed JSON](https://unpoly.com/api/relaxed-json.md) in an [`[up-data]`](https://unpoly.com/api/up-data.md) attribute:

```html
<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](https://unpoly.com/api/up.compiler.md) as a second argument:

```js
up.compiler('.user', function(element, data) {
  console.log(data.age)  // result: 31
  console.log(data.name) // result: "Alice"
})
```

See [Attaching data to elements](https://unpoly.com/learn/data.md) for more details and examples.

## Cleaning up after yourself {#destructor}

In Unpoly the JavaScript environment persists through many page navigations. To prevent memory leaks, it is important that any compiler effects can be garbage collected when the element is destroyed.

### Element-local effects require no clean-up {#element-local-effects-require-no-clean-up}

When a compiler binds an event listener to the compiling element (or its descendants), it can be garbage collected once the element leaves the DOM, no further steps required:

```js
// ✔️ Garbage collectable
up.compiler('.click-to-hide', function(element) {
  let hide = () => element.style.display = 'none'
  element.addEventListener('click', hide)
})
```

### Global effects require a destructor function {#global-effects-require-a-destructor-function}

When your compiler registers effects _outside_ the compiling element's subtree, that effect is _not_ cleaned up automatically.

For example, this compiler registers a `scroll` listener on the global `window` object. Every compilation will subscribe another listener that is never removed, causing a memory leak:

```js
// ❌ 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:

```js
// ✔️ Garbage collectable
up.compiler('.scroll-to-hide', function(element) {
  let hide = () => element.style.display = 'none'
  window.addEventListener('scroll', hide)
  return () => window.removeEventListener('scroll', hide)
})
```

This compiler function is now safe for garbage collection.

Destructors run when Unpoly removes the element, e.g. when its fragment is swapped, when it is destroyed with [`up.destroy()`](https://unpoly.com/api/up.destroy.md), or when its overlay closes. When your own code removes an element, use [`up.destroy()`](https://unpoly.com/api/up.destroy.md) so its destructors run.

> #### Important
>
> The destructor function is _not_ expected to remove the element from the DOM.

### Alternative ways to register destructors {#alternative-ways-to-register-destructors}

To run multiple functions when the element is destroyed, return an array of functions:

```js
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()`](https://unpoly.com/api/up.destructor.md). This helps placing the clean-up logic close to the effect that it reverts:

```js
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
>
> Unlike `addEventListener()`, the [`up.on()`](https://unpoly.com/api/up.on.md) function returns a function that unbinds the listener.

## Accessing information about the render pass {#meta}

Compilers may accept a third argument with information about the current [render pass](https://unpoly.com/api/up.render.md):

```js
up.compiler('.user', function(element, data, meta) {
  console.log(meta.layer.mode)   // result: "root"
  console.log(meta.ok)           // result: true
  console.log(meta.revalidating) // result: false
})
```

The following properties are available:

| Property | Type | Description |
| --- | --- | --- |
| `meta.layer` | [`up.Layer`](https://unpoly.com/api/up.Layer.md) | The [layer](https://unpoly.com/api/up.layer.md) of the compiling fragment. This has the same value as [`up.layer.current`](https://unpoly.com/api/up.layer.current.md). |
| `meta.ok` | `boolean` | Whether the element was loaded from a [successful response](https://unpoly.com/learn/error-responses.md#fail-options). |
| `meta.revalidating` | `boolean` | Whether the element was reloaded for the purpose of [cache revalidation](https://unpoly.com/learn/caching.md#revalidation). |

## Defining new attributes with macros {#macros}

A _macro_ is a compiler that runs before all other compilers, registered with [`up.macro()`](https://unpoly.com/api/up.macro.md). This lets a macro set `[up-...]` attributes that will be compiled afterwards. A regular compiler may set such attributes too late, e.g. when the attribute is itself processed by a compiler, like [`[up-poll]`](https://unpoly.com/api/up-poll.md).

Macros are useful to define a shorthand for a combination of attributes that you keep repeating. This macro opens a modal with a [custom animation](https://unpoly.com/api/up.animation.md) `shake`:

```js
up.macro('[shake-modal]', function(link) {
  link.setAttribute('up-layer', 'new modal')
  link.setAttribute('up-animation', 'shake')
})
```

With this macro, links can open a shaking modal with a single attribute:

```html
<a href="/contracts/new" shake-modal>New contract</a>
```

## Compiling elements inserted by other code {#hello}

When you render with Unpoly (by following a link, submitting a form or calling a function like [`up.render()`](https://unpoly.com/api/up.render.md)), new elements are compiled automatically.

When elements are created by other means, e.g. by setting an `innerHTML` property or through a third-party library, pass the new element to [`up.hello()`](https://unpoly.com/api/up.hello.md):

```js
let element = document.createElement('div')
element.innerHTML = '<a href="/path" up-follow>Click me</a>'
up.hello(element)
```

This runs all registered macros and compilers on the element and its subtree. It is safe to call [`up.hello()`](https://unpoly.com/api/up.hello.md) multiple times: every compiler function is guaranteed to run only once for each matching element.

<nav aria-label="Reading order">

- [Previous: Scripting](https://unpoly.com/learn/scripting.md)
- [Next: Attaching data to elements](https://unpoly.com/learn/data.md)

</nav>

