mirror of
https://github.com/jaandrle/deka-dom-el
synced 2025-04-01 19:55:53 +02:00
Compare commits
14 Commits
1b0312f6bd
...
17e40fdd9c
Author | SHA1 | Date | |
---|---|---|---|
17e40fdd9c | |||
05413cb2bb | |||
5a6f011823 | |||
49243b978a | |||
02f7b3fd67 | |||
7078ec68c1 | |||
41d7728d18 | |||
8f0879196f | |||
9ed6de2f8a | |||
2a3b6dc5cd | |||
1c5f0dab5e | |||
e1f2b32736 | |||
e2df9705d1 | |||
209fa49dee |
166
README.md
166
README.md
@ -2,94 +2,114 @@
|
||||
| [source code on GitHub](https://github.com/jaandrle/deka-dom-el)
|
||||
| [*mirrored* on Gitea](https://gitea.jaandrle.cz/jaandrle/deka-dom-el)
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/assets/logo.svg" alt="Deka DOM Elements Logo" width="180" height="180">
|
||||
</p>
|
||||
|
||||
# Deka DOM Elements
|
||||
|
||||
***Vanilla for flavouring — a full-fledged feast for large projects***
|
||||
|
||||
*…use simple DOM API by default and library tools and logic when you need them*
|
||||
|
||||
```javascript
|
||||
// 🌟 Reactive component with clear separation of concerns
|
||||
document.body.append(
|
||||
el(HelloWorldComponent, { initial: "🚀" })
|
||||
el(EmojiCounter, { initial: "🚀" })
|
||||
);
|
||||
/** @typedef {"🎉" | "🚀"} Emoji */
|
||||
/** @param {{ initial: Emoji }} attrs */
|
||||
function HelloWorldComponent({ initial }){
|
||||
const clicks= S(0);
|
||||
const emoji= S(initial);
|
||||
/** @param {HTMLOptionElement} el */
|
||||
const isSelected= el=> (el.selected= el.value===initial);
|
||||
// @ts-expect-error 2339: The <select> has only two options with {@link Emoji}
|
||||
const onChange= on("change", event=> emoji.set(event.target.value));
|
||||
|
||||
return el().append(
|
||||
el("p", {
|
||||
textContent: S(() => `Hello World ${emoji.get().repeat(clicks.get())}`),
|
||||
className: "example",
|
||||
ariaLive: "polite", //OR ariaset: { live: "polite" },
|
||||
dataset: { example: "Example" }, //OR dataExample: "Example",
|
||||
}),
|
||||
el("button",
|
||||
{ textContent: "Fire", type: "button" },
|
||||
on("click", ()=> clicks.set(clicks.get() + 1)),
|
||||
on("keyup", ()=> clicks.set(clicks.get() - 2)),
|
||||
),
|
||||
el("select", null, onChange).append(
|
||||
el(OptionComponent, "🎉", isSelected),//OR { textContent: "🎉" }
|
||||
el(OptionComponent, "🚀", isSelected),//OR { textContent: "🚀" }
|
||||
)
|
||||
);
|
||||
function EmojiCounter({ initial }) {
|
||||
// ✨ State - Define reactive data
|
||||
const count = S(0);
|
||||
const emoji = S(initial);
|
||||
|
||||
/** @param {HTMLOptionElement} el */
|
||||
const isSelected= el=> (el.selected= el.value===initial);
|
||||
|
||||
// 🔄 View - UI updates automatically when signals change
|
||||
return el().append(
|
||||
el("p", {
|
||||
className: "output",
|
||||
textContent: S(() =>
|
||||
`Hello World ${emoji.get().repeat(clicks.get())}`),
|
||||
}),
|
||||
|
||||
// 🎮 Controls - Update state on events
|
||||
el("button", { textContent: "Add Emoji" },
|
||||
on("click", () => count.set(count.get() + 1))
|
||||
),
|
||||
|
||||
el("select", null, on("change", e => emoji.set(e.target.value)))
|
||||
.append(
|
||||
el(Option, "🎉", isSelected),
|
||||
el(Option, "🚀", isSelected),
|
||||
el(Option, "💖", isSelected),
|
||||
)
|
||||
);
|
||||
}
|
||||
function OptionComponent({ textContent }){
|
||||
return el("option", { value: textContent, textContent })
|
||||
function Option({ textContent }){
|
||||
return Ol("option", { value: textContent, textContent });
|
||||
}
|
||||
```
|
||||
# Deka DOM Elements
|
||||
Creating reactive elements, components and Web components using [IDL](https://developer.mozilla.org/en-US/docs/
|
||||
Glossary/IDL)/JavaScript DOM API and [**signals/observables**](#signals).
|
||||
|
||||
## Inspiration and suggested alternatives
|
||||
- my previous library (mostly used internaly): [jaandrle/dollar_dom_component: Functional DOM components without
|
||||
JSX and virtual DOM.](https://github.com/jaandrle/dollar_dom_component)
|
||||
- [vanjs-org/van: 🍦 VanJS: World's smallest reactive UI framework. Incredibly Powerful, Insanely Small -
|
||||
Everyone can build a useful UI app in an hour.](https://github.com/vanjs-org/van)
|
||||
- [hyperhype/hyperscript: Create HyperText with JavaScript.](https://github.com/hyperhype/hyperscript)
|
||||
- [adamhaile/S: S.js - Simple, Clean, Fast Reactive Programming in Javascript](https://github.com/adamhaile/S)
|
||||
([adamhaile/surplus: High performance JSX web views for S.js applications](https://github.com/adamhaile/surplus))
|
||||
- [potch/signals: a small reactive signals library](https://github.com/potch/signals)
|
||||
Creating reactive elements, components, and Web Components using the native
|
||||
[IDL](https://developer.mozilla.org/en-US/docs/Glossary/IDL)/JavaScript DOM API enhanced with
|
||||
[**signals/observables**](#understanding-signals).
|
||||
|
||||
## Why an another one?
|
||||
This library falls somewhere between van/hyperscript and [solid-js](https://github.com/solidjs/solid) in terms of size,
|
||||
complexity, and usability.
|
||||
## Features at a Glance
|
||||
|
||||
Another goal is to proceed in the best spirit of functional programming. This involves starting with
|
||||
pure JavaScript (DOM API) and gradually adding auxiliary functions, ranging from “minor” improvements
|
||||
to the capability of writing complete declarative reactive UI templates.
|
||||
- ✅ **No build step required** — use directly in browsers or Node.js
|
||||
- ☑️ **Lightweight** — ~10-15kB minified (original goal 10kB) with zero/minimal dependencies
|
||||
- ✅ **Declarative & functional approach** for clean, maintainable code
|
||||
- ✅ **Optional signals** with support for custom reactive implementations
|
||||
- ✅ **Server-side rendering** support via [jsdom](https://github.com/jsdom/jsdom)
|
||||
- 🔄 **TypeScript support** (work in progress)
|
||||
- 🔄 **Enhanced Web Components** support (work in progress)
|
||||
|
||||
As a result, any “internal” function (`assign`, `classListDeclarative`, `on`, `dispatchEvent`, …, `S`, …)
|
||||
can be used independently, although they are primarily designed for use in combination. This can also,
|
||||
hopefully, help in integrating the library into existing projects.
|
||||
## Why Another Library?
|
||||
|
||||
To balance these requirements, numerous compromises have been made. To summarize:
|
||||
- [ ] Library size: 10–15kB minified (the original goal was a maximum of 10kB)
|
||||
- [x] Optional use of *signals* with the ability to register *your own signals/observables implementation*
|
||||
- [x] *No build step required*
|
||||
- [x] Preference for a *declarative/functional* approach
|
||||
- [x] Focus on zero/minimal dependencies
|
||||
- [ ] Support for/enhancement of custom elements (web components)
|
||||
- [x] Support for SSR ([jsdom](https://github.com/jsdom/jsdom))
|
||||
- [ ] WIP providing types
|
||||
This library bridges the gap between minimal solutions like van/hyperscript and more comprehensive frameworks like [solid-js](https://github.com/solidjs/solid), offering a balanced trade-off between size, complexity, and usability.
|
||||
|
||||
## First steps
|
||||
- [**Guide**](https://jaandrle.github.io/deka-dom-el)
|
||||
- Documentation
|
||||
- Installation
|
||||
- npm
|
||||
- [dist/](dist/) (`https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/`…)
|
||||
Following functional programming principles, Deka DOM Elements starts with pure JavaScript (DOM API) and gradually adds auxiliary functions. These range from minor improvements to advanced features for building complete declarative reactive UI templates.
|
||||
|
||||
## Signals
|
||||
- [Signals — whats going on behind the scenes \| by Ryan Hoffnan \|
|
||||
ITNEXT](https://itnext.io/signals-whats-going-on-behind-the-scenes-ec858589ea63)
|
||||
- [The Evolution of Signals in JavaScript - DEV
|
||||
Community](https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob)
|
||||
- there is also [tc39/proposal-signals:
|
||||
A proposal to add signals to JavaScript.](https://github.com/tc39/proposal-signals)
|
||||
- [Observer pattern - Wikipedia](https://en.wikipedia.org/wiki/Observer_pattern)
|
||||
A key advantage: any internal function (`assign`, `classListDeclarative`, `on`, `dispatchEvent`, `S`, etc.) can be used independently while also working seamlessly together. This modular approach makes it easier to integrate the library into existing projects.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installation
|
||||
|
||||
#### npm
|
||||
```bash
|
||||
# TBD
|
||||
# npm install deka-dom-el
|
||||
```
|
||||
|
||||
#### Direct Script
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/dde-with-signals.min.js"></script>
|
||||
<script type="module">
|
||||
const { el, S } = dde;
|
||||
</script>
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
- [**Interactive Guide**](https://jaandrle.github.io/deka-dom-el): WIP
|
||||
- [Examples](./examples/): TBD/WIP
|
||||
|
||||
## Understanding Signals
|
||||
|
||||
Signals are the reactive backbone of Deka DOM Elements:
|
||||
|
||||
- [Signals — what's going on behind the scenes](https://itnext.io/signals-whats-going-on-behind-the-scenes-ec858589ea63)
|
||||
- [The Evolution of Signals in JavaScript](https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob)
|
||||
- [TC39 Signals Proposal](https://github.com/tc39/proposal-signals) (future standard)
|
||||
- [Observer pattern](https://en.wikipedia.org/wiki/Observer_pattern) (underlying concept)
|
||||
|
||||
## Inspiration and Alternatives
|
||||
|
||||
- [vanjs-org/van](https://github.com/vanjs-org/van) - World's smallest reactive UI framework
|
||||
- [adamhaile/S](https://github.com/adamhaile/S) - Simple, clean, fast reactive programming
|
||||
- [hyperhype/hyperscript](https://github.com/hyperhype/hyperscript) - Create HyperText with JavaScript
|
||||
- [potch/signals](https://github.com/potch/signals) - A small reactive signals library
|
||||
- [jaandrle/dollar_dom_component](https://github.com/jaandrle/dollar_dom_component) - Functional DOM components without JSX/virtual DOM
|
||||
|
11
bs/docs.js
11
bs/docs.js
@ -14,6 +14,10 @@ if(s.test("-d", path_target.root)){
|
||||
echo("Creating directory…");
|
||||
s.mkdir("-p", path_target.root);
|
||||
}
|
||||
|
||||
// Create assets directory in target
|
||||
echo("Creating assets directory…");
|
||||
s.mkdir("-p", path_target.root+"assets");
|
||||
echo("Collecting list of pages…");
|
||||
const pages= s.ls($.xdg.main`../docs/*.html.js`).map(addPage);
|
||||
for(const { id, info } of pages){
|
||||
@ -33,6 +37,13 @@ for(const { id, info } of pages){
|
||||
s.echo(serverDOM.serialize()).to(path_target.root+id+".html");
|
||||
}
|
||||
s.echo(styles.content).to(path_target.css+styles.name);
|
||||
|
||||
// Copy assets
|
||||
echo("Copying assets…");
|
||||
if(s.test("-d", "docs/assets")) {
|
||||
s.cp("-r", "docs/assets/*", path_target.assets);
|
||||
}
|
||||
|
||||
dispatchEvent("onssrend");
|
||||
echo("Done");
|
||||
|
||||
|
9
dist/dde-with-signals.js
vendored
9
dist/dde-with-signals.js
vendored
@ -177,6 +177,11 @@ var scope = {
|
||||
pop() {
|
||||
if (scopes.length === 1) return;
|
||||
return scopes.pop();
|
||||
},
|
||||
isolate(fn) {
|
||||
this.push({ prevent: true });
|
||||
fn();
|
||||
this.pop();
|
||||
}
|
||||
};
|
||||
function append(...els) {
|
||||
@ -594,6 +599,10 @@ function observedAttributes2(instance) {
|
||||
|
||||
// src/events.js
|
||||
function dispatchEvent(name, options, host) {
|
||||
if (typeof options === "function") {
|
||||
host = options;
|
||||
options = null;
|
||||
}
|
||||
if (!options) options = {};
|
||||
return function dispatch(element, ...d) {
|
||||
if (host) {
|
||||
|
2
dist/dde-with-signals.min.js
vendored
2
dist/dde-with-signals.min.js
vendored
File diff suppressed because one or more lines are too long
9
dist/dde.js
vendored
9
dist/dde.js
vendored
@ -154,6 +154,11 @@ var scope = {
|
||||
pop() {
|
||||
if (scopes.length === 1) return;
|
||||
return scopes.pop();
|
||||
},
|
||||
isolate(fn) {
|
||||
this.push({ prevent: true });
|
||||
fn();
|
||||
this.pop();
|
||||
}
|
||||
};
|
||||
function append(...els) {
|
||||
@ -571,6 +576,10 @@ function observedAttributes2(instance) {
|
||||
|
||||
// src/events.js
|
||||
function dispatchEvent(name, options, host) {
|
||||
if (typeof options === "function") {
|
||||
host = options;
|
||||
options = null;
|
||||
}
|
||||
if (!options) options = {};
|
||||
return function dispatch(element, ...d) {
|
||||
if (host) {
|
||||
|
10
dist/dde.min.js
vendored
10
dist/dde.min.js
vendored
File diff suppressed because one or more lines are too long
39
dist/esm-with-signals.d.min.ts
vendored
39
dist/esm-with-signals.d.min.ts
vendored
@ -54,7 +54,8 @@ type IsReadonly<T, K extends keyof T> =
|
||||
type ElementAttributes<T extends SupportedElement>= Partial<{
|
||||
[K in keyof _fromElsInterfaces<T>]:
|
||||
_fromElsInterfaces<T>[K] extends ((...p: any[])=> any)
|
||||
? _fromElsInterfaces<T>[K] | ((...p: Parameters<_fromElsInterfaces<T>[K]>)=> ddeSignal<ReturnType<_fromElsInterfaces<T>[K]>>)
|
||||
? _fromElsInterfaces<T>[K] | ((...p: Parameters<_fromElsInterfaces<T>[K]>)=>
|
||||
ddeSignal<ReturnType<_fromElsInterfaces<T>[K]>>)
|
||||
: (IsReadonly<_fromElsInterfaces<T>, K> extends false
|
||||
? _fromElsInterfaces<T>[K] | ddeSignal<_fromElsInterfaces<T>[K]>
|
||||
: ddeStringable)
|
||||
@ -71,6 +72,26 @@ export function assignAttribute<El extends SupportedElement, ATT extends keyof E
|
||||
): ElementAttributes<El>[ATT]
|
||||
|
||||
type ExtendedHTMLElementTagNameMap= HTMLElementTagNameMap & CustomElementTagNameMap;
|
||||
export function el<
|
||||
A extends ddeComponentAttributes,
|
||||
EL extends SupportedElement | ddeDocumentFragment
|
||||
>(
|
||||
component: (attr: A)=> EL,
|
||||
attrs?: NoInfer<A>,
|
||||
...addons: ddeElementAddon<EL>[]
|
||||
): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
|
||||
? EL
|
||||
: ( EL extends ddeDocumentFragment ? EL : ddeHTMLElement )
|
||||
export function el<
|
||||
A extends { textContent: ddeStringable },
|
||||
EL extends SupportedElement | ddeDocumentFragment
|
||||
>(
|
||||
component: (attr: A)=> EL,
|
||||
attrs?: NoInfer<A>["textContent"],
|
||||
...addons: ddeElementAddon<EL>[]
|
||||
): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
|
||||
? EL
|
||||
: ( EL extends ddeDocumentFragment ? EL : ddeHTMLElement )
|
||||
export function el<
|
||||
TAG extends keyof ExtendedHTMLElementTagNameMap,
|
||||
>(
|
||||
@ -88,16 +109,6 @@ export function el(
|
||||
attrs?: ElementAttributes<HTMLElement> | ddeStringable,
|
||||
...addons: ddeElementAddon<HTMLElement>[]
|
||||
): ddeHTMLElement
|
||||
|
||||
export function el<
|
||||
C extends (attr: ddeComponentAttributes)=> SupportedElement | ddeDocumentFragment
|
||||
>(
|
||||
component: C,
|
||||
attrs?: Parameters<C>[0] | ddeStringable,
|
||||
...addons: ddeElementAddon<ReturnType<C>>[]
|
||||
): ReturnType<C> extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
|
||||
? ReturnType<C>
|
||||
: ( ReturnType<C> extends ddeDocumentFragment ? ReturnType<C> : ddeHTMLElement )
|
||||
export { el as createElement }
|
||||
|
||||
export function elNS(
|
||||
@ -146,6 +157,8 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(
|
||||
body: EL,
|
||||
): EL
|
||||
|
||||
export function dispatchEvent(name: keyof DocumentEventMap | string, element: SupportedElement):
|
||||
(data?: any)=> void;
|
||||
export function dispatchEvent(name: keyof DocumentEventMap | string, options?: EventInit):
|
||||
(element: SupportedElement, data?: any)=> void;
|
||||
export function dispatchEvent(
|
||||
@ -219,11 +232,13 @@ export const scope: {
|
||||
|
||||
state: Scope[],
|
||||
/** Adds new child scope. All attributes are inherited by default. */
|
||||
push(scope: Partial<Scope>): ReturnType<Array<Scope>["push"]>,
|
||||
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>,
|
||||
/** Adds root scope as a child of the current scope. */
|
||||
pushRoot(): ReturnType<Array<Scope>["push"]>,
|
||||
/** Removes last/current child scope. */
|
||||
pop(): ReturnType<Array<Scope>["pop"]>,
|
||||
/** Runs function in a new (isolated) scope */
|
||||
isolate(fn: Function): void,
|
||||
};
|
||||
|
||||
export function customElementRender<
|
||||
|
39
dist/esm-with-signals.d.ts
vendored
39
dist/esm-with-signals.d.ts
vendored
@ -54,7 +54,8 @@ type IsReadonly<T, K extends keyof T> =
|
||||
type ElementAttributes<T extends SupportedElement>= Partial<{
|
||||
[K in keyof _fromElsInterfaces<T>]:
|
||||
_fromElsInterfaces<T>[K] extends ((...p: any[])=> any)
|
||||
? _fromElsInterfaces<T>[K] | ((...p: Parameters<_fromElsInterfaces<T>[K]>)=> ddeSignal<ReturnType<_fromElsInterfaces<T>[K]>>)
|
||||
? _fromElsInterfaces<T>[K] | ((...p: Parameters<_fromElsInterfaces<T>[K]>)=>
|
||||
ddeSignal<ReturnType<_fromElsInterfaces<T>[K]>>)
|
||||
: (IsReadonly<_fromElsInterfaces<T>, K> extends false
|
||||
? _fromElsInterfaces<T>[K] | ddeSignal<_fromElsInterfaces<T>[K]>
|
||||
: ddeStringable)
|
||||
@ -71,6 +72,26 @@ export function assignAttribute<El extends SupportedElement, ATT extends keyof E
|
||||
): ElementAttributes<El>[ATT]
|
||||
|
||||
type ExtendedHTMLElementTagNameMap= HTMLElementTagNameMap & CustomElementTagNameMap;
|
||||
export function el<
|
||||
A extends ddeComponentAttributes,
|
||||
EL extends SupportedElement | ddeDocumentFragment
|
||||
>(
|
||||
component: (attr: A)=> EL,
|
||||
attrs?: NoInfer<A>,
|
||||
...addons: ddeElementAddon<EL>[]
|
||||
): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
|
||||
? EL
|
||||
: ( EL extends ddeDocumentFragment ? EL : ddeHTMLElement )
|
||||
export function el<
|
||||
A extends { textContent: ddeStringable },
|
||||
EL extends SupportedElement | ddeDocumentFragment
|
||||
>(
|
||||
component: (attr: A)=> EL,
|
||||
attrs?: NoInfer<A>["textContent"],
|
||||
...addons: ddeElementAddon<EL>[]
|
||||
): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
|
||||
? EL
|
||||
: ( EL extends ddeDocumentFragment ? EL : ddeHTMLElement )
|
||||
export function el<
|
||||
TAG extends keyof ExtendedHTMLElementTagNameMap,
|
||||
>(
|
||||
@ -88,16 +109,6 @@ export function el(
|
||||
attrs?: ElementAttributes<HTMLElement> | ddeStringable,
|
||||
...addons: ddeElementAddon<HTMLElement>[]
|
||||
): ddeHTMLElement
|
||||
|
||||
export function el<
|
||||
C extends (attr: ddeComponentAttributes)=> SupportedElement | ddeDocumentFragment
|
||||
>(
|
||||
component: C,
|
||||
attrs?: Parameters<C>[0] | ddeStringable,
|
||||
...addons: ddeElementAddon<ReturnType<C>>[]
|
||||
): ReturnType<C> extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
|
||||
? ReturnType<C>
|
||||
: ( ReturnType<C> extends ddeDocumentFragment ? ReturnType<C> : ddeHTMLElement )
|
||||
export { el as createElement }
|
||||
|
||||
export function elNS(
|
||||
@ -146,6 +157,8 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(
|
||||
body: EL,
|
||||
): EL
|
||||
|
||||
export function dispatchEvent(name: keyof DocumentEventMap | string, element: SupportedElement):
|
||||
(data?: any)=> void;
|
||||
export function dispatchEvent(name: keyof DocumentEventMap | string, options?: EventInit):
|
||||
(element: SupportedElement, data?: any)=> void;
|
||||
export function dispatchEvent(
|
||||
@ -219,11 +232,13 @@ export const scope: {
|
||||
|
||||
state: Scope[],
|
||||
/** Adds new child scope. All attributes are inherited by default. */
|
||||
push(scope: Partial<Scope>): ReturnType<Array<Scope>["push"]>,
|
||||
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>,
|
||||
/** Adds root scope as a child of the current scope. */
|
||||
pushRoot(): ReturnType<Array<Scope>["push"]>,
|
||||
/** Removes last/current child scope. */
|
||||
pop(): ReturnType<Array<Scope>["pop"]>,
|
||||
/** Runs function in a new (isolated) scope */
|
||||
isolate(fn: Function): void,
|
||||
};
|
||||
|
||||
export function customElementRender<
|
||||
|
9
dist/esm-with-signals.js
vendored
9
dist/esm-with-signals.js
vendored
@ -175,6 +175,11 @@ var scope = {
|
||||
pop() {
|
||||
if (scopes.length === 1) return;
|
||||
return scopes.pop();
|
||||
},
|
||||
isolate(fn) {
|
||||
this.push({ prevent: true });
|
||||
fn();
|
||||
this.pop();
|
||||
}
|
||||
};
|
||||
function append(...els) {
|
||||
@ -592,6 +597,10 @@ function observedAttributes2(instance) {
|
||||
|
||||
// src/events.js
|
||||
function dispatchEvent(name, options, host) {
|
||||
if (typeof options === "function") {
|
||||
host = options;
|
||||
options = null;
|
||||
}
|
||||
if (!options) options = {};
|
||||
return function dispatch(element, ...d) {
|
||||
if (host) {
|
||||
|
2
dist/esm-with-signals.min.js
vendored
2
dist/esm-with-signals.min.js
vendored
File diff suppressed because one or more lines are too long
39
dist/esm.d.min.ts
vendored
39
dist/esm.d.min.ts
vendored
@ -54,7 +54,8 @@ type IsReadonly<T, K extends keyof T> =
|
||||
type ElementAttributes<T extends SupportedElement>= Partial<{
|
||||
[K in keyof _fromElsInterfaces<T>]:
|
||||
_fromElsInterfaces<T>[K] extends ((...p: any[])=> any)
|
||||
? _fromElsInterfaces<T>[K] | ((...p: Parameters<_fromElsInterfaces<T>[K]>)=> ddeSignal<ReturnType<_fromElsInterfaces<T>[K]>>)
|
||||
? _fromElsInterfaces<T>[K] | ((...p: Parameters<_fromElsInterfaces<T>[K]>)=>
|
||||
ddeSignal<ReturnType<_fromElsInterfaces<T>[K]>>)
|
||||
: (IsReadonly<_fromElsInterfaces<T>, K> extends false
|
||||
? _fromElsInterfaces<T>[K] | ddeSignal<_fromElsInterfaces<T>[K]>
|
||||
: ddeStringable)
|
||||
@ -71,6 +72,26 @@ export function assignAttribute<El extends SupportedElement, ATT extends keyof E
|
||||
): ElementAttributes<El>[ATT]
|
||||
|
||||
type ExtendedHTMLElementTagNameMap= HTMLElementTagNameMap & CustomElementTagNameMap;
|
||||
export function el<
|
||||
A extends ddeComponentAttributes,
|
||||
EL extends SupportedElement | ddeDocumentFragment
|
||||
>(
|
||||
component: (attr: A)=> EL,
|
||||
attrs?: NoInfer<A>,
|
||||
...addons: ddeElementAddon<EL>[]
|
||||
): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
|
||||
? EL
|
||||
: ( EL extends ddeDocumentFragment ? EL : ddeHTMLElement )
|
||||
export function el<
|
||||
A extends { textContent: ddeStringable },
|
||||
EL extends SupportedElement | ddeDocumentFragment
|
||||
>(
|
||||
component: (attr: A)=> EL,
|
||||
attrs?: NoInfer<A>["textContent"],
|
||||
...addons: ddeElementAddon<EL>[]
|
||||
): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
|
||||
? EL
|
||||
: ( EL extends ddeDocumentFragment ? EL : ddeHTMLElement )
|
||||
export function el<
|
||||
TAG extends keyof ExtendedHTMLElementTagNameMap,
|
||||
>(
|
||||
@ -88,16 +109,6 @@ export function el(
|
||||
attrs?: ElementAttributes<HTMLElement> | ddeStringable,
|
||||
...addons: ddeElementAddon<HTMLElement>[]
|
||||
): ddeHTMLElement
|
||||
|
||||
export function el<
|
||||
C extends (attr: ddeComponentAttributes)=> SupportedElement | ddeDocumentFragment
|
||||
>(
|
||||
component: C,
|
||||
attrs?: Parameters<C>[0] | ddeStringable,
|
||||
...addons: ddeElementAddon<ReturnType<C>>[]
|
||||
): ReturnType<C> extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
|
||||
? ReturnType<C>
|
||||
: ( ReturnType<C> extends ddeDocumentFragment ? ReturnType<C> : ddeHTMLElement )
|
||||
export { el as createElement }
|
||||
|
||||
export function elNS(
|
||||
@ -146,6 +157,8 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(
|
||||
body: EL,
|
||||
): EL
|
||||
|
||||
export function dispatchEvent(name: keyof DocumentEventMap | string, element: SupportedElement):
|
||||
(data?: any)=> void;
|
||||
export function dispatchEvent(name: keyof DocumentEventMap | string, options?: EventInit):
|
||||
(element: SupportedElement, data?: any)=> void;
|
||||
export function dispatchEvent(
|
||||
@ -219,11 +232,13 @@ export const scope: {
|
||||
|
||||
state: Scope[],
|
||||
/** Adds new child scope. All attributes are inherited by default. */
|
||||
push(scope: Partial<Scope>): ReturnType<Array<Scope>["push"]>,
|
||||
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>,
|
||||
/** Adds root scope as a child of the current scope. */
|
||||
pushRoot(): ReturnType<Array<Scope>["push"]>,
|
||||
/** Removes last/current child scope. */
|
||||
pop(): ReturnType<Array<Scope>["pop"]>,
|
||||
/** Runs function in a new (isolated) scope */
|
||||
isolate(fn: Function): void,
|
||||
};
|
||||
|
||||
export function customElementRender<
|
||||
|
39
dist/esm.d.ts
vendored
39
dist/esm.d.ts
vendored
@ -54,7 +54,8 @@ type IsReadonly<T, K extends keyof T> =
|
||||
type ElementAttributes<T extends SupportedElement>= Partial<{
|
||||
[K in keyof _fromElsInterfaces<T>]:
|
||||
_fromElsInterfaces<T>[K] extends ((...p: any[])=> any)
|
||||
? _fromElsInterfaces<T>[K] | ((...p: Parameters<_fromElsInterfaces<T>[K]>)=> ddeSignal<ReturnType<_fromElsInterfaces<T>[K]>>)
|
||||
? _fromElsInterfaces<T>[K] | ((...p: Parameters<_fromElsInterfaces<T>[K]>)=>
|
||||
ddeSignal<ReturnType<_fromElsInterfaces<T>[K]>>)
|
||||
: (IsReadonly<_fromElsInterfaces<T>, K> extends false
|
||||
? _fromElsInterfaces<T>[K] | ddeSignal<_fromElsInterfaces<T>[K]>
|
||||
: ddeStringable)
|
||||
@ -71,6 +72,26 @@ export function assignAttribute<El extends SupportedElement, ATT extends keyof E
|
||||
): ElementAttributes<El>[ATT]
|
||||
|
||||
type ExtendedHTMLElementTagNameMap= HTMLElementTagNameMap & CustomElementTagNameMap;
|
||||
export function el<
|
||||
A extends ddeComponentAttributes,
|
||||
EL extends SupportedElement | ddeDocumentFragment
|
||||
>(
|
||||
component: (attr: A)=> EL,
|
||||
attrs?: NoInfer<A>,
|
||||
...addons: ddeElementAddon<EL>[]
|
||||
): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
|
||||
? EL
|
||||
: ( EL extends ddeDocumentFragment ? EL : ddeHTMLElement )
|
||||
export function el<
|
||||
A extends { textContent: ddeStringable },
|
||||
EL extends SupportedElement | ddeDocumentFragment
|
||||
>(
|
||||
component: (attr: A)=> EL,
|
||||
attrs?: NoInfer<A>["textContent"],
|
||||
...addons: ddeElementAddon<EL>[]
|
||||
): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
|
||||
? EL
|
||||
: ( EL extends ddeDocumentFragment ? EL : ddeHTMLElement )
|
||||
export function el<
|
||||
TAG extends keyof ExtendedHTMLElementTagNameMap,
|
||||
>(
|
||||
@ -88,16 +109,6 @@ export function el(
|
||||
attrs?: ElementAttributes<HTMLElement> | ddeStringable,
|
||||
...addons: ddeElementAddon<HTMLElement>[]
|
||||
): ddeHTMLElement
|
||||
|
||||
export function el<
|
||||
C extends (attr: ddeComponentAttributes)=> SupportedElement | ddeDocumentFragment
|
||||
>(
|
||||
component: C,
|
||||
attrs?: Parameters<C>[0] | ddeStringable,
|
||||
...addons: ddeElementAddon<ReturnType<C>>[]
|
||||
): ReturnType<C> extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
|
||||
? ReturnType<C>
|
||||
: ( ReturnType<C> extends ddeDocumentFragment ? ReturnType<C> : ddeHTMLElement )
|
||||
export { el as createElement }
|
||||
|
||||
export function elNS(
|
||||
@ -146,6 +157,8 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(
|
||||
body: EL,
|
||||
): EL
|
||||
|
||||
export function dispatchEvent(name: keyof DocumentEventMap | string, element: SupportedElement):
|
||||
(data?: any)=> void;
|
||||
export function dispatchEvent(name: keyof DocumentEventMap | string, options?: EventInit):
|
||||
(element: SupportedElement, data?: any)=> void;
|
||||
export function dispatchEvent(
|
||||
@ -219,11 +232,13 @@ export const scope: {
|
||||
|
||||
state: Scope[],
|
||||
/** Adds new child scope. All attributes are inherited by default. */
|
||||
push(scope: Partial<Scope>): ReturnType<Array<Scope>["push"]>,
|
||||
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>,
|
||||
/** Adds root scope as a child of the current scope. */
|
||||
pushRoot(): ReturnType<Array<Scope>["push"]>,
|
||||
/** Removes last/current child scope. */
|
||||
pop(): ReturnType<Array<Scope>["pop"]>,
|
||||
/** Runs function in a new (isolated) scope */
|
||||
isolate(fn: Function): void,
|
||||
};
|
||||
|
||||
export function customElementRender<
|
||||
|
9
dist/esm.js
vendored
9
dist/esm.js
vendored
@ -152,6 +152,11 @@ var scope = {
|
||||
pop() {
|
||||
if (scopes.length === 1) return;
|
||||
return scopes.pop();
|
||||
},
|
||||
isolate(fn) {
|
||||
this.push({ prevent: true });
|
||||
fn();
|
||||
this.pop();
|
||||
}
|
||||
};
|
||||
function append(...els) {
|
||||
@ -569,6 +574,10 @@ function observedAttributes2(instance) {
|
||||
|
||||
// src/events.js
|
||||
function dispatchEvent(name, options, host) {
|
||||
if (typeof options === "function") {
|
||||
host = options;
|
||||
options = null;
|
||||
}
|
||||
if (!options) options = {};
|
||||
return function dispatch(element, ...d) {
|
||||
if (host) {
|
||||
|
2
dist/esm.min.js
vendored
2
dist/esm.min.js
vendored
File diff suppressed because one or more lines are too long
28
docs/assets/favicon.svg
Normal file
28
docs/assets/favicon.svg
Normal file
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#333333" />
|
||||
<stop offset="100%" stop-color="#222222" />
|
||||
</linearGradient>
|
||||
<linearGradient id="textGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#e32c2c" />
|
||||
<stop offset="100%" stop-color="#ff5252" />
|
||||
</linearGradient>
|
||||
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
|
||||
<feDropShadow dx="0" dy="0.5" stdDeviation="0.5" flood-color="#000" flood-opacity="0.3"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Square background with rounded corners -->
|
||||
<rect x="2" y="2" width="28" height="28" rx="4" ry="4" fill="url(#bgGradient)" />
|
||||
|
||||
<!-- Subtle code brackets as background element -->
|
||||
<g opacity="0.15" fill="#fff">
|
||||
<path d="M10,7.5 L6.25,16 L10,24.5" stroke="#fff" stroke-width="1" fill="none"/>
|
||||
<path d="M22,7.5 L25.75,16 L22,24.5" stroke="#fff" stroke-width="1" fill="none"/>
|
||||
</g>
|
||||
|
||||
<!-- lowercase dde letters -->
|
||||
<text x="16" y="21" text-anchor="middle" font-family="'Fira Code', 'JetBrains Mono', 'Source Code Pro', 'SF Mono', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace" font-size="14" font-weight="bold" fill="url(#textGradient)" filter="url(#shadow)">dde</text>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
33
docs/assets/logo.svg
Normal file
33
docs/assets/logo.svg
Normal file
@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Gradients and effects -->
|
||||
<defs>
|
||||
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#333333" />
|
||||
<stop offset="100%" stop-color="#222222" />
|
||||
</linearGradient>
|
||||
<linearGradient id="textGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#e32c2c" />
|
||||
<stop offset="100%" stop-color="#ff5252" />
|
||||
</linearGradient>
|
||||
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.3"/>
|
||||
</filter>
|
||||
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="4" result="blur"/>
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Square background with rounded corners -->
|
||||
<rect x="18" y="18" width="220" height="220" rx="20" ry="20" fill="url(#bgGradient)" />
|
||||
|
||||
<!-- Subtle code brackets as background element -->
|
||||
<g opacity="0.15" fill="#fff" filter="url(#glow)">
|
||||
<path d="M80,60 L50,128 L80,196" stroke="#fff" stroke-width="8" fill="none"/>
|
||||
<path d="M176,60 L206,128 L176,196" stroke="#fff" stroke-width="8" fill="none"/>
|
||||
</g>
|
||||
|
||||
<!-- lowercase dde letters with shadow effect -->
|
||||
<text x="128" y="154" text-anchor="middle" font-family="'Fira Code', 'JetBrains Mono', 'Source Code Pro', 'SF Mono', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace" font-size="100" font-weight="bold" fill="url(#textGradient)" filter="url(#shadow)">dde</text>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
@ -31,7 +31,7 @@ ${host} {
|
||||
overflow: auto;
|
||||
border-radius: var(--border-radius);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8rem;
|
||||
font-size: .85rem;
|
||||
line-height: 1.5;
|
||||
position: relative;
|
||||
margin-block: 1rem;
|
||||
@ -195,7 +195,7 @@ export function code({ id, src, content, language= "js", className= host.slice(1
|
||||
dataJS= "todo";
|
||||
}
|
||||
return el("div", { id, className, dataJS }).append(
|
||||
el("code", { className: "language-"+language, textContent: content })
|
||||
el("code", { className: "language-"+language, textContent: content.trim() })
|
||||
);
|
||||
}
|
||||
let is_registered= {};
|
||||
|
@ -3,9 +3,8 @@ const host= "."+example.name;
|
||||
styles.css`
|
||||
${host} {
|
||||
grid-column: full-main;
|
||||
width: 100%;
|
||||
max-width: calc(9/5 * var(--body-max-width));
|
||||
height: calc(3/5 * var(--body-max-width));
|
||||
width: calc(100% - .75em);
|
||||
height: calc(4/6 * var(--body-max-width));
|
||||
margin: 2rem auto;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow);
|
||||
@ -19,14 +18,13 @@ ${host} .runtime {
|
||||
.CodeMirror {
|
||||
height: 100% !important;
|
||||
font-family: var(--font-mono) !important;
|
||||
font-size: 0.8rem !important;
|
||||
font-size: .85rem !important;
|
||||
line-height: 1.5 !important;
|
||||
}
|
||||
|
||||
/* Dark mode styles for CodeMirror */
|
||||
.CodeMirror, .CodeMirror-gutters {
|
||||
background: var(--code-bg) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
|
14
docs/components/examples/elements/dde-dom-create.js
Normal file
14
docs/components/examples/elements/dde-dom-create.js
Normal file
@ -0,0 +1,14 @@
|
||||
import { el } from "deka-dom-el";
|
||||
|
||||
// Create element with properties
|
||||
const button = el("button", {
|
||||
textContent: "Click me",
|
||||
className: "primary",
|
||||
disabled: true
|
||||
});
|
||||
|
||||
// Shorter and more expressive
|
||||
// than the native approach
|
||||
|
||||
// Add to DOM
|
||||
document.body.append(button);
|
11
docs/components/examples/elements/dde-dom-tree.js
Normal file
11
docs/components/examples/elements/dde-dom-tree.js
Normal file
@ -0,0 +1,11 @@
|
||||
import { el } from "deka-dom-el";
|
||||
|
||||
// Chainable, natural nesting
|
||||
// append() returns parent element
|
||||
// making chains easy and intuitive
|
||||
document.body.append(
|
||||
el("div").append(
|
||||
el("h1", "Title"),
|
||||
el("p", "Paragraph")
|
||||
)
|
||||
);
|
19
docs/components/examples/elements/native-dom-create.js
Normal file
19
docs/components/examples/elements/native-dom-create.js
Normal file
@ -0,0 +1,19 @@
|
||||
// Create element with properties
|
||||
const button = document.createElement('button');
|
||||
button.textContent = "Click me";
|
||||
button.className = "primary";
|
||||
button.disabled = true;
|
||||
|
||||
// Or using Object.assign()
|
||||
const button2 = Object.assign(
|
||||
document.createElement('button'),
|
||||
{
|
||||
textContent: "Click me",
|
||||
className: "primary",
|
||||
disabled: true
|
||||
}
|
||||
);
|
||||
|
||||
// Add to DOM
|
||||
document.body.appendChild(button);
|
||||
document.body.appendChild(button2);
|
15
docs/components/examples/elements/native-dom-tree.js
Normal file
15
docs/components/examples/elements/native-dom-tree.js
Normal file
@ -0,0 +1,15 @@
|
||||
// Verbose, needs temp variables
|
||||
const div = document.createElement('div');
|
||||
const h1 = document.createElement('h1');
|
||||
h1.textContent = 'Title';
|
||||
div.appendChild(h1);
|
||||
|
||||
const p = document.createElement('p');
|
||||
p.textContent = 'Paragraph';
|
||||
div.appendChild(p);
|
||||
|
||||
// appendChild doesn't return parent
|
||||
// so chaining is not possible
|
||||
|
||||
// Add to DOM
|
||||
document.body.appendChild(div);
|
8
docs/components/examples/events/append-event.js
Normal file
8
docs/components/examples/events/append-event.js
Normal file
@ -0,0 +1,8 @@
|
||||
import { el, on } from "deka-dom-el";
|
||||
|
||||
// Third approach - append with on addon
|
||||
el("button", {
|
||||
textContent: "click me"
|
||||
}).append(
|
||||
on("click", (e) => console.log("Clicked!", e))
|
||||
);
|
7
docs/components/examples/events/attribute-event.js
Normal file
7
docs/components/examples/events/attribute-event.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { el } from "deka-dom-el";
|
||||
|
||||
// Using events with HTML attribute style
|
||||
el("button", {
|
||||
textContent: "click me",
|
||||
"=onclick": "console.log(event)"
|
||||
});
|
8
docs/components/examples/events/chain-event.js
Normal file
8
docs/components/examples/events/chain-event.js
Normal file
@ -0,0 +1,8 @@
|
||||
import { el, on } from "deka-dom-el";
|
||||
|
||||
// Using events as addons - chainable approach
|
||||
el("button", {
|
||||
textContent: "click me",
|
||||
},
|
||||
on("click", (e) => console.log("Clicked!", e))
|
||||
);
|
2
docs/components/examples/events/native-event.js
Normal file
2
docs/components/examples/events/native-event.js
Normal file
@ -0,0 +1,2 @@
|
||||
// Standard DOM event listener approach
|
||||
element.addEventListener('click', callback, options);
|
7
docs/components/examples/events/property-event.js
Normal file
7
docs/components/examples/events/property-event.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { el } from "deka-dom-el";
|
||||
|
||||
// Using events with property assignment
|
||||
el("button", {
|
||||
textContent: "click me",
|
||||
onclick: console.log
|
||||
});
|
14
docs/components/examples/introducing/3ps-before.js
Normal file
14
docs/components/examples/introducing/3ps-before.js
Normal file
@ -0,0 +1,14 @@
|
||||
// pseudocode
|
||||
// Mixed concerns make code hard to maintain
|
||||
const button = document.querySelector('button');
|
||||
let count = 0;
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
count++;
|
||||
document.querySelector('p').textContent =
|
||||
'Clicked ' + count + ' times';
|
||||
|
||||
if (count > 10) {
|
||||
button.disabled = true;
|
||||
}
|
||||
});
|
@ -1,6 +1,14 @@
|
||||
// pseudo code!
|
||||
const onchage=
|
||||
event=>
|
||||
console.log("Reacting to the:", event); // A
|
||||
input.addEventListener("change", onchange); // B
|
||||
input.dispatchEvent(new Event("change")); // C
|
||||
// pseudocode
|
||||
// 1. Create state
|
||||
const count = S(0);
|
||||
|
||||
// 2. React to state changes
|
||||
S.on(count, value => {
|
||||
updateUI(value);
|
||||
if (value > 10) disableButton();
|
||||
});
|
||||
|
||||
// 3. Update state on events
|
||||
button.addEventListener('click', () => {
|
||||
count.set(count.get() + 1);
|
||||
});
|
||||
|
@ -1,19 +1,30 @@
|
||||
import { el } from "deka-dom-el";
|
||||
import { el, on } from "deka-dom-el";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
const threePS= ({ emoji= "🚀" })=> {
|
||||
const clicks= S(0); // A
|
||||
return el().append(
|
||||
el("p", S(()=>
|
||||
"Hello World "+emoji.repeat(clicks.get()) // B
|
||||
)),
|
||||
el("button", {
|
||||
type: "button",
|
||||
onclick: ()=> clicks.set(clicks.get()+1), // C
|
||||
textContent: "Fire",
|
||||
})
|
||||
);
|
||||
};
|
||||
document.body.append(
|
||||
el(threePS, { emoji: "🎉" }),
|
||||
);
|
||||
|
||||
// A HelloWorld component using the 3PS pattern
|
||||
function HelloWorld({ emoji = "🚀" }) {
|
||||
// PART 1: Create reactive state
|
||||
const clicks = S(0);
|
||||
|
||||
return el().append(
|
||||
// PART 2: Bind state to UI elements
|
||||
el("p", {
|
||||
className: "greeting",
|
||||
// This paragraph automatically updates when clicks changes
|
||||
textContent: S(() => `Hello World ${emoji.repeat(clicks.get())}`)
|
||||
}),
|
||||
|
||||
// PART 3: Update state in response to events
|
||||
el("button", {
|
||||
type: "button",
|
||||
textContent: "Add emoji",
|
||||
// When clicked, update the state
|
||||
onclick: () => clicks.set(clicks.get() + 1)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Use the component in your app
|
||||
document.body.append(
|
||||
el(HelloWorld, { emoji: "🎉" })
|
||||
);
|
14
docs/components/examples/scopes/custom-scope.js
Normal file
14
docs/components/examples/scopes/custom-scope.js
Normal file
@ -0,0 +1,14 @@
|
||||
import { scope } from "deka-dom-el";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
|
||||
function customSignalLogic() {
|
||||
// Create an isolated scope for a specific operation
|
||||
scope.push(); // Start new scope
|
||||
|
||||
// These signals are in the new scope
|
||||
const isolatedCount = S(0);
|
||||
const isolatedDerived = S(() => isolatedCount.get() * 2);
|
||||
|
||||
// Clean up by returning to previous scope
|
||||
scope.pop();
|
||||
}
|
@ -1,35 +1,26 @@
|
||||
/* PSEUDO-CODE!!! */
|
||||
import { el } from "deka-dom-el";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
function component(){
|
||||
/* prepare changeable data */
|
||||
const dataA= S("data");
|
||||
const dataB= S("data");
|
||||
/* define data flow (can be asynchronous) */
|
||||
fetchAPI().then(data_new=> dataA(data_new));
|
||||
setTimeout(()=> dataB("DATA"));
|
||||
/* declarative UI */
|
||||
return el().append(
|
||||
el("h1", {
|
||||
textContent: "Example",
|
||||
/* declarative attribute(s) */
|
||||
classList: { declarative: dataB }
|
||||
}),
|
||||
el("ul").append(
|
||||
/* declarative element(s) */
|
||||
S.el(dataA, data=> data.map(d=> el("li", d)))
|
||||
),
|
||||
el("ul").append(
|
||||
/* declarative component(s) */
|
||||
S.el(dataA, data=> data.map(d=> el(subcomponent, d)))
|
||||
)
|
||||
function Counter() {
|
||||
// Define state
|
||||
const count = S(0);
|
||||
|
||||
// Define behavior
|
||||
const increment = () => count.set(count.get() + 1);
|
||||
|
||||
// Define data flow
|
||||
setTimeout(increment, 1000);
|
||||
// or fetchAPI().then(increment);
|
||||
|
||||
// Declarative UI (how to render data/`count`)
|
||||
// …automatically updates when changes
|
||||
return el("div").append(
|
||||
// declarative element(s)
|
||||
el("p", S(() => "Count: " + count.get())),
|
||||
el("button", {
|
||||
onclick: increment,
|
||||
textContent: "Increment",
|
||||
// declarative attribute(s)
|
||||
disabled: S(() => count.get() >= 10)
|
||||
})
|
||||
);
|
||||
}
|
||||
function subcomponent({ id }){
|
||||
/* prepare changeable data */
|
||||
const textContent= S("…");
|
||||
/* define data flow (can be asynchronous) */
|
||||
fetchAPI(id).then(text=> textContent(text));
|
||||
/* declarative UI */
|
||||
return el("li", { textContent, dataId: id });
|
||||
}
|
||||
|
@ -1,31 +1,25 @@
|
||||
/* PSEUDO-CODE!!! */
|
||||
import { el, on, scope } from "deka-dom-el";
|
||||
function component(){
|
||||
const { host }= scope;
|
||||
const ul= el("ul");
|
||||
const ac= new AbortController();
|
||||
fetchAPI({ signal: ac.signal }).then(data=> {
|
||||
data.forEach(d=> ul.append(el("li", d)));
|
||||
});
|
||||
host(
|
||||
/* element was remove before data fetched */
|
||||
on.disconnected(()=> ac.abort())
|
||||
import { el, scope } from "deka-dom-el";
|
||||
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;
|
||||
host().querySelector("button").disabled = count >= 10;
|
||||
};
|
||||
setTimeout(increment, 1000);
|
||||
// or fetchAPI().then(increment);
|
||||
|
||||
return el("div").append(
|
||||
counterText,
|
||||
el("button", {
|
||||
onclick: increment,
|
||||
textContent: "Increment"
|
||||
})
|
||||
);
|
||||
return ul;
|
||||
/**
|
||||
* NEVER EVER!!
|
||||
* let data;
|
||||
* fetchAPI().then(d=> data= O(d));
|
||||
*
|
||||
* OR NEVER EVER!!
|
||||
* const ul= el("ul");
|
||||
* fetchAPI().then(d=> {
|
||||
* const data= O("data");
|
||||
* ul.append(el("li", data));
|
||||
* });
|
||||
*
|
||||
* // THE HOST IS PROBABLY DIFFERENT THAN
|
||||
* // YOU EXPECT AND OBSERVABLES MAY BE
|
||||
* // UNEXPECTEDLY REMOVED!!!
|
||||
* */
|
||||
}
|
||||
|
38
docs/components/examples/scopes/mixed.js
Normal file
38
docs/components/examples/scopes/mixed.js
Normal file
@ -0,0 +1,38 @@
|
||||
/* PSEUDO-CODE!!! */
|
||||
import { el, scope } from "deka-dom-el";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
function Counter() {
|
||||
const { host } = scope;
|
||||
|
||||
let count = S(0);
|
||||
const counterText = el("p", "Count: 0");
|
||||
S.on(count, c=> counterText.textContent= "Count: " + c);
|
||||
|
||||
// Manually update DOM element
|
||||
const increment = () => {
|
||||
count.set(count.get() + 1);
|
||||
// NEVER EVER
|
||||
// count = S(count.get() + 1);
|
||||
// THE HOST IS PROBABLY DIFFERENT THAN
|
||||
// YOU EXPECT AND SIGNAL MAY BE
|
||||
// UNEXPECTEDLY REMOVED!!!
|
||||
host().querySelector("button").disabled = count.get() >= 10;
|
||||
};
|
||||
setTimeout(()=> {
|
||||
// ok, BUT consider extract to separate function
|
||||
// see section below for more info
|
||||
scope.push();
|
||||
const ok= S(0);
|
||||
scope.pop();
|
||||
S.on(ok, console.log);
|
||||
setInterval(()=> ok.set(ok.get() + 1), 100);
|
||||
}, 100);
|
||||
|
||||
return el("div").append(
|
||||
counterText,
|
||||
el("button", {
|
||||
onclick: increment,
|
||||
textContent: "Increment"
|
||||
})
|
||||
);
|
||||
}
|
45
docs/components/examples/scopes/with-scope.js
Normal file
45
docs/components/examples/scopes/with-scope.js
Normal file
@ -0,0 +1,45 @@
|
||||
import { el, scope } from "deka-dom-el";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
|
||||
function CounterWithIsolatedTimer() {
|
||||
const { host } = scope;
|
||||
|
||||
// Main component state
|
||||
const count = S(0);
|
||||
|
||||
// Create a timer in an isolated scope
|
||||
scope.isolate(() => {
|
||||
// These subscriptions won't be tied to the component lifecycle
|
||||
// They would continue to run even if the component was removed
|
||||
const timer = S(0);
|
||||
|
||||
// Not recommended for real applications!
|
||||
// Just demonstrating scope isolation
|
||||
setInterval(() => {
|
||||
timer.set(timer.get() + 1);
|
||||
console.log(`Timer: ${timer.get()}`);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// Normal component functionality within main scope
|
||||
function increment() {
|
||||
count.set(count.get() + 1);
|
||||
}
|
||||
|
||||
return el("div").append(
|
||||
el("p").append(
|
||||
"Count: ",
|
||||
el("#text", S(() => count.get()))
|
||||
),
|
||||
el("button", {
|
||||
textContent: "Increment",
|
||||
onclick: increment
|
||||
}),
|
||||
el("p", "An isolated timer runs in console")
|
||||
);
|
||||
}
|
||||
|
||||
// Usage
|
||||
document.body.append(
|
||||
el(CounterWithIsolatedTimer)
|
||||
);
|
@ -1,12 +1,12 @@
|
||||
import { S } from "deka-dom-el/signals";
|
||||
// α — `signal` represents a reactive value
|
||||
// PART 1 — `signal` represents a reactive value
|
||||
const signal= S(0);
|
||||
// β — just reacts on signal changes
|
||||
// PART 2 — just reacts on signal changes
|
||||
S.on(signal, console.log);
|
||||
// γ — just updates the value
|
||||
// PART 3 — just updates the value
|
||||
const update= ()=> signal.set(signal.get()+1);
|
||||
|
||||
update();
|
||||
const interval= 5*1000;
|
||||
setTimeout(clearInterval, 10*interval,
|
||||
setInterval(update, interval));
|
||||
setInterval(update, interval));
|
||||
|
@ -17,8 +17,14 @@ export function mnemonic(){
|
||||
" — just ", el("code", "<element>.dispatchEvent(new Event(<event>[, <options>]))")
|
||||
),
|
||||
el("li").append(
|
||||
el("code", "dispatchEvent(<event>[, <options>])(element, detail)"),
|
||||
" — just ", el("code", "<element>.dispatchEvent(new CustomEvent(<event>, { detail, ...<options> }))")
|
||||
el("code", "dispatchEvent(<event>, <element>)([<detail>])"),
|
||||
" — just ", el("code", "<element>.dispatchEvent(new Event(<event>))"), " or ",
|
||||
el("code", "<element>.dispatchEvent(new CustomEvent(<event>, { detail: <detail> }))")
|
||||
),
|
||||
el("li").append(
|
||||
el("code", "dispatchEvent(<event>[, <options>])(<element>[, <detail>])"),
|
||||
" — just ", el("code", "<element>.dispatchEvent(new Event(<event>[, <options>] ))"), " or ",
|
||||
el("code", "<element>.dispatchEvent(new CustomEvent(<event>, { detail: <detail> }))")
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { styles } from "./ssr.js";
|
||||
styles.css`
|
||||
:root {
|
||||
--primary: #b71c1c;
|
||||
--primary-light: #f05545;
|
||||
--primary-dark: #7f0000;
|
||||
--primary-rgb: 183, 28, 28;
|
||||
--secondary: #700037;
|
||||
--secondary-light: #ae1357;
|
||||
--secondary-dark: #4a0027;
|
||||
--secondary-rgb: 112, 0, 55;
|
||||
--primary: hsl(0, 74%, 42%);
|
||||
--primary-light: hsl(5, 87%, 61%);
|
||||
--primary-dark: hsl(0, 100%, 25%);
|
||||
--primary-hs: 0, 74%;
|
||||
--secondary: hsl(330, 100%, 22%);
|
||||
--secondary-light: hsl(339, 80%, 38%);
|
||||
--secondary-dark: hsl(328, 100%, 15%);
|
||||
--secondary-hs: 330, 100%;
|
||||
|
||||
--font-main: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||
Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
@ -20,50 +20,49 @@ styles.css`
|
||||
--header-height: 4rem;
|
||||
--border-radius: 0.375rem;
|
||||
|
||||
--bg: #ffffff;
|
||||
--bg-sidebar: #fff5f5;
|
||||
--text: #1a1313;
|
||||
--text-light: #555050;
|
||||
--code-bg: #f9f2f2;
|
||||
--code-text: #9a0000;
|
||||
--border: #d8c0c0;
|
||||
--selection: rgba(183, 28, 28, 0.15);
|
||||
--marked: #b71c1c;
|
||||
--bg: hsl(0, 0%, 100%);
|
||||
--bg-sidebar: hsl(0, 100%, 98%);
|
||||
--text: hsl(0, 16%, 15%);
|
||||
--text-light: hsl(0, 4%, 33%);
|
||||
--code-bg: hsl(0, 39%, 97%);
|
||||
--code-text: hsl(0, 100%, 30%);
|
||||
--border: hsl(0, 32%, 80%);
|
||||
--selection: hsl(var(--primary-hs), 90%);
|
||||
--marked: var(--primary);
|
||||
--accent: var(--secondary);
|
||||
--shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
--shadow: 0 2px 6px hsla(0, 0%, 0%, 0.15);
|
||||
--shadow-sm: 0 2px 4px hsla(0, 0%, 0%, 0.15);
|
||||
|
||||
--link-color: #9a0000;
|
||||
--link-hover: #7f0000;
|
||||
--button-text: #ffffff;
|
||||
|
||||
--link-color: hsl(0, 100%, 30%);
|
||||
--link-hover: hsl(0, 100%, 25%);
|
||||
--button-text: hsl(0, 0%, 100%);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #121212;
|
||||
--bg-sidebar: #1a1212;
|
||||
--text: #ffffff;
|
||||
--text-light: #cccccc;
|
||||
--code-bg: #2c2020;
|
||||
--code-text: #ff9e80;
|
||||
--border: #4d3939;
|
||||
--selection: rgba(255, 99, 71, 0.25);
|
||||
--primary: #b74141;
|
||||
--primary-light: #ff867f;
|
||||
--primary-dark: #c62828;
|
||||
--secondary: #f02b47;
|
||||
--secondary-light: #ff6090;
|
||||
--secondary-dark: #b0003a;
|
||||
--bg: hsl(0, 0%, 7%);
|
||||
--bg-sidebar: hsl(0, 9%, 9%);
|
||||
--text: hsl(0, 0%, 100%);
|
||||
--text-light: hsl(0, 0%, 80%);
|
||||
--code-bg: hsl(0, 18%, 15%);
|
||||
--code-text: hsl(20, 100%, 75%);
|
||||
--border: hsl(0, 14%, 27%);
|
||||
--selection: hsla(9, 100%, 64%, 0.25);
|
||||
--primary: hsl(0, 48%, 49%);
|
||||
--primary-light: hsl(5, 100%, 75%);
|
||||
--primary-dark: hsl(0, 67%, 47%);
|
||||
--secondary: hsl(350, 87%, 55%);
|
||||
--secondary-light: hsl(341, 100%, 69%);
|
||||
--secondary-dark: hsl(340, 100%, 35%);
|
||||
--accent: var(--secondary);
|
||||
|
||||
--link-color: #ff5252;
|
||||
--link-hover: #ff867f;
|
||||
--button-text: #ffffff;
|
||||
--link-color: hsl(0, 100%, 66%);
|
||||
--link-hover: hsl(5, 100%, 75%);
|
||||
--button-text: hsl(0, 0%, 100%);
|
||||
|
||||
--nav-current-bg: #aa2222;
|
||||
--nav-current-text: #ffffff;
|
||||
|
||||
--primary-rgb: 255, 82, 82;
|
||||
--secondary-rgb: 233, 30, 99;
|
||||
--primary-hs: 0, 48%;
|
||||
--secondary-hs: 350, 87%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,7 +77,7 @@ html {
|
||||
|
||||
/* Accessibility improvements */
|
||||
:focus {
|
||||
outline: 3px solid rgba(63, 81, 181, 0.5);
|
||||
outline: 3px solid hsl(231, 48%, 70%);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@ -87,7 +86,7 @@ html {
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 3px solid rgba(63, 81, 181, 0.5);
|
||||
outline: 3px solid hsl(231, 48%, 70%);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@ -127,7 +126,7 @@ body {
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
font-size: 1rem;
|
||||
font-size: 1.05rem;
|
||||
display: grid;
|
||||
grid-template-columns: 100%;
|
||||
grid-template-areas:
|
||||
@ -209,6 +208,12 @@ pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
.illustration:not(:has( .comparison)) pre {
|
||||
background: none;
|
||||
border-style: dashed !important;
|
||||
width: fit-content;
|
||||
padding: 1em 2em;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
@media (min-width: 768px) {
|
||||
@ -224,7 +229,7 @@ pre code {
|
||||
/* Main content */
|
||||
body > main {
|
||||
grid-area: content;
|
||||
padding: 2rem;
|
||||
padding-block: 2rem;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
display: grid;
|
||||
@ -242,83 +247,54 @@ body > main > *, body > main slot > * {
|
||||
}
|
||||
|
||||
/* Page title with ID anchor for skip link */
|
||||
body > main .page-title {
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--primary);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Section headings with better visual hierarchy */
|
||||
body > main h2, body > main h3 {
|
||||
body > main h3, body > main h4 {
|
||||
scroll-margin-top: calc(var(--header-height) + 1rem);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body > main h3 {
|
||||
border-left: 3px solid var(--primary);
|
||||
position: relative;
|
||||
left: -1.5rem;
|
||||
padding-inline-start: 1em;
|
||||
}
|
||||
|
||||
/* Make clickable heading links for better navigation */
|
||||
.heading-anchor {
|
||||
position: absolute;
|
||||
color: var(--text-light);
|
||||
left: -2rem;
|
||||
text-decoration: none;
|
||||
font-weight: normal;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
h2:hover .heading-anchor,
|
||||
h3:hover .heading-anchor {
|
||||
opacity: 0.8;
|
||||
opacity: .4;
|
||||
transition: opacity .2s;
|
||||
|
||||
&:hover { opacity: .8; }
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
body > main {
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
body > main h2, body > main h3 {
|
||||
left: 1rem;
|
||||
width: calc(100% - 1rem);
|
||||
}
|
||||
.heading-anchor {
|
||||
opacity: 0.4;
|
||||
/* Boxes */
|
||||
.comparison {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.comparison {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Example boxes */
|
||||
.example {
|
||||
.good-practice, .bad-practice {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--border-radius);
|
||||
margin: 2rem 0;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.example:hover {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.example-header {
|
||||
background-color: var(--bg-sidebar);
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.example-content {
|
||||
padding: 1.25rem;
|
||||
border-top: 4px solid var(--practice-color);
|
||||
padding: 0 1rem;
|
||||
}
|
||||
.good-practice {
|
||||
--practice-color: hsl(145, 63%, 49%); }
|
||||
.bad-practice {
|
||||
--practice-color: hsl(340, 82%, 52%); }
|
||||
|
||||
/* Icon styling */
|
||||
.icon {
|
||||
@ -332,52 +308,116 @@ h3:hover .heading-anchor {
|
||||
}
|
||||
|
||||
/* Information blocks */
|
||||
.note, .tip, .warning {
|
||||
.note, .tip, .warning, .callout, .troubleshooting {
|
||||
padding: 1rem 1.25rem;
|
||||
margin: 1.5rem 0;
|
||||
border-radius: var(--border-radius);
|
||||
position: relative;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.note {
|
||||
background-color: rgba(63, 81, 181, 0.08);
|
||||
border-left: 4px solid var(--primary);
|
||||
background-color: var(--bg-sidebar);
|
||||
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
||||
}
|
||||
--block-color: var(--primary);
|
||||
border-left: 4px solid var(--block-color);
|
||||
|
||||
h4 { margin-top: 0; }
|
||||
}
|
||||
.tip {
|
||||
background-color: rgba(46, 204, 113, 0.08);
|
||||
border-left: 4px solid #2ecc71;
|
||||
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
||||
}
|
||||
|
||||
--block-color: hsl(145, 63%, 49%); }
|
||||
.warning {
|
||||
background-color: rgba(241, 196, 15, 0.08);
|
||||
border-left: 4px solid #f1c40f;
|
||||
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
||||
}
|
||||
--block-color: hsl(48, 89%, 50%); }
|
||||
|
||||
.note::before, .tip::before, .warning::before {
|
||||
.callout {
|
||||
--block-color: hsl(231, 48%, 70%); }
|
||||
|
||||
.troubleshooting {
|
||||
--block-color: hsl(340, 82%, 52%); }
|
||||
|
||||
.note::before, .tip::before, .warning::before, .callout::before, .troubleshooting::before {
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--block-color);
|
||||
}
|
||||
|
||||
.note::before {
|
||||
content: "Note";
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.tip::before {
|
||||
content: "Tip";
|
||||
color: #2ecc71;
|
||||
}
|
||||
|
||||
content: "Note"; }
|
||||
.warning::before {
|
||||
content: "Warning";
|
||||
color: #f1c40f;
|
||||
content: "Warning"; }
|
||||
|
||||
/* Function table styling */
|
||||
.function-table {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1rem;
|
||||
margin: 1.5rem 0;
|
||||
background-color: var(--bg-sidebar);
|
||||
}
|
||||
|
||||
.function-table h4 {
|
||||
margin-top: 0;
|
||||
color: var(--primary);
|
||||
font-family: var(--font-mono);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.function-table dl {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.function-table dt {
|
||||
font-weight: 600;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.function-table dd {
|
||||
margin-left: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Tabs styling */
|
||||
.tabs {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--border-radius);
|
||||
margin: 1.5rem 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.tab:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.tab h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
min-width: 50%;
|
||||
border-top: none;
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.tab:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Prev/Next buttons */
|
||||
|
@ -2,7 +2,8 @@ import { t, T } from "./utils/index.js";
|
||||
export const info= {
|
||||
href: "./",
|
||||
title: t`Introduction`,
|
||||
description: t`Introducing a library.`,
|
||||
fullTitle: t`Vanilla for flavouring — a full-fledged feast for large projects`,
|
||||
description: t`A lightweight, reactive DOM library for creating dynamic UIs with a declarative syntax`,
|
||||
};
|
||||
|
||||
import { el } from "deka-dom-el";
|
||||
@ -26,37 +27,85 @@ const references= {
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("p", t`The library tries to provide pure JavaScript tool(s) to create reactive interfaces using …`),
|
||||
|
||||
el(h3, t`Event-driven programming (3 parts separation ≡ 3PS)`),
|
||||
el("p").append(t`
|
||||
Let's introduce the basic principle on which the library is built. We'll use the JavaScript listener as
|
||||
a starting point.
|
||||
`),
|
||||
el(code, { src: fileURL("./components/examples/introducing/3ps.js"), page_id }),
|
||||
el("p").append(...T`
|
||||
As we can see, in the code at location “A” we define ${el("em", t`how to react`)} when the function
|
||||
is called with any event as an argument. At that moment, we ${el("em", t`don’t care who/why/how`)}
|
||||
the function was called. Similarly, at point “B”, we reference to a function to be called on the event
|
||||
${el("em", t`without caring`)} what the function will do at that time. Finally, at point “C”, we tell
|
||||
the application that a change has occurred, in the input, and we ${el("em", t`don't care if/how someone`)}
|
||||
is listening for the event.
|
||||
Welcome to Deka DOM Elements (DDE) — a lightweight library for building dynamic UIs with a
|
||||
declarative syntax that stays close to the native DOM API. DDE gives you powerful reactive
|
||||
tools without the complexity and overhead of larger frameworks.
|
||||
`),
|
||||
el("div", { className: "callout" }).append(
|
||||
el("h4", t`What Makes DDE Special`),
|
||||
el("ul").append(
|
||||
el("li", t`No build step required — use directly in the browser`),
|
||||
el("li", t`Lightweight core (~10-15kB minified) with zero dependencies`),
|
||||
el("li", t`Natural DOM API — work with real DOM nodes, not abstractions`),
|
||||
el("li", t`Built-in reactivity with powerful signals system`),
|
||||
el("li", t`Clean code organization with the 3PS pattern`)
|
||||
)
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/introducing/helloWorld.js"), page_id }),
|
||||
|
||||
el(h3, { textContent: t`The 3PS Pattern: A Better Way to Build UIs`, id: "h-3ps" }),
|
||||
el("p").append(...T`
|
||||
The library introduces a new “type” of variable/constant called ${el("em", t`signal`)} allowing us to
|
||||
to use introduced 3PS pattern in our applications. As you can see it in the example above.
|
||||
At the heart of DDE is the 3PS (3-Part Separation) pattern. This simple yet powerful approach helps you
|
||||
organize your UI code into three distinct areas, making your applications more maintainable and easier to reason about.
|
||||
`),
|
||||
el("div", { className: "illustration" }).append(
|
||||
el("div", { className: "tabs" }).append(
|
||||
el("div", { className: "tab" }).append(
|
||||
el("h5", t`Traditional DOM Manipulation`),
|
||||
el(code, { src: fileURL("./components/examples/introducing/3ps-before.js"), page_id }),
|
||||
),
|
||||
el("div", { className: "tab" }).append(
|
||||
el("h5", t`DDE's 3PS Pattern`),
|
||||
el(code, { src: fileURL("./components/examples/introducing/3ps.js"), page_id }),
|
||||
)
|
||||
)
|
||||
),
|
||||
el("p").append(...T`
|
||||
Also please notice that there is very similar 3PS pattern used for separate creation of UI and
|
||||
business logic.
|
||||
The 3PS pattern separates your code into three clear parts:
|
||||
`),
|
||||
el("ol").append(
|
||||
el("li").append(...T`
|
||||
${el("strong", "Create State")}: Define your application's reactive data using signals
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Bind to Elements")}: Define how UI elements react to state changes
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Update State")}: Modify state in response to user events or other triggers
|
||||
`)
|
||||
),
|
||||
|
||||
el("p").append(...T`
|
||||
The 3PS is very simplified definition of the pattern. There are more deep/academic definitions more precisely
|
||||
describe usage in specific situations, see for example ${el("a", { textContent: t`MVVM`, ...references.w_mvv })}
|
||||
or ${el("a", { textContent: t`MVC`, ...references.w_mvc })}.
|
||||
By separating these concerns, your code becomes more modular, testable, and easier to maintain. This approach
|
||||
shares principles with more formal patterns like ${el("a", { textContent: "MVVM", ...references.w_mvv })} and
|
||||
${el("a", { textContent: "MVC", ...references.w_mvc })}, but with less overhead and complexity.
|
||||
`),
|
||||
|
||||
el(h3, t`Organization of the documentation`),
|
||||
el("div", { className: "note" }).append(
|
||||
el("p").append(...T`
|
||||
The 3PS pattern becomes especially powerful when combined with components, allowing you to create
|
||||
reusable pieces of UI with encapsulated state and behavior. You'll learn more about this in the
|
||||
following sections.
|
||||
`)
|
||||
),
|
||||
|
||||
el(h3, t`How to Use This Documentation`),
|
||||
el("p").append(...T`
|
||||
This guide will take you through DDE's features step by step:
|
||||
`),
|
||||
el("ol").append(
|
||||
el("li").append(...T`${el("strong", "Elements")} — Creating and manipulating DOM elements`),
|
||||
el("li").append(...T`${el("strong", "Events")} — Handling user interactions and lifecycle events`),
|
||||
el("li").append(...T`${el("strong", "Signals")} — Adding reactivity to your UI`),
|
||||
el("li").append(...T`${el("strong", "Scopes")} — Managing component lifecycles`),
|
||||
el("li").append(...T`${el("strong", "Custom Elements")} — Building web components`),
|
||||
el("li").append(...T`${el("strong", "Debugging")} — Tools to help you build and fix your apps`),
|
||||
el("li").append(...T`${el("strong", "SSR")} — Server-side rendering with DDE`)
|
||||
),
|
||||
el("p").append(...T`
|
||||
Each section builds on the previous ones, so we recommend following them in order.
|
||||
Let's get started with the basics of creating elements!
|
||||
`),
|
||||
);
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ ${host} h1 {
|
||||
|
||||
${host} .version-badge {
|
||||
font-size: 0.75rem;
|
||||
background-color: rgba(150, 150, 150, 0.2);
|
||||
background-color: hsla(0, 0%, 59%, 0.2);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
@ -49,22 +49,11 @@ ${host} p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
${host} .github-link {
|
||||
${host_nav} .github-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: white;
|
||||
font-size: 0.875rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: var(--border-radius);
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
text-decoration: none;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
${host} .github-link:hover {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
@ -85,46 +74,27 @@ ${host_nav} a {
|
||||
padding: 0.625rem 0.75rem;
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
transition: background-color 0.2s ease, color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
|
||||
transition: background-color 0.1s ease, color 0.1s ease, transform 0.1s ease, box-shadow 0.1s ease;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
${host_nav} a:hover {
|
||||
background-color: rgba(var(--primary-rgb), 0.08); /* Using CSS variables for better theming */
|
||||
text-decoration: none;
|
||||
transform: translateY(-1px);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
${host_nav} a.current,
|
||||
${host_nav} a[aria-current=page] {
|
||||
background-color: var(--nav-current-bg, var(--primary-dark));
|
||||
color: var(--nav-current-text, white);
|
||||
background-color: hsl(var(--primary-hs), 40%);
|
||||
color: whitesmoke;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
|
||||
box-shadow: var(--shadow);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
${host_nav} a.current:hover,
|
||||
${host_nav} a[aria-current=page]:hover {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
${host_nav} a:hover {
|
||||
background-color: hsl(var(--primary-hs), 45%);
|
||||
color: whitesmoke;
|
||||
transform: translateY(-1px);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
${host_nav} a .nav-number {
|
||||
display: inline-block;
|
||||
width: 1.5rem;
|
||||
text-align: right;
|
||||
margin-right: 0.5rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
${host_nav} a:first-child {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: rgb(from currentColor r g b / .75);
|
||||
}
|
||||
|
||||
/* Mobile navigation */
|
||||
@ -172,24 +142,17 @@ export function header({ info: { href, title, description }, pkg }){
|
||||
head({ title: pageTitle, description, pkg })
|
||||
);
|
||||
|
||||
// Add theme color meta tag
|
||||
document.head.append(
|
||||
el("meta", { name: "theme-color", content: "#3f51b5" })
|
||||
);
|
||||
|
||||
return el().append(
|
||||
// Header section with accessibility support
|
||||
el("header", { role: "banner", className: header.name }).append(
|
||||
el("div", { className: "header-title" }).append(
|
||||
el("a", {
|
||||
href: pkg.homepage,
|
||||
className: "github-link",
|
||||
"aria-label": "View on GitHub",
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer"
|
||||
}).append(
|
||||
el(iconGitHub),
|
||||
),
|
||||
el("img", {
|
||||
src: "assets/logo.svg",
|
||||
alt: "DDE Logo",
|
||||
width: "32",
|
||||
height: "32",
|
||||
style: "margin-right: 0.5rem;"
|
||||
}),
|
||||
el("h1").append(
|
||||
el("a", { href: pages[0].href, textContent: pkg.name, title: "Go to documentation homepage" }),
|
||||
),
|
||||
@ -203,15 +166,25 @@ export function header({ info: { href, title, description }, pkg }){
|
||||
),
|
||||
|
||||
// Navigation between pages
|
||||
nav({ href })
|
||||
nav({ href, pkg })
|
||||
);
|
||||
}
|
||||
function nav({ href }){
|
||||
function nav({ href, pkg }){
|
||||
return el("nav", {
|
||||
role: "navigation",
|
||||
"aria-label": "Main navigation",
|
||||
className: nav.name
|
||||
}).append(
|
||||
el("a", {
|
||||
href: pkg.homepage,
|
||||
className: "github-link",
|
||||
"aria-label": "View on GitHub",
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer",
|
||||
}).append(
|
||||
el(iconGitHub),
|
||||
"GitHub"
|
||||
),
|
||||
...pages.map((p, i) => {
|
||||
const isIndex = p.href === "index";
|
||||
const isCurrent = p.href === href;
|
||||
@ -220,16 +193,15 @@ function nav({ href }){
|
||||
href: isIndex ? "./" : p.href,
|
||||
title: p.description || `Go to ${p.title}`,
|
||||
"aria-current": isCurrent ? "page" : null,
|
||||
classList: { current: isCurrent }
|
||||
}).append(
|
||||
el("span", {
|
||||
className: "nav-number",
|
||||
"aria-hidden": "true",
|
||||
textContent: `${i+1}.`
|
||||
textContent: `${i+1}. `
|
||||
}),
|
||||
p.title
|
||||
);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
function head({ title, description, pkg }){
|
||||
@ -237,6 +209,7 @@ function head({ title, description, pkg }){
|
||||
el("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
|
||||
el("meta", { name: "description", content: description }),
|
||||
el("meta", { name: "theme-color", content: "#b71c1c" }),
|
||||
el("link", { rel: "icon", href: "assets/favicon.svg", type: "image/svg+xml" }),
|
||||
el("title", title),
|
||||
el(metaAuthor),
|
||||
el(metaTwitter, pkg),
|
||||
@ -257,7 +230,7 @@ function metaTwitter({ name, description, homepage }){
|
||||
el("meta", { name: "twitter:url", content: homepage }),
|
||||
el("meta", { name: "twitter:title", content: name }),
|
||||
el("meta", { name: "twitter:description", content: description }),
|
||||
//el("meta", { name: "twitter:image", content: "" }),
|
||||
el("meta", { name: "twitter:image", content: homepage + "/assets/logo.svg" }),
|
||||
el("meta", { name: "twitter:creator", content: "@jaandrle" }),
|
||||
);
|
||||
}
|
||||
@ -266,7 +239,7 @@ function metaFacebook({ name, description, homepage }){
|
||||
el("meta", { name: "og:url", content: homepage }),
|
||||
el("meta", { name: "og:title", content: name }),
|
||||
el("meta", { name: "og:description", content: description }),
|
||||
//el("meta", { name: "og:image", content: "" }),
|
||||
el("meta", { name: "og:image", content: homepage + "/assets/logo.svg" }),
|
||||
el("meta", { name: "og:creator", content: "@jaandrle" }),
|
||||
);
|
||||
}
|
||||
|
@ -19,8 +19,7 @@ export function simplePage({ pkg, info }){
|
||||
|
||||
// Main content area
|
||||
el("main", { id: "main-content", role: "main" }).append(
|
||||
// Page title as an h1
|
||||
el("h1", { className: "page-title", textContent: info.title }),
|
||||
el("h2", { textContent: info.fullTitle || info.title }),
|
||||
|
||||
// Main content from child elements
|
||||
el("slot"),
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { T, t } from "./utils/index.js";
|
||||
export const info= {
|
||||
title: t`Elements`,
|
||||
description: t`Basic concepts of elements modifications and creations.`,
|
||||
fullTitle: t`Declarative DOM Element Creation`,
|
||||
description: t`Building user interfaces with declarative DOM element creation.`,
|
||||
};
|
||||
|
||||
import { el } from "deka-dom-el";
|
||||
@ -48,107 +49,173 @@ const references= {
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("h2", t`Native JavaScript DOM elements creations`),
|
||||
el("p", t`
|
||||
Let’s go through all patterns we would like to use and what needs to be improved for better experience.
|
||||
el("p").append(...T`
|
||||
Building user interfaces in JavaScript often involves creating and manipulating DOM elements.
|
||||
DDE provides a simple yet powerful approach to element creation that is declarative, chainable,
|
||||
and maintains a clean syntax close to HTML structure.
|
||||
`),
|
||||
el("div", { class: "callout" }).append(
|
||||
el("h4", t`DDE Elements: Key Benefits`),
|
||||
el("ul").append(
|
||||
el("li", t`Declarative element creation with intuitive property assignment`),
|
||||
el("li", t`Chainable methods for natural DOM tree construction`),
|
||||
el("li", t`Simplified component patterns for code reuse`),
|
||||
el("li", t`Normalized property/attribute handling across browsers`),
|
||||
el("li", t`Smart element return values for cleaner code flow`)
|
||||
)
|
||||
),
|
||||
|
||||
el(code, { src: fileURL("./components/examples/elements/intro.js"), page_id }),
|
||||
|
||||
el(h3, t`Creating element(s) (with custom attributes)`),
|
||||
el(h3, t`Creating Elements: Native vs DDE`),
|
||||
el("p").append(...T`
|
||||
You can create a native DOM element by using the ${el("a", references.mdn_create).append(
|
||||
el("code", "document.createElement()") )}. It is also possible to provide a some attribute(s) declaratively
|
||||
using ${el("code", "Object.assign()")}. More precisely, this way you can sets some
|
||||
${el("a", references.mdn_idl).append( el("abbr", { textContent: "IDL", title: "Interface Description Language" }))}
|
||||
also known as a JavaScript property.
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/elements/nativeCreateElement.js"), page_id }),
|
||||
el("p").append(...T`
|
||||
To make this easier, you can use the ${el("code", "el")} function. Internally in basic examples,
|
||||
it is wrapper around ${el("code", "assign(document.createElement(…), { … })")}.
|
||||
In standard JavaScript, you create DOM elements using the
|
||||
${el("a", references.mdn_create).append(el("code", "document.createElement()"))} method
|
||||
and then set properties individually or with Object.assign():
|
||||
`),
|
||||
el("div", { class: "illustration" }).append(
|
||||
el("div", { class: "comparison" }).append(
|
||||
el("div").append(
|
||||
el("h5", t`Native DOM API`),
|
||||
el(code, { src: fileURL("./components/examples/elements/native-dom-create.js"), page_id })
|
||||
),
|
||||
el("div").append(
|
||||
el("h5", t`DDE Approach`),
|
||||
el(code, { src: fileURL("./components/examples/elements/dde-dom-create.js"), page_id })
|
||||
)
|
||||
)
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/elements/dekaCreateElement.js"), page_id }),
|
||||
el("p").append(...T`
|
||||
The ${el("code", "assign")} function provides improved behaviour of ${el("code", "Object.assign()")}.
|
||||
You can declaratively sets any IDL and attribute of the given element. Function prefers IDL and fallback
|
||||
to the ${el("code", "element.setAttribute")} if there is no writable property in the element prototype.
|
||||
The ${el("code", "el")} function provides a simple wrapper around ${el("code", "document.createElement")}
|
||||
with enhanced property assignment.
|
||||
`),
|
||||
|
||||
el(h3, t`Advanced Property Assignment`),
|
||||
el("p").append(...T`
|
||||
You can study all JavaScript elements interfaces to the corresponding HTML elements. All HTML elements
|
||||
inherits from ${el("a", { textContent: "HTMLElement", ...references.mdn_el })}. To see
|
||||
all available IDLs for example for paragraphs, see ${el("a", { textContent: "HTMLParagraphElement",
|
||||
...references.mdn_p })}. Moreover, the ${el("code", "assign")} provides a way to sets declaratively
|
||||
some convenient properties:
|
||||
The ${el("code", "assign")} function is the heart of DDE's element property handling. It provides
|
||||
intelligent assignment of both properties (IDL) and attributes:
|
||||
`),
|
||||
el("ul").append(
|
||||
el("li").append(...T`
|
||||
It is possible to sets ${el("code", "data-*")}/${el("code", "aria-*")} attributes using object notation.
|
||||
`),
|
||||
el("li").append(...T`
|
||||
In opposite, it is also possible to sets ${el("code", "data-*")}/${el("code", "aria-*")} attribute
|
||||
using camelCase notation.
|
||||
`),
|
||||
el("li").append(...T`You can use string or object as a value for ${el("code", "style")} property.`),
|
||||
el("li").append(...T`
|
||||
${el("code", "className")} (IDL – preffered)/${el("code", "class")} are ways to add CSS classes
|
||||
to the element. You can use string (similarly to ${el("code", "class=\"…\"")} syntax in HTML).
|
||||
`),
|
||||
el("li").append(...T`
|
||||
Use ${el("code", "classList")} to toggle specific classes. This will be handy later when
|
||||
the reactivity via signals is beeing introduced.
|
||||
`),
|
||||
el("li").append(...T`
|
||||
The ${el("code", "assign")} also accepts the ${el("code", "undefined")} as a value for any property
|
||||
to remove it from the element declaratively. Also for some IDL the corresponding attribute is removed
|
||||
as it can be confusing. ${el("em").append(...T`For example, natievly the element’s ${el("code", "id")}
|
||||
is removed by setting the IDL to an empty string.`)}
|
||||
`),
|
||||
el("li").append(...T`
|
||||
You can use ${el("code", "=")} or ${el("code", ".")} to force processing given key as attribute/property
|
||||
of the element.
|
||||
`)
|
||||
el("div", { class: "function-table" }).append(
|
||||
el("dl").append(
|
||||
el("dt", t`Property vs Attribute Priority`),
|
||||
el("dd", t`Prefers IDL properties, falls back to setAttribute() when no writable property exists`),
|
||||
|
||||
el("dt", t`Data and ARIA Attributes`),
|
||||
el("dd", t`Both dataset.* and data-* syntaxes supported (same for ARIA)`),
|
||||
|
||||
el("dt", t`Style Handling`),
|
||||
el("dd", t`Accepts string or object notation for style property`),
|
||||
|
||||
el("dt", t`Class Management`),
|
||||
el("dd", t`Works with className, class, or classList object for toggling classes`),
|
||||
|
||||
el("dt", t`Force Modes`),
|
||||
el("dd", t`Use = prefix to force attribute mode, . prefix to force property mode`),
|
||||
|
||||
el("dt", t`Attribute Removal`),
|
||||
el("dd", t`Pass undefined to remove a property or attribute`)
|
||||
)
|
||||
),
|
||||
el("p").append(...T`
|
||||
For processing, the ${el("code", "assign")} internally uses ${el("code", "assignAttribute")} and
|
||||
${el("code", "classListDeclarative")}.
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/elements/dekaAssign.js"), page_id }),
|
||||
|
||||
el(h3, t`Native JavaScript templating`),
|
||||
el("p", t`By default, the native JS has no good way to define HTML template using DOM API:`),
|
||||
el(example, { src: fileURL("./components/examples/elements/nativeAppend.js"), page_id }),
|
||||
el("div", { class: "note" }).append(
|
||||
el("p").append(...T`
|
||||
You can explore standard HTML element properties in the MDN documentation for
|
||||
${el("a", { textContent: "HTMLElement", ...references.mdn_el })} (base class)
|
||||
and specific element interfaces like ${el("a", { textContent: "HTMLParagraphElement", ...references.mdn_p })}.
|
||||
`)
|
||||
),
|
||||
|
||||
el(h3, t`Building DOM Trees with Chainable Methods`),
|
||||
el("p").append(...T`
|
||||
This library therefore overwrites the ${el("code", "append")} method of created elements to always return
|
||||
parent element.
|
||||
One of the most powerful features of DDE is its approach to building element trees.
|
||||
Unlike the native DOM API which doesn't return the parent after appendChild(), DDE's
|
||||
append() always returns the parent element:
|
||||
`),
|
||||
el("div", { class: "illustration" }).append(
|
||||
el("div", { class: "comparison" }).append(
|
||||
el("div", { class: "bad-practice" }).append(
|
||||
el("h5", t`❌ Native DOM API`),
|
||||
el(code, { src: fileURL("./components/examples/elements/native-dom-tree.js"), page_id })
|
||||
),
|
||||
el("div", { class: "good-practice" }).append(
|
||||
el("h5", t`✅ DDE Approach`),
|
||||
el(code, { src: fileURL("./components/examples/elements/dde-dom-tree.js"), page_id })
|
||||
)
|
||||
)
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/elements/dekaAppend.js"), page_id }),
|
||||
|
||||
|
||||
el(h3, t`Basic (state-less) components`),
|
||||
el("p").append(...T`
|
||||
You can use functions for encapsulation (repeating) logic. The ${el("code", "el")} accepts function
|
||||
as first argument. In that case, the function should return dom elements and the second argument for
|
||||
${el("code", "el")} is argument for given element.
|
||||
This chainable approach results in code that more closely mirrors the structure of your HTML,
|
||||
making it easier to understand and maintain.
|
||||
`),
|
||||
|
||||
el(h3, t`Building Reusable Components`),
|
||||
el("p").append(...T`
|
||||
DDE makes it simple to create reusable element components through regular JavaScript functions.
|
||||
The ${el("code", "el()")} function accepts a component function as its first argument:
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/elements/dekaBasicComponent.js"), page_id }),
|
||||
el("p").append(...T`
|
||||
As you can see, in case of state-less/basic components there is no difference between calling component
|
||||
function directly or using ${el("code", "el")} function.
|
||||
`),
|
||||
el("p", { className: "notice" }).append(...T`
|
||||
It is nice to use similar naming convention as native DOM API. This allows us to use
|
||||
${el("a", { textContent: t`the destructuring assignment syntax`, ...references.mdn_destruct })} and keep
|
||||
track of the native API (things are best remembered through regular use).
|
||||
Component functions receive props as their argument and return element(s). This pattern
|
||||
encourages code reuse and better organization of your UI code.
|
||||
`),
|
||||
el("div", { class: "tip" }).append(
|
||||
el("p").append(...T`
|
||||
It's helpful to use naming conventions similar to native DOM elements for your components.
|
||||
This allows you to use ${el("a", { textContent: "destructuring assignment", ...references.mdn_destruct })}
|
||||
and keeps your code consistent with the DOM API.
|
||||
`)
|
||||
),
|
||||
|
||||
el(h3, t`Creating non-HTML elements`),
|
||||
el(h3, t`Working with SVG and Other Namespaces`),
|
||||
el("p").append(...T`
|
||||
Similarly to the native DOM API (${el("a", references.mdn_ns).append(el("code", "document.createElementNS"))})
|
||||
for non-HTML elements we need to tell JavaScript which kind of the element to create. We can use
|
||||
the ${el("code", "elNS")} function:
|
||||
For non-HTML elements like SVG, MathML, or custom namespaces, DDE provides the ${el("code", "elNS")} function
|
||||
which corresponds to the native ${el("a", references.mdn_ns).append(el("code", "document.createElementNS"))}:
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/elements/dekaElNS.js"), page_id }),
|
||||
el("p").append(...T`
|
||||
This function returns a namespace-specific element creator, allowing you to work with any element type
|
||||
using the same consistent interface.
|
||||
`),
|
||||
|
||||
el(h3, t`Best Practices`),
|
||||
el("ol").append(
|
||||
el("li").append(...T`
|
||||
${el("strong", "Prefer composition over complexity")}: Create small component functions that can be
|
||||
combined rather than large, complex templates
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Use meaningful component names")}: Name your component functions after the elements or
|
||||
patterns they create
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Destructure for better readability")}: Use ${el("code", "const { div, p, button } = el")}
|
||||
to create element-specific functions
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Be consistent with property usage")}: Prefer using the same pattern (property vs attribute)
|
||||
throughout your code
|
||||
`)
|
||||
),
|
||||
|
||||
el("div", { class: "troubleshooting" }).append(
|
||||
el("h4", t`Common Element Creation Pitfalls`),
|
||||
el("dl").append(
|
||||
el("dt", t`Elements not showing up in DOM`),
|
||||
el("dd", t`Remember to append elements to the document or a parent that's already in the document`),
|
||||
|
||||
el("dt", t`Properties not being applied correctly`),
|
||||
el("dd", t`Check if you're mixing up property (IDL) names with attribute names (e.g., className vs class)`),
|
||||
|
||||
el("dt", t`Event listeners not working`),
|
||||
el("dd", t`Ensure you're using the correct event binding approach (see Events section)`),
|
||||
|
||||
el("dt", t`SVG elements not rendering correctly`),
|
||||
el("dd", t`Make sure you're using elNS with the correct SVG namespace for SVG elements`)
|
||||
)
|
||||
),
|
||||
|
||||
el(mnemonic)
|
||||
);
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { T, t } from "./utils/index.js";
|
||||
export const info= {
|
||||
title: t`Events and Addons`,
|
||||
description: t`Using not only events in UI declaratively.`,
|
||||
fullTitle: t`Declarative Event Handling and Addons`,
|
||||
description: t`Using events and addons for declarative UI interactions.`,
|
||||
};
|
||||
|
||||
import { el } from "deka-dom-el";
|
||||
@ -15,7 +16,7 @@ const fileURL= url=> new URL(url, import.meta.url);
|
||||
const references= {
|
||||
/** element.addEventListener() */
|
||||
mdn_listen: {
|
||||
title: t`MDN documentation page for elemetn.addEventListener`,
|
||||
title: t`MDN documentation page for element.addEventListener`,
|
||||
href: "https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener",
|
||||
},
|
||||
/** AbortSignal+element.addEventListener */
|
||||
@ -45,108 +46,196 @@ const references= {
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("h2", t`Listenning to the native DOM events and other Addons`),
|
||||
el("p").append(...T`
|
||||
We quickly introduce helper to listening to the native DOM events. And library syntax/pattern so-called
|
||||
${el("em", t`Addon`)} to incorporate not only this in UI templates declaratively.
|
||||
Events are at the core of interactive web applications. DDE provides a clean, declarative approach to
|
||||
handling DOM events and extends this pattern with a powerful Addon system to incorporate additional
|
||||
functionalities into your UI templates.
|
||||
`),
|
||||
el("div", { className: "callout" }).append(
|
||||
el("h4", t`Why DDE's Event System and Addons Matters`),
|
||||
el("ul").append(
|
||||
el("li", t`Integrate event handling directly in element declarations`),
|
||||
el("li", t`Leverage lifecycle events for better component design`),
|
||||
el("li", t`Clean up listeners automatically with abort signals`),
|
||||
el("li", t`Extend elements with custom behaviors using Addons`),
|
||||
el("li", t`Maintain clean, readable code with consistent patterns`)
|
||||
)
|
||||
),
|
||||
|
||||
el(code, { src: fileURL("./components/examples/events/intro.js"), page_id }),
|
||||
|
||||
el(h3, t`Events and listenners`),
|
||||
el(h3, t`Events and Listeners: Two Approaches`),
|
||||
el("p").append(...T`
|
||||
In JavaScript you can listen to the native DOM events of the given element by using
|
||||
${el("a", references.mdn_listen).append( el("code", "element.addEventListener(type, listener, options)") )}.
|
||||
The library provides an alternative (${el("code", "on")}) accepting the differen order of the arguments:
|
||||
In JavaScript you can listen to native DOM events using
|
||||
${el("a", references.mdn_listen).append(el("code", "element.addEventListener(type, listener, options)"))}.
|
||||
DDE provides an alternative approach with arguments ordered differently to better fit its declarative style:
|
||||
`),
|
||||
el("div", { className: "illustration" }).append(
|
||||
el("div", { className: "tabs" }).append(
|
||||
el("div", { className: "tab" }).append(
|
||||
el("h5", t`Native DOM API`),
|
||||
el(code, { content: `element.addEventListener('click', callback, options);`, page_id })
|
||||
),
|
||||
el("div", { className: "tab" }).append(
|
||||
el("h5", t`DDE Approach`),
|
||||
el(code, { content: `on('click', callback, options)(element);`, page_id })
|
||||
)
|
||||
)
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/events/compare.js"), page_id }),
|
||||
el("p").append(...T`
|
||||
…this is actually one of the two differences. The another one is that ${el("code", "on")} accepts only
|
||||
object as the ${el("code", "options")} (but it is still optional).
|
||||
The main benefit of DDE's approach is that it works as an Addon, making it easy to integrate
|
||||
directly into element declarations.
|
||||
`),
|
||||
el("p", { className: "notice" }).append(...T`
|
||||
The other difference is that there is ${el("strong", "no")} ${el("code", "off")} function. You can remove
|
||||
listener declaratively using ${el("a", { textContent: "AbortSignal", ...references.mdn_abortListener })}:
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/events/abortSignal.js"), page_id }),
|
||||
el("div", { className: "notice" }).append(
|
||||
el("p", t`So, there are (typically) three ways to handle events. You can use:`),
|
||||
el("ul").append(
|
||||
el("li").append( el("code", `el("button", { textContent: "click me", "=onclick": "console.log(event)" })`)),
|
||||
el("li").append( el("code", `el("button", { textContent: "click me", onclick: console.log })`)),
|
||||
el("li").append( el("code", `el("button", { textContent: "click me" }, on("click", console.log))`))
|
||||
),
|
||||
|
||||
el(h3, t`Removing Event Listeners`),
|
||||
el("div", { className: "note" }).append(
|
||||
el("p").append(...T`
|
||||
In the first example we force to use HTML attribute (it corresponds to
|
||||
${el("code", `<button onclick="console.log(event)">click me</button>`)}). ${el("em", t`Side note:
|
||||
this can be useful in case of SSR.`)} To study difference, you can read a nice summary here:
|
||||
${el("a", { textContent: "GIST @WebReflection/web_events.md", ...references.web_events })}.
|
||||
Unlike the native addEventListener/removeEventListener pattern, DDE uses the ${el("a", {
|
||||
textContent: "AbortSignal", ...references.mdn_abortListener })} for declarative approach for removal:
|
||||
`)
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/events/abortSignal.js"), page_id }),
|
||||
|
||||
el(h3, t`Addons`),
|
||||
el(h3, t`Three Ways to Handle Events`),
|
||||
el("div", { className: "tabs" }).append(
|
||||
el("div", { className: "tab", "data-tab": "html-attr" }).append(
|
||||
el("h4", t`HTML Attribute Style`),
|
||||
el(code, { src: fileURL("./components/examples/events/attribute-event.js"), page_id }),
|
||||
el("p").append(...T`
|
||||
Forces usage as an HTML attribute. Corresponds to
|
||||
${el("code", `<button onclick="console.log(event)">click me</button>`)}. This can be particularly
|
||||
useful for SSR scenarios.
|
||||
`)
|
||||
),
|
||||
el("div", { className: "tab", "data-tab": "property" }).append(
|
||||
el("h4", t`Property Assignment`),
|
||||
el(code, { src: fileURL("./components/examples/events/property-event.js"), page_id }),
|
||||
el("p", t`Assigns the event handler directly to the element's property.`)
|
||||
),
|
||||
el("div", { className: "tab", "data-tab": "addon" }).append(
|
||||
el("h4", t`Addon Approach`),
|
||||
el(code, { src: fileURL("./components/examples/events/chain-event.js"), page_id }),
|
||||
el("p", t`Uses the addon pattern, see above.`)
|
||||
)
|
||||
),
|
||||
el("p").append(...T`
|
||||
From practical point of view, ${el("em", t`Addons`)} are just functions that accept any HTML element as
|
||||
their first parameter. You can see that the ${el("code", "on(…)")} fullfills this requirement.
|
||||
For a deeper comparison of these approaches, see
|
||||
${el("a", { textContent: "WebReflection's detailed analysis", ...references.web_events })}.
|
||||
`),
|
||||
|
||||
el(h3, t`Understanding Addons`),
|
||||
el("p").append(...T`
|
||||
You can use Addons as ≥3rd argument of ${el("code", "el")} function. This way is possible to extends your
|
||||
templates by additional (3rd party) functionalities. But for now mainly, you can add events listeners:
|
||||
Addons are a powerful pattern in DDE that extends beyond just event handling.
|
||||
An Addon is any function that accepts an HTML element as its first parameter.
|
||||
`),
|
||||
el("div", { className: "callout" }).append(
|
||||
el("h4", t`What Can Addons Do?`),
|
||||
el("ul").append(
|
||||
el("li", t`Add event listeners to elements`),
|
||||
el("li", t`Set up lifecycle behaviors`),
|
||||
el("li", t`Integrate third-party libraries`),
|
||||
el("li", t`Create reusable element behaviors`),
|
||||
el("li", t`Capture element references`)
|
||||
)
|
||||
),
|
||||
el("p").append(...T`
|
||||
You can use Addons as ≥3rd argument of the ${el("code", "el")} function, making it possible to
|
||||
extend your templates with additional functionality:
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/events/templateWithListeners.js"), page_id }),
|
||||
el("p").append(...T`
|
||||
As the example shows, you can also provide types in JSDoc+TypeScript by using global type
|
||||
${el("code", "ddeElementAddon")}. Also notice, you can use Addons to get element reference.
|
||||
As the example shows, you can provide types in JSDoc+TypeScript using the global type
|
||||
${el("code", "ddeElementAddon")}. Notice how Addons can also be used to get element references.
|
||||
`),
|
||||
el(h3, t`Life-cycle events`),
|
||||
|
||||
el(h3, t`Lifecycle Events`),
|
||||
el("p").append(...T`
|
||||
Addons are called immediately when the element is created, even it is not connected to live DOM yet.
|
||||
Therefore, you can understand the Addon to be “oncreate” event.
|
||||
Addons are called immediately when an element is created, even before it's connected to the live DOM.
|
||||
You can think of an Addon as an "oncreate" event handler.
|
||||
`),
|
||||
el("p").append(...T`
|
||||
The library provide three additional live-cycle events corresponding to how they are named in a case of
|
||||
custom elements: ${el("code", "on.connected")}, ${el("code", "on.disconnected")} and ${el("code",
|
||||
"on.attributeChanged")}.
|
||||
DDE provides three additional lifecycle events that correspond to custom element lifecycle callbacks:
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/events/live-cycle.js"), page_id }),
|
||||
el("p").append(...T`
|
||||
For Custom elements, we will later introduce a way to replace ${el("code", "*Callback")} syntax with
|
||||
${el("code", "dde:*")} events. The ${el("code", "on.*")} functions then listen to the appropriate
|
||||
Custom Elements events (see ${el("a", { textContent: t`Custom element lifecycle callbacks | MDN`,
|
||||
...references.mdn_customElement })}).
|
||||
`),
|
||||
el("p").append(...T`
|
||||
But, in case of regular elemnets the ${el("a", references.mdn_mutation).append(el("code",
|
||||
"MutationObserver"), " | MDN")} is internaly used to track these events. Therefore, there are some
|
||||
drawbacks:
|
||||
`),
|
||||
el("ul").append(
|
||||
el("li").append(...T`
|
||||
To proper listener registration, you need to use ${el("code", "on.*")} not \`on("dde:*", …)\`!
|
||||
`),
|
||||
el("li").append(...T`
|
||||
Use sparingly! Internally, library must loop of all registered events and fires event properly.
|
||||
${el("strong", t`It is good practice to use the fact that if an element is removed, its children are
|
||||
also removed!`)} In this spirit, we will introduce later the ${el("strong", t`host`)} syntax to
|
||||
register, clean up procedures when the component is removed from the app.
|
||||
`),
|
||||
el("div", { className: "function-table" }).append(
|
||||
el("dl").append(
|
||||
el("dt", t`on.connected(callback)`),
|
||||
el("dd", t`Fires when the element is added to the DOM`),
|
||||
|
||||
el("dt", t`on.disconnected(callback)`),
|
||||
el("dd", t`Fires when the element is removed from the DOM`),
|
||||
|
||||
el("dt", t`on.attributeChanged(callback, attributeName)`),
|
||||
el("dd", t`Fires when the specified attribute changes`)
|
||||
)
|
||||
),
|
||||
el("p").append(...T`
|
||||
To provide intuitive behaviour, similar also to how the life-cycle events works in other
|
||||
frameworks/libraries, deka-dom-el library ensures that ${el("code", "on.connected")} and
|
||||
${el("code", "on.disconnected")} are called only once and only when the element, is (dis)connected to
|
||||
live DOM. The solution is inspired by ${el("a", { textContent: "Vue", ...references.vue_fix })}. For using
|
||||
native behaviour re-(dis)connecting element, use:
|
||||
`),
|
||||
el("ul").append(
|
||||
el("li").append(...T`custom ${el("code", "MutationObserver")} or logic in (dis)${el("code",
|
||||
"connectedCallback")} or…`),
|
||||
el("li").append(...T`re-add ${el("code", "on.connected")} or ${el("code", "on.disconnected")} listeners.`)
|
||||
el(example, { src: fileURL("./components/examples/events/live-cycle.js"), page_id }),
|
||||
|
||||
el("div", { className: "note" }).append(
|
||||
el("p").append(...T`
|
||||
For regular elements (non-custom elements), DDE uses
|
||||
${el("a", references.mdn_mutation).append(el("code", "MutationObserver"), " | MDN")}
|
||||
internally to track lifecycle events.
|
||||
`)
|
||||
),
|
||||
|
||||
el(h3, t`Final notes`),
|
||||
el("p", t`The library also provides a method to dispatch the events.`),
|
||||
el("div", { className: "warning" }).append(
|
||||
el("ul").append(
|
||||
el("li").append(...T`
|
||||
Always use ${el("code", "on.*")} functions, not ${el("code", "on('dde:*', ...)")}, for proper registration
|
||||
`),
|
||||
el("li").append(...T`
|
||||
Use lifecycle events sparingly, as they require internal tracking
|
||||
`),
|
||||
el("li").append(...T`
|
||||
Leverage parent-child relationships: when a parent is removed, all children are also removed
|
||||
`),
|
||||
el("li").append(...T`
|
||||
…see section later in documentation regarding hosts elements
|
||||
`),
|
||||
el("li").append(...T`
|
||||
DDE ensures that connected/disconnected events fire only once for better predictability
|
||||
`)
|
||||
)
|
||||
),
|
||||
|
||||
el(h3, t`Dispatching Custom Events`),
|
||||
el(example, { src: fileURL("./components/examples/events/compareDispatch.js"), page_id }),
|
||||
el("p").append(...T`
|
||||
This makes it easy to implement component communication through events,
|
||||
following standard web platform patterns. The curried approach allows for easy reuse
|
||||
of event dispatchers throughout your application.
|
||||
`),
|
||||
|
||||
el(h3, t`Best Practices`),
|
||||
el("ol").append(
|
||||
el("li").append(...T`
|
||||
${el("strong", "Clean up listeners")}: Use AbortSignal to prevent memory leaks
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Leverage lifecycle events")}: For component setup and teardown
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Delegate when possible")}: Add listeners to container elements when handling many similar elements
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Maintain consistency")}: Choose one event binding approach and stick with it
|
||||
`)
|
||||
),
|
||||
|
||||
el("div", { className: "troubleshooting" }).append(
|
||||
el("h4", t`Common Event Pitfalls`),
|
||||
el("dl").append(
|
||||
el("dt", t`Event listeners not working`),
|
||||
el("dd", t`Ensure element is in the DOM before expecting events to fire`),
|
||||
|
||||
el("dt", t`Memory leaks`),
|
||||
el("dd", t`Use AbortController to clean up listeners when elements are removed`),
|
||||
|
||||
el("dt", t`Lifecycle events firing unexpectedly`),
|
||||
el("dd", t`Remember that on.connected and on.disconnected events only fire once per connection state`)
|
||||
)
|
||||
),
|
||||
|
||||
el(mnemonic)
|
||||
);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { T, t } from "./utils/index.js";
|
||||
export const info= {
|
||||
title: t`Signals and Reactivity`,
|
||||
fullTitle: t`Building Reactive UIs with Signals`,
|
||||
description: t`Managing reactive UI state with signals.`,
|
||||
};
|
||||
|
||||
@ -43,20 +44,18 @@ const references= {
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("h2", t`Building Reactive UIs with Signals`),
|
||||
el("p").append(...T`
|
||||
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("div", { class: "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("li").append(...T`${el("strong", "In future")} no dependencies or framework lock-in`)
|
||||
)
|
||||
),
|
||||
el(code, { src: fileURL("./components/examples/signals/intro.js"), page_id }),
|
||||
@ -64,38 +63,39 @@ export function page({ pkg, info }){
|
||||
el(h3, t`The 3-Part Structure of Signals`),
|
||||
el("p").append(...T`
|
||||
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("a", { textContent: t`3PS principle`, href: "./#h-3ps" })}:
|
||||
`),
|
||||
el("div", { class: "dde-signal-diagram" }).append(
|
||||
el("div", { class: "signal-diagram" }).append(
|
||||
el("div", { class: "signal-part" }).append(
|
||||
el("h4", t`α: Create Signal`),
|
||||
el("h4", t`PART 1: 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("h4", t`PART 2: 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("h4", t`PART 3: 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("div", { class: "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.
|
||||
without direct dependencies on each other. Compare for example with ${el("a", { textContent:
|
||||
t`fpubsub library`, ...references.fpubsub })}.
|
||||
`)
|
||||
),
|
||||
|
||||
el(h3, t`Signal Essentials: Core API`),
|
||||
el("div", { class: "dde-function-table" }).append(
|
||||
el("div", { class: "function-table" }).append(
|
||||
el("dl").append(
|
||||
el("dt", t`Creating a Signal`),
|
||||
el("dd", t`S(initialValue) → creates a signal with the given value`),
|
||||
@ -110,7 +110,8 @@ export function page({ pkg, info }){
|
||||
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("dd").append(...T`S.on(signal, callback, { signal: abortController.signal }) → Similarly to the
|
||||
${el("code", "on")} function to register DOM events listener.`)
|
||||
)
|
||||
),
|
||||
el("p").append(...T`
|
||||
@ -136,18 +137,9 @@ export function page({ pkg, info }){
|
||||
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("div", { class: "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([], {
|
||||
@ -159,8 +151,25 @@ items.push("New todo");
|
||||
|
||||
// Use the action
|
||||
S.action(todos, "add", "New todo");`, page_id })
|
||||
)
|
||||
),
|
||||
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("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")}).
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/signals/actions-demo.js"), page_id }),
|
||||
|
||||
el("p").append(...T`
|
||||
@ -177,14 +186,12 @@ S.action(todos, "add", "New todo");`, page_id })
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/signals/actions-todos.js"), page_id }),
|
||||
|
||||
el("div", { class: "dde-tip" }).append(
|
||||
el("div", { class: "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("li", t`[S.symbols.onclear]() - Called when the signal is cleared. Use it to clean up resources.`),
|
||||
)
|
||||
),
|
||||
|
||||
@ -193,7 +200,7 @@ S.action(todos, "add", "New todo");`, page_id })
|
||||
Signals really shine when connected to your UI. DDE provides several ways to bind signals to DOM elements:
|
||||
`),
|
||||
|
||||
el("div", { class: "dde-tabs" }).append(
|
||||
el("div", { class: "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:`),
|
||||
@ -229,12 +236,12 @@ 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`
|
||||
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(example, { src: fileURL("./components/examples/signals/dom-attrs.js"), page_id }),
|
||||
|
||||
el("p").append(...T`
|
||||
${el("code", "S.el()")} is especially powerful for conditional rendering and lists:
|
||||
@ -263,7 +270,7 @@ S.action(items, "push", "Dragonfruit"); // List updates automatically`, page_id
|
||||
`)
|
||||
),
|
||||
|
||||
el("div", { class: "dde-troubleshooting" }).append(
|
||||
el("div", { class: "troubleshooting" }).append(
|
||||
el("h4", t`Common Signal Pitfalls`),
|
||||
el("dl").append(
|
||||
el("dt", t`UI not updating when array/object changes`),
|
||||
|
@ -1,6 +1,7 @@
|
||||
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`,
|
||||
};
|
||||
|
||||
@ -28,56 +29,46 @@ const references= {
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("h2", t`Building Maintainable UIs with Scopes and Components`),
|
||||
el("p").append(...T`
|
||||
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.
|
||||
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 })}.
|
||||
`),
|
||||
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`Understanding Host Elements and Scopes`),
|
||||
el("div", { class: "dde-illustration" }).append(
|
||||
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>)")}.
|
||||
`),
|
||||
el("div", { className: "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") │
|
||||
│ ); │
|
||||
│ } │
|
||||
└─────────────────────────────────┘
|
||||
`))
|
||||
// 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")
|
||||
);
|
||||
}
|
||||
`.trim()))
|
||||
),
|
||||
el("p").append(...T`
|
||||
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("div", { className: "function-table" }).append(
|
||||
el("h4", t`scope.host()`),
|
||||
el("dl").append(
|
||||
el("dt", t`When called with no arguments`),
|
||||
@ -89,42 +80,32 @@ export function page({ pkg, info }){
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/scopes/scopes-and-hosts.js"), page_id }),
|
||||
|
||||
el("div", { class: "dde-tip" }).append(
|
||||
el("div", { className: "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`
|
||||
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`
|
||||
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("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("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.
|
||||
If you are interested in the implementation details, see Class-Based Components section.
|
||||
`)
|
||||
),
|
||||
el(code, { src: fileURL("./components/examples/scopes/good-practise.js"), page_id }),
|
||||
|
||||
el(h3, t`Class-Based Components`),
|
||||
el("p").append(...T`
|
||||
While functional components are the primary pattern in DDE, 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"), page_id }),
|
||||
|
||||
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", { class: "dde-illustration" }).append(
|
||||
el("div", { className: "illustration" }).append(
|
||||
el("h4", t`Lifecycle Flow`),
|
||||
el("pre").append(el("code", `
|
||||
1. Component created → scope established
|
||||
@ -139,7 +120,7 @@ export function page({ pkg, info }){
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/scopes/cleaning.js"), page_id }),
|
||||
|
||||
el("div", { class: "dde-note" }).append(
|
||||
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 the signal subscription that updates the text content.
|
||||
@ -149,77 +130,82 @@ export function page({ pkg, info }){
|
||||
|
||||
el(h3, t`Declarative vs Imperative Components`),
|
||||
el("p").append(...T`
|
||||
Scopes work best with a declarative approach to UI building, especially when combined
|
||||
with ${el("a", { textContent: "signals", ...references.signals })} for state management.
|
||||
The library DOM API and signals works best when used declaratively. It means, you split your app logic
|
||||
into three parts as it was itroduced in ${el("a", { textContent: "Signals", ...references.signals })}.
|
||||
`),
|
||||
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("div", { class: "dde-note" }).append(
|
||||
el("div", { className: "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.
|
||||
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", { className: "tabs" }).append(
|
||||
el("div", { className: "tab", "data-tab": "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"), page_id }),
|
||||
),
|
||||
el("div", { className: "tab", "data-tab": "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"), page_id }),
|
||||
),
|
||||
el("div", { className: "tab", "data-tab": "mixed" }).append(
|
||||
el("h4", t`❌ Mixed Approach`),
|
||||
el("p", t`Just AVOID:`),
|
||||
el(code, { src: fileURL("./components/examples/scopes/mixed.js"), page_id }),
|
||||
),
|
||||
),
|
||||
|
||||
el(h3, t`Advanced: Custom Scoping Control`),
|
||||
el("p").append(...T`
|
||||
In more complex applications, you may need finer control over scopes. DDE provides
|
||||
manual scope control mechanisms through ${el("code", "scope.push()")} and ${el("code", "scope.pop()")}.
|
||||
`),
|
||||
el("div", { className: "function-table" }).append(
|
||||
el("h4", t`Manual Scope Control API`),
|
||||
el("dl").append(
|
||||
el("dt", t`scope.current`),
|
||||
el("dd", t`Returns the currently active scope object.`),
|
||||
|
||||
el("dt", t`scope.isolate(callback)`),
|
||||
el("dd", t`Executes the callback function within a temporary scope, then automatically restores the previous scope.
|
||||
Safer than manual push/pop for most use cases.`),
|
||||
|
||||
el("dt", t`scope.push()`),
|
||||
el("dd", t`Creates a new scope and makes it the current active scope. All signals and subscriptions
|
||||
created after this call will be associated with this new scope.`),
|
||||
|
||||
el("dt", t`scope.pop()`),
|
||||
el("dd", t`Restores the previous scope that was active before the matching push() call.`),
|
||||
)
|
||||
),
|
||||
el("p").append(...T`
|
||||
Custom scoping is particularly useful for:
|
||||
`),
|
||||
el("ul").append(
|
||||
el("li", t`Isolating signal dependencies in async operations`),
|
||||
el("li", t`Creating detached reactive logic that shouldn't be tied to a component's lifecycle`),
|
||||
el("li", t`Building utilities that work with signals but need scope isolation`)
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/scopes/custom-scope.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/scopes/with-scope.js"), page_id }),
|
||||
el("div", { className: "warning" }).append(
|
||||
el("p").append(...T`
|
||||
${el("strong", "Be careful with manual scope control!")} Always ensure you have matching push() and pop() calls,
|
||||
preferably in the same function. Unbalanced scope management can lead to memory leaks or unexpected behavior.
|
||||
`),
|
||||
el("p").append(...T`
|
||||
For most use cases, prefer using the automatic scope management provided by components.
|
||||
Manual scope control should be considered an advanced feature.
|
||||
`)
|
||||
),
|
||||
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
|
||||
`),
|
||||
@ -231,7 +217,7 @@ export function page({ pkg, info }){
|
||||
`)
|
||||
),
|
||||
|
||||
el("div", { class: "dde-troubleshooting" }).append(
|
||||
el("div", { className: "troubleshooting" }).append(
|
||||
el("h4", t`Common Scope Pitfalls`),
|
||||
el("dl").append(
|
||||
el("dt", t`Losing host reference in async code`),
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { T, t } from "./utils/index.js";
|
||||
export const info= {
|
||||
title: t`Web Components`,
|
||||
fullTitle: t`Using Web Components with DDE: Better Together`,
|
||||
description: t`Using custom elements in combinantion with DDE`,
|
||||
};
|
||||
|
||||
@ -53,13 +54,12 @@ const references= {
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("h2", t`Using Web Components with DDE: Better Together`),
|
||||
el("p").append(...T`
|
||||
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("div", { className: "callout" }).append(
|
||||
el("h4", t`Why Combine DDE with Web Components?`),
|
||||
el("ul").append(
|
||||
el("li", t`Declarative DOM creation within your components`),
|
||||
@ -92,7 +92,7 @@ export function page({ pkg, info }){
|
||||
`),
|
||||
el(code, { src: fileURL("./components/examples/customElement/native-basic.js"), page_id }),
|
||||
|
||||
el("div", { class: "dde-note" }).append(
|
||||
el("div", { className: "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`))}.
|
||||
@ -107,7 +107,7 @@ export function page({ pkg, info }){
|
||||
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("div", { className: "function-table" }).append(
|
||||
el("h4", t`customElementWithDDE`),
|
||||
el("dl").append(
|
||||
el("dt", t`Purpose`),
|
||||
@ -120,7 +120,7 @@ export function page({ pkg, info }){
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/customElement/customElementWithDDE.js"), page_id }),
|
||||
|
||||
el("div", { class: "dde-tip" }).append(
|
||||
el("div", { className: "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.
|
||||
@ -132,7 +132,7 @@ export function page({ pkg, info }){
|
||||
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("div", { className: "function-table" }).append(
|
||||
el("h4", t`customElementRender`),
|
||||
el("dl").append(
|
||||
el("dt", t`Purpose`),
|
||||
@ -151,7 +151,7 @@ export function page({ pkg, info }){
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/customElement/dde.js"), page_id }),
|
||||
|
||||
el("div", { class: "dde-note" }).append(
|
||||
el("div", { className: "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, ...)")}.
|
||||
@ -163,7 +163,7 @@ export function page({ pkg, info }){
|
||||
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("div", { className: "tip" }).append(
|
||||
el("p").append(...T`
|
||||
${el("strong", "Two Ways to Handle Attributes:")}
|
||||
`),
|
||||
@ -182,15 +182,15 @@ export function page({ pkg, info }){
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/customElement/observedAttributes.js"), page_id }),
|
||||
|
||||
el("div", { class: "dde-callout" }).append(
|
||||
el("div", { className: "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("ol").append(
|
||||
el("li", t`Takes each attribute listed in static observedAttributes`),
|
||||
el("li", t`Creates a DDE signal for each one`),
|
||||
el("li", t`Automatically updates these signals when attributes change`),
|
||||
el("li", t`Passes the signals to your component function`),
|
||||
el("li", t`Your component reacts to changes through signal subscriptions`)
|
||||
)
|
||||
),
|
||||
|
||||
el(h3, t`Working with Shadow DOM`),
|
||||
@ -198,25 +198,19 @@ export function page({ pkg, info }){
|
||||
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("div", { className: "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> │ │ │
|
||||
│ │ └─────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ └─────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────┘
|
||||
<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 }),
|
||||
@ -229,10 +223,11 @@ export function page({ pkg, info }){
|
||||
|
||||
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:
|
||||
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("div", { class: "dde-function-table" }).append(
|
||||
el(example, { src: fileURL("./components/examples/customElement/simulateSlots.js"), page_id }),
|
||||
el("div", { className: "function-table" }).append(
|
||||
el("h4", t`simulateSlots`),
|
||||
el("dl").append(
|
||||
el("dt", t`Purpose`),
|
||||
@ -241,15 +236,6 @@ export function page({ pkg, info }){
|
||||
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`
|
||||
@ -273,7 +259,7 @@ export function page({ pkg, info }){
|
||||
`)
|
||||
),
|
||||
|
||||
el("div", { class: "dde-troubleshooting" }).append(
|
||||
el("div", { className: "troubleshooting" }).append(
|
||||
el("h4", t`Common Issues`),
|
||||
el("dl").append(
|
||||
el("dt", t`Events not firing properly`),
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { T, t } from "./utils/index.js";
|
||||
export const info= {
|
||||
title: t`Debugging`,
|
||||
fullTitle: t`Debugging applications with deka-dom-el`,
|
||||
description: t`Techniques for debugging applications using deka-dom-el, especially signals.`,
|
||||
};
|
||||
|
||||
@ -16,7 +17,6 @@ const fileURL= url=> new URL(url, import.meta.url);
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("h2", t`Debugging applications with deka-dom-el`),
|
||||
el("p").append(...T`
|
||||
Debugging is an essential part of application development. This guide provides techniques
|
||||
and best practices for debugging applications built with deka-dom-el, with a focus on signals.
|
||||
|
@ -1,102 +0,0 @@
|
||||
import { T, t } from "./utils/index.js";
|
||||
export const info= {
|
||||
title: t`Debugging`,
|
||||
description: t`Techniques for debugging applications using deka-dom-el, especially signals.`,
|
||||
};
|
||||
|
||||
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 { code } from "./components/code.html.js";
|
||||
/** @param {string} url */
|
||||
const fileURL= url=> new URL(url, import.meta.url);
|
||||
|
||||
/** @param {import("./types.d.ts").PageAttrs} attrs */
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("h2", t`Debugging applications with deka-dom-el`),
|
||||
el("p").append(...T`
|
||||
Debugging is an essential part of application development. This guide provides techniques
|
||||
and best practices for debugging applications built with deka-dom-el, with a focus on signals.
|
||||
`),
|
||||
|
||||
el(h3, t`Debugging signals`),
|
||||
el("p").append(...T`
|
||||
Signals are reactive primitives that update the UI when their values change. When debugging signals,
|
||||
you need to track their values, understand their dependencies, and identify why updates are or aren't happening.
|
||||
`),
|
||||
|
||||
el("h4", t`Inspecting signal values`),
|
||||
el("p").append(...T`
|
||||
The simplest way to debug a signal is to log its current value by calling the get method:
|
||||
`),
|
||||
el("pre").append(
|
||||
el("code", "const signal = S(0);\nconsole.log('Current value:', signal.get());")
|
||||
),
|
||||
el("p").append(...T`
|
||||
You can also monitor signal changes by adding a listener:
|
||||
`),
|
||||
el("pre").append(
|
||||
el("code", "// Log every time the signal changes\nS.on(signal, value => console.log('Signal changed:', value));")
|
||||
),
|
||||
|
||||
el("h4", t`Debugging derived signals`),
|
||||
el("p").append(...T`
|
||||
With derived signals (created with S(() => computation)), debugging is a bit more complex
|
||||
because the value depends on other signals. To understand why a derived signal isn't updating correctly:
|
||||
`),
|
||||
el("ol").append(
|
||||
el("li", t`Check that all dependency signals are updating correctly`),
|
||||
el("li", t`Add logging inside the computation function to see when it runs`),
|
||||
el("li", t`Verify that the computation function actually accesses the signal values with .get()`)
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/debugging/consoleLog.js"), page_id }),
|
||||
|
||||
el(h3, t`Common signal debugging issues`),
|
||||
el("h4", t`Signal updates not triggering UI changes`),
|
||||
el("p").append(...T`
|
||||
If signal updates aren't reflected in the UI, check:
|
||||
`),
|
||||
el("ul").append(
|
||||
el("li", t`That you're using signal.set() to update the value, not modifying objects/arrays directly`),
|
||||
el("li", t`For mutable objects, ensure you're using actions or making proper copies before updating`),
|
||||
el("li", t`That the signal is actually connected to the DOM element (check your S.el or attribute binding code)`)
|
||||
),
|
||||
el(code, { src: fileURL("./components/examples/debugging/mutations.js"), page_id }),
|
||||
|
||||
el("h4", t`Memory leaks with signal listeners`),
|
||||
el("p").append(...T`
|
||||
Signal listeners can cause memory leaks if not properly cleaned up. Always use AbortSignal
|
||||
to cancel listeners.
|
||||
`),
|
||||
|
||||
el("h4", t`Performance issues with frequently updating signals`),
|
||||
el("p").append(...T`
|
||||
If you notice performance issues with signals that update very frequently:
|
||||
`),
|
||||
el("ul").append(
|
||||
el("li", t`Consider debouncing or throttling signal updates`),
|
||||
el("li", t`Make sure derived signals don't perform expensive calculations unnecessarily`),
|
||||
el("li", t`Keep signal computations focused and minimal`)
|
||||
),
|
||||
el(code, { src: fileURL("./components/examples/debugging/debouncing.js"), page_id }),
|
||||
|
||||
el(h3, t`Browser DevTools tips for deka-dom-el`),
|
||||
el("p").append(...T`
|
||||
When debugging in the browser, here are some helpful techniques:
|
||||
`),
|
||||
el("ul").append(
|
||||
el("li").append(...T`
|
||||
Use the Elements panel to inspect the DOM structure created by deka-dom-el
|
||||
`),
|
||||
el("li").append(...T`
|
||||
Set breakpoints in your signal handlers and actions
|
||||
`),
|
||||
el("li").append(...T`
|
||||
Use performance profiling to identify bottlenecks in signal updates
|
||||
`),
|
||||
),
|
||||
);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { T, t } from "./utils/index.js";
|
||||
export const info= {
|
||||
title: t`Server-Side Rendering (SSR)`,
|
||||
fullTitle: t`Server-Side Rendering with deka-dom-el`,
|
||||
description: t`Using deka-dom-el for server-side rendering with jsdom to generate static HTML.`,
|
||||
};
|
||||
|
||||
@ -15,7 +16,6 @@ const fileURL= url=> new URL(url, import.meta.url);
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("h2", t`Server-Side Rendering with deka-dom-el`),
|
||||
el("p").append(...T`
|
||||
deka-dom-el isn't limited to browser environments. Thanks to its flexible architecture,
|
||||
it can be used for server-side rendering (SSR) to generate static HTML files.
|
||||
|
@ -1,7 +1,8 @@
|
||||
export { t } from "./utils/index.js";
|
||||
export const path_target= {
|
||||
root: "dist/docs/",
|
||||
css: "dist/docs/"
|
||||
css: "dist/docs/",
|
||||
assets: "dist/docs/assets/"
|
||||
};
|
||||
/**
|
||||
* This variable will be filled with the list of pages during the build process (see `bs/docs.js`).
|
||||
|
1
docs/types.d.ts
vendored
1
docs/types.d.ts
vendored
@ -4,6 +4,7 @@ export type Info= {
|
||||
id: string,
|
||||
href: string,
|
||||
title: string,
|
||||
fullTitle: string,
|
||||
description: string,
|
||||
}
|
||||
export type Pages=Info[];
|
||||
|
@ -45,7 +45,7 @@ export function todosComponent({ todos= [ "Task A" ] }= {}){
|
||||
: el("ul").append(
|
||||
...Array.from(ts).map(([ value, textContent ])=>
|
||||
memo(value, ()=> el(todoComponent, { textContent, value, className }, onremove)))
|
||||
)
|
||||
),
|
||||
),
|
||||
el("p", "Click to the text to edit it.")
|
||||
),
|
||||
@ -54,7 +54,7 @@ export function todosComponent({ todos= [ "Task A" ] }= {}){
|
||||
el("label", "New todo: ").append(
|
||||
el("input", { name, type: "text", required: true }),
|
||||
),
|
||||
el("button", "+")
|
||||
el("button", "+"),
|
||||
),
|
||||
el("div").append(
|
||||
el("h3", "Output (JSON):"),
|
||||
|
36
index.d.ts
vendored
36
index.d.ts
vendored
@ -72,6 +72,26 @@ export function assignAttribute<El extends SupportedElement, ATT extends keyof E
|
||||
): ElementAttributes<El>[ATT]
|
||||
|
||||
type ExtendedHTMLElementTagNameMap= HTMLElementTagNameMap & CustomElementTagNameMap;
|
||||
export function el<
|
||||
A extends ddeComponentAttributes,
|
||||
EL extends SupportedElement | ddeDocumentFragment
|
||||
>(
|
||||
component: (attr: A)=> EL,
|
||||
attrs?: NoInfer<A>,
|
||||
...addons: ddeElementAddon<EL>[]
|
||||
): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
|
||||
? EL
|
||||
: ( EL extends ddeDocumentFragment ? EL : ddeHTMLElement )
|
||||
export function el<
|
||||
A extends { textContent: ddeStringable },
|
||||
EL extends SupportedElement | ddeDocumentFragment
|
||||
>(
|
||||
component: (attr: A)=> EL,
|
||||
attrs?: NoInfer<A>["textContent"],
|
||||
...addons: ddeElementAddon<EL>[]
|
||||
): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
|
||||
? EL
|
||||
: ( EL extends ddeDocumentFragment ? EL : ddeHTMLElement )
|
||||
export function el<
|
||||
TAG extends keyof ExtendedHTMLElementTagNameMap,
|
||||
>(
|
||||
@ -89,16 +109,6 @@ export function el(
|
||||
attrs?: ElementAttributes<HTMLElement> | ddeStringable,
|
||||
...addons: ddeElementAddon<HTMLElement>[]
|
||||
): ddeHTMLElement
|
||||
|
||||
export function el<
|
||||
C extends (attr: ddeComponentAttributes)=> SupportedElement | ddeDocumentFragment
|
||||
>(
|
||||
component: C,
|
||||
attrs?: Parameters<C>[0] | ddeStringable,
|
||||
...addons: ddeElementAddon<ReturnType<C>>[]
|
||||
): ReturnType<C> extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
|
||||
? ReturnType<C>
|
||||
: ( ReturnType<C> extends ddeDocumentFragment ? ReturnType<C> : ddeHTMLElement )
|
||||
export { el as createElement }
|
||||
|
||||
export function elNS(
|
||||
@ -147,6 +157,8 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(
|
||||
body: EL,
|
||||
): EL
|
||||
|
||||
export function dispatchEvent(name: keyof DocumentEventMap | string, element: SupportedElement):
|
||||
(data?: any)=> void;
|
||||
export function dispatchEvent(name: keyof DocumentEventMap | string, options?: EventInit):
|
||||
(element: SupportedElement, data?: any)=> void;
|
||||
export function dispatchEvent(
|
||||
@ -220,11 +232,13 @@ export const scope: {
|
||||
|
||||
state: Scope[],
|
||||
/** Adds new child scope. All attributes are inherited by default. */
|
||||
push(scope: Partial<Scope>): ReturnType<Array<Scope>["push"]>,
|
||||
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>,
|
||||
/** Adds root scope as a child of the current scope. */
|
||||
pushRoot(): ReturnType<Array<Scope>["push"]>,
|
||||
/** Removes last/current child scope. */
|
||||
pop(): ReturnType<Array<Scope>["pop"]>,
|
||||
/** Runs function in a new (isolated) scope */
|
||||
isolate(fn: Function): void,
|
||||
};
|
||||
|
||||
export function customElementRender<
|
||||
|
26
nohup.out
Normal file
26
nohup.out
Normal file
@ -0,0 +1,26 @@
|
||||
Markserv boot: starting Markserv...
|
||||
(node:170089) [DEP0128] DeprecationWarning: Invalid 'main' field in '/home/jaandrle/.npm/_npx/13a70f167aa91a98/node_modules/implant/package.json' of 'implant'. Please either fix that or report it to the module author
|
||||
(Use `node --trace-deprecation ...` to show where the warning was created)
|
||||
(node:170089) [DEP0128] DeprecationWarning: Invalid 'main' field in '/home/jaandrle/.npm/_npx/13a70f167aa91a98/node_modules/balanced-pairs/package.json' of 'balanced-pairs'. Please either fix that or report it to the module author
|
||||
(node:170089) [DEP0128] DeprecationWarning: Invalid 'main' field in '/home/jaandrle/.npm/_npx/13a70f167aa91a98/node_modules/super-split/package.json' of 'super-split'. Please either fix that or report it to the module author
|
||||
Markserv address: http://localhost:8642
|
||||
Markserv path: /home/jaandrle/Vzdálené/GitHub/deka-dom-el
|
||||
Markserv livereload: communicating on port: 35729
|
||||
Markserv process: your pid is: 170089
|
||||
Markserv stop: press [Ctrl + C] or type "sudo kill -9 170089"
|
||||
GitHub Contribute on Github - github.com/markserv
|
||||
Markserv upgrade: checking for upgrade...
|
||||
Markserv upgrade: no upgrade available
|
||||
Markserv dir: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/
|
||||
Markserv markdown: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/README.md
|
||||
Markserv file: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/docs/assets/logo.svg
|
||||
Markserv markdown: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/README.md
|
||||
Markserv file: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/docs/assets/logo.svg
|
||||
Markserv markdown: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/README.md
|
||||
Markserv file: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/docs/assets/logo.svg
|
||||
Markserv markdown: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/README.md
|
||||
Markserv file: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/docs/assets/logo.svg
|
||||
Markserv markdown: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/README.md
|
||||
Markserv file: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/docs/assets/logo.svg
|
||||
Markserv markdown: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/README.md
|
||||
Markserv file: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/docs/assets/logo.svg
|
@ -72,6 +72,12 @@ export const scope= {
|
||||
if(scopes.length===1) return;
|
||||
return scopes.pop();
|
||||
},
|
||||
|
||||
isolate(fn){
|
||||
this.push({ prevent: true });
|
||||
fn();
|
||||
this.pop();
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Chainable append function for elements
|
||||
|
@ -11,6 +11,7 @@ import { oAssign, onAbort } from './helpers.js';
|
||||
* @returns {Function} Function that dispatches the event
|
||||
*/
|
||||
export function dispatchEvent(name, options, host){
|
||||
if(typeof options==="function"){ host= options; options= null; }
|
||||
if(!options) options= {};
|
||||
return function dispatch(element, ...d){
|
||||
if(host){
|
||||
|
Loading…
x
Reference in New Issue
Block a user