`deka-dom-el` — Web Components

Using custom elements in combinantion with DDE

Using web components in combinantion with DDE

The DDE library allows for use within Web Components for dom-tree generation. However, in order to be able to use signals (possibly mapping to registered observedAttributes) and additional functionality is (unfortunately) required to use helpers provided by the library.

// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-signals.js import { customElementRender, customElementWithDDE, observedAttributes, } from "deka-dom-el"; /** @type {ddePublicElementTagNameMap} */ import { S } from "deka-dom-el/signals"; S.observedAttributes; // “internal” utils import { lifecyclesToEvents } from "deka-dom-el";

# Custom Elements Introduction

Web Components, specifically Custom Elements, are a set of web platform APIs that allow you to create new HTML tags with custom functionality encapsulated within them. This allows for the creation of reusable components that can be used across web applications.

To start with, let’s see how to use native Custom Elements. As starting point please read Using Custom Elementson MDN. To sum up and for mnemonic see following code overview:

export class HTMLCustomElement extends HTMLElement{ static tagName= "custom-element"; // just suggestion, we can use `el(HTMLCustomElement.tagName)` static observedAttributes= [ "custom-attribute" ]; constructor(){ super(); // nice place to prepare custom element } connectedCallback(){ // nice place to render custom element } attributeChangedCallback(name, oldValue, newValue){ // listen to attribute changes (see `observedAttributes`) } disconnectedCallback(){ // nice place to clean up } // for example, we can mirror get/set prop to attribute get customAttribute(){ return this.getAttribute("custom-attribute"); } set customAttribute(value){ this.setAttribute("custom-attribute", value); } } customElements.define(HTMLCustomElement.tagName, HTMLCustomElement);

For more advanced use of Custom Elements, the summary Handy Custom Elements Patterns may be useful. Especially pay attention to linking HTML attributes and defining setters/getters, this is very helpful to use in combination with the library (el(HTMLCustomElement.tagName, { customAttribute: "new-value" });).

Also see the Life Cycle Events sections, very similarly we would like to use DDE events. To do it, the library provides function customElementWithDDE

import { customElementWithDDE, el, on } from "./esm-with-signals.js"; export class HTMLCustomElement extends HTMLElement{ static tagName= "custom-element"; connectedCallback(){ this.append( el("p", "Hello from custom element!") ); } } customElementWithDDE(HTMLCustomElement); customElements.define(HTMLCustomElement.tagName, HTMLCustomElement); const instance= el(HTMLCustomElement.tagName); on.connected( // preffered e=> console.log("Element connected to the DOM (v1):", e) )(instance); instance.addEventListener( "dde:connected", e=> console.log("Element connected to the DOM (v2):", e) ); document.body.append( instance, );

Custom Elements with DDE

The customElementWithDDE function is only (small) part of the inregration of the library. More important for coexistence is render component function as a body of the Custom Element. For that, you can use customElementRender with arguments instance reference, target for connection, render function and optional properties (will be passed to the render function) see later…

import { customElementRender, customElementWithDDE, } from "./esm-with-signals.js"; export class HTMLCustomElement extends HTMLElement{ static tagName= "custom-element"; static observedAttributes= [ "attr" ]; connectedCallback(){ customElementRender( this.attachShadow({ mode: "open" }), ddeComponent ); } set attr(value){ this.setAttribute("attr", value); } get attr(){ return this.getAttribute("attr"); } } import { el, on, scope } from "./esm-with-signals.js"; function ddeComponent({ attr }){ scope.host( on.connected(e=> console.log(e.target.outerHTML)), ); return el().append( el("p", `Hello from Custom Element with attribute '${attr}'`) ); } customElementWithDDE(HTMLCustomElement); customElements.define(HTMLCustomElement.tagName, HTMLCustomElement); document.body.append( el(HTMLCustomElement.tagName, { attr: "Attribute" }) );

…as you can see, you can use components created based on the documentation previously introduced. To unlock full potential, use with combination customElementWithDDE (allows to use livecycle events) and observedAttributes (converts attributes to render function arguments — default) or S.observedAttributes (converts attributes to signals).

import { customElementRender, customElementWithDDE, observedAttributes, el, on, scope, } from "./esm-with-signals.js"; import { S } from "./esm-with-signals.js"; export class HTMLCustomElement extends HTMLElement{ static tagName= "custom-element"; static observedAttributes= [ "attr" ]; connectedCallback(){ console.log(observedAttributes(this)); customElementRender( this.attachShadow({ mode: "open" }), ddeComponent, S.observedAttributes ); } set attr(value){ this.setAttribute("attr", value); } get attr(){ return this.getAttribute("attr"); } } /** @param {{ attr: ddeSignal<string, {}> }} props */ function ddeComponent({ attr }){ scope.host( on.connected(e=> console.log(( /** @type {HTMLParagraphElement} */ (e.target)).outerHTML)), ); return el().append( el("p", S(()=> `Hello from Custom Element with attribute '${attr()}'`)) ); } customElementWithDDE(HTMLCustomElement); customElements.define(HTMLCustomElement.tagName, HTMLCustomElement); document.body.append( el(HTMLCustomElement.tagName, { attr: "Attribute" }) ); setTimeout( ()=> document.querySelector(HTMLCustomElement.tagName).setAttribute("attr", "New Value"), 3*750 );

# Shadow DOM

Shadow DOM is a web platform feature that allows for the encapsulation of a component’s internal DOM tree from the rest of the document. This means that styles and scripts applied to the document will not affect the component’s internal DOM, and vice versa.

import { el, customElementRender, customElementWithDDE, } from "./esm-with-signals.js"; function ddeComponent(){ return el().append( el("style", ` .red{ color: firebrick; } `), el("p", { className: "red" }).append( "Hello from ", el("slot", "Custom Element"), "!" ) ); } export class A extends HTMLElement{ static tagName= "custom-element-without"; connectedCallback(){ customElementRender(this, ddeComponent); } } customElementWithDDE(A); customElements.define(A.tagName, A); export class B extends HTMLElement{ static tagName= "custom-element-open"; connectedCallback(){ customElementRender( this.attachShadow({ mode: "open" }), ddeComponent ); } } customElementWithDDE(B); customElements.define(B.tagName, B); export class C extends HTMLElement{ static tagName= "custom-element-closed"; connectedCallback(){ customElementRender( this.attachShadow({ mode: "closed" }), ddeComponent ); } } customElementWithDDE(C); customElements.define(C.tagName, C); document.body.append( el(A.tagName).append("Without shadowRoot"), el("hr"), el(B.tagName).append("Open shadowRoot"), el("hr"), el(C.tagName).append("Closed shadowRoot"), el("style", ` .red{ color: crimson; } `), ); console.log(A.tagName, "expect modifications"); document.body.querySelector(A.tagName).querySelector("p").textContent+= " (editable with JS)"; console.log(B.tagName, "expect modifications"); document.body.querySelector(B.tagName).shadowRoot.querySelector("p").textContent+= " (editable with JS)"; console.log(C.tagName, "expect error ↓"); document.body.querySelector(C.tagName).querySelector("p").textContent+= " (editable with JS)";

Regarding to this.attachShadow({ mode: 'open' }) see quick overview Using Shadow DOM. An another source of information can be Shadow DOM in Depth.

Besides the encapsulation, the Shadow DOM allows for using the <slot>element(s). You can simulate this feature using simulateSlots:

import { customElementRender, customElementWithDDE, el, simulateSlots } from "./esm-with-signals.js"; export class HTMLCustomElement extends HTMLElement{ static tagName= "custom-slotting"; connectedCallback(){ const c= ()=> simulateSlots(this, ddeComponent()); customElementRender(this, c); } } customElementWithDDE(HTMLCustomElement); customElements.define(HTMLCustomElement.tagName, HTMLCustomElement); document.body.append( el(HTMLCustomElement.tagName), el(HTMLCustomElement.tagName).append( "Slot" ), el(ddeComponentSlot), el(ddeComponentSlot).append( "Slot" ), ); function ddeComponent(){ return el().append( el("p").append( "Hello ", el("slot", "World") ) ); } function ddeComponentSlot(){ return simulateSlots(el().append( el("p").append( "Hello ", el("slot", "World") ) )); }

To sum up:

# Mnemonic

  • customElementRender(<connect-target>, <render-function>[, <properties>]) — use function to render DOM structure for given custom element (or its Shadow DOM)
  • customElementWithDDE(<custom-element>) — register <custom-element> to DDE library, see also `lifecyclesToEvents`, can be also used as decorator
  • observedAttributes(<custom-element>) — returns record of observed attributes (keys uses camelCase)
  • S.observedAttributes(<custom-element>) — returns record of observed attributes (keys uses camelCase and values are signals)
  • lifecyclesToEvents(<class-declaration>) — convert lifecycle methods to events, can be also used as decorator
  • simulateSlots(<class-instance>, <body>) — simulate slots for Custom Elements without shadow DOM
  • simulateSlots(<dde-component>[, <mapper>]) — simulate slots for “dde”/functional components