diff --git a/docs/p05-scopes.html b/docs/p05-scopes.html index 2ab2fa7..cc87757 100644 --- a/docs/p05-scopes.html +++ b/docs/p05-scopes.html @@ -1,5 +1,5 @@ `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, on } from "deka-dom-el"; +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"; const { host }= scope; @@ -34,7 +34,7 @@ function component(){ el("strong", "Component") ); } -

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

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

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

import { el } from "https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js"; class Test { constructor(params){ this._params= params; @@ -49,22 +49,25 @@ document.body.append( elClass(Test, { textContent: "Hello World" }) ); -function elClass(c, props, ...addons){ +import { chainableAppend, scope } from "https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js"; +function elClass(_class, attributes, ...addons){ let element, element_host; scope.push({ - scope: c, //just informative purposes - host: (...c)=> c.length - ? (!element - ? addons.unshift(...c) - : c.forEach(c=> c(element_host)), undefined) - : element_host + 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 C= new c(props); - element= C.render(); - const is_fragment= el instanceof DocumentFragment; + 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: C.name, + name: _class.name, host: is_fragment ? "this" : "parentElement", }); element.prepend(el_mark); @@ -75,4 +78,106 @@ function elClass(c, props, ...addons){ scope.pop(); return element; } -

# Mnemonic

\ No newline at end of file +

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, observables 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 } from "https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js"; +document.body.append( + el(component), + el("button", { + textContent: "Remove", + onclick: ()=> empty(document.body), + type: "button" + }) +); +import { on } 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"; +function component(){ + const textContent= O("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 observable 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.disconnect(/* remove the listener */)(host()) to the host element.

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

/* PSEUDO-CODE!!! */ +import { el } from "deka-dom-el"; +import { O } from "deka-dom-el/observables"; +function component(){ + /* prepare changeable data */ + const dataA= O("data"); + const dataB= O("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) */ + O.el(dataA, data=> data.map(d=> el("li", d))) + ), + el("ul").append( + /* declarative component(s) */ + O.el(dataA, data=> data.map(d=> el(subcomponent, d))) + ) + ); +} +function subcomponent({ id }){ + /* prepare changeable data */ + const textContent= O("…"); + /* 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 observables) and imperative manipulation of elements.

/* PSEUDO-CODE!!! */ +import { el, on, scope } from "deka-dom-el"; +function component(){ + const ul= el("ul"); + const ac= new AbortController(); + fetchAPI({ signal: ac.signal }).then(data=> { + data.forEach(d=> ul.append(el("li", d))); + }); + scope.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

\ No newline at end of file diff --git a/docs_src/components/examples/scopes/class-component.js b/docs_src/components/examples/scopes/class-component.js index 5e88740..45a1bfd 100644 --- a/docs_src/components/examples/scopes/class-component.js +++ b/docs_src/components/examples/scopes/class-component.js @@ -1,4 +1,4 @@ -import { chainableAppend, el, scope } from "deka-dom-el"; +import { el } from "deka-dom-el"; class Test { constructor(params){ this._params= params; @@ -13,22 +13,25 @@ document.body.append( elClass(Test, { textContent: "Hello World" }) ); -function elClass(c, props, ...addons){ +import { chainableAppend, scope } from "deka-dom-el"; +function elClass(_class, attributes, ...addons){ let element, element_host; scope.push({ - scope: c, //just informative purposes - host: (...c)=> c.length - ? (!element - ? addons.unshift(...c) - : c.forEach(c=> c(element_host)), undefined) - : element_host + 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 C= new c(props); - element= C.render(); - const is_fragment= el instanceof DocumentFragment; + const instance= new _class(attributes); + element= instance.render(); + const is_fragment= element instanceof DocumentFragment; const el_mark= el.mark({ //this creates html comment `` type: "class-component", - name: C.name, + name: _class.name, host: is_fragment ? "this" : "parentElement", }); element.prepend(el_mark); diff --git a/docs_src/components/examples/scopes/cleaning.js b/docs_src/components/examples/scopes/cleaning.js new file mode 100644 index 0000000..7b0e598 --- /dev/null +++ b/docs_src/components/examples/scopes/cleaning.js @@ -0,0 +1,19 @@ +import { el, empty } from "deka-dom-el"; +document.body.append( + el(component), + el("button", { + textContent: "Remove", + onclick: ()=> empty(document.body), + type: "button" + }) +); +import { on } from "deka-dom-el"; +import { O } from "deka-dom-el/observables"; +function component(){ + const textContent= O("Click to change text."); + + const onclickChange= on("click", function redispatch(){ + textContent("Text changed! "+(new Date()).toString()) + }); + return el("p", textContent, onclickChange); +} diff --git a/docs_src/components/examples/scopes/declarative.js b/docs_src/components/examples/scopes/declarative.js new file mode 100644 index 0000000..46716ad --- /dev/null +++ b/docs_src/components/examples/scopes/declarative.js @@ -0,0 +1,35 @@ +/* PSEUDO-CODE!!! */ +import { el } from "deka-dom-el"; +import { O } from "deka-dom-el/observables"; +function component(){ + /* prepare changeable data */ + const dataA= O("data"); + const dataB= O("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) */ + O.el(dataA, data=> data.map(d=> el("li", d))) + ), + el("ul").append( + /* declarative component(s) */ + O.el(dataA, data=> data.map(d=> el(subcomponent, d))) + ) + ); +} +function subcomponent({ id }){ + /* prepare changeable data */ + const textContent= O("…"); + /* define data flow (can be asynchronous) */ + fetchAPI(id).then(text=> textContent(text)); + /* declarative UI */ + return el("li", { textContent, dataId: id }); +} diff --git a/docs_src/components/examples/scopes/good-practise.js b/docs_src/components/examples/scopes/good-practise.js new file mode 100644 index 0000000..06d408f --- /dev/null +++ b/docs_src/components/examples/scopes/good-practise.js @@ -0,0 +1,18 @@ +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!"); +} diff --git a/docs_src/components/examples/scopes/imperative.js b/docs_src/components/examples/scopes/imperative.js new file mode 100644 index 0000000..f9aed54 --- /dev/null +++ b/docs_src/components/examples/scopes/imperative.js @@ -0,0 +1,30 @@ +/* PSEUDO-CODE!!! */ +import { el, on, scope } from "deka-dom-el"; +function component(){ + const ul= el("ul"); + const ac= new AbortController(); + fetchAPI({ signal: ac.signal }).then(data=> { + data.forEach(d=> ul.append(el("li", d))); + }); + scope.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!!! + * */ +} diff --git a/docs_src/components/examples/scopes/intro.js b/docs_src/components/examples/scopes/intro.js index d0f1ab0..2706af3 100644 --- a/docs_src/components/examples/scopes/intro.js +++ b/docs_src/components/examples/scopes/intro.js @@ -1,3 +1,3 @@ // use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js -import { scope, el, on } from "deka-dom-el"; +import { scope, el } from "deka-dom-el"; /** @type {ddeElementAddon} */ diff --git a/docs_src/p05-scopes.html.js b/docs_src/p05-scopes.html.js index 7a48be0..01c0ee3 100644 --- a/docs_src/p05-scopes.html.js +++ b/docs_src/p05-scopes.html.js @@ -35,6 +35,40 @@ export function page({ pkg, info }){ " component as class instances." ), el(example, { src: fileURL("./components/examples/scopes/class-component.js"), page_id }), + el("p").append( + "As you can see, the ", el("code", "scope.host()"), " is stored temporarily and synchronously.", + " Therefore, at least in the beginning of using library, it is the good practise to store", + " ", el("code", "host"), " in the root of your component. As it may be changed, typically when", + " there is asynchronous code in the component." + ), + el(code, { src: fileURL("./components/examples/scopes/good-practise.js"), page_id }), + + el(h3, "Scopes, observables and cleaning magic"), + el("p").append( + "The ", el("code", "host"), " is internally used to register the cleaning procedure,", + " when the component (", el("code", "host"), " element) is removed from the DOM." + ), + el(example, { src: fileURL("./components/examples/scopes/cleaning.js"), page_id }), + el("p").append( + "The text content of the paragraph is changing when the value of the observable ", el("code", "textContent"), + " is changed. Internally, there is association between ", el("code", "textContent"), " and the paragraph", + " similar to using ", el("code", "S.on(textContent, /* update the paragraph */)"), "." + ), + el("p").append( + "This listener must be removed when the component is removed from the DOM. To do it, the library", + " assign internally ", el("code", "on.disconnect(/* remove the listener */)(host())"), " to the host element." + ), + el("p", { className: "notice" }).append( + "The library DOM API and observables works ideally when used declaratively.", + " It means, you split your app logic into three parts as it was itroduced in ", el("a", { textContent: "Observables", href: "http://localhost:40911/docs/p04-observables#h-introducing-observables" }), "." + ), + el(code, { src: fileURL("./components/examples/scopes/declarative.js"), page_id }), + el("p").append( + "Strictly speaking, the imperative way of using the library is not prohibited.", + " Just be careful (rather avoid) mixing declarative approach (using observables)", + " and imperative manipulation of elements.", + ), + el(code, { src: fileURL("./components/examples/scopes/imperative.js"), page_id }), el(mnemonic) );