mirror of
https://github.com/jaandrle/deka-dom-el
synced 2025-07-29 07:00:16 +02:00
🔤
This commit is contained in:
@@ -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)
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user