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

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)
);
}