From e2df9705d1e68488f2c3d8d3eeeed8dce2d11660 Mon Sep 17 00:00:00 2001 From: Jan Andrle Date: Wed, 5 Mar 2025 11:53:32 +0100 Subject: [PATCH] :abc: updates texts --- docs/components/code.html.js | 2 +- .../components/examples/scopes/declarative.js | 53 +++--- docs/components/examples/scopes/imperative.js | 50 +++-- docs/components/examples/scopes/mixed.js | 34 ++++ docs/components/examples/signals/signals.js | 8 +- docs/global.css.js | 6 + docs/p04-signals.html.js | 49 ++--- docs/p05-scopes.html.js | 176 ++++++------------ docs/p06-customElement.html.js | 40 ++-- 9 files changed, 189 insertions(+), 229 deletions(-) create mode 100644 docs/components/examples/scopes/mixed.js diff --git a/docs/components/code.html.js b/docs/components/code.html.js index b586b65..d736a5d 100644 --- a/docs/components/code.html.js +++ b/docs/components/code.html.js @@ -195,7 +195,7 @@ export function code({ id, src, content, language= "js", className= host.slice(1 dataJS= "todo"; } return el("div", { id, className, dataJS }).append( - el("code", { className: "language-"+language, textContent: content }) + el("code", { className: "language-"+language, textContent: content.trim() }) ); } let is_registered= {}; diff --git a/docs/components/examples/scopes/declarative.js b/docs/components/examples/scopes/declarative.js index e87c7a2..2cfa978 100644 --- a/docs/components/examples/scopes/declarative.js +++ b/docs/components/examples/scopes/declarative.js @@ -1,35 +1,26 @@ -/* 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 Counter() { + // Define state + const count = S(0); + + // Define behavior + const increment = () => count.set(count.get() + 1); + + // Define data flow + setTimeout(increment, 1000); + // or fetchAPI().then(increment); + + // Declarative UI (how to render data/`count`) + // …automatically updates when changes + return el("div").append( + // declarative element(s) + el("p", S(() => "Count: " + count.get())), + el("button", { + onclick: increment, + textContent: "Increment", + // declarative attribute(s) + disabled: S(() => count.get() >= 10) + }) ); } -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 }); -} diff --git a/docs/components/examples/scopes/imperative.js b/docs/components/examples/scopes/imperative.js index 012514c..e994560 100644 --- a/docs/components/examples/scopes/imperative.js +++ b/docs/components/examples/scopes/imperative.js @@ -1,31 +1,25 @@ /* 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()) +import { el, scope } from "deka-dom-el"; +function Counter() { + const { host } = scope; + + let count = 0; + const counterText = el("p", "Count: 0"); + + // Manually update DOM element + const increment = () => { + count++; + counterText.textContent = "Count: " + count; + host().querySelector("button").disabled = count >= 10; + }; + setTimeout(increment, 1000); + // or fetchAPI().then(increment); + + return el("div").append( + counterText, + el("button", { + onclick: increment, + textContent: "Increment" + }) ); - 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/components/examples/scopes/mixed.js b/docs/components/examples/scopes/mixed.js new file mode 100644 index 0000000..e1d4c56 --- /dev/null +++ b/docs/components/examples/scopes/mixed.js @@ -0,0 +1,34 @@ +/* PSEUDO-CODE!!! */ +import { el, scope } from "deka-dom-el"; +import { S } from "deka-dom-el/signals"; +function Counter() { + const { host } = scope; + + let count = S(0); + const counterText = el("p", "Count: 0"); + S.on(count, c=> counterText.textContent= "Count: " + c); + + // Manually update DOM element + const increment = () => { + count.set(count.get() + 1); + // NEVER EVER + // count = S(count.get() + 1); + host().querySelector("button").disabled = count.get() >= 10; + }; + // NEVER EVER + // setTimeout(()=> { + // const wrong= S(0); + // // THE HOST IS PROBABLY DIFFERENT THAN + // // YOU EXPECT AND OBSERVABLES MAY BE + // // UNEXPECTEDLY REMOVED!!! + // counterText.textContent= "Count: " + wrong.get(); + // }, 1000); + + return el("div").append( + counterText, + el("button", { + onclick: increment, + textContent: "Increment" + }) + ); +} diff --git a/docs/components/examples/signals/signals.js b/docs/components/examples/signals/signals.js index bfaec09..172b3a0 100644 --- a/docs/components/examples/signals/signals.js +++ b/docs/components/examples/signals/signals.js @@ -1,12 +1,12 @@ import { S } from "deka-dom-el/signals"; -// α — `signal` represents a reactive value +// A — `signal` represents a reactive value const signal= S(0); -// β — just reacts on signal changes +// B — just reacts on signal changes S.on(signal, console.log); -// γ — just updates the value +// C — just updates the value const update= ()=> signal.set(signal.get()+1); update(); const interval= 5*1000; setTimeout(clearInterval, 10*interval, - setInterval(update, interval)); \ No newline at end of file + setInterval(update, interval)); diff --git a/docs/global.css.js b/docs/global.css.js index 5829a0b..f238185 100644 --- a/docs/global.css.js +++ b/docs/global.css.js @@ -208,6 +208,12 @@ pre code { background-color: transparent; padding: 0; } +.illustration:not(:has( .comparison)) pre { + background: none; + border-style: dashed !important; + width: fit-content; + padding: 1em 2em; +} /* Layout */ @media (min-width: 768px) { diff --git a/docs/p04-signals.html.js b/docs/p04-signals.html.js index f4fc83e..90b6f60 100644 --- a/docs/p04-signals.html.js +++ b/docs/p04-signals.html.js @@ -55,8 +55,7 @@ export function page({ pkg, info }){ el("li", t`Automatic UI updates when data changes`), el("li", t`Clean separation between data, logic, and UI`), el("li", t`Small runtime with minimal overhead`), - el("li", t`Works seamlessly with DDE's DOM creation`), - el("li", t`No dependencies or framework lock-in`) + el("li").append(...T`${el("strong", "In future")} no dependencies or framework lock-in`) ) ), el(code, { src: fileURL("./components/examples/signals/intro.js"), page_id }), @@ -68,17 +67,17 @@ export function page({ pkg, info }){ `), el("div", { class: "signal-diagram" }).append( el("div", { class: "signal-part" }).append( - el("h4", t`α: Create Signal`), + el("h4", t`A: Create Signal`), el(code, { content: "const count = S(0);", page_id }), el("p", t`Define a reactive value that can be observed and changed`) ), el("div", { class: "signal-part" }).append( - el("h4", t`β: React to Changes`), + el("h4", t`B: React to Changes`), el(code, { content: "S.on(count, value => updateUI(value));", page_id }), el("p", t`Subscribe to signal changes with callbacks or effects`) ), el("div", { class: "signal-part" }).append( - el("h4", t`γ: Update Signal`), + el("h4", t`C: Update Signal`), el(code, { content: "count.set(count.get() + 1);", page_id }), el("p", t`Modify the signal value, which automatically triggers updates`) ) @@ -90,7 +89,8 @@ export function page({ pkg, info }){ Signals implement the ${el("a", { textContent: t`Publish–subscribe pattern`, ...references.wiki_pubsub })}, a form of ${el("a", { textContent: t`Event-driven programming`, ...references.wiki_event_driven })}. This architecture allows different parts of your application to stay synchronized through a shared signal, - without direct dependencies on each other. + without direct dependencies on each other. Compare for example with ${el("a", { textContent: + t`fpubsub library`, ...references.fpubsub })}. `) ), @@ -110,7 +110,8 @@ export function page({ pkg, info }){ el("dd", t`S.on(signal, callback) → runs callback whenever signal changes`), el("dt", t`Unsubscribing`), - el("dd", t`S.on(signal, callback, { signal: abortController.signal })`) + el("dd").append(...T`S.on(signal, callback, { signal: abortController.signal }) → Similarly to the + ${el("code", "on")} function to register DOM events listener.`) ) ), el("p").append(...T` @@ -139,15 +140,6 @@ export function page({ pkg, info }){ el("div", { class: "illustration" }).append( el("h4", t`Actions vs. Direct Mutation`), el("div", { class: "comparison" }).append( - el("div", { class: "bad-practice" }).append( - el("h5", t`❌ Without Actions`), - el(code, { content: ` -const todos = S([]); -// Directly mutating the array -const items = todos.get(); -items.push("New todo"); -// This WON'T trigger updates!`, page_id })) - ), el("div", { class: "good-practice" }).append( el("h5", t`✅ With Actions`), el(code, { content: `const todos = S([], { @@ -159,8 +151,25 @@ items.push("New todo"); // Use the action S.action(todos, "add", "New todo");`, page_id }) - ) + ), + el("div", { class: "bad-practice" }).append( + el("h5", t`❌ Without Actions`), + el(code, { content: ` +const todos = S([]); +// Directly mutating the array +const items = todos.get(); +items.push("New todo"); +// This WON'T trigger updates!`, page_id })) + ), ), + el("p").append(...T` + In some way, you can compare it with ${el("a", { textContent: "useReducer", ...references.mdn_use_reducer })} + hook from React. So, the ${el("code", "S(, )")} pattern creates a store “machine”. We can + then invoke (dispatch) registered action by calling ${el("code", "S.action(, , ...)")} + after the action call the signal calls all its listeners. This can be stopped by calling + ${el("code", "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 (${el("code", "this.value")}). + `), el(example, { src: fileURL("./components/examples/signals/actions-demo.js"), page_id }), el("p").append(...T` @@ -182,9 +191,7 @@ S.action(todos, "add", "New todo");`, page_id }) ${el("strong", "Special Action Methods")}: Signal actions can implement special lifecycle hooks: `), el("ul").append( - el("li", t`[S.symbols.onclear]() - Called when the signal is cleared`), - el("li", t`[S.symbols.onget]() - Called when the signal value is read`), - el("li", t`[S.symbols.onset]() - Called after the signal value is changed`) + el("li", t`[S.symbols.onclear]() - Called when the signal is cleared. Use it to clean up resources.`), ) ), @@ -229,12 +236,12 @@ S.action(items, "push", "Dragonfruit"); // List updates automatically`, page_id ) ), - el(example, { src: fileURL("./components/examples/signals/dom-attrs.js"), page_id }), el("p").append(...T` The ${el("code", "assign")} and ${el("code", "el")} functions detect signals automatically and handle binding. You can use special properties like ${el("code", "dataset")}, ${el("code", "ariaset")}, and ${el("code", "classList")} for fine-grained control over specific attribute types. `), + el(example, { src: fileURL("./components/examples/signals/dom-attrs.js"), page_id }), el("p").append(...T` ${el("code", "S.el()")} is especially powerful for conditional rendering and lists: diff --git a/docs/p05-scopes.html.js b/docs/p05-scopes.html.js index 76f6f43..6765106 100644 --- a/docs/p05-scopes.html.js +++ b/docs/p05-scopes.html.js @@ -30,53 +30,44 @@ export function page({ pkg, info }){ return el(simplePage, { info, pkg }).append( el("h2", t`Building Maintainable UIs with Scopes and Components`), el("p").append(...T` - Scopes provide a structured way to organize your UI code into reusable components that properly - manage their lifecycle, handle cleanup, and maintain clear boundaries between different parts of your application. + 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 ${el("a", { textContent: t`Garbage collection`, ...references.garbage_collection })}. `), - el("div", { className: "callout" }).append( - el("h4", t`Why Use Scopes?`), - el("ul").append( - el("li", t`Automatic resource cleanup when components are removed from DOM`), - el("li", t`Clear component boundaries with explicit host elements`), - el("li", t`Simplified event handling with proper "this" binding`), - el("li", t`Seamless integration with signals for reactive components`), - el("li", t`Better memory management with ${el("a", { textContent: t`GC`, ...references.garbage_collection })}`) - ) - ), el(code, { src: fileURL("./components/examples/scopes/intro.js"), page_id }), + el("p").append(...T`The library therefore use ${el("em", t`scopes`)} to provide these functionalities.`), el(h3, t`Understanding Host Elements and Scopes`), + el("p").append(...T` + The ${el("strong", "host")} is the name for the element representing the component. This is typically + element returned by function. To get reference, you can use ${el("code", "scope.host()")} to applly addons + just use ${el("code", "scope.host(...)")}. + `), el("div", { className: "illustration" }).append( el("h4", t`Component Anatomy`), el("pre").append(el("code", ` -┌─────────────────────────────────┐ -│ // 1. Component scope created │ -│ el(MyComponent); │ -│ │ -│ function MyComponent() { │ -│ // 2. access the host element │ -│ const { host } = scope; │ -│ │ -│ // 3. Add behavior to host │ -│ host( │ -│ on.click(handleClick) │ -│ ); │ -│ │ -│ // 4. Return the host element │ -│ return el("div", { │ -│ className: "my-component" │ -│ }).append( │ -│ el("h2", "Title"), │ -│ el("p", "Content") │ -│ ); │ -│ } │ -└─────────────────────────────────┘ - `)) +// 1. Component scope created +el(MyComponent); + +function MyComponent() { + // 2. access the host element + const { host } = scope; + + // 3. Add behavior to host + host( + on.click(handleClick) + ); + + // 4. Return the host element + return el("div", { + className: "my-component" + }).append( + el("h2", "Title"), + el("p", "Content") + ); +} + `.trim())) ), - el("p").append(...T` - The ${el("strong", "host element")} is the root element of your component - typically the element returned - by your component function. It serves as the identity of your component in the DOM. - `), el("div", { className: "function-table" }).append( el("h4", t`scope.host()`), el("dl").append( @@ -91,34 +82,24 @@ export function page({ pkg, info }){ el("div", { className: "tip" }).append( el("p").append(...T` - ${el("strong", "Best Practice:")} Always capture the host reference at the beginning of your component function - using ${el("code", "const { host } = scope")} to avoid scope-related issues, especially with asynchronous code. - `) - ), - - el(h3, t`Class-Based Components`), - el("p").append(...T` - While functional components are the primary pattern in DDE, you can also create class-based components - for more structured organization of component logic. - `), - el(example, { src: fileURL("./components/examples/scopes/class-component.js"), page_id }), - - el("p").append(...T` - This pattern can be useful when: - `), - el("ul").append( - el("li", t`You have complex component logic that benefits from object-oriented organization`), - el("li", t`You need private methods and properties for your component`), - el("li", t`You're transitioning from another class-based component system`) - ), - el("div", { className: "tip" }).append( + ${el("strong", "Best Practice:")} Always capture the host reference at the beginning of your component + function using ${el("code", "const { host } = scope")} to avoid scope-related issues, especially with + asynchronous code. + `), el("p").append(...T` - ${el("strong", "Note:")} Even with class-based components, follow the best practice of storing the host reference - early in your component code. This ensures proper access to the host throughout the component's lifecycle. + If you are interested in the implementation details, see Class-Based Components section. `) ), el(code, { src: fileURL("./components/examples/scopes/good-practise.js"), page_id }), + el(h3, t`Class-Based Components`), + el("p").append(...T` + While functional components are the primary pattern in DDE, you can also create class-based components. + For this, we implement function ${el("code", "elClass")} and use it to demonstrate implementation details + for better understanding of the scope logic. + `), + el(example, { src: fileURL("./components/examples/scopes/class-component.js"), page_id }), + el(h3, t`Automatic Cleanup with Scopes`), el("p").append(...T` One of the most powerful features of scopes is automatic cleanup when components are removed from the DOM. @@ -149,77 +130,38 @@ export function page({ pkg, info }){ el(h3, t`Declarative vs Imperative Components`), el("p").append(...T` - Scopes work best with a declarative approach to UI building, especially when combined - with ${el("a", { textContent: "signals", ...references.signals })} for state management. + The library DOM API and signals works best when used declaratively. It means, you split your app logic + into three parts as it was itroduced in ${el("a", { textContent: "Signals", ...references.signals })}. `), + el("div", { className: "note" }).append( + el("p").append(...T` + 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. + `) + ), el("div", { className: "tabs" }).append( el("div", { className: "tab", "data-tab": "declarative" }).append( el("h4", t`✅ Declarative Approach`), el("p", t`Define what your UI should look like based on state:`), - el("pre").append(el("code", `function Counter() { - const { host } = scope; - - // Define state - const count = S(0); - - // Define behavior - const increment = () => count.set(count.get() + 1); - - // UI automatically updates when count changes - return el("div").append( - el("p", S(() => "Count: " + count.get())), - el("button", { - onclick: increment, - textContent: "Increment" - }) - ); -}`)) + el(code, { src: fileURL("./components/examples/scopes/declarative.js"), page_id }), ), el("div", { className: "tab", "data-tab": "imperative" }).append( el("h4", t`⚠️ Imperative Approach`), el("p", t`Manually update the DOM in response to events:`), - el("pre").append(el("code", `function Counter() { - const { host } = scope; - - let count = 0; - const counterText = el("p", "Count: 0"); - - // Manually update DOM element - const increment = () => { - count++; - counterText.textContent = "Count: " + count; - }; - - return el("div").append( - counterText, - el("button", { - onclick: increment, - textContent: "Increment" - }) - ); -}`)) - ) + el(code, { src: fileURL("./components/examples/scopes/imperative.js"), page_id }), + ), + el("div", { className: "tab", "data-tab": "imperative" }).append( + el("h4", t`❌ Mixed Approach`), + el("p", t`Just AVOID:`), + el(code, { src: fileURL("./components/examples/scopes/mixed.js"), page_id }), + ), ), - el(code, { src: fileURL("./components/examples/scopes/declarative.js"), page_id }), - - el("div", { className: "note" }).append( - el("p").append(...T` - While DDE supports both declarative and imperative approaches, the declarative style is recommended - as it leads to more maintainable code with fewer opportunities for bugs. Signals handle the complexity - of keeping your UI in sync with your data. - `) - ), - el(code, { src: fileURL("./components/examples/scopes/imperative.js"), page_id }), - el(h3, t`Best Practices for Scopes and Components`), el("ol").append( el("li").append(...T` ${el("strong", "Capture host early:")} Use ${el("code", "const { host } = scope")} at component start `), - el("li").append(...T` - ${el("strong", "Return a single root element:")} Components should have one host element that contains all others - `), el("li").append(...T` ${el("strong", "Prefer declarative patterns:")} Use signals to drive UI updates rather than manual DOM manipulation `), diff --git a/docs/p06-customElement.html.js b/docs/p06-customElement.html.js index c7cd90d..fe7d0bc 100644 --- a/docs/p06-customElement.html.js +++ b/docs/p06-customElement.html.js @@ -201,22 +201,16 @@ export function page({ pkg, info }){ el("div", { className: "illustration" }).append( el("h4", t`Shadow DOM Encapsulation`), el("pre").append(el("code", ` -┌─────────────────────────────────┐ -│ │ -│ │ -│ ┌─────────────────────────┐ │ -│ │ #shadow-root │ │ -│ │ │ │ -│ │ Created with DDE: │ │ -│ │ ┌─────────────────┐ │ │ -│ │ │
│ │ │ -│ │ │

Title

│ │ │ -│ │ │

Content

│ │ │ -│ │ └─────────────────┘ │ │ -│ │ │ │ -│ └─────────────────────────┘ │ -│ │ -└─────────────────────────────────┘ + + + ┌─────────────────────────┐ + #shadow-root + + Created with DDE: + ┌──────────────────┐ +
+

Title

+

Content

`)) ), el(example, { src: fileURL("./components/examples/customElement/shadowRoot.js"), page_id }), @@ -229,9 +223,10 @@ export function page({ pkg, info }){ el(h3, t`Working with Slots`), el("p").append(...T` - Slots allow users of your component to insert content inside it. When using DDE, you can simulate the - slot mechanism with the ${el("code", "simulateSlots")} function: + Besides the encapsulation, the Shadow DOM allows for using the ${el("a", references.mdn_shadow_dom_slot).append( + el("strong", t``), t` element(s)`)}. You can simulate this feature using ${el("code", "simulateSlots")}: `), + el(example, { src: fileURL("./components/examples/customElement/simulateSlots.js"), page_id }), el("div", { className: "function-table" }).append( el("h4", t`simulateSlots`), el("dl").append( @@ -241,15 +236,6 @@ export function page({ pkg, info }){ el("dd", t`A mapping object of slot names to DOM elements`) ) ), - el(example, { src: fileURL("./components/examples/customElement/simulateSlots.js"), page_id }), - - el("div", { className: "tip" }).append( - el("p").append(...T` - ${el("strong", "When to use simulateSlots:")} This approach is useful when you need to distribute - content from the light DOM into specific locations in the shadow DOM, particularly in environments - where native slots might not be fully supported. - `) - ), el(h3, t`Best Practices for Web Components with DDE`), el("p").append(...T`