`deka-dom-el` — Scopes and components

Organizing UI into components

Using functions as UI components

For state-less components we can use functions as UI components (see “Elements” page). But in real life, we may need to handle the component live-cycle and provide JavaScript the way to properly use the Garbage collection.

// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-signals.js import { scope, el } from "deka-dom-el"; /** @type {ddeElementAddon} */

The library therefore use scopes to provide these functionalities.

# Scopes and hosts

The host is the name for the element representing the component. This is typically element returned by function. To get reference, you can use scope.host() to applly addons just use scope.host(...<addons>).

import { el, on, scope } from "./esm-with-signals.js"; const { host }= scope; host( element=> console.log( "This represents Addon/oninit for root", element.outerHTML ) ); console.log( "This represents the reference to the host element of root", host().outerHTML ); document.body.append( el(component) ); function component(){ const { host }= scope; host( element=> console.log( "This represents Addon/oninit for the component", element.outerHTML ) ); const onclick= on("click", function(ev){ console.log( "This represents the reference to the host element of the component", host().outerHTML ); }) return el("div", null, onclick).append( el("strong", "Component") ); }

To better understanding we implement function elClass helping to create component as class instances.

import { el } from "./esm-with-signals.js"; class Test { constructor(params){ this._params= params; } render(){ return el("div").append( this._params.textContent ); } } document.body.append( elClass(Test, { textContent: "Hello World" }) ); import { chainableAppend, scope } from "./esm-with-signals.js"; function elClass(_class, attributes, ...addons){ let element, element_host; scope.push({ scope: _class, //just informative purposes host: (...addons_append)=> addons_append.length ? ( !element ? addons.unshift(...addons_append) : addons_append.forEach(c=> c(element_host)) , undefined) : element_host }); const instance= new _class(attributes); element= instance.render(); const is_fragment= element instanceof DocumentFragment; const el_mark= el.mark({ //this creates html comment `<dde:mark …/>` type: "class-component", name: _class.name, host: is_fragment ? "this" : "parentElement", }); element.prepend(el_mark); if(is_fragment) element_host= el_mark; chainableAppend(element); addons.forEach(c=> c(element_host)); scope.pop(); return element; }

As you can see, the scope.host() is stored temporarily and synchronously. Therefore, at least in the beginning of using library, it is the good practise to store host in the root of your component. As it may be changed, typically when there is asynchronous code in the component.

import { el, scope, on, dispatchEvent } from "deka-dom-el"; document.body.append( el(component) ); function component(){ const { host }= scope; // good practise! host( console.log, on("click", function redispatch(){ // this `host` ↘ still corresponds to the host ↖ of the component dispatchEvent("redispatch")(host()); }) ); // this `host` ↘ still corresponds to the host ↖ of the component setTimeout(()=> dispatchEvent("timeout")(host()), 750) return el("p", "Clickable paragraph!"); }

# Scopes, signals and cleaning magic

The host is internally used to register the cleaning procedure, when the component (host element) is removed from the DOM.

import { el, empty, on } from "./esm-with-signals.js"; document.body.append( el(component), el("button", { textContent: "Remove", onclick: ()=> empty(document.body), type: "button" }) ); import { S } from "./esm-with-signals.js"; function component(){ const textContent= S("Click to change text."); const onclickChange= on("click", function redispatch(){ textContent("Text changed! "+(new Date()).toString()) }); return el("p", textContent, onclickChange); }

The text content of the paragraph is changing when the value of the signal textContent is changed. Internally, there is association between textContent and the paragraph similar to using S.on(textContent, /* update the paragraph */).

This listener must be removed when the component is removed from the DOM. To do it, the library assign internally on.disconnected(/* remove the listener */)(host()) to the host element.

The library DOM API and signals works ideally when used declaratively. It means, you split your app logic into three parts as it was itroduced in Signals.

/* PSEUDO-CODE!!! */ import { el } from "deka-dom-el"; import { S } from "deka-dom-el/signals"; function component(){ /* prepare changeable data */ const dataA= S("data"); const dataB= S("data"); /* define data flow (can be asynchronous) */ fetchAPI().then(data_new=> dataA(data_new)); setTimeout(()=> dataB("DATA")); /* declarative UI */ return el().append( el("h1", { textContent: "Example", /* declarative attribute(s) */ classList: { declarative: dataB } }), el("ul").append( /* declarative element(s) */ S.el(dataA, data=> data.map(d=> el("li", d))) ), el("ul").append( /* declarative component(s) */ S.el(dataA, data=> data.map(d=> el(subcomponent, d))) ) ); } function subcomponent({ id }){ /* prepare changeable data */ const textContent= S("…"); /* define data flow (can be asynchronous) */ fetchAPI(id).then(text=> textContent(text)); /* declarative UI */ return el("li", { textContent, dataId: id }); }

Strictly speaking, the imperative way of using the library is not prohibited. Just be careful (rather avoid) mixing declarative approach (using signals) and imperative manipulation of elements.

/* PSEUDO-CODE!!! */ import { el, on, scope } from "deka-dom-el"; function component(){ const { host }= scope; const ul= el("ul"); const ac= new AbortController(); fetchAPI({ signal: ac.signal }).then(data=> { data.forEach(d=> ul.append(el("li", d))); }); host( /* element was remove before data fetched */ on.disconnected(()=> ac.abort()) ); return ul; /** * NEVER EVER!! * let data; * fetchAPI().then(d=> data= O(d)); * * OR NEVER EVER!! * const ul= el("ul"); * fetchAPI().then(d=> { * const data= O("data"); * ul.append(el("li", data)); * }); * * // THE HOST IS PROBABLY DIFFERENT THAN * // YOU EXPECT AND OBSERVABLES MAY BE * // UNEXPECTEDLY REMOVED!!! * */ }

# Mnemonic