1
0
mirror of https://github.com/jaandrle/deka-dom-el synced 2025-04-01 19:55:53 +02:00
This commit is contained in:
Jan Andrle 2025-03-04 17:52:55 +01:00
parent 508d93bb1a
commit 1b0312f6bd
4 changed files with 639 additions and 158 deletions

View File

@ -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"

View File

@ -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`
Lets 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`Publishsubscribe 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`Publishsubscribe 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(<data>, <actions>)")} pattern creates a store machine. We can
then invoke (dispatch) registered action by calling ${el("code", "S.action(<signal>, <name>, ...<args>)")}
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)
);
}

View File

@ -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(...<addons>)")}.
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)
);
}

View File

@ -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, lets 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 components internal DOM tree
from the rest of the document. This means that styles and scripts applied to the document will not affect
the components 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`<slot>`), 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", `
<my-custom-element>
#shadow-root
Created with DDE:
<div>
<h2>Title</h2>
<p>Content</p>
`))
),
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)