1
0
mirror of https://github.com/jaandrle/deka-dom-el synced 2025-04-02 04:05:52 +02:00
deka-dom-el/docs/p05-scopes.html.js
Jan Andrle 4c450ae763
🐛 🔤 v0.9.4-alpha (#42)
* 🐛 fixes #41

*  adjust package size limits

* 🔤

* 📺 requestIdleCallback doesn need to be global

* 🔤 corrects irland page headers

* 📺 version

*  Signal ← SignalReadonly

* 🐛 ensures only one disconncetd listener

…for cleanup

*  🔤 Better build and improve texting

* 🐛 logo alignemt (due to gh)

* 🔤 md enhancements

* 🔤  products
2025-03-19 17:10:43 +01:00

206 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { T, t } from "./utils/index.js";
export const info= {
title: t`Scopes and Components`,
fullTitle: t`Building Maintainable UIs with Scopes and Components`,
description: t`Organizing UI into reusable, manageable components`,
};
import { el } from "deka-dom-el";
import { simplePage } from "./layout/simplePage.html.js";
import { example } from "./components/example.html.js";
import { h3 } from "./components/pageUtils.html.js";
import { mnemonic } from "./components/mnemonic/scopes-init.js";
import { code, pre } from "./components/code.html.js";
/** @param {string} url */
const fileURL= url=> new URL(url, import.meta.url);
const references= {
/** Garbage collection on MDN */
garbage_collection: {
title: t`MDN documentation page for Garbage collection`,
href: "https://developer.mozilla.org/en-US/docs/Glossary/Garbage_collection",
},
/** Signals */
signals: {
title: t`Signals section on this library`,
href: "./p04-signals#h-introducing-signals",
}
};
/** @param {import("./types.d.ts").PageAttrs} attrs */
export function page({ pkg, info }){
return el(simplePage, { info, pkg }).append(
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 components life-cycle and provide JavaScript the way to properly use
the ${el("a", { textContent: t`Garbage collection`, ...references.garbage_collection })}.
`),
el(code, { src: fileURL("./components/examples/scopes/intro.js") }),
el("p").append(T`The library therefore uses ${el("em", t`scopes`)} to provide these functionalities.`),
el(h3, t`Understanding Host Elements and Scopes`),
el("p").append(T`
The ${el("strong", "host")} is the name for the element representing the component. This is typically the
element returned by a function. To get a reference, you can use ${el("code", "scope.host()")}. To apply addons,
just use ${el("code", "scope.host(...<addons>)")}.
`),
el("p").append(T`
Scopes are primarily needed when signals are used in DOM templates (with ${el("code", "el")}, ${el("code",
"assign")}, or ${el("code", "S.el")}). They provide a way for automatically removing signal listeners
and cleaning up unused signals when components are removed from the DOM.
`),
el("div", { className: "illustration" }).append(
el("h4", t`Component Anatomy`),
el(pre, { content: `
// 1. Component scope created
el(MyComponent);
function MyComponent() {
// 2. access the host element (or other scope related values)
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("div", { className: "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") }),
el("div", { className: "tip" }).append(
el("p").append(T`
${el("strong", "Best Practice:")} Always capture the host reference (or other scope related values) at
the beginning of your component function using ${el("code", "const { host } = scope")} to avoid
scope-related issues, especially with ${el("em", "asynchronous code")}.
`),
el("p").append(T`
If you are interested in the implementation details, see Class-Based Components section.
`)
),
el(code, { src: fileURL("./components/examples/scopes/good-practise.js") }),
el(h3, t`Class-Based Components`),
el("p").append(T`
While functional components are the primary pattern in dd<el>, you can also create class-based components.
For this, we implement function ${el("code", "elClass")} and use it to demonstrate implementation details
for better understanding of the scope logic.
`),
el(example, { src: fileURL("./components/examples/scopes/class-component.js") }),
el(h3, t`Automatic Cleanup with Scopes`),
el("p").append(T`
One of the most powerful features of scopes is automatic cleanup when components are removed from the DOM.
This prevents memory leaks and ensures resources are properly released.
`),
el("div", { className: "illustration" }).append(
el("h4", t`Lifecycle Flow`),
el(pre, { content: `
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 (browser)
- Signal subscriptions (dd<el> and browser)
- Custom cleanup code (dd<el> and user)
` })
),
el(example, { src: fileURL("./components/examples/scopes/cleaning.js") }),
el("div", { className: "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 ${el("em",
"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 library DOM API and signals work best when used declaratively. It means you split your apps logic
into three parts as introduced in ${el("a", { textContent: "Signals (3PS)", ...references.signals })}.
`),
el("div", { className: "note" }).append(
el("p").append(T`
Strictly speaking, the imperative way of using the library is not prohibited. Just be careful to avoid
mixing the declarative approach (using signals) with imperative manipulation of elements.
`)
),
el("div", { className: "tabs" }).append(
el("div", { className: "tab", dataTab: "declarative" }).append(
el("h4", t`✅ Declarative Approach`),
el("p", t`Define what your UI should look like based on state:`),
el(code, { src: fileURL("./components/examples/scopes/declarative.js") })
),
el("div", { className: "tab", dataTab: "imperative" }).append(
el("h4", t`⚠️ Imperative Approach`),
el("p", t`Manually update the DOM in response to events:`),
el(code, { src: fileURL("./components/examples/scopes/imperative.js") })
),
el("div", { className: "tab", dataTab: "mixed" }).append(
el("h4", t`❌ Mixed Approach`),
el("p", t`This approach should be avoided:`),
el(code, { src: fileURL("./components/examples/scopes/mixed.js") })
)
),
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", "Define signals as constants:")} ${el("code", "const counter = S(0);")}
`),
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 dd<el>, use ${el("code",
"on.disconnected")}
`)
),
el("div", { className: "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(s)`)
)
),
el(mnemonic)
);
}