diff --git a/docs/components/examples/signals/derived.js b/docs/components/examples/signals/derived.js new file mode 100644 index 0000000..e1e4d88 --- /dev/null +++ b/docs/components/examples/signals/derived.js @@ -0,0 +1,13 @@ +import { S } from "deka-dom-el/signals"; + +// Create base signals +const firstName = S("John"); +const lastName = S("Doe"); + +// Create a derived signal +const fullName = S(() => firstName.get() + " " + lastName.get()); + +// The fullName signal updates automatically when either dependency changes +S.on(fullName, name => console.log("Name changed to:", name)); + +firstName.set("Jane"); // logs: "Name changed to: Jane Doe" diff --git a/docs/p04-signals.html.js b/docs/p04-signals.html.js index 77c29a8..bc48008 100644 --- a/docs/p04-signals.html.js +++ b/docs/p04-signals.html.js @@ -1,6 +1,6 @@ import { T, t } from "./utils/index.js"; export const info= { - title: t`Signals and reactivity`, + title: t`Signals and Reactivity`, description: t`Managing reactive UI state with signals.`, }; @@ -43,87 +43,243 @@ const references= { export function page({ pkg, info }){ const page_id= info.id; return el(simplePage, { info, pkg }).append( - el("h2", t`Using signals to manage reactivity`), + el("h2", t`Building Reactive UIs with Signals`), el("p").append(...T` - 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, signals may be a viable approach. + Signals provide a simple yet powerful way to create reactive applications with DDE. They handle the + fundamental challenge of keeping your UI in sync with changing data in a declarative, efficient way. `), + el("div", { class: "dde-callout" }).append( + el("h4", t`What Makes Signals Special?`), + el("ul").append( + el("li", t`Fine-grained reactivity without complex state management`), + 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(code, { src: fileURL("./components/examples/signals/intro.js"), page_id }), - el(h3, t`Introducing signals`), + el(h3, t`The 3-Part Structure of Signals`), el("p").append(...T` - Let’s re-introduce - ${el("a", { textContent: t`3PS principle`, href: "./#h-event-driven-programming--parts-separation--ps" })}. - `), - el("p").append(...T` - Using signals, we split program logic into the three parts. Firstly (α), we create a variable (constant) - representing reactive value. Somewhere later, we can register (β) a logic reacting to the signal value - changes. Similarly, in a remaining part (γ), we can update the signal value. + Signals organize your code into three distinct parts, following the + ${el("a", { textContent: t`3PS principle`, href: "./#h-event-driven-programming--parts-separation--ps" })}: `), + el("div", { class: "dde-signal-diagram" }).append( + el("div", { class: "signal-part" }).append( + el("h4", t`α: 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(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(code, { content: "count.set(count.get() + 1);", page_id }), + el("p", t`Modify the signal value, which automatically triggers updates`) + ) + ), el(example, { src: fileURL("./components/examples/signals/signals.js"), page_id }), + + el("div", { class: "dde-note" }).append( + el("p").append(...T` + 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. + `) + ), + + el(h3, t`Signal Essentials: Core API`), + el("div", { class: "dde-function-table" }).append( + el("dl").append( + el("dt", t`Creating a Signal`), + el("dd", t`S(initialValue) → creates a signal with the given value`), + + el("dt", t`Reading a Signal`), + el("dd", t`signal.get() → returns the current value`), + + el("dt", t`Writing to a Signal`), + el("dd", t`signal.set(newValue) → updates the value and notifies subscribers`), + + el("dt", t`Subscribing to Changes`), + 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("p").append(...T` - All this is just an example of - ${el("a", { textContent: t`Event-driven programming`, ...references.wiki_event_driven })} and - ${el("a", { textContent: t`Publish–subscribe pattern`, ...references.wiki_pubsub })} (compare for example - with ${el("a", { textContent: t`fpubsub library`, ...references.fpubsub })}). All three parts can be in - some manner independent and still connected to the same reactive entity. + Signals can be created with any type of value, but they work best with + ${el("a", { textContent: t`primitive types`, ...references.mdn_primitive })} like strings, numbers, and booleans. + For complex data types like objects and arrays, you'll want to use Actions (covered below). `), + + el(h3, t`Derived Signals: Computed Values`), el("p").append(...T` - Signals are implemented in the library as objects with methods. To see current value of signal, - call the get method ${el("code", "console.log(signal.get())")}. To update the signal value, use the set method - ${el("code", `signal.set('${t`a new value`}')`)}. For listenning the signal value changes, use - ${el("code", "S.on(signal, console.log)")}. + Computed values (also called derived signals) automatically update when their dependencies change. + Create them by passing a function to S(): `), + el(example, { src: fileURL("./components/examples/signals/derived.js"), page_id }), el("p").append(...T` - Similarly to the ${el("code", "on")} function to register DOM events listener. You can use - ${el("code", "AbortController")}/${el("code", "AbortSignal")} to ${el("em", "off")}/stop listenning. In - example, you also found the way for representing “live” piece of code computation pattern (derived signal): + Derived signals are read-only - you can't call .set() on them. Their value is always computed + from their dependencies. They're perfect for transforming or combining data from other signals. `), el(example, { src: fileURL("./components/examples/signals/computations-abort.js"), page_id }), - el(h3, t`Signals and actions`), + el(h3, t`Signal Actions: For Complex State`), el("p").append(...T` - ${el("code", `S(/* ${t`primitive`} */)`)} allows you to declare simple reactive variables, typically, around - ${el("em", t`immutable`)} ${el("a", { textContent: t`primitive types`, ...references.mdn_primitive })}. - However, it may also be necessary to use reactive arrays, objects, or other complex reactive structures. + When working with objects, arrays, or other complex data structures, Signal Actions provide + a structured way to modify state while maintaining reactivity. `), + el("div", { class: "dde-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([], { + add(text) { + this.value.push(text); + // Subscribers notified automatically + } +}); + +// Use the action +S.action(todos, "add", "New todo");`, page_id }) + ) + ), el(example, { src: fileURL("./components/examples/signals/actions-demo.js"), page_id }), - el("p", t`…but typical user-case is object/array (maps, sets and other mutable objects):`), - el(example, { src: fileURL("./components/examples/signals/actions-todos.js"), 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")}). + Actions provide these benefits: + `), + el("ul").append( + el("li", t`Encapsulate state change logic in named methods`), + el("li", t`Guarantee notifications when state changes`), + el("li", t`Prevent accidental direct mutations`), + el("li", t`Act similar to reducers in other state management libraries`) + ), + el("p").append(...T` + Here's a more complete example of a todo list using signal actions: + `), + el(example, { src: fileURL("./components/examples/signals/actions-todos.js"), page_id }), + + el("div", { class: "dde-tip" }).append( + el("p").append(...T` + ${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(h3, t`Connecting Signals to the DOM`), + el("p").append(...T` + Signals really shine when connected to your UI. DDE provides several ways to bind signals to DOM elements: `), - el(h3, t`Reactive DOM attributes and elements`), - el("p", t`There are two fundamental ways to make your DOM reactive with signals:`), - el("ol").append( - el("li", t`Reactive attributes: Update properties, attributes, and styles of existing elements`), - el("li").append(...T`Reactive elements: Dynamically create or update DOM elements based on data changes - (for conditions and loops)`) + el("div", { class: "dde-tabs" }).append( + el("div", { class: "tab", "data-tab": "attributes" }).append( + el("h4", t`Reactive Attributes`), + el("p", t`Bind signal values directly to element attributes, properties, or styles:`), + el(code, { content: `// Create a signal +const color = S("blue"); + +// Bind it to an element's style +el("div", { + style: { + color, // Updates when signal changes + fontWeight: S(() => color.get() === "red" ? "bold" : "normal") + } +}, "This text changes color"); + +// Later: +color.set("red"); // UI updates automatically`, page_id }) + ), + el("div", { class: "tab", "data-tab": "elements" }).append( + el("h4", t`Reactive Elements`), + el("p", t`Dynamically create or update elements based on signal values:`), + el(code, { content: `// Create an array signal +const items = S(["Apple", "Banana", "Cherry"]); + +// Create a dynamic list that updates when items change +el("ul").append( + S.el(items, items => + items.map(item => el("li", item)) + ) +); + +// Later: +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` - To derived attribute based on value of signal variable just use the signal as a value of the attribute - (${el("code", "assign(element, { attribute: S('value') })")}). ${el("code", "assign")}/${el("code", "el")} - provides ways to glue reactive attributes/classes more granularly into the DOM. Just use dedicated build-in - attributes ${el("code", "dataset")}, ${el("code", "ariaset")} and ${el("code", "classList")}. + 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("p").append(...T` - For computation, you can use the “derived signal” (see above) like - ${el("code", "assign(element, { textContent: S(()=> 'Hello '+WorldSignal.get()) })")}. This is read-only signal - its value is computed based on given function and updated when any signal used in the function changes. - `), - el("p").append(...T` - To represent part of the template filled dynamically based on the signal value use - ${el("code", "S.el(signal, DOMgenerator)")}. This was already used in the todo example above or see: + ${el("code", "S.el()")} is especially powerful for conditional rendering and lists: `), el(example, { src: fileURL("./components/examples/signals/dom-el.js"), page_id }), + el(h3, t`Best Practices for Signals`), + el("p").append(...T` + Follow these guidelines to get the most out of signals: + `), + el("ol").append( + el("li").append(...T` + ${el("strong", "Keep signals small and focused")}: Use many small signals rather than a few large ones + `), + el("li").append(...T` + ${el("strong", "Use derived signals for computations")}: Don't recompute values in multiple places + `), + el("li").append(...T` + ${el("strong", "Clean up signal subscriptions")}: Use AbortController or scope.host() to prevent memory leaks + `), + el("li").append(...T` + ${el("strong", "Use actions for complex state")}: Don't directly mutate objects or arrays in signals + `), + el("li").append(...T` + ${el("strong", "Avoid infinite loops")}: Be careful when one signal updates another in a subscription + `) + ), + + el("div", { class: "dde-troubleshooting" }).append( + el("h4", t`Common Signal Pitfalls`), + el("dl").append( + el("dt", t`UI not updating when array/object changes`), + el("dd", t`Use signal actions instead of direct mutation`), + + el("dt", t`Infinite update loops`), + el("dd", t`Check for circular dependencies between signals`), + + el("dt", t`Memory leaks`), + el("dd", t`Use AbortController or scope.host() to clean up subscriptions`), + + el("dt", t`Multiple elements updating unnecessarily`), + el("dd", t`Split large signals into smaller, more focused ones`) + ) + ), + el(mnemonic) ); } diff --git a/docs/p05-scopes.html.js b/docs/p05-scopes.html.js index 2572d40..392e7b2 100644 --- a/docs/p05-scopes.html.js +++ b/docs/p05-scopes.html.js @@ -1,7 +1,7 @@ import { T, t } from "./utils/index.js"; export const info= { - title: t`Scopes and components`, - description: t`Organizing UI into components`, + title: t`Scopes and Components`, + description: t`Organizing UI into reusable, manageable components`, }; import { el } from "deka-dom-el"; @@ -28,60 +28,226 @@ const references= { export function page({ pkg, info }){ const page_id= info.id; return el(simplePage, { info, pkg }).append( - el("h2", t`Using functions as UI components`), + el("h2", t`Building Maintainable UIs with Scopes and Components`), el("p").append(...T` - 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 })}. + 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. `), + el("div", { class: "dde-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`Scopes and hosts`), + el(h3, t`Understanding Host Elements and Scopes`), + el("div", { class: "dde-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") │ +│ ); │ +│ } │ +└─────────────────────────────────┘ + `)) + ), 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(...)")}. + 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", { class: "dde-function-table" }).append( + el("h4", t`scope.host()`), + el("dl").append( + el("dt", t`When called with no arguments`), + el("dd", t`Returns a reference to the host element (the root element of your component)`), + + el("dt", t`When called with addons/callbacks`), + el("dd", t`Applies the addons to the host element and returns the host element`) + ) + ), el(example, { src: fileURL("./components/examples/scopes/scopes-and-hosts.js"), page_id }), + + el("div", { class: "dde-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` - To better understanding we implement function ${el("code", "elClass")} helping to create component as - class instances. + 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` - 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. + 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", { class: "dde-tip" }).append( + 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. + `) + ), el(code, { src: fileURL("./components/examples/scopes/good-practise.js"), page_id }), - el(h3, t`Scopes, signals and cleaning magic`), + el(h3, t`Automatic Cleanup with Scopes`), el("p").append(...T` - The ${el("code", "host")} is internally used to register the cleaning procedure, when the component - (${el("code", "host")} element) is removed from the DOM. + One of the most powerful features of scopes is automatic cleanup when components are removed from the DOM. + This prevents memory leaks and ensures resources are properly released. `), + el("div", { class: "dde-illustration" }).append( + el("h4", t`Lifecycle Flow`), + el("pre").append(el("code", ` +1. Component created → scope established +2. Component added to DOM → connected event +3. Component interactions happen +4. Component removed from DOM → disconnected event +5. Automatic cleanup of: + - Event listeners + - Signal subscriptions + - Custom cleanup code + `)) + ), el(example, { src: fileURL("./components/examples/scopes/cleaning.js"), page_id }), + + el("div", { class: "dde-note" }).append( + el("p").append(...T` + In this example, when you click "Remove", the component is removed from the DOM, and all its associated + resources are automatically cleaned up, including the signal subscription that updates the text content. + This happens because the library internally registers a disconnected event handler on the host element. + `) + ), + + el(h3, t`Declarative vs Imperative Components`), el("p").append(...T` - The text content of the paragraph is changing when the value of the signal ${el("code", "textContent")} - is changed. Internally, there is association between ${el("code", "textContent")} and the paragraph, - similar to using ${el("code", `S.on(textContent, /* ${t`update the paragraph`} */)`)}. - `), - el("p").append(...T` - This listener must be removed when the component is removed from the DOM. To do it, the library assign - internally ${el("code", `on.disconnected(/* ${t`remove the listener`} */)(host())`)} to the host element. - `), - el("p", { className: "notice" }).append(...T` - 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 ${el("a", { textContent: "Signals", ...references.signals })}. + Scopes work best with a declarative approach to UI building, especially when combined + with ${el("a", { textContent: "signals", ...references.signals })} for state management. `), + el("div", { class: "dde-tabs" }).append( + el("div", { class: "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("div", { class: "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/declarative.js"), page_id }), - 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", { class: "dde-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 + `), + el("li").append(...T` + ${el("strong", "Keep components focused:")} Each component should do one thing well + `), + el("li").append(...T` + ${el("strong", "Add explicit cleanup:")} For resources not managed by DDE, use ${el("code", "on.disconnected")} + `) + ), + + el("div", { class: "dde-troubleshooting" }).append( + el("h4", t`Common Scope Pitfalls`), + el("dl").append( + el("dt", t`Losing host reference in async code`), + el("dd", t`Store host reference early with const { host } = scope`), + + el("dt", t`Memory leaks from custom resources`), + el("dd", t`Use host(on.disconnected(cleanup)) for manual resource cleanup`), + + el("dt", t`Event handlers with incorrect 'this'`), + el("dd", t`Use arrow functions or .bind() to preserve context`), + + el("dt", t`Mixing declarative and imperative styles`), + el("dd", t`Choose one approach and be consistent throughout a component`) + ) + ), + el(mnemonic) ); } diff --git a/docs/p06-customElement.html.js b/docs/p06-customElement.html.js index 0bc4c81..a740e4b 100644 --- a/docs/p06-customElement.html.js +++ b/docs/p06-customElement.html.js @@ -53,90 +53,236 @@ const references= { export function page({ pkg, info }){ const page_id= info.id; return el(simplePage, { info, pkg }).append( - el("h2", t`Using web components in combinantion with DDE`), + el("h2", t`Using Web Components with DDE: Better Together`), el("p").append(...T` - The DDE library allows for use within ${el("a", references.mdn_web_components).append( el("strong", - t`Web Components`) )} for dom-tree generation. However, in order to be able to use signals (possibly - mapping to registered ${el("a", references.mdn_observedAttributes).append( el("code", "observedAttributes") - )}) and additional functionality is (unfortunately) required to use helpers provided by the library. + DDE pairs powerfully with ${el("a", references.mdn_web_components).append(el("strong", t`Web Components`))} + to create reusable, encapsulated custom elements with all the benefits of DDE's declarative DOM + construction and reactivity system. `), + el("div", { class: "dde-callout" }).append( + el("h4", t`Why Combine DDE with Web Components?`), + el("ul").append( + el("li", t`Declarative DOM creation within your components`), + el("li", t`Reactive attribute updates through signals`), + el("li", t`Simplified event handling with the same events API`), + el("li", t`Clean component lifecycle management`), + el("li", t`Improved code organization with scopes`) + ) + ), el(code, { src: fileURL("./components/examples/customElement/intro.js"), page_id }), - el(h3, t`Custom Elements Introduction`), + el(h3, t`Getting Started: Web Components Basics`), el("p").append(...T` - 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. - `), - el("p").append(...T` - To start with, let’s see how to use native Custom Elements. As starting point please read - ${el("a", references.mdn_custom_elements).append( el("strong", t`Using Custom Elements`), t` on MDN` )}. - To sum up and for mnemonic see following code overview: - `), - el(code, { src: fileURL("./components/examples/customElement/native-basic.js"), page_id }), - el("p").append(...T` - For more advanced use of Custom Elements, the summary ${el("a", references.custom_elements_tips) - .append( el("strong", t`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("code", `el(HTMLCustomElement.tagName, { customAttribute: "${t`new-value`}" });`)}). - `), - el("p").append(...T` - Also see the Life Cycle Events sections, very similarly we would like to use - ${el("a", { textContent: t`DDE events`, href: "./p03-events.html", title: t`See events part of the library - documentation` })}. To do it, the library provides function ${el("code", "customElementWithDDE")}… - `), - el(example, { src: fileURL("./components/examples/customElement/customElementWithDDE.js"), page_id }), - - el("h3", t`Custom Elements with DDE`), - el("p").append(...T` - The ${el("code", "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 ${el("code", "customElementRender")} with arguments instance reference, target for connection, - render function and optional properties (will be passed to the render function) see later… - `), - el(example, { src: fileURL("./components/examples/customElement/dde.js"), page_id }), - el("p").append(...T` - …as you can see, you can use components created based on the documentation previously introduced. To unlock - full potential, use with combination ${el("code", "customElementWithDDE")} (allows to use livecycle events) - and ${el("code", "observedAttributes")} (converts attributes to render function arguments — - ${el("em", "default")}) or ${el("code", "S.observedAttributes")} (converts attributes to signals). - `), - el(example, { src: fileURL("./components/examples/customElement/observedAttributes.js"), page_id }), - - - el(h3, t`Shadow DOM`), - el("p").append(...T` - 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. - `), - el(example, { src: fileURL("./components/examples/customElement/shadowRoot.js"), page_id }), - el("p").append(...T` - Regarding to ${el("code", "this.attachShadow({ mode: 'open' })")} see quick overview - ${el("a", { textContent: t`Using Shadow DOM`, ...references.mdn_shadow_dom_depth })}. An another source of - information can be ${el("a", { textContent: t`Shadow DOM in Depth`, ...references.shadow_dom_depth })}. - `), - el("p").append(...T` - 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("p").append(...T` - To sum up: + Web Components are a set of standard browser APIs that let you create custom HTML elements with + encapsulated functionality. They consist of three main technologies: `), el("ul").append( el("li").append(...T` - The use of shadow DOM to encapsulate the internal structure of the custom element, which affects how - the custom element can be styled and modified using JavaScript and CSS. + ${el("strong", "Custom Elements:")} Create your own HTML tags with JS-defined behavior `), el("li").append(...T` - The ability to access and modify the internal structure of the custom element using JavaScript, which - is affected by the use of shadow DOM and the mode of the shadow DOM. + ${el("strong", "Shadow DOM:")} Encapsulate styles and markup within a component `), el("li").append(...T` - The use of slots to allow for the insertion of content from the parent document into the custom - element, which is affected by the use of shadow DOM and the mode of the shadow DOM. + ${el("strong", "HTML Templates:")} Define reusable markup structures + `) + ), + el("p").append(...T` + Let's start with a basic Custom Element example without DDE to establish the foundation: + `), + el(code, { src: fileURL("./components/examples/customElement/native-basic.js"), page_id }), + + el("div", { class: "dde-note" }).append( + el("p").append(...T` + For complete information on Web Components, see the + ${el("a", references.mdn_custom_elements).append(el("strong", t`MDN documentation`))}. + Also, ${el("a", references.custom_elements_tips).append(el("strong", t`Handy Custom Elements Patterns`))} + provides useful techniques for connecting attributes with properties. + `) + ), + + el(h3, t`DDE Integration: Step 1 - Event Handling`), + el("p").append(...T` + The first step in integrating DDE with Web Components is enabling DDE's event system to work with your + Custom Elements. This is done with ${el("code", "customElementWithDDE")}, which makes your Custom Element + compatible with DDE's event handling. + `), + el("div", { class: "dde-function-table" }).append( + el("h4", t`customElementWithDDE`), + el("dl").append( + el("dt", t`Purpose`), + el("dd", t`Enables DDE's event system to work with your Custom Element`), + el("dt", t`Usage`), + el("dd", t`customElementWithDDE(YourElementClass)`), + el("dt", t`Benefits`), + el("dd", t`Allows using on.connected(), on.disconnected(), etc. with your element`) + ) + ), + el(example, { src: fileURL("./components/examples/customElement/customElementWithDDE.js"), page_id }), + + el("div", { class: "dde-tip" }).append( + el("p").append(...T` + ${el("strong", "Key Point:")} The ${el("code", "customElementWithDDE")} function adds event dispatching + to your Custom Element lifecycle methods, making them work seamlessly with DDE's event system. + `) + ), + + el(h3, t`DDE Integration: Step 2 - Rendering Components`), + el("p").append(...T` + The next step is to use DDE's component rendering within your Custom Element. This is done with + ${el("code", "customElementRender")}, which connects your DDE component function to the Custom Element. + `), + el("div", { class: "dde-function-table" }).append( + el("h4", t`customElementRender`), + el("dl").append( + el("dt", t`Purpose`), + el("dd", t`Connects a DDE component function to a Custom Element`), + el("dt", t`Parameters`), + el("dd").append( + el("ol").append( + el("li", t`Target (usually this or this.shadowRoot)`), + el("li", t`Component function that returns a DOM tree`), + el("li", t`Optional: Attributes transformer function (default or S.observedAttributes)`) + ) + ), + el("dt", t`Returns`), + el("dd", t`The rendered DOM tree`) + ) + ), + el(example, { src: fileURL("./components/examples/customElement/dde.js"), page_id }), + + el("div", { class: "dde-note" }).append( + el("p").append(...T` + In this example, we're using Shadow DOM (${el("code", "this.attachShadow()")}) for encapsulation, + but you can also render directly to the element with ${el("code", "customElementRender(this, ...)")}. + `) + ), + + el(h3, t`Reactive Web Components with Signals`), + el("p").append(...T` + One of the most powerful features of integrating DDE with Web Components is connecting HTML attributes + to DDE's reactive signals system. This creates truly reactive custom elements. + `), + el("div", { class: "dde-tip" }).append( + el("p").append(...T` + ${el("strong", "Two Ways to Handle Attributes:")} `), + el("ol").append( + el("li").append(...T` + ${el("code", "observedAttributes")} - Passes attributes as regular values (static) + `), + el("li").append(...T` + ${el("code", "S.observedAttributes")} - Transforms attributes into signals (reactive) + `) + ) + ), + el("p").append(...T` + Using ${el("code", "S.observedAttributes")} creates a reactive connection between your element's attributes + and its internal rendering. When attributes change, your component automatically updates! + `), + el(example, { src: fileURL("./components/examples/customElement/observedAttributes.js"), page_id }), + + el("div", { class: "dde-callout" }).append( + el("h4", t`How S.observedAttributes Works`), + el("p").append(...T` + 1. Takes each attribute listed in static observedAttributes + 2. Creates a DDE signal for each one + 3. Automatically updates these signals when attributes change + 4. Passes the signals to your component function + 5. Your component reacts to changes through signal subscriptions + `) + ), + + el(h3, t`Working with Shadow DOM`), + el("p").append(...T` + Shadow DOM provides encapsulation for your component's styles and markup. When using DDE with Shadow DOM, + you get the best of both worlds: encapsulation plus declarative DOM creation. + `), + el("div", { class: "dde-illustration" }).append( + el("h4", t`Shadow DOM Encapsulation`), + el("pre").append(el("code", ` +┌─────────────────────────────────┐ +│ │ +│ │ +│ ┌─────────────────────────┐ │ +│ │ #shadow-root │ │ +│ │ │ │ +│ │ Created with DDE: │ │ +│ │ ┌─────────────────┐ │ │ +│ │ │
│ │ │ +│ │ │

Title

│ │ │ +│ │ │

Content

│ │ │ +│ │ └─────────────────┘ │ │ +│ │ │ │ +│ └─────────────────────────┘ │ +│ │ +└─────────────────────────────────┘ + `)) + ), + el(example, { src: fileURL("./components/examples/customElement/shadowRoot.js"), page_id }), + + el("p").append(...T` + For more information on Shadow DOM, see + ${el("a", { textContent: t`Using Shadow DOM`, ...references.mdn_shadow_dom_depth })}, or the comprehensive + ${el("a", { textContent: t`Shadow DOM in Depth`, ...references.shadow_dom_depth })}. + `), + + 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: + `), + el("div", { class: "dde-function-table" }).append( + el("h4", t`simulateSlots`), + el("dl").append( + el("dt", t`Purpose`), + el("dd", t`Provides slot functionality when you cannot/do not want to use shadow DOM`), + el("dt", t`Parameters`), + 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", { class: "dde-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` + When combining DDE with Web Components, follow these recommendations: + `), + el("ol").append( + el("li").append(...T` + ${el("strong", "Always use customElementWithDDE")} to enable event integration + `), + el("li").append(...T` + ${el("strong", "Prefer S.observedAttributes")} for reactive attribute connections + `), + el("li").append(...T` + ${el("strong", "Create reusable component functions")} that your custom elements render + `), + el("li").append(...T` + ${el("strong", "Use scope.host()")} to clean up event listeners and subscriptions + `), + el("li").append(...T` + ${el("strong", "Add setters and getters")} for better property access to your element + `) + ), + + el("div", { class: "dde-troubleshooting" }).append( + el("h4", t`Common Issues`), + el("dl").append( + el("dt", t`Events not firing properly`), + el("dd", t`Make sure you called customElementWithDDE before defining the element`), + el("dt", t`Attributes not updating`), + el("dd", t`Check that you've properly listed them in static observedAttributes`), + el("dt", t`Component not rendering`), + el("dd", t`Verify customElementRender is called in connectedCallback, not constructor`) + ) ), el(mnemonic)