From 13c75fede1f0a67bb2aecc02f737ef9112b86e42 Mon Sep 17 00:00:00 2001 From: Jan Andrle Date: Tue, 9 Jan 2024 21:18:43 +0100 Subject: [PATCH] :boom: WIP custom elements docs and types and utils --- docs/index.html | 2 +- docs/p02-elements.html | 2 +- docs/p03-events.html | 2 +- docs/p04-observables.html | 2 +- docs/p05-scopes.html | 4 +-- docs/p06-customElement.html | 34 +++++++++++++++++++ .../examples/customElement/intro.js | 12 +++++++ .../examples/customElement/native-basic.js | 21 ++++++++++++ .../components/mnemonic/customElement-init.js | 22 ++++++++++++ docs_src/p06-customElement.html.js | 32 +++++++++++++++++ docs_src/ssr.js | 1 + index.d.ts | 12 +++++++ observables.d.ts | 2 +- src/customElement.js | 1 - 14 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 docs/p06-customElement.html create mode 100644 docs_src/components/examples/customElement/intro.js create mode 100644 docs_src/components/examples/customElement/native-basic.js create mode 100644 docs_src/components/mnemonic/customElement-init.js create mode 100644 docs_src/p06-customElement.html.js diff --git a/docs/index.html b/docs/index.html index dbaa66f..fe5e309 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,4 +1,4 @@ -`deka-dom-el` — Introduction

`deka-dom-el` — Introduction

Introducing a library.

The library tries to provide pure JavaScript tool(s) to create reactive interfaces.

We start with creating and modifying a static elements and end up with UI templates. From document.createElement to el. Then we go through the native events system and the way to include it declaratively in UI templates. From element.addEventListener to on.

Next step is providing interactivity not only for our UI templates. We introduce observables (O) and how them incorporate to UI templates.

Now we will clarify how the observables are incorporated into our templates with regard to application performance. This is not the only reason the library uses scopes. We will look at how they work in components represented in JavaScript by functions.

import { el } from "https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js"; +`deka-dom-el` — Introduction

`deka-dom-el` — Introduction

Introducing a library.

The library tries to provide pure JavaScript tool(s) to create reactive interfaces.

We start with creating and modifying a static elements and end up with UI templates. From document.createElement to el. Then we go through the native events system and the way to include it declaratively in UI templates. From element.addEventListener to on.

Next step is providing interactivity not only for our UI templates. We introduce observables (O) and how them incorporate to UI templates.

Now we will clarify how the observables are incorporated into our templates with regard to application performance. This is not the only reason the library uses scopes. We will look at how they work in components represented in JavaScript by functions.

import { el } from "https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js"; import { O } from "https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js"; const clicks= O(0); document.body.append( diff --git a/docs/p02-elements.html b/docs/p02-elements.html index ed6b19e..661212c 100644 --- a/docs/p02-elements.html +++ b/docs/p02-elements.html @@ -1,4 +1,4 @@ -`deka-dom-el` — Elements

`deka-dom-el` — Elements

Basic concepts of elements modifications and creations.

Native JavaScript DOM elements creations

Let’s go through all patterns we would like to use and what needs to be improved for better experience.

// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm.js +`deka-dom-el` — Elements

`deka-dom-el` — Elements

Basic concepts of elements modifications and creations.

Native JavaScript DOM elements creations

Let’s go through all patterns we would like to use and what needs to be improved for better experience.

// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm.js import { assign, el, createElement, diff --git a/docs/p03-events.html b/docs/p03-events.html index 55f7ded..cd6c5b4 100644 --- a/docs/p03-events.html +++ b/docs/p03-events.html @@ -1,4 +1,4 @@ -`deka-dom-el` — Events and Addons

`deka-dom-el` — Events and Addons

Using not only events in UI declaratively.

Listenning to the native DOM events and other Addons

We quickly introduce helper to listening to the native DOM events. And library syntax/pattern so-called Addon to incorporate not only this in UI templates declaratively.

// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm.js +`deka-dom-el` — Events and Addons

`deka-dom-el` — Events and Addons

Using not only events in UI declaratively.

Listenning to the native DOM events and other Addons

We quickly introduce helper to listening to the native DOM events. And library syntax/pattern so-called Addon to incorporate not only this in UI templates declaratively.

// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm.js import { on, dispatchEvent } from "deka-dom-el"; /** @type {ddeElementAddon} */ diff --git a/docs/p04-observables.html b/docs/p04-observables.html index ae89d3b..f3fc25b 100644 --- a/docs/p04-observables.html +++ b/docs/p04-observables.html @@ -1,4 +1,4 @@ -`deka-dom-el` — Observables and reactivity

`deka-dom-el` — Observables and reactivity

Handling reactivity in UI via observables.

Using observables to manage reactivity

How a program responds to variable data or user interactions is one of the fundamental problems of programming. If we desire to solve the issue in a declarative manner, observables may be a viable approach.

// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js +`deka-dom-el` — Observables and reactivity

`deka-dom-el` — Observables and reactivity

Handling reactivity in UI via observables.

Using observables to manage reactivity

How a program responds to variable data or user interactions is one of the fundamental problems of programming. If we desire to solve the issue in a declarative manner, observables may be a viable approach.

// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js import { O, observable } from "deka-dom-el/observables"; O===observable /** @type {ddeObservable} */ diff --git a/docs/p05-scopes.html b/docs/p05-scopes.html index a41f796..691a79d 100644 --- a/docs/p05-scopes.html +++ b/docs/p05-scopes.html @@ -1,4 +1,4 @@ -`deka-dom-el` — Scopes and components

`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-observables.js +`deka-dom-el` — Scopes and components

`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-observables.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 "https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js"; @@ -181,4 +181,4 @@ function component(){ * // UNEXPECTEDLY REMOVED!!! * */ } -

# Mnemonic

  • el(<function>, <function-argument(s)>)[.append(...)]: <element-returned-by-function> — using component represented by function
  • scope.host() — get current component reference
  • scope.host(...<addons>) — use addons to current component
\ No newline at end of file +

# Mnemonic

  • el(<function>, <function-argument(s)>)[.append(...)]: <element-returned-by-function> — using component represented by function
  • scope.host() — get current component reference
  • scope.host(...<addons>) — use addons to current component
\ No newline at end of file diff --git a/docs/p06-customElement.html b/docs/p06-customElement.html new file mode 100644 index 0000000..c231a03 --- /dev/null +++ b/docs/p06-customElement.html @@ -0,0 +1,34 @@ +`deka-dom-el` — Custom elements

`deka-dom-el` — Custom elements

Using custom elements in combinantion with DDE

Using custom elements in combinantion with DDE

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

# Custom Elements Introduction

Using custom elements

class CustomHTMLElement extends HTMLElement{ + static tagName = "custom-element"; // just suggestion, we can use `el(CustomHTMLElement.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(CustomHTMLElement.tagName, CustomHTMLElement); +

Handy Custom Elements' Patterns

# Mnemonic

  • customElementRender(<custom-element>, <render-function>[, <properties>]) — use function to render DOM structure for given <custom-element>
  • customElementWithDDE(<custom-element>) — register <custom-element> to DDE library, see also `lifecycleToEvents`, can be also used as decorator
  • observedAttributes(<custom-element>) — returns record of observed attributes (keys uses camelCase)
  • O.observedAttributes(<custom-element>) — returns record of observed attributes (keys uses camelCase and values are observables)
  • lifecycleToEvents(<class-declaration>) — convert lifecycle methods to events, can be also used as decorator
\ No newline at end of file diff --git a/docs_src/components/examples/customElement/intro.js b/docs_src/components/examples/customElement/intro.js new file mode 100644 index 0000000..85d89ca --- /dev/null +++ b/docs_src/components/examples/customElement/intro.js @@ -0,0 +1,12 @@ +// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js +import { + customElementRender, + customElementWithDDE, + observedAttributes, +} from "deka-dom-el"; +/** @type {ddePublicElementTagNameMap} */ +import { O } from "deka-dom-el/observables"; +O.observedAttributes; + +// “internal” utils +import { lifecycleToEvents } from "deka-dom-el"; diff --git a/docs_src/components/examples/customElement/native-basic.js b/docs_src/components/examples/customElement/native-basic.js new file mode 100644 index 0000000..8559c9d --- /dev/null +++ b/docs_src/components/examples/customElement/native-basic.js @@ -0,0 +1,21 @@ +class CustomHTMLElement extends HTMLElement{ + static tagName = "custom-element"; // just suggestion, we can use `el(CustomHTMLElement.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(CustomHTMLElement.tagName, CustomHTMLElement); diff --git a/docs_src/components/mnemonic/customElement-init.js b/docs_src/components/mnemonic/customElement-init.js new file mode 100644 index 0000000..e3da612 --- /dev/null +++ b/docs_src/components/mnemonic/customElement-init.js @@ -0,0 +1,22 @@ +import { el } from "deka-dom-el"; +import { mnemonicUl } from "../mnemonicUl.html.js"; + +export function mnemonic(){ + return mnemonicUl().append( + el("li").append( + el("code", "customElementRender(, [, ])"), " — use function to render DOM structure for given ", + ), + el("li").append( + el("code", "customElementWithDDE()"), " — register to DDE library, see also `lifecycleToEvents`, can be also used as decorator", + ), + el("li").append( + el("code", "observedAttributes()"), " — returns record of observed attributes (keys uses camelCase)", + ), + el("li").append( + el("code", "O.observedAttributes()"), " — returns record of observed attributes (keys uses camelCase and values are observables)", + ), + el("li").append( + el("code", "lifecycleToEvents()"), " — convert lifecycle methods to events, can be also used as decorator", + ) + ); +} diff --git a/docs_src/p06-customElement.html.js b/docs_src/p06-customElement.html.js new file mode 100644 index 0000000..b36f51c --- /dev/null +++ b/docs_src/p06-customElement.html.js @@ -0,0 +1,32 @@ +import { simplePage } from "./layout/simplePage.html.js"; + +import { el } from "deka-dom-el"; +import { example } from "./components/example.html.js"; +import { h3 } from "./components/pageUtils.html.js"; +import { mnemonic } from "./components/mnemonic/customElement-init.js"; +import { code } from "./components/code.html.js"; +/** @param {string} url */ +const fileURL= url=> new URL(url, import.meta.url); + +/** @param {import("./types.d.ts").PageAttrs} attrs */ +export function page({ pkg, info }){ + const page_id= info.id; + return el(simplePage, { info, pkg }).append( + el("h2", "Using custom elements in combinantion with DDE"), + el("p").append( + + ), + el(code, { src: fileURL("./components/examples/customElement/intro.js"), page_id }), + + el(h3, "Custom Elements Introduction"), + el("p").append( + el("a", { textContent: "Using custom elements", href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements", title: "Article about custom elements on MDN" }) + ), + el(code, { src: fileURL("./components/examples/customElement/native-basic.js"), page_id }), + el("p").append( + el("a", { textContent: "Handy Custom Elements' Patterns", href: "https://gist.github.com/WebReflection/ec9f6687842aa385477c4afca625bbf4", title: "Ideas and tips from WebReflection" }) + ), + + el(mnemonic) + ); +} diff --git a/docs_src/ssr.js b/docs_src/ssr.js index 817bbaa..9c430a8 100644 --- a/docs_src/ssr.js +++ b/docs_src/ssr.js @@ -8,6 +8,7 @@ export const pages= [ { id: "p03-events", href: "p03-events", title: "Events and Addons", description: "Using not only events in UI declaratively." }, { id: "p04-observables", href: "p04-observables", title: "Observables and reactivity", description: "Handling reactivity in UI via observables." }, { id: "p05-scopes", href: "p05-scopes", title: "Scopes and components", description: "Organizing UI into components" }, + { id: "p06-customElement", href: "p06-customElement", title: "Custom elements", description: "Using custom elements in combinantion with DDE" }, ]; /** * @typedef registerClientFile diff --git a/index.d.ts b/index.d.ts index a106eff..e99ce12 100644 --- a/index.d.ts +++ b/index.d.ts @@ -175,6 +175,18 @@ export const scope: { pop(): ReturnType["pop"]>, }; +export function customElementRender< + EL extends HTMLElement, + P extends any = Record +>( + custom_element: EL, + render: (props: P)=> SupportedElement, + props?: P | ((...args: any[])=> P) +): EL +export function customElementWithDDE(custom_element: EL): EL +export function lifecycleToEvents(custom_element: EL): EL +export function observedAttributes(custom_element: HTMLElement): Record + /* TypeScript MEH // TODO for SVG */ type ddeAppend= (...nodes: (Node | string)[])=> el; declare global{ diff --git a/observables.d.ts b/observables.d.ts index 2c17223..da4b77a 100644 --- a/observables.d.ts +++ b/observables.d.ts @@ -54,7 +54,7 @@ interface observable{ * */ el(observable: Observable, el: (v: S)=> Element | Element[] | DocumentFragment): DocumentFragment; - attribute(name: string, initial?: string): Observable; + observedAttributes(custom_element: HTMLElement): Record>; } export const observable: observable; export const O: observable; diff --git a/src/customElement.js b/src/customElement.js index a769f85..1267c73 100644 --- a/src/customElement.js +++ b/src/customElement.js @@ -27,7 +27,6 @@ export function lifecycleToEvents(class_declaration){ class_declaration.prototype.__dde_lifecycleToEvents= true; return class_declaration; } -// https://gist.github.com/WebReflection/ec9f6687842aa385477c4afca625bbf4 export { lifecycleToEvents as customElementWithDDE }; function wrapMethod(obj, method, apply){ obj[method]= new Proxy(obj[method] || (()=> {}), { apply });