From 25ae6d44feb680ee7a669211619e254d229f0155 Mon Sep 17 00:00:00 2001 From: Jan Andrle Date: Fri, 22 Nov 2024 10:31:39 +0100 Subject: [PATCH] :zap: Build Docs --- bs/docs.js | 2 +- docs/global.css | 6 ++---- docs/index.html | 2 +- docs/p02-elements.html | 14 +++++++------- docs/p03-events.html | 10 +++++----- docs/p04-signals.html | 12 ++++++------ docs/p05-scopes.html | 6 +++--- docs/p06-customElement.html | 10 +++++----- 8 files changed, 30 insertions(+), 32 deletions(-) diff --git a/bs/docs.js b/bs/docs.js index 9836d29..ea67131 100755 --- a/bs/docs.js +++ b/bs/docs.js @@ -2,7 +2,7 @@ /* jshint esversion: 11,-W097, -W040, module: true, node: true, expr: true, undef: true *//* global echo, $, pipe, s, fetch, cyclicLoop */ echo("Building static documentation files…"); echo("Preparing…"); -import { path_target, pages as pages_registered, styles, dispatchEvent } from "../docs_src/ssr.js"; +import { path_target, pages as pages_registered, styles, dispatchEvent, t } from "../docs_src/ssr.js"; import { createHTMl } from "./docs/jsdom.js"; import { register } from "../jsdom.js"; const pkg= s.cat("package.json").xargs(JSON.parse); diff --git a/docs/global.css b/docs/global.css index 7842668..a658cc0 100644 --- a/docs/global.css +++ b/docs/global.css @@ -145,16 +145,14 @@ main > *{ --shiki-token-punctuation: var(--code); --shiki-token-link: #EE0000; white-space: pre; - - tab-size: 2; + tab-size: 2; /* TODO: allow custom tab size?! */ overflow: scroll; } .code[data-js=todo]{ border: 1px solid var(--border); border-radius: var(--standard-border-radius); margin-bottom: 1rem; - - margin-top: 18.4px; + margin-top: 18.4px; /* to fix shift when → dataJS=done */ padding: 1rem 1.4rem; } .example{ diff --git a/docs/index.html b/docs/index.html index e50d02b..ac682d9 100644 --- a/docs/index.html +++ b/docs/index.html @@ -19,4 +19,4 @@ document.body.append( }) ) ); -

The library introduces a new “type” of variable/constant called signal allowing us to to use introduced 3PS pattern in our applications. As you can see it in the example above.

Also please notice that there is very similar 3PS pattern used for separate creation of UI and business logic.

The 3PS is very simplified definition of the pattern. There are more deep/academic definitions more precisely describe usage in specific situations, see for example MVVM or MVC.

# Organization of the documentation

\ No newline at end of file +

The library introduces a new “type” of variable/constant called signal allowing us to to use introduced 3PS pattern in our applications. As you can see it in the example above.

Also please notice that there is very similar 3PS pattern used for separate creation of UI and business logic.

The 3PS is very simplified definition of the pattern. There are more deep/academic definitions more precisely describe usage in specific situations, see for example MVVM or MVC.

# Organization of the documentation

\ No newline at end of file diff --git a/docs/p02-elements.html b/docs/p02-elements.html index 62b07d5..9b225bc 100644 --- a/docs/p02-elements.html +++ b/docs/p02-elements.html @@ -26,7 +26,7 @@ document.body.append( { textContent: "Element’s text content.", style: "color: coral;" } ) ); -

To make this easier, you can use the el function. Internally in basic examples, it is wrapper around assign(document.createElement(…), { … }).

import { el, assign } from "./esm-with-signals.js"; +

To make this easier, you can use the el function. Internally in basic examples, it is wrapper around assign(document.createElement(…), { … }).

import { el, assign } from "./esm-with-signals.js"; const color= "lightcoral"; document.body.append( el("p", { textContent: "Hello (first time)", style: { color } }) @@ -37,7 +37,7 @@ document.body.append( { textContent: "Hello (second time)", style: { color } } ) ); -

The assign function provides improved behaviour of Object.assign(). You can declaratively sets any IDL and attribute of the given element. Function prefers IDL and fallback to the element.setAttribute if there is no writable property in the element prototype.

You can study all JavaScript elements interfaces to the corresponding HTML elements. All HTML elements inherits from HTMLElement. To see all available IDLs for example for paragraphs, see HTMLParagraphElement. Moreover, the assign provides a way to sets declaratively some convenient properties:

For processing, the assign internally uses assignAttribute and classListDeclarative.

import { assign, assignAttribute, classListDeclarative } from "./esm-with-signals.js"; +

The assign function provides improved behaviour of Object.assign(). You can declaratively sets any IDL and attribute of the given element. Function prefers IDL and fallback to the element.setAttribute if there is no writable property in the element prototype.

You can study all JavaScript elements interfaces to the corresponding HTML elements. All HTML elements inherits from HTMLElement. To see all available IDLs for example for paragraphs, see HTMLParagraphElement. Moreover, the assign provides a way to sets declaratively some convenient properties:

For processing, the assign internally uses assignAttribute and classListDeclarative.

import { assign, assignAttribute, classListDeclarative } from "./esm-with-signals.js"; const paragraph= document.createElement("p"); assignAttribute(paragraph, "textContent", "Hello, world!"); @@ -70,7 +70,7 @@ console.log("paragraph.something=", paragraph.something); document.body.append( paragraph ); -

# Native JavaScript templating

By default, the native JS has no good way to define HTML template using DOM API:

document.body.append( +

# Native JavaScript templating

By default, the native JS has no good way to define HTML template using DOM API:

document.body.append( document.createElement("div"), document.createElement("span"), document.createElement("main") @@ -81,7 +81,7 @@ const template= document.createElement("main").append( document.createElement("span"), ); console.log(typeof template==="undefined"); -

This library therefore overwrites the append method of created elements to always return parent element.

import { el } from "./esm-with-signals.js"; +

This library therefore overwrites the append method of created elements to always return parent element.

import { el } from "./esm-with-signals.js"; document.head.append( el("style").append( "tr, td{ border: 1px solid red; padding: 1em; }", @@ -116,7 +116,7 @@ document.body.append( ) ) ); -

# Basic (state-less) components

You can use functions for encapsulation (repeating) logic. The el accepts function as first argument. In that case, the function should return dom elements and the second argument for el is argument for given element.

import { el } from "./esm-with-signals.js"; +

# Basic (state-less) components

You can use functions for encapsulation (repeating) logic. The el accepts function as first argument. In that case, the function should return dom elements and the second argument for el is argument for given element.

import { el } from "./esm-with-signals.js"; document.head.append( el("style").append( ".class1{ font-weight: bold; }", @@ -133,7 +133,7 @@ function component({ className, textContent }){ el("p", textContent) ); } -

As you can see, in case of state-less/basic components there is no difference between calling component function directly or using el function.

It is nice to use similar naming convention as native DOM API. This allows us to use the destructuring assignment syntax and keep track of the native API (things are best remembered through regular use).

# Creating non-HTML elements

Similarly to the native DOM API (document.createElementNS) for non-HTML elements we need to tell JavaScript which kind of the element to create. We can use the elNS function:

import { elNS, assign } from "./esm-with-signals.js"; +

As you can see, in case of state-less/basic components there is no difference between calling component function directly or using el function.

It is nice to use similar naming convention as native DOM API. This allows us to use the destructuring assignment syntax and keep track of the native API (things are best remembered through regular use).

# Creating non-HTML elements

Similarly to the native DOM API (document.createElementNS) for non-HTML elements we need to tell JavaScript which kind of the element to create. We can use the elNS function:

import { elNS, assign } from "./esm-with-signals.js"; const elSVG= elNS("http://www.w3.org/2000/svg"); const elMath= elNS("http://www.w3.org/1998/Math/MathML"); document.body.append( @@ -144,4 +144,4 @@ document.body.append( console.log( document.body.innerHTML.includes("<svg></svg><math></math>") ) -

# Mnemonic

\ No newline at end of file +

# Mnemonic

\ No newline at end of file diff --git a/docs/p03-events.html b/docs/p03-events.html index 244b215..d72dca4 100644 --- a/docs/p03-events.html +++ b/docs/p03-events.html @@ -12,7 +12,7 @@ on("click", log("`on`"), { once: true })(button); document.body.append( button ); -

…this is actually one of the two differences. The another one is that on accepts only object as the options (but it is still optional).

The other difference is that there is no off function. You can remove listener declaratively using AbortSignal:

import { el, on } from "./esm-with-signals.js"; +

…this is actually one of the two differences. The another one is that on accepts only object as the options (but it is still optional).

The other difference is that there is no off function. You can remove listener declaratively using AbortSignal:

import { el, on } from "./esm-with-signals.js"; const log= mark=> console.log.bind(console, mark); const abort_controller= new AbortController(); @@ -25,7 +25,7 @@ on("click", log("`on`"), { signal })(button); document.body.append( button, " ", el("button", { textContent: "Off", onclick: ()=> abort_controller.abort() }) ); -

So, there are (typically) three ways to handle events. You can use:

In the first example we force to use HTML attribute (it corresponds to <button onclick="console.log(event)">click me</button>). Side note: this can be useful in case of SSR. To study difference, you can read a nice summary here: GIST @WebReflection/web_events.md.

# Addons

From practical point of view, Addons are just functions that accept any HTML element as their first parameter. You can see that the on(…) fullfills this requirement.

You can use Addons as ≥3rd argument of el function. This way is possible to extends your templates by additional (3rd party) functionalities. But for now mainly, you can add events listeners:

import { el, on } from "./esm-with-signals.js"; +

So, there are (typically) three ways to handle events. You can use:

In the first example we force to use HTML attribute (it corresponds to <button onclick="console.log(event)">click me</button>). Side note: this can be useful in case of SSR. To study difference, you can read a nice summary here: GIST @WebReflection/web_events.md.

# Addons

From practical point of view, Addons are just functions that accept any HTML element as their first parameter. You can see that the on(…) fullfills this requirement.

You can use Addons as ≥3rd argument of el function. This way is possible to extends your templates by additional (3rd party) functionalities. But for now mainly, you can add events listeners:

import { el, on } from "./esm-with-signals.js"; const abort_controller= new AbortController(); const { signal }= abort_controller; /** @type {ddeElementAddon<HTMLButtonElement>} */ @@ -49,7 +49,7 @@ function update(event){ "\n" ); } -

As the example shows, you can also provide types in JSDoc+TypeScript by using global type ddeElementAddon. Also notice, you can use Addons to get element reference.

# Life-cycle events

Addons are called immediately when the element is created, even it is not connected to live DOM yet. Therefore, you can understand the Addon to be “oncreate” event.

The library provide three additional live-cycle events corresponding to how they are named in a case of custom elements: on.connected, on.disconnected and on.attributeChanged.

import { el, on } from "./esm-with-signals.js"; +

As the example shows, you can also provide types in JSDoc+TypeScript by using global type ddeElementAddon. Also notice, you can use Addons to get element reference.

# Life-cycle events

Addons are called immediately when the element is created, even it is not connected to live DOM yet. Therefore, you can understand the Addon to be “oncreate” event.

The library provide three additional live-cycle events corresponding to how they are named in a case of custom elements: on.connected, on.disconnected and on.attributeChanged.

import { el, on } from "./esm-with-signals.js"; const paragraph= el("p", "See live-cycle events in console.", el=> log({ type: "dde:created", detail: el }), on.connected(log), @@ -67,7 +67,7 @@ document.body.append( function log({ type, detail }){ console.log({ _this: this, type, detail }); } -

For Custom elements, we will later introduce a way to replace *Callback syntax with dde:* events. The on.* functions then listen to the appropriate Custom Elements events (see Custom element lifecycle callbacks | MDN).

But, in case of regular elemnets the MutationObserver | MDN is internaly used to track these events. Therefore, there are some drawbacks:

To provide intuitive behaviour, similar also to how the life-cycle events works in other frameworks/libraries, deka-dom-el library ensures that on.connected and on.disconnected are called only once and only when the element, is (dis)connected to live DOM. The solution is inspired by Vue. For using native behaviour re-(dis)connecting element, use:

# Final notes

The library also provides a method to dispatch the events.

import { el, on, dispatchEvent } from "./esm-with-signals.js"; +

For Custom elements, we will later introduce a way to replace *Callback syntax with dde:* events. The on.* functions then listen to the appropriate Custom Elements events (see Custom element lifecycle callbacks | MDN).

But, in case of regular elemnets the MutationObserver | MDN is internaly used to track these events. Therefore, there are some drawbacks:

To provide intuitive behaviour, similar also to how the life-cycle events works in other frameworks/libraries, deka-dom-el library ensures that on.connected and on.disconnected are called only once and only when the element, is (dis)connected to live DOM. The solution is inspired by Vue. For using native behaviour re-(dis)connecting element, use:

# Final notes

The library also provides a method to dispatch the events.

import { el, on, dispatchEvent } from "./esm-with-signals.js"; document.body.append( el("p", "Listenning to `test` event.", on("test", console.log)).append( el("br"), @@ -91,4 +91,4 @@ function dde(){ function ddeOptions(){ dispatchEvent("test", { bubbles: true })(this, "hi"); } -

# Mnemonic

\ No newline at end of file +

# Mnemonic

\ No newline at end of file diff --git a/docs/p04-signals.html b/docs/p04-signals.html index 710914d..35cf3f3 100644 --- a/docs/p04-signals.html +++ b/docs/p04-signals.html @@ -16,7 +16,7 @@ update(); const interval= 5*1000; setTimeout(clearInterval, 10*interval, setInterval(update, interval)); -

All this is just an example of Event-driven programming and Publish–subscribe pattern (compare for example with fpubsub library). All three parts can be in some manner independent and still connected to the same reactive entity.

Signals are implemented in the library as functions. To see current value of signal, just call it without any arguments console.log(signal()). To update the signal value, pass any argument signal('a new value'). For listenning the signal value changes, use S.on(signal, console.log).

Similarly to the on function to register DOM events listener. You can use AbortController/AbortSignal to off/stop listenning. In example, you also found the way for representing “live” piece of code computation pattern (derived signal):

import { S } from "./esm-with-signals.js"; +

All this is just an example of Event-driven programming and Publish–subscribe pattern (compare for example with fpubsub library). All three parts can be in some manner independent and still connected to the same reactive entity.

Signals are implemented in the library as functions. To see current value of signal, just call it without any arguments console.log(signal()). To update the signal value, pass any argument signal('a new value'). For listenning the signal value changes, use S.on(signal, console.log).

Similarly to the on function to register DOM events listener. You can use AbortController/AbortSignal to off/stop listenning. In example, you also found the way for representing “live” piece of code computation pattern (derived signal):

import { S } from "./esm-with-signals.js"; const signal= S(0); // computation pattern const double= S(()=> 2*signal()); @@ -32,7 +32,7 @@ ac.signal.addEventListener("abort", ()=> setTimeout(()=> clearInterval(id), 2*interval)); setTimeout(()=> ac.abort(), 3*interval) -

# Signals and actions

S(/* primitive */) allows you to declare simple reactive variables, typically, around immutable primitive types. However, it may also be necessary to use reactive arrays, objects, or other complex reactive structures.

import { S } from "./esm-with-signals.js"; +

# Signals and actions

S(/* primitive */) allows you to declare simple reactive variables, typically, around immutable primitive types. However, it may also be necessary to use reactive arrays, objects, or other complex reactive structures.

import { S } from "./esm-with-signals.js"; const signal= S(0, { increaseOnlyOdd(add){ console.info(add); @@ -50,7 +50,7 @@ setTimeout( 10*interval, setInterval(oninterval, interval) ); -

…but typical user-case is object/array (maps, sets and other mutable objects):

import { S } from "./esm-with-signals.js"; +

…but typical user-case is object/array (maps, sets and other mutable objects):

import { S } from "./esm-with-signals.js"; const todos= S([], { push(item){ this.value.push(S(item)); @@ -106,7 +106,7 @@ function radio({ textContent, checked= false }){ " ",textContent ) } -

In some way, you can compare it with useReducer hook from React. So, the S(<data>, <actions>) pattern creates a store “machine”. We can then invoke (dispatch) registered action by calling S.action(<signal>, <name>, ...<args>) after the action call the signal calls all its listeners. This can be stopped by calling this.stopPropagation() in the method representing the given action. As it can be seen in examples, the “store” value is available also in the function for given action (this.value).

# Reactive DOM attributes and elements

There are on basic level two distinc situation to mirror dynamic value into the DOM/UI

  1. to change some attribute(s) of existing element(s)
  2. to generate elements itself dynamically – this covers conditions and loops
import { S } from "./esm-with-signals.js"; +

In some way, you can compare it with useReducer hook from React. So, the S(<data>, <actions>) pattern creates a store “machine”. We can then invoke (dispatch) registered action by calling S.action(<signal>, <name>, ...<args>) after the action call the signal calls all its listeners. This can be stopped by calling this.stopPropagation() in the method representing the given action. As it can be seen in examples, the “store” value is available also in the function for given action (this.value).

# Reactive DOM attributes and elements

There are on basic level two distinc situation to mirror dynamic value into the DOM/UI

  1. to change some attribute(s) of existing element(s)
  2. to generate elements itself dynamically – this covers conditions and loops
import { S } from "./esm-with-signals.js"; const count= S(0); import { el } from "./esm-with-signals.js"; @@ -121,7 +121,7 @@ document.head.append( const interval= 5 * 1000; setTimeout(clearInterval, 10*interval, setInterval(()=> count(count()+1), interval)); -

To derived attribute based on value of signal variable just use the signal as a value of the attribute (assign(element, { attribute: S('value') })). assign/el provides ways to glue reactive attributes/classes more granularly into the DOM. Just use dedicated build-in attributes dataset, ariaset and classList.

For computation, you can use the “derived signal” (see above) like assign(element, { textContent: S(()=> 'Hello '+WorldSignal()) }). This is read-only signal its value is computed based on given function and updated when any signal used in the function changes.

To represent part of the template filled dynamically based on the signal value use S.el(signal, DOMgenerator). This was already used in the todo example above or see:

import { S } from "./esm-with-signals.js"; +

To derived attribute based on value of signal variable just use the signal as a value of the attribute (assign(element, { attribute: S('value') })). assign/el provides ways to glue reactive attributes/classes more granularly into the DOM. Just use dedicated build-in attributes dataset, ariaset and classList.

For computation, you can use the “derived signal” (see above) like assign(element, { textContent: S(()=> 'Hello '+WorldSignal()) }). This is read-only signal its value is computed based on given function and updated when any signal used in the function changes.

To represent part of the template filled dynamically based on the signal value use S.el(signal, DOMgenerator). This was already used in the todo example above or see:

import { S } from "./esm-with-signals.js"; const count= S(0, { add(){ this.value= this.value + Math.round(Math.random()*10); } }); @@ -147,4 +147,4 @@ setTimeout(clearInterval, 10*interval, setInterval(function(){ S.action(count, "add"); S.action(numbers, "push", count()); }, interval)); -

# Mnemonic

\ No newline at end of file +

# Mnemonic

\ No newline at end of file diff --git a/docs/p05-scopes.html b/docs/p05-scopes.html index ab43f49..8bfb5e0 100644 --- a/docs/p05-scopes.html +++ b/docs/p05-scopes.html @@ -34,7 +34,7 @@ function component(){ el("strong", "Component") ); } -

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

import { el } from "./esm-with-signals.js"; +

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; @@ -78,7 +78,7 @@ function elClass(_class, attributes, ...addons){ 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"; +

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) ); @@ -114,7 +114,7 @@ function component(){ }); 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!!! */ +

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(){ diff --git a/docs/p06-customElement.html b/docs/p06-customElement.html index f37eabe..ed6737e 100644 --- a/docs/p06-customElement.html +++ b/docs/p06-customElement.html @@ -54,7 +54,7 @@ instance.addEventListener( 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 { +

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"; @@ -87,7 +87,7 @@ 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 { +

…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, @@ -129,7 +129,7 @@ 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 { +

# 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, @@ -198,7 +198,7 @@ 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 { +

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, @@ -239,4 +239,4 @@ function ddeComponentSlot(){ ) )); } -

To sum up:

# Mnemonic

\ No newline at end of file +

To sum up:

# Mnemonic

\ No newline at end of file