Directives

Overview

Directives in AgnosUI are inspired by the actions in Svelte. These directives are element-level lifecycle functions that are executed when the element is created. They are updated (if an update function is provided) when a parameter changes, and finally, they are executed when the element is destroyed. These directives are typically used to add custom event handlers. For instance, core services like focus track and floating ui create directives that are utilized by components such as Select.

Usage

The usage of this function is dependent on the JavaScript framework (if any) that the application utilizes. For instance, in this guide, we are using simple TypeScript without any specific framework.

How to create a directive

A directive is a function that takes as input an HTMLElement and an optional parameter, for example an object representing a configuration. This function will execute a first time when the directive is applied to an element (usually when the DOM element is created) and either it returns void, or 2 optional functions:

  • update function, called when the context changed and you need either to update the directive configuration or to re-execute partially the original function or modify some internal state of the directive.
  • destroy function, used to remove eventual listeners, subscriptions or any reactive objects created by the directive. This function is usually called when the DOM element on which you applied the directive gets removed from the DOM.
const createSampleDirective: Directive<string, HTMLElement> = (element: HTMLElement, text: string) => {
    console.log('Directive has been executed on node ', element.id);
    const clickListener = (event: Event) => {
        console.log(text, event.target);
    };
    element.addEventListener('click', clickListener);
    return {
        update: (newText) => (text = newText),
        destroy: () => {
            element.removeEventListener('click', clickListener);
            console.log('Destroy function executed ', element.id);
        },
    };
};

This example shows a very simple directive that creates an event listener on click events that happen on the HTML element and then print the parameter text in the console, along with the clicked target.

Both update and destroy functions are provided by the directive, as following:

  • update: gives a way to modify the original text to something new, when the context changes (see in the next section).
  • destroy: remove the previously created listener, so that you don't bloat the main thread with unused listeners.

How to use a directive

Considering this HTML page, in which you have a div container focus-element that includes 2 button, an input text clickText and another button that removes the container.

<div id="content">
    <div id="focus-element">
        <button>button 1</button>
        <button>button 2</button>
    </div>
    <hr />
    <input id="clickText" type="text" />
    <hr />
    <button onclick="document.getElementById('focus-element').remove()">remove dom element</button>
</div>

To use the directive in vanilla TS you need the following steps:

  1. Create the directive on a DOM element. In this case, our elementwill be the focus-element container.
    const trackElement = document.getElementById('focus-element');
    const focusElementDirective = createSampleDirective(trackElement, 'focus-element click');
    When the browser loads, you will get the following log, since the directive executes a first time and it creates its event listener.
    Directive has been executed on node focus-element
    If you click on button 1, the listener triggers clickListener function and you get a log into the console:
    focus element clicked <button>​button 1​</button>​
    Same if you click on button 2, since it also belongs to the element focus-element on which we applied the directive.
    focus element clicked <button>​button 2</button>​

Changing the input does not change anything, because the update function has no bindings and it is never called. To solve it, we need the next step.

  1. Observe context changes and call the update function accordingly. For example, we want to change the text to the value of the clickText input when it changes.
    const input = document.getElementById('clickText');
    input.addEventListener('change', (event) => {
        focusElementDirective?.update(event.target.value);
    });
    Now we bound the directive update function to a change event on the input clickText. Type Update has been called! in the input and then click again on button 1, you get:
    Update has been called! <button>​button 1</button>​
    The directive has been correctly updated according to the context change. The last life-cycle event to implement will be the destroy.
  2. Clean up when the focus-element gets remove from the DOM. This can be done for example using a MutationObserver on the parent DOM element.
    const cleanup = (mutations: MutationRecord[]) => {
        mutations.forEach((mutation) => {
            for (let entry of mutation.removedNodes.entries()) {
                if (entry.includes(trackElement)) {
                    focusElementDirective?.destroy();
                }
            }
        });
    };
    const observer = new MutationObserver(cleanup);
    observer.observe(document.getElementById('content'), {childList: true});
    Click on remove dom element, that remove completely the focus-element container from the DOM. Last log will be printed:
    Destroy function executed  focus-element

Browser-only vs SSR-compatible

Directives are traditionnally run only on the browser. However, you may want to create a directive that updates attributes / class / style of an element. In those cases, being compatible with the framework server-side rendering capabilities is important to avoid any flickering.

In that context, AgnosUI provides the following utilities:

  • browserDirective wraps a browser-only directive to make sure it is not run on the server.
  • createAttributesDirective is a simple directive factory that allows users to update attributes / class / style and bind events. The resulting directive can be run on the server.

Frameworks usage

As seen in the previous section, a Directive is framework agnostic. But the way frameworks expose the DOM element and binds life-cycle events can be very different. Frameworks controls life-cycle events of the DOM elements, in particular initialization, updates and destroy.

The headless libraries of AgnosUI contain adapters to bind directives in the correct way, so that the corresponding events are called at the right moment benefiting from framework features.

Svelte usage

Browser-only directives are natively supported by Svelte, because they inherit the concept from this framework. You can provide it to the use directive, and that's it.

If your app is configured for SSR, you will want to setup our AgnosUI Svelte preprocessor.
It allows to preprocess directives that update atttributes / class /style so that they are applied also server-side. You can find the documention in this README.

Example
Loading...

Advanced

Merge directives

Agnos has a utility mergeDirectives to merge directives into one, with a limitation on the argument: all directives receive the same argument upon initialization and update. Directives are created and updated in the same order as they appear in the arguments list, they are destroyed in the reverse order. All calls to the directives (to create, update and destroy them) are wrapped in a call to the batch function of tansu

Note that it is not mandatory to use mergeDirectives to use multiple directives on the same element as frameworks support using multiple directives on the same element.


PREVIOUS
Slots