mirror of
https://github.com/jaandrle/deka-dom-el
synced 2025-03-31 11:35:52 +02:00
⚡ 🐛 🔤 v0.9.4-alpha (#42)
* 🐛 fixes #41 * ⚡ adjust package size limits * 🔤 * 📺 requestIdleCallback doesn need to be global * 🔤 corrects irland page headers * 📺 version * ⚡ Signal ← SignalReadonly * 🐛 ensures only one disconncetd listener …for cleanup * ⚡ 🔤 Better build and improve texting * 🐛 logo alignemt (due to gh) * 🔤 md enhancements * 🔤 ⚡ products
This commit is contained in:
parent
04f93345f8
commit
4c450ae763
@ -75,11 +75,14 @@ where everyone feels comfortable participating.
|
||||
|
||||
…see [BS folder](./bs/README.md) for more info.
|
||||
|
||||
## Categorizing [](https://robinpokorny.github.io/git3moji/) <!-- editorconfig-checker-disable-line -->
|
||||
We use [git3moji](https://git3moji.netlify.app/) for commit messages, issue titles, pull request titles and in other
|
||||
areas. To make categorizing quick and consistent.
|
||||
|
||||
## Commit Guidelines
|
||||
|
||||
We use
|
||||
[](https://robinpokorny.github.io/git3moji/) <!-- editorconfig-checker-disable-line -->
|
||||
for commit messages. This helps keep the commit history clear and consistent.
|
||||
We use [git3moji](https://git3moji.netlify.app/) for commit messages. This helps keep the commit history clear and
|
||||
consistent.
|
||||
|
||||
```
|
||||
:emoji: Short summary of the change
|
||||
|
61
README.md
61
README.md
@ -1,18 +1,11 @@
|
||||
**Alpha**
|
||||
| [source code on GitHub](https://github.com/jaandrle/deka-dom-el)
|
||||
| [*mirrored* on Gitea](https://gitea.jaandrle.cz/jaandrle/deka-dom-el)
|
||||
| [](https://robinpokorny.github.io/git3moji/) <!-- editorconfig-checker-disable-line -->
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/assets/logo.svg" alt="Deka DOM Elements Logo" width="180" height="180">
|
||||
</p>
|
||||
|
||||
# Deka DOM Elements (dd\<el\> or DDE)
|
||||
| [Docs](https://jaandrle.github.io/deka-dom-el "Official documentation and guide site")
|
||||
| [NPM](https://www.npmjs.com/package/deka-dom-el "Official NPM package page")
|
||||
| [GitHub](https://github.com/jaandrle/deka-dom-el "Official GitHub repository")
|
||||
([*Gitea*](https://gitea.jaandrle.cz/jaandrle/deka-dom-el "GitHub repository mirror on my own Gitea instance"))
|
||||
|
||||
***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(
|
||||
@ -20,28 +13,23 @@ document.body.append(
|
||||
);
|
||||
|
||||
function EmojiCounter({ initial }) {
|
||||
// ✨ State - Define reactive data
|
||||
// ✨ - Define reactive data
|
||||
const count = S(0);
|
||||
const emoji = S(initial);
|
||||
const textContent = S(() => `Hello World ${emoji.get().repeat(count.get())}`);
|
||||
|
||||
/** @param {HTMLOptionElement} el */
|
||||
const isSelected= el=> (el.selected= el.value===initial);
|
||||
|
||||
// 🔄 View - UI updates automatically when signals change
|
||||
// 🔄 - UI updates automatically when signals change
|
||||
return el().append(
|
||||
el("p", {
|
||||
className: "output",
|
||||
textContent: S(() =>
|
||||
`Hello World ${emoji.get().repeat(count.get())}`),
|
||||
}),
|
||||
el("p", { textContent, className: "output" }),
|
||||
|
||||
// 🎮 Controls - Update state on events
|
||||
// 🎮 - Update state on events
|
||||
el("button", { textContent: "Add Emoji" },
|
||||
on("click", () => count.set(count.get() + 1))
|
||||
on("click", () => count.set(count.get() + 1)),
|
||||
),
|
||||
|
||||
el("select", null, on.defer(el=> el.value= initial),
|
||||
on("change", e => emoji.set(e.target.value))
|
||||
el("select", null,
|
||||
on.defer(el=> el.value= initial),
|
||||
on("change", e => emoji.set(e.target.value)),
|
||||
).append(
|
||||
el(Option, "🎉"),
|
||||
el(Option, "🚀"),
|
||||
@ -53,6 +41,13 @@ function Option({ textContent }){
|
||||
return el("option", { value: textContent, textContent });
|
||||
}
|
||||
```
|
||||
*…use simple DOM API by default and library tools and logic when you need them*
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/assets/logo.svg" alt="Deka DOM Elements Logo" width="180" height="180">
|
||||
</p>
|
||||
|
||||
# Deka DOM Elements (dd\<el\> or DDE)
|
||||
|
||||
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
|
||||
@ -64,19 +59,21 @@ Creating reactive elements, components, and Web Components using the native
|
||||
- ☑️ **Lightweight** — ~10-15kB minified (original goal 10kB) with **zero**/minimal dependencies
|
||||
- ✅ **Declarative & functional approach** for clean, maintainable code
|
||||
- ✅ **Signals and events** for reactive UI
|
||||
- ✅ **Auto-releasing resources** for memory management but nice development experience
|
||||
- ✅ **Memoization for performance** — optimize rendering with intelligent caching
|
||||
- ✅ **Optional build-in signals** with support for custom reactive implementations (#39)
|
||||
- ✅ **Server-side rendering** support via [jsdom](https://github.com/jsdom/jsdom)
|
||||
- ☑️ **Optional build-in signals** with support for custom reactive implementations (#39)
|
||||
- ☑️ **Server-side rendering** support via [jsdom](https://github.com/jsdom/jsdom)
|
||||
- ✅ **TypeScript support**
|
||||
- ☑️ **Support for debugging with browser DevTools** without extensions
|
||||
- ✅ **Support for debugging with browser DevTools** without extensions
|
||||
- ☑️ **Enhanced Web Components** support
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Documentation
|
||||
### Quick Links
|
||||
|
||||
- [**Documentation and Guide**](https://jaandrle.github.io/deka-dom-el)
|
||||
- [**Examples**](https://jaandrle.github.io/deka-dom-el/p15-examples.html)
|
||||
- [**Changelog**](https://github.com/jaandrle/deka-dom-el/releases)
|
||||
|
||||
### Installation
|
||||
|
||||
@ -87,7 +84,7 @@ npm install deka-dom-el --save
|
||||
…or via CDN / Direct Script:
|
||||
|
||||
For CDN links and various build formats (ESM/IIFE, with/without signals, minified/unminified), see the [interactive
|
||||
format selector](https://jaandrle.github.io/deka-dom-el/) on the documentation site.
|
||||
format selector](https://jaandrle.github.io/deka-dom-el/#h-getting-started) on the documentation site.
|
||||
|
||||
```html
|
||||
<!-- Example with IIFE build (creates a global DDE object) -->
|
||||
@ -141,9 +138,9 @@ get started, coding standards, commit guidelines, and the pull request process.
|
||||
interfaces or HTML code.
|
||||
- [pota](https://pota.quack.uy/) — small and pluggable Reactive Web Renderer. It's compiler-less, includes an html
|
||||
function, and a optimized babel preset in case you fancy JSX.
|
||||
- [jaandrle/dollar_dom_component](https://github.com/jaandrle/dollar_dom_component) —
|
||||
Functional DOM components without JSX/virtual DOM
|
||||
- [TarekRaafat/eleva](https://github.com/TarekRaafat/eleva) — A minimalist, lightweight, pure vanilla JavaScript
|
||||
frontend runtime framework.
|
||||
- [didi/mpx](https://github.com/didi/mpx) — Mpx,一款具有优秀开发体验和深度性能优化的增强型跨端小程序框架
|
||||
- [mxjp/rvx](https://github.com/mxjp/rvx) — A signal based frontend framework
|
||||
- [jaandrle/dollar_dom_component](https://github.com/jaandrle/dollar_dom_component) —
|
||||
Functional DOM components without JSX/virtual DOM (my old library)
|
||||
|
@ -2,7 +2,7 @@
|
||||
/* jshint esversion: 11,-W097, -W040, module: true, node: true, expr: true, undef: true *//* global echo, $, pipe, s, fetch, cyclicLoop */// editorconfig-checker-disable-line
|
||||
echo("Building static documentation files…");
|
||||
echo("Preparing…");
|
||||
import { path_target, pages as pages_registered, styles, dispatchEvent, t } from "../docs/ssr.js";
|
||||
import { path_target, pages as pages_registered, styles, currentPageId, dispatchEvent, t } from "../docs/ssr.js";
|
||||
import { createHTMl } from "./docs/jsdom.js";
|
||||
import { register, queue } from "../jsdom.js";
|
||||
const pkg= s.cat("package.json").xargs(JSON.parse);
|
||||
@ -28,6 +28,7 @@ for(const { id, info } of pages){
|
||||
);
|
||||
const { el }= await register(serverDOM.dom);
|
||||
const { page }= await import(`../docs/${id}.html.js`);
|
||||
currentPageId(id)
|
||||
serverDOM.document.body.append(
|
||||
el(page, { pkg, info }),
|
||||
);
|
||||
|
35
dist/esm-with-signals.js
vendored
35
dist/esm-with-signals.js
vendored
@ -41,6 +41,11 @@ function observedAttributes(instance, observedAttribute2) {
|
||||
function kebabToCamel(name) {
|
||||
return name.replace(/-./g, (x) => x[1].toUpperCase());
|
||||
}
|
||||
function requestIdle() {
|
||||
return new Promise(function(resolve) {
|
||||
(globalThis.requestIdleCallback || requestAnimationFrame)(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
// src/dom-lib/common.js
|
||||
var enviroment = {
|
||||
@ -179,11 +184,6 @@ function connectionsChangesObserverConstructor() {
|
||||
is_observing = false;
|
||||
observer.disconnect();
|
||||
}
|
||||
function requestIdle() {
|
||||
return new Promise(function(resolve) {
|
||||
(requestIdleCallback || requestAnimationFrame)(resolve);
|
||||
});
|
||||
}
|
||||
async function collectChildren(element) {
|
||||
if (store.size > 30)
|
||||
await requestIdle();
|
||||
@ -696,13 +696,10 @@ var queueSignalWrite = /* @__PURE__ */ (() => {
|
||||
})();
|
||||
|
||||
// src/signals-lib/signals-lib.js
|
||||
var Signal = oCreate(null, {
|
||||
var SignalReadOnly = oCreate(null, {
|
||||
get: { value() {
|
||||
return read(this);
|
||||
} },
|
||||
set: { value(...v) {
|
||||
return write(this, ...v);
|
||||
} },
|
||||
toJSON: { value() {
|
||||
return read(this);
|
||||
} },
|
||||
@ -710,9 +707,9 @@ var Signal = oCreate(null, {
|
||||
return this[mark] && this[mark].value;
|
||||
} }
|
||||
});
|
||||
var SignalReadOnly = oCreate(Signal, {
|
||||
set: { value() {
|
||||
return;
|
||||
var Signal = oCreate(SignalReadOnly, {
|
||||
set: { value(...v) {
|
||||
return write(this, ...v);
|
||||
} }
|
||||
});
|
||||
function isSignal(candidate) {
|
||||
@ -824,7 +821,7 @@ signal.el = function(s, map) {
|
||||
};
|
||||
function requestCleanUpReactives(host) {
|
||||
if (!host || !host[key_reactive]) return;
|
||||
(requestIdleCallback || setTimeout)(function() {
|
||||
requestIdle().then(function() {
|
||||
host[key_reactive] = host[key_reactive].filter(([s, el]) => el.isConnected ? true : (removeSignalListener(...s), false));
|
||||
});
|
||||
}
|
||||
@ -889,9 +886,10 @@ var signals_config = {
|
||||
function removeSignalsFromElements(s, listener, ...notes) {
|
||||
const { current } = scope;
|
||||
current.host(function(element) {
|
||||
if (!element[key_reactive]) element[key_reactive] = [];
|
||||
const is_first = !element[key_reactive];
|
||||
if (is_first) element[key_reactive] = [];
|
||||
element[key_reactive].push([[s, listener], ...notes]);
|
||||
if (current.prevent) return;
|
||||
if (!is_first || current.prevent) return;
|
||||
on.disconnected(
|
||||
() => (
|
||||
/*! Clears all Signals listeners added in the current scope/host (`S.el`, `assign`, …?).
|
||||
@ -906,7 +904,7 @@ var cleanUpRegistry = new FinalizationRegistry(function(s) {
|
||||
});
|
||||
function create(is_readonly, value, actions) {
|
||||
const varS = oCreate(is_readonly ? SignalReadOnly : Signal);
|
||||
const SI = toSignal(varS, value, actions, is_readonly);
|
||||
const SI = toSignal(varS, value, actions);
|
||||
cleanUpRegistry.register(SI, SI[mark]);
|
||||
return SI;
|
||||
}
|
||||
@ -918,7 +916,7 @@ var protoSigal = oAssign(oCreate(), {
|
||||
this.skip = true;
|
||||
}
|
||||
});
|
||||
function toSignal(s, value, actions, readonly = false) {
|
||||
function toSignal(s, value, actions) {
|
||||
const onclear = [];
|
||||
if (typeOf(actions) !== "[object Object]")
|
||||
actions = {};
|
||||
@ -934,8 +932,7 @@ function toSignal(s, value, actions, readonly = false) {
|
||||
actions,
|
||||
onclear,
|
||||
host,
|
||||
listeners: /* @__PURE__ */ new Set(),
|
||||
readonly
|
||||
listeners: /* @__PURE__ */ new Set()
|
||||
}),
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
|
6
dist/esm-with-signals.min.js
vendored
6
dist/esm-with-signals.min.js
vendored
File diff suppressed because one or more lines are too long
10
dist/esm.js
vendored
10
dist/esm.js
vendored
@ -25,6 +25,11 @@ function onAbort(signal, listener) {
|
||||
signal.removeEventListener("abort", listener);
|
||||
};
|
||||
}
|
||||
function requestIdle() {
|
||||
return new Promise(function(resolve) {
|
||||
(globalThis.requestIdleCallback || requestAnimationFrame)(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
// src/dom-lib/common.js
|
||||
var enviroment = {
|
||||
@ -163,11 +168,6 @@ function connectionsChangesObserverConstructor() {
|
||||
is_observing = false;
|
||||
observer.disconnect();
|
||||
}
|
||||
function requestIdle() {
|
||||
return new Promise(function(resolve) {
|
||||
(requestIdleCallback || requestAnimationFrame)(resolve);
|
||||
});
|
||||
}
|
||||
async function collectChildren(element) {
|
||||
if (store.size > 30)
|
||||
await requestIdle();
|
||||
|
2
dist/esm.min.js
vendored
2
dist/esm.min.js
vendored
File diff suppressed because one or more lines are too long
35
dist/iife-with-signals.js
vendored
35
dist/iife-with-signals.js
vendored
@ -86,6 +86,11 @@ var DDE = (() => {
|
||||
function kebabToCamel(name) {
|
||||
return name.replace(/-./g, (x) => x[1].toUpperCase());
|
||||
}
|
||||
function requestIdle() {
|
||||
return new Promise(function(resolve) {
|
||||
(globalThis.requestIdleCallback || requestAnimationFrame)(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
// src/dom-lib/common.js
|
||||
var enviroment = {
|
||||
@ -224,11 +229,6 @@ var DDE = (() => {
|
||||
is_observing = false;
|
||||
observer.disconnect();
|
||||
}
|
||||
function requestIdle() {
|
||||
return new Promise(function(resolve) {
|
||||
(requestIdleCallback || requestAnimationFrame)(resolve);
|
||||
});
|
||||
}
|
||||
async function collectChildren(element) {
|
||||
if (store.size > 30)
|
||||
await requestIdle();
|
||||
@ -741,13 +741,10 @@ var DDE = (() => {
|
||||
})();
|
||||
|
||||
// src/signals-lib/signals-lib.js
|
||||
var Signal = oCreate(null, {
|
||||
var SignalReadOnly = oCreate(null, {
|
||||
get: { value() {
|
||||
return read(this);
|
||||
} },
|
||||
set: { value(...v) {
|
||||
return write(this, ...v);
|
||||
} },
|
||||
toJSON: { value() {
|
||||
return read(this);
|
||||
} },
|
||||
@ -755,9 +752,9 @@ var DDE = (() => {
|
||||
return this[mark] && this[mark].value;
|
||||
} }
|
||||
});
|
||||
var SignalReadOnly = oCreate(Signal, {
|
||||
set: { value() {
|
||||
return;
|
||||
var Signal = oCreate(SignalReadOnly, {
|
||||
set: { value(...v) {
|
||||
return write(this, ...v);
|
||||
} }
|
||||
});
|
||||
function isSignal(candidate) {
|
||||
@ -869,7 +866,7 @@ var DDE = (() => {
|
||||
};
|
||||
function requestCleanUpReactives(host) {
|
||||
if (!host || !host[key_reactive]) return;
|
||||
(requestIdleCallback || setTimeout)(function() {
|
||||
requestIdle().then(function() {
|
||||
host[key_reactive] = host[key_reactive].filter(([s, el]) => el.isConnected ? true : (removeSignalListener(...s), false));
|
||||
});
|
||||
}
|
||||
@ -934,9 +931,10 @@ var DDE = (() => {
|
||||
function removeSignalsFromElements(s, listener, ...notes) {
|
||||
const { current } = scope;
|
||||
current.host(function(element) {
|
||||
if (!element[key_reactive]) element[key_reactive] = [];
|
||||
const is_first = !element[key_reactive];
|
||||
if (is_first) element[key_reactive] = [];
|
||||
element[key_reactive].push([[s, listener], ...notes]);
|
||||
if (current.prevent) return;
|
||||
if (!is_first || current.prevent) return;
|
||||
on.disconnected(
|
||||
() => (
|
||||
/*! Clears all Signals listeners added in the current scope/host (`S.el`, `assign`, …?).
|
||||
@ -951,7 +949,7 @@ var DDE = (() => {
|
||||
});
|
||||
function create(is_readonly, value, actions) {
|
||||
const varS = oCreate(is_readonly ? SignalReadOnly : Signal);
|
||||
const SI = toSignal(varS, value, actions, is_readonly);
|
||||
const SI = toSignal(varS, value, actions);
|
||||
cleanUpRegistry.register(SI, SI[mark]);
|
||||
return SI;
|
||||
}
|
||||
@ -963,7 +961,7 @@ var DDE = (() => {
|
||||
this.skip = true;
|
||||
}
|
||||
});
|
||||
function toSignal(s, value, actions, readonly = false) {
|
||||
function toSignal(s, value, actions) {
|
||||
const onclear = [];
|
||||
if (typeOf(actions) !== "[object Object]")
|
||||
actions = {};
|
||||
@ -979,8 +977,7 @@ var DDE = (() => {
|
||||
actions,
|
||||
onclear,
|
||||
host,
|
||||
listeners: /* @__PURE__ */ new Set(),
|
||||
readonly
|
||||
listeners: /* @__PURE__ */ new Set()
|
||||
}),
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
|
4
dist/iife-with-signals.min.js
vendored
4
dist/iife-with-signals.min.js
vendored
File diff suppressed because one or more lines are too long
10
dist/iife.js
vendored
10
dist/iife.js
vendored
@ -67,6 +67,11 @@ var DDE = (() => {
|
||||
signal.removeEventListener("abort", listener);
|
||||
};
|
||||
}
|
||||
function requestIdle() {
|
||||
return new Promise(function(resolve) {
|
||||
(globalThis.requestIdleCallback || requestAnimationFrame)(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
// src/dom-lib/common.js
|
||||
var enviroment = {
|
||||
@ -205,11 +210,6 @@ var DDE = (() => {
|
||||
is_observing = false;
|
||||
observer.disconnect();
|
||||
}
|
||||
function requestIdle() {
|
||||
return new Promise(function(resolve) {
|
||||
(requestIdleCallback || requestAnimationFrame)(resolve);
|
||||
});
|
||||
}
|
||||
async function collectChildren(element) {
|
||||
if (store.size > 30)
|
||||
await requestIdle();
|
||||
|
2
dist/iife.min.js
vendored
2
dist/iife.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,4 +1,4 @@
|
||||
import { registerClientFile, styles } from "../ssr.js";
|
||||
import { page_id, registerClientFile, styles } from "../ssr.js";
|
||||
const host= "."+code.name;
|
||||
styles.css`
|
||||
/* Code block styling */
|
||||
@ -177,6 +177,9 @@ ${host}:hover .copy-button {
|
||||
}
|
||||
`;
|
||||
import { el } from "deka-dom-el";
|
||||
/**
|
||||
* @typedef {"js"|"ts"|"html"|"css"|"shell"|"-"} Language
|
||||
* */
|
||||
/**
|
||||
* Prints code to the page and registers flems to make it interactive.
|
||||
* @param {object} attrs
|
||||
@ -184,15 +187,17 @@ import { el } from "deka-dom-el";
|
||||
* @param {string} [attrs.className]
|
||||
* @param {URL} [attrs.src] Example code file path
|
||||
* @param {string} [attrs.content] Example code
|
||||
* @param {"js"|"ts"|"html"|"css"|"shell"} [attrs.language="js"] Language of the code
|
||||
* @param {string} [attrs.page_id] ID of the page, if setted it registers shiki
|
||||
* @param {Language} [attrs.language="-s"] Language of the code
|
||||
* */
|
||||
export function code({ id, src, content, language= "js", className= host.slice(1), page_id }){
|
||||
if(src) content= s.cat(src);
|
||||
export function code({ id, src, content, language= "-", className= host.slice(1) }){
|
||||
if(src){
|
||||
content= s.cat(src);
|
||||
if(language=== "-") language= /** @type {Language} */(src.pathname.split(".").pop());
|
||||
}
|
||||
content= normalizeIndentation(content);
|
||||
let dataJS;
|
||||
if(page_id){
|
||||
registerClientPart(page_id);
|
||||
if(language!== "-"){
|
||||
registerClientPart();
|
||||
dataJS= "todo";
|
||||
}
|
||||
return el("div", { id, className, dataJS, tabIndex: 0 }).append(
|
||||
@ -204,8 +209,7 @@ export function pre({ content }){
|
||||
return el("pre").append(el("code", content.trim()));
|
||||
}
|
||||
let is_registered= {};
|
||||
/** @param {string} page_id */
|
||||
function registerClientPart(page_id){
|
||||
function registerClientPart(){
|
||||
if(is_registered[page_id]) return;
|
||||
|
||||
// Add Shiki with a more reliable loading method
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { styles } from "../ssr.js";
|
||||
import { page_id, styles } from "../ssr.js";
|
||||
|
||||
styles.css`
|
||||
#html-to-dde-converter {
|
||||
@ -149,12 +149,11 @@ import { ireland } from "./ireland.html.js";
|
||||
import { el } from "deka-dom-el";
|
||||
const fileURL= url=> new URL(url, import.meta.url);
|
||||
|
||||
export function converter({ page_id }){
|
||||
export function converter(){
|
||||
registerClientPart(page_id);
|
||||
return el(ireland, {
|
||||
src: fileURL("./converter.js.js"),
|
||||
exportName: "converter",
|
||||
page_id,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { styles } from "../ssr.js";
|
||||
import { page_id, styles } from "../ssr.js";
|
||||
const host= "."+example.name;
|
||||
styles.css`
|
||||
${host} {
|
||||
@ -119,9 +119,8 @@ import { relative } from "node:path";
|
||||
* @param {URL} attrs.src Example code file path
|
||||
* @param {"js"|"ts"|"html"|"css"} [attrs.language="js"] Language of the code
|
||||
* @param {"normal"|"big"} [attrs.variant="normal"] Size of the example
|
||||
* @param {string} attrs.page_id ID of the page
|
||||
* */
|
||||
export function example({ src, language= "js", variant= "normal", page_id }){
|
||||
export function example({ src, language= "js", variant= "normal" }){
|
||||
registerClientPart(page_id);
|
||||
const content= s.cat(src).toString()
|
||||
.replaceAll(/ from "deka-dom-el(\/signals)?";/g, ' from "./esm-with-signals.js";');
|
||||
|
@ -303,6 +303,7 @@ document.body.append(
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
|
501
docs/components/examples/case-studies/products.js
Normal file
501
docs/components/examples/case-studies/products.js
Normal file
@ -0,0 +1,501 @@
|
||||
import { el, on } from "deka-dom-el";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
|
||||
export function ProductCatalog() {
|
||||
const itemsPerPage = 5;
|
||||
const products = asyncSignal(S, fetchProducts, { initial: [], keepLast: true });
|
||||
const searchTerm = S("");
|
||||
const handleSearch = (e) => searchTerm.set(e.target.value);
|
||||
const sortOrder = S("default");
|
||||
const handleSort = (e) => sortOrder.set(e.target.value);
|
||||
const page = S(1);
|
||||
const handlePageChange = (newPage) => page.set(newPage);
|
||||
const resetFilters = () => {
|
||||
searchTerm.set("");
|
||||
sortOrder.set("default");
|
||||
page.set(1);
|
||||
};
|
||||
|
||||
const filteredProducts = S(() => {
|
||||
if (products.status.get() !== "resolved") return [];
|
||||
|
||||
const results = products.result.get().filter(product =>
|
||||
product.title.toLowerCase().includes(searchTerm.get().toLowerCase()) ||
|
||||
product.description.toLowerCase().includes(searchTerm.get().toLowerCase())
|
||||
);
|
||||
|
||||
return [...results].sort((a, b) => {
|
||||
const order = sortOrder.get();
|
||||
if (order === "price-asc") return a.price - b.price;
|
||||
if (order === "price-desc") return b.price - a.price;
|
||||
if (order === "rating") return b.rating - a.rating;
|
||||
return 0; // default: no sorting
|
||||
});
|
||||
});
|
||||
const totalPages = S(() => Math.ceil(filteredProducts.get().length / itemsPerPage));
|
||||
const paginatedProducts = S(() => {
|
||||
const currentPage = page.get();
|
||||
const filtered = filteredProducts.get();
|
||||
const start = (currentPage - 1) * itemsPerPage;
|
||||
return filtered.slice(start, start + itemsPerPage);
|
||||
});
|
||||
|
||||
// Component structure
|
||||
return el("div", { className: "product-catalog" }).append(
|
||||
el("header", { className: "catalog-header" }).append(
|
||||
el("h2", "Product Catalog"),
|
||||
el("div", { className: "toolbar" }).append(
|
||||
el("button", {
|
||||
className: "refresh-btn",
|
||||
textContent: "Refresh Products",
|
||||
type: "button",
|
||||
onclick: () => products.invoke(),
|
||||
}),
|
||||
el("button", {
|
||||
className: "reset-btn",
|
||||
textContent: "Reset Filters",
|
||||
type: "button",
|
||||
onclick: resetFilters,
|
||||
})
|
||||
)
|
||||
),
|
||||
|
||||
// Search and filter controls
|
||||
el("div", { className: "controls" }).append(
|
||||
el("div", { className: "search-box" }).append(
|
||||
el("input", {
|
||||
type: "search",
|
||||
placeholder: "Search products...",
|
||||
value: searchTerm,
|
||||
oninput: handleSearch,
|
||||
})
|
||||
),
|
||||
el("div", { className: "sort-options" }).append(
|
||||
el("label", "Sort by: "),
|
||||
el("select", { onchange: handleSort }, on.defer(el => el.value = sortOrder.get())).append(
|
||||
el("option", { value: "default", textContent: "Default" }),
|
||||
el("option", { value: "price-asc", textContent: "Price: Low to High" }),
|
||||
el("option", { value: "price-desc", textContent: "Price: High to Low" }),
|
||||
el("option", { value: "rating", textContent: "Top Rated" })
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// Status indicators
|
||||
el("div", { className: "status-container" }).append(
|
||||
S.el(products.status, status =>
|
||||
status === "pending" ?
|
||||
el("div", { className: "loader" }).append(
|
||||
el("div", { className: "spinner" }),
|
||||
el("p", "Loading products...")
|
||||
)
|
||||
: status === "rejected" ?
|
||||
el("div", { className: "error-message" }).append(
|
||||
el("p", products.error.get().message),
|
||||
el("button", {
|
||||
textContent: "Try Again",
|
||||
onclick: () => products.invoke()
|
||||
})
|
||||
)
|
||||
: el()
|
||||
)
|
||||
),
|
||||
|
||||
// Results count
|
||||
S.el(S(()=> [filteredProducts.get(), searchTerm.get()]), ([filtered, term]) =>
|
||||
products.status.get() === "resolved"
|
||||
? el("div", {
|
||||
className: "results-info",
|
||||
textContent: term ?
|
||||
`Found ${filtered.length} products matching "${term}"`
|
||||
: `Showing all ${filtered.length} products`
|
||||
})
|
||||
: el()
|
||||
),
|
||||
|
||||
// Products grid
|
||||
el("div", { className: "products-grid" }).append(
|
||||
S.el(paginatedProducts, paginatedItems =>
|
||||
products.status.get() === "resolved" && paginatedItems.length > 0 ?
|
||||
paginatedItems.map(product => el(ProductCard, { product }))
|
||||
: products.status.get() === "resolved" && paginatedItems.length === 0 ?
|
||||
el("p", { className: "no-results", textContent: "No products found matching your criteria." })
|
||||
: el()
|
||||
)
|
||||
),
|
||||
|
||||
// Pagination
|
||||
S.el(S(()=> [totalPages.get(), page.get()]), ([total, current]) =>
|
||||
products.status.get() === "resolved" && total > 1 ?
|
||||
el("div", { className: "pagination" }).append(
|
||||
el("button", {
|
||||
textContent: "Previous",
|
||||
disabled: current === 1,
|
||||
onclick: () => handlePageChange(current - 1)
|
||||
}),
|
||||
...Array.from({ length: total }, (_, i) => i + 1).map(num =>
|
||||
el("button", {
|
||||
className: num === current ? "current-page" : "",
|
||||
textContent: num,
|
||||
onclick: () => handlePageChange(num)
|
||||
})
|
||||
),
|
||||
el("button", {
|
||||
textContent: "Next",
|
||||
disabled: current === total,
|
||||
onclick: () => handlePageChange(current + 1)
|
||||
})
|
||||
)
|
||||
: el()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Product card component
|
||||
function ProductCard({ product }) {
|
||||
const showDetails = S(false);
|
||||
|
||||
return el("div", { className: "product-card" }).append(
|
||||
el("div", { className: "product-image" }).append(
|
||||
el("img", { src: product.thumbnail, alt: product.title })
|
||||
),
|
||||
el("div", { className: "product-info" }).append(
|
||||
el("h3", { className: "product-title", textContent: product.title }),
|
||||
el("div", { className: "product-price-rating" }).append(
|
||||
el("span", { className: "product-price", textContent: `$${product.price.toFixed(2)}` }),
|
||||
el("span", { className: "product-rating" }).append(
|
||||
el("span", { className: "stars", textContent: "★".repeat(Math.round(product.rating)) }),
|
||||
el("span", { className: "rating-value", textContent: `(${product.rating})` }),
|
||||
)
|
||||
),
|
||||
el("p", { className: "product-category", textContent: `Category: ${product.category}` }),
|
||||
S.el(showDetails, details =>
|
||||
details ?
|
||||
el("div", { className: "product-details" }).append(
|
||||
el("p", { className: "product-description", textContent: product.description }),
|
||||
el("div", { className: "product-meta" }).append(
|
||||
el("p", `Brand: ${product.brand}`),
|
||||
el("p", `Stock: ${product.stock} units`),
|
||||
el("p", `Discount: ${product.discountPercentage}%`)
|
||||
)
|
||||
)
|
||||
: el()
|
||||
),
|
||||
el("div", { className: "product-actions" }).append(
|
||||
el("button", {
|
||||
className: "details-btn",
|
||||
textContent: S(() => showDetails.get() ? "Hide Details" : "Show Details"),
|
||||
onclick: () => showDetails.set(!showDetails.get())
|
||||
}),
|
||||
el("button", {
|
||||
className: "add-to-cart-btn",
|
||||
textContent: "Add to Cart"
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Data fetching function
|
||||
async function fetchProducts({ signal }) {
|
||||
await simulateNetworkDelay();
|
||||
// Simulate random errors for demonstration
|
||||
if (Math.random() > 0.9) throw new Error("Failed to load products. Network error.");
|
||||
|
||||
const response = await fetch("https://dummyjson.com/products", { signal });
|
||||
if (!response.ok) throw new Error(`API error: ${response.status}`);
|
||||
|
||||
const data = await response.json();
|
||||
return data.products.slice(0, 20); // Limit to 20 products for the demo
|
||||
}
|
||||
|
||||
// Utility for simulating network latency
|
||||
function simulateNetworkDelay(min = 300, max = 1200) {
|
||||
const delay = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
return new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for async data fetching with signals
|
||||
* @template T
|
||||
* @param {typeof S} S - Signal constructor
|
||||
* @param {(params: { signal: AbortSignal }) => Promise<T>} invoker - Async function to execute
|
||||
* @param {{ initial?: T, keepLast?: boolean }} options - Configuration options
|
||||
* @returns {Object} Status signals and control methods
|
||||
*/
|
||||
export function asyncSignal(S, invoker, { initial, keepLast } = {}) {
|
||||
// Status tracking signals
|
||||
const status = S("pending");
|
||||
const result = S(initial);
|
||||
const error = S(null);
|
||||
let controller = null;
|
||||
|
||||
// Function to trigger data fetching
|
||||
async function invoke() {
|
||||
// Cancel any in-flight request
|
||||
if (controller) controller.abort();
|
||||
controller = new AbortController();
|
||||
|
||||
status.set("pending");
|
||||
error.set(null);
|
||||
if (!keepLast) result.set(initial);
|
||||
|
||||
try {
|
||||
const data = await invoker({
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!controller.signal.aborted) {
|
||||
status.set("resolved");
|
||||
result.set(data);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.name !== "AbortError") {
|
||||
error.set(e);
|
||||
status.set("rejected");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initial data fetch
|
||||
invoke();
|
||||
|
||||
return { status, result, error, invoke };
|
||||
}
|
||||
|
||||
// Initialize the component
|
||||
document.body.append(
|
||||
el(ProductCatalog),
|
||||
el("style", `
|
||||
.product-catalog {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.catalog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.toolbar button {
|
||||
margin-left: 10px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
background: #4a6cf7;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.sort-options select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.loader {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(0, 0, 0, 0.1);
|
||||
border-left-color: #4a6cf7;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.results-info {
|
||||
margin-bottom: 15px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.products-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.product-image img {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.product-title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 1.1rem;
|
||||
height: 2.4rem;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.product-price-rating {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-weight: bold;
|
||||
color: #4a6cf7;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.stars {
|
||||
color: gold;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.product-category {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.product-details {
|
||||
margin: 15px 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.product-description {
|
||||
line-height: 1.5;
|
||||
margin-bottom: 10px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.product-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
color: #666;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.product-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.product-actions button {
|
||||
flex: 1;
|
||||
padding: 8px 0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.details-btn {
|
||||
background: #eee;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.add-to-cart-btn {
|
||||
background: #4a6cf7;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.pagination button {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pagination button.current-page {
|
||||
background: #4a6cf7;
|
||||
color: white;
|
||||
border-color: #4a6cf7;
|
||||
}
|
||||
|
||||
.pagination button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.products-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
}
|
||||
}
|
||||
`),
|
||||
);
|
@ -483,7 +483,7 @@ document.body.append(
|
||||
|
||||
.task-board {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
|
@ -4,11 +4,11 @@ import { el } from "deka-dom-el";
|
||||
const button = el("button", {
|
||||
textContent: "Click me",
|
||||
className: "primary",
|
||||
disabled: true
|
||||
disabled: true,
|
||||
});
|
||||
|
||||
// Shorter and more expressive
|
||||
// than the native approach
|
||||
|
||||
// Add to DOM
|
||||
document.body.append(button);
|
||||
document.body.append(button);
|
||||
|
@ -6,6 +6,6 @@ import { el } from "deka-dom-el";
|
||||
document.body.append(
|
||||
el("div").append(
|
||||
el("h1", "Title"),
|
||||
el("p", "Paragraph")
|
||||
)
|
||||
el("p", "Paragraph"),
|
||||
),
|
||||
);
|
||||
|
@ -81,11 +81,11 @@ function Todos(){
|
||||
)
|
||||
)
|
||||
),
|
||||
S.el(todosS, todos => !todos.length
|
||||
S.el(todosS, ({ length }) => !length
|
||||
? el()
|
||||
: el("footer", { className: "footer" }).append(
|
||||
el("span", { className: "todo-count" }).append(
|
||||
noOfLeft()
|
||||
el("strong", length + " " + (length === 1 ? "item" : "items")),
|
||||
),
|
||||
memo("filters", ()=>
|
||||
el("ul", { className: "filters" }).append(
|
||||
@ -100,7 +100,7 @@ function Todos(){
|
||||
)
|
||||
),
|
||||
),
|
||||
todos.length - todosRemainingS.get() === 0
|
||||
length - todosRemainingS.get() === 0
|
||||
? el()
|
||||
: memo("delete", () =>
|
||||
el("button",
|
||||
@ -110,13 +110,6 @@ function Todos(){
|
||||
)
|
||||
)
|
||||
);
|
||||
function noOfLeft(){
|
||||
const length = todosRemainingS.get();
|
||||
return el("strong").append(
|
||||
length + " ",
|
||||
length === 1 ? "item left" : "items left"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { styles } from "../ssr.js";
|
||||
import { styles, page_id } from "../ssr.js";
|
||||
|
||||
styles.css`
|
||||
#library-url-form {
|
||||
@ -74,7 +74,7 @@ styles.css`
|
||||
import { el } from "deka-dom-el";
|
||||
import { ireland } from "./ireland.html.js";
|
||||
|
||||
export function getLibraryUrl({ page_id }){
|
||||
export function getLibraryUrl(){
|
||||
return el(ireland, {
|
||||
src: new URL("./getLibraryUrl.js.js", import.meta.url),
|
||||
exportName: "getLibraryUrl",
|
||||
|
@ -43,7 +43,6 @@ const componentsRegistry = new Map();
|
||||
* @param {object} attrs
|
||||
* @param {URL} attrs.src - Path to the file containing the component
|
||||
* @param {string} [attrs.exportName="default"] - Name of the export to use
|
||||
* @param {string} attrs.page_id - ID of the current page
|
||||
* @param {object} [attrs.props={}] - Props to pass to the component
|
||||
*/
|
||||
export function ireland({ src, exportName = "default", props = {} }) {
|
||||
|
@ -12,6 +12,10 @@ export function mnemonic(){
|
||||
" — corresponds to custom elemnets callbacks ", el("code", "<live-cycle>Callback(...){...}"),
|
||||
". To connect to custom element see following page, else it is simulated by MutationObserver."
|
||||
),
|
||||
el("li").append(
|
||||
el("code", "on.defer(<identity>=> <identity>)(<identity>)"),
|
||||
" — calls callback later",
|
||||
),
|
||||
el("li").append(
|
||||
el("code", "dispatchEvent(<event>[, <options>])(element)"),
|
||||
" — just ", el("code", "<element>.dispatchEvent(new Event(<event>[, <options>]))")
|
||||
|
@ -3,8 +3,8 @@ import { t, T } from "./utils/index.js";
|
||||
export const info= {
|
||||
href: "./",
|
||||
title: t`Introduction`,
|
||||
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`,
|
||||
fullTitle: t`Vanilla for flavouring — a full-fledged feast for large projects`,
|
||||
description: t`Reactive DOM library for creating dynamic UIs with a declarative syntax`,
|
||||
};
|
||||
|
||||
import { el } from "deka-dom-el";
|
||||
@ -16,7 +16,11 @@ import { getLibraryUrl } from "./components/getLibraryUrl.html.js";
|
||||
/** @param {string} url */
|
||||
const fileURL= url=> new URL(url, import.meta.url);
|
||||
const references= {
|
||||
w_mvv:{
|
||||
npm: {
|
||||
title: t`NPM package page for dd<el>`,
|
||||
href: "https://www.npmjs.com/package/deka-dom-el",
|
||||
},
|
||||
w_mvv: {
|
||||
title: t`Wikipedia: Model–view–viewmodel`,
|
||||
href: "https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel",
|
||||
},
|
||||
@ -27,10 +31,9 @@ const references= {
|
||||
};
|
||||
/** @param {import("./types.d.ts").PageAttrs} attrs */
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("p").append(T`
|
||||
Welcome to Deka DOM Elements (dd<el> or DDE) — a lightweight library for building dynamic UIs with
|
||||
Welcome to Deka DOM Elements (dd<el> or DDE) — a library for building dynamic UIs with
|
||||
a declarative syntax that stays close to the native DOM API. dd<el> gives you powerful reactive tools
|
||||
without the complexity and overhead of larger frameworks.
|
||||
`),
|
||||
@ -40,11 +43,11 @@ export function page({ pkg, info }){
|
||||
el("li", t`No build step required — use directly in the browser`),
|
||||
el("li", t`Lightweight core (~10–15kB minified) without unnecessary dependencies (0 at now 😇)`),
|
||||
el("li", t`Natural DOM API — work with real DOM nodes, not abstractions`),
|
||||
el("li", t`Built-in reactivity with simplified but powerful signals system`),
|
||||
el("li", t`Built-in (but optional) reactivity with simplified but 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(example, { src: fileURL("./components/examples/introducing/helloWorld.js") }),
|
||||
|
||||
el(h3, { textContent: t`The 3PS Pattern: Simplified architecture pattern`, id: "h-3ps" }),
|
||||
el("p").append(T`
|
||||
@ -56,11 +59,11 @@ export function page({ pkg, info }){
|
||||
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(code, { src: fileURL("./components/examples/introducing/3ps-before.js") }),
|
||||
),
|
||||
el("div", { className: "tab" }).append(
|
||||
el("h5", t`dd<el>'s 3PS Pattern`),
|
||||
el(code, { src: fileURL("./components/examples/introducing/3ps.js"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/introducing/3ps.js") }),
|
||||
)
|
||||
)
|
||||
),
|
||||
@ -101,16 +104,20 @@ export function page({ pkg, info }){
|
||||
|
||||
el(h3, t`Getting Started`),
|
||||
el("p").append(T`
|
||||
There are multiple ways to include dd<el> in your project. You can use npm for a full development setup,
|
||||
or directly include it from a CDN for quick prototyping.
|
||||
There are multiple ways to include dd<el> in your project. You can use npm for a full development setup,
|
||||
or directly include it from a CDN for quick prototyping.
|
||||
`),
|
||||
el("h4", "npm installation"),
|
||||
el(code, { content: "npm install deka-dom-el --save", language: "shell", page_id }),
|
||||
el(code, { content: "npm install deka-dom-el --save", language: "shell" }),
|
||||
el("p").append(T`
|
||||
…see ${el("a", { textContent: "package page", ...references.npm, target: "_blank" })}.
|
||||
`),
|
||||
|
||||
el("h4", "CDN / Direct Script Usage"),
|
||||
el("p").append(T`
|
||||
Use the interactive selector below to choose your preferred format:
|
||||
`),
|
||||
el(getLibraryUrl, { page_id }),
|
||||
el(getLibraryUrl),
|
||||
el("div", { className: "note" }).append(
|
||||
el("p").append(T`
|
||||
Based on your selection, you can use dd<el> in your project like this:
|
||||
@ -119,10 +126,10 @@ export function page({ pkg, info }){
|
||||
// ESM format (modern JavaScript with import/export)
|
||||
import { el, on } from "https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-signals.min.js";
|
||||
|
||||
// Or with IIFE format (creates a global DDE object)
|
||||
// Or with IIFE format (creates a global DDE object)
|
||||
// <script src="https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/iife-with-signals.min.js"></script>
|
||||
const { el, on } = DDE;
|
||||
`, language: "js", page_id }),
|
||||
`, language: "js" }),
|
||||
),
|
||||
|
||||
el(h3, t`How to Use This Documentation`),
|
||||
@ -146,7 +153,7 @@ export function page({ pkg, info }){
|
||||
Integrating third-party functionalities`),
|
||||
el("li").append(T`${el("a", { href: "p09-optimization.html" })
|
||||
.append(el("strong", "Performance Optimization"))} — Techniques for optimizing your applications`),
|
||||
el("li").append(T`${el("a", { href: "p10-todomvc.html" }).append(el("strong", "TodoMVC"))} — A real-world
|
||||
el("li").append(T`${el("a", { href: "p10-todomvc.html" }).append(el("strong", "TodoMVC"))} — A real-world
|
||||
application implementation`),
|
||||
el("li").append(T`${el("a", { href: "p11-ssr.html" }).append(el("strong", "SSR"))} — Server-side
|
||||
rendering with dd<el>`),
|
||||
|
@ -47,12 +47,11 @@ const references= {
|
||||
};
|
||||
/** @param {import("./types.d.ts").PageAttrs} attrs */
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("p").append(T`
|
||||
Building user interfaces in JavaScript often involves creating and manipulating DOM elements.
|
||||
dd<el> provides a simple yet powerful approach to element creation that is declarative, chainable,
|
||||
and maintains a clean syntax close to HTML structure.
|
||||
dd<el> provides a simple yet powerful approach to element creation that is declarative, chainable,
|
||||
and maintains a clean syntax close to HTML structure.
|
||||
`),
|
||||
el("div", { className: "callout" }).append(
|
||||
el("h4", t`dd<el> Elements: Key Benefits`),
|
||||
@ -65,7 +64,7 @@ export function page({ pkg, info }){
|
||||
)
|
||||
),
|
||||
|
||||
el(code, { src: fileURL("./components/examples/elements/intro.js"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/elements/intro.js") }),
|
||||
|
||||
el(h3, t`Creating Elements: Native vs dd<el>`),
|
||||
el("p").append(T`
|
||||
@ -77,11 +76,11 @@ export function page({ pkg, info }){
|
||||
el("div", { className: "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(code, { src: fileURL("./components/examples/elements/native-dom-create.js") })
|
||||
),
|
||||
el("div").append(
|
||||
el("h5", t`dd<el> Approach`),
|
||||
el(code, { src: fileURL("./components/examples/elements/dde-dom-create.js"), page_id })
|
||||
el(code, { src: fileURL("./components/examples/elements/dde-dom-create.js") })
|
||||
)
|
||||
)
|
||||
),
|
||||
@ -89,7 +88,7 @@ export function page({ pkg, info }){
|
||||
The ${el("code", "el")} function provides a simple wrapper around ${el("code", "document.createElement")}
|
||||
with enhanced property assignment.
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/elements/dekaCreateElement.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/elements/dekaCreateElement.js") }),
|
||||
|
||||
el(h3, t`Advanced Property Assignment`),
|
||||
el("p").append(T`
|
||||
@ -122,7 +121,7 @@ export function page({ pkg, info }){
|
||||
el("dd").append(T`Pass ${el("code", "undefined")} to remove a property or attribute`)
|
||||
)
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/elements/dekaAssign.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/elements/dekaAssign.js") }),
|
||||
|
||||
el("div", { className: "note" }).append(
|
||||
el("p").append(T`
|
||||
@ -142,11 +141,11 @@ export function page({ pkg, info }){
|
||||
el("div", { className: "comparison" }).append(
|
||||
el("div", { className: "bad-practice" }).append(
|
||||
el("h5", t`❌ Native DOM API`),
|
||||
el(code, { src: fileURL("./components/examples/elements/native-dom-tree.js"), page_id })
|
||||
el(code, { src: fileURL("./components/examples/elements/native-dom-tree.js") })
|
||||
),
|
||||
el("div", { className: "good-practice" }).append(
|
||||
el("h5", t`✅ dd<el> Approach`),
|
||||
el(code, { src: fileURL("./components/examples/elements/dde-dom-tree.js"), page_id })
|
||||
el(code, { src: fileURL("./components/examples/elements/dde-dom-tree.js") })
|
||||
)
|
||||
)
|
||||
),
|
||||
@ -154,14 +153,14 @@ export function page({ pkg, info }){
|
||||
This chainable pattern is much cleaner and easier to follow, especially for deeply nested elements.
|
||||
It also makes it simple to add multiple children to a parent element in a single fluent expression.
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/elements/dekaAppend.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/elements/dekaAppend.js") }),
|
||||
|
||||
el(h3, t`Using Components to Build UI Fragments`),
|
||||
el("p").append(T`
|
||||
The ${el("code", "el")} function is overloaded to support both tag names and function components.
|
||||
This lets you refactor complex UI trees into reusable pieces:
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/elements/dekaBasicComponent.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/elements/dekaBasicComponent.js") }),
|
||||
el("p").append(T`
|
||||
Component functions receive the properties object as their first argument, just like regular elements.
|
||||
This makes it easy to pass data down to components and create reusable UI fragments.
|
||||
@ -185,9 +184,9 @@ export function page({ pkg, info }){
|
||||
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(example, { src: fileURL("./components/examples/elements/dekaElNS.js") }),
|
||||
el("p").append(T`
|
||||
This function returns a namespace-specific element creator, allowing you to work with any element type
|
||||
This function returns a namespace-specific element creator, allowing you to work with any element type
|
||||
using the same consistent interface.
|
||||
`),
|
||||
|
||||
|
@ -39,7 +39,6 @@ const references= {
|
||||
};
|
||||
/** @param {import("./types.d.ts").PageAttrs} attrs */
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("p").append(T`
|
||||
Events are at the core of interactive web applications. dd<el> provides a clean, declarative approach to
|
||||
@ -57,7 +56,7 @@ export function page({ pkg, info }){
|
||||
)
|
||||
),
|
||||
|
||||
el(code, { src: fileURL("./components/examples/events/intro.js"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/events/intro.js") }),
|
||||
|
||||
el(h3, t`Events and Listeners: Two Approaches`),
|
||||
el("p").append(T`
|
||||
@ -70,11 +69,11 @@ export function page({ pkg, info }){
|
||||
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(code, { content: `element.addEventListener("click", callback, options);`, language: "js" })
|
||||
),
|
||||
el("div", { className: "tab" }).append(
|
||||
el("h5", t`dd<el> Approach`),
|
||||
el(code, { content: `on("click", callback, options)(element);`, page_id })
|
||||
el(code, { content: `on("click", callback, options)(element);`, language: "js" })
|
||||
)
|
||||
)
|
||||
),
|
||||
@ -82,7 +81,7 @@ export function page({ pkg, info }){
|
||||
The main benefit of dd<el>’s approach is that it works as an Addon (see below), making it easy to integrate
|
||||
directly into element declarations.
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/events/compare.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/events/compare.js") }),
|
||||
|
||||
el(h3, t`Removing Event Listeners`),
|
||||
el("div", { className: "note" }).append(
|
||||
@ -91,7 +90,7 @@ export function page({ pkg, info }){
|
||||
${el("a", { textContent: "AbortSignal", ...references.mdn_abortListener })} for declarative removal:
|
||||
`)
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/events/abortSignal.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/events/abortSignal.js") }),
|
||||
el("p").append(T`
|
||||
This is the same for signals (see next section) and works well with scopes and library extendability (
|
||||
see scopes and extensions section — mainly ${el("code", "scope.signal")}).
|
||||
@ -101,26 +100,26 @@ export function page({ pkg, info }){
|
||||
el("div", { className: "tabs" }).append(
|
||||
el("div", { className: "tab", dataTab: "html-attr" }).append(
|
||||
el("h4", t`HTML Attribute Style`),
|
||||
el(code, { src: fileURL("./components/examples/events/attribute-event.js"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/events/attribute-event.js") }),
|
||||
el("p").append(T`
|
||||
Forces usage as an HTML attribute. Corresponds to
|
||||
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", dataTab: "property" }).append(
|
||||
el("h4", t`Property Assignment`),
|
||||
el(code, { src: fileURL("./components/examples/events/property-event.js"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/events/property-event.js") }),
|
||||
el("p", t`Assigns the event handler directly to the element’s property.`)
|
||||
),
|
||||
el("div", { className: "tab", dataTab: "addon" }).append(
|
||||
el("h4", t`Addon Approach`),
|
||||
el(code, { src: fileURL("./components/examples/events/chain-event.js"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/events/chain-event.js") }),
|
||||
el("p", t`Uses the addon pattern (so adds the event listener to the element), see above.`)
|
||||
)
|
||||
),
|
||||
el("p").append(T`
|
||||
For a deeper comparison of these approaches, see
|
||||
For a deeper comparison of these approaches, see
|
||||
${el("a", { textContent: "WebReflection’s detailed analysis", ...references.web_events })}.
|
||||
`),
|
||||
|
||||
@ -143,7 +142,7 @@ export function page({ pkg, info }){
|
||||
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(example, { src: fileURL("./components/examples/events/templateWithListeners.js") }),
|
||||
el("p").append(T`
|
||||
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.
|
||||
@ -167,7 +166,7 @@ export function page({ pkg, info }){
|
||||
el("dd", t`Fires when the element is removed from the DOM`),
|
||||
)
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/events/live-cycle.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/events/live-cycle.js") }),
|
||||
|
||||
el("div", { className: "note" }).append(
|
||||
el("p").append(T`
|
||||
@ -217,8 +216,8 @@ export function page({ pkg, info }){
|
||||
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(example, { src: fileURL("./components/examples/events/compareDispatch.js"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/events/dispatch.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/events/compareDispatch.js") }),
|
||||
el(code, { src: fileURL("./components/examples/events/dispatch.js") }),
|
||||
|
||||
el(h3, t`Best Practices`),
|
||||
el("ol").append(
|
||||
@ -251,6 +250,6 @@ export function page({ pkg, info }){
|
||||
)
|
||||
),
|
||||
|
||||
el(mnemonic)
|
||||
el(mnemonic),
|
||||
);
|
||||
}
|
||||
|
@ -42,7 +42,6 @@ const references= {
|
||||
};
|
||||
/** @param {import("./types.d.ts").PageAttrs} attrs */
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("p").append(T`
|
||||
Signals provide a simple yet powerful way to create reactive applications with dd<el>. They handle the
|
||||
@ -58,7 +57,7 @@ export function page({ pkg, info }){
|
||||
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 }),
|
||||
el(code, { src: fileURL("./components/examples/signals/intro.js") }),
|
||||
|
||||
el(h3, t`The 3-Part Structure of Signals`),
|
||||
el("p").append(T`
|
||||
@ -68,21 +67,21 @@ export function page({ pkg, info }){
|
||||
el("div", { className: "signal-diagram" }).append(
|
||||
el("div", { className: "signal-part" }).append(
|
||||
el("h4", t`PART 1: Create Signal`),
|
||||
el(code, { content: "const count = S(0);", page_id }),
|
||||
el(code, { content: "const count = S(0);", language: "js" }),
|
||||
el("p", t`Define a reactive value that can be observed and changed`)
|
||||
),
|
||||
el("div", { className: "signal-part" }).append(
|
||||
el("h4", t`PART 2: React to Changes`),
|
||||
el(code, { content: "S.on(count, value => updateUI(value));", page_id }),
|
||||
el(code, { content: "S.on(count, value => updateUI(value));", language: "js" }),
|
||||
el("p", t`Subscribe to signal changes with callbacks or effects`)
|
||||
),
|
||||
el("div", { className: "signal-part" }).append(
|
||||
el("h4", t`PART 3: Update Signal`),
|
||||
el(code, { content: "count.set(count.get() + 1);", page_id }),
|
||||
el(code, { content: "count.set(count.get() + 1);", language: "js" }),
|
||||
el("p", t`Modify the signal value, which automatically triggers updates`)
|
||||
)
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/signals/signals.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/signals/signals.js") }),
|
||||
|
||||
el("div", { className: "note" }).append(
|
||||
el("p").append(T`
|
||||
@ -125,12 +124,12 @@ export function page({ pkg, info }){
|
||||
Computed values (also called derived signals) automatically update when their dependencies change.
|
||||
Create them by passing ${el("strong", "a function")} to ${el("code", "S()")}:
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/signals/derived.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/signals/derived.js") }),
|
||||
el("p").append(T`
|
||||
Derived signals are read-only - you can’t call ${el("code", ".set()")} on them. Their value is always
|
||||
computed from their dependencies. They’re perfect for transforming or combining data from other signals.
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/signals/computations-abort.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/signals/computations-abort.js") }),
|
||||
|
||||
el(h3, t`Signal Actions: For Complex State`),
|
||||
el("p").append(T`
|
||||
@ -151,7 +150,7 @@ export function page({ pkg, info }){
|
||||
});
|
||||
// Use the action
|
||||
S.action(todos, "add", "New todo");
|
||||
`, page_id })
|
||||
`, language: "js" })
|
||||
),
|
||||
el("div", { className: "bad-practice" }).append(
|
||||
el("h5", t`❌ Without Actions`),
|
||||
@ -161,7 +160,7 @@ export function page({ pkg, info }){
|
||||
const items = todos.get();
|
||||
items.push("New todo");
|
||||
// This WON’T trigger updates!
|
||||
`, page_id }))
|
||||
`, language: "js" }))
|
||||
),
|
||||
),
|
||||
el("p").append(T`
|
||||
@ -172,7 +171,7 @@ export function page({ pkg, info }){
|
||||
${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(example, { src: fileURL("./components/examples/signals/actions-demo.js") }),
|
||||
|
||||
el("p").append(T`
|
||||
Actions provide these benefits:
|
||||
@ -186,7 +185,7 @@ export function page({ pkg, info }){
|
||||
el("p").append(T`
|
||||
Here’s a more complete example of a todo list using signal actions:
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/signals/actions-todos.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/signals/actions-todos.js") }),
|
||||
|
||||
el("div", { className: "tip" }).append(
|
||||
el("p").append(T`
|
||||
@ -223,7 +222,7 @@ export function page({ pkg, info }){
|
||||
|
||||
// Later:
|
||||
color.set("red"); // UI updates automatically
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
),
|
||||
el("div", { className: "tab", dataTab: "elements" }).append(
|
||||
el("h4", t`Reactive Elements`),
|
||||
@ -241,7 +240,7 @@ export function page({ pkg, info }){
|
||||
|
||||
// Later:
|
||||
S.action(items, "push", "Dragonfruit"); // List updates automatically
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
)
|
||||
),
|
||||
|
||||
@ -250,12 +249,12 @@ export function page({ pkg, info }){
|
||||
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(example, { src: fileURL("./components/examples/signals/dom-attrs.js") }),
|
||||
|
||||
el("p").append(T`
|
||||
${el("code", "S.el()")} is especially powerful for conditional rendering and lists:
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/signals/dom-el.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/signals/dom-el.js") }),
|
||||
|
||||
el(h3, t`Best Practices for Signals`),
|
||||
el("p").append(T`
|
||||
@ -289,7 +288,7 @@ export function page({ pkg, info }){
|
||||
el("h4", t`We can process form events without signals`),
|
||||
el("p", t`This can be used when the form data doesn’t need to be reactive and we just waiting for
|
||||
results.`),
|
||||
el(code, { page_id, content: `
|
||||
el(code, { content: `
|
||||
const onFormSubmit = on("submit", e => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
@ -301,13 +300,13 @@ export function page({ pkg, info }){
|
||||
return el("form", null, onFormSubmit).append(
|
||||
// …
|
||||
);
|
||||
` })
|
||||
`, language: "js" })
|
||||
),
|
||||
|
||||
el("div", { className: "tab", dataTab: "variables" }).append(
|
||||
el("h4", t`We can use variables without signals`),
|
||||
el("p", t`We use this when we dont’t need to reflect changes in the elsewhere (UI).`),
|
||||
el(code, { page_id, content: `
|
||||
el(code, { content: `
|
||||
let canSubmit = false;
|
||||
|
||||
const onFormSubmit = on("submit", e => {
|
||||
@ -318,14 +317,14 @@ export function page({ pkg, info }){
|
||||
const onAllowSubmit = on("click", e => {
|
||||
canSubmit = true;
|
||||
});
|
||||
`}),
|
||||
`, language: "js" }),
|
||||
),
|
||||
|
||||
el("div", { className: "tab", dataTab: "state" }).append(
|
||||
el("h4", t`Using signals`),
|
||||
el("p", t`We use this when we need to reflect changes for example in the UI (e.g. enable/disable
|
||||
buttons).`),
|
||||
el(code, { page_id, content: `
|
||||
el(code, { content: `
|
||||
const canSubmit = S(false);
|
||||
|
||||
const onFormSubmit = on("submit", e => {
|
||||
@ -341,7 +340,7 @@ export function page({ pkg, info }){
|
||||
el("button", { textContent: "Allow Submit", type: "button" }, onAllowSubmit),
|
||||
el("button", { disabled: S(()=> !canSubmit), textContent: "Submit" })
|
||||
);
|
||||
`}),
|
||||
`, language: "js" }),
|
||||
),
|
||||
),
|
||||
|
||||
|
@ -27,14 +27,13 @@ const references= {
|
||||
};
|
||||
/** @param {import("./types.d.ts").PageAttrs} attrs */
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("p").append(T`
|
||||
For state-less components we can use functions as UI components (see “Elements” page). But in real life,
|
||||
we may need to handle the component’s life-cycle and provide JavaScript the way to properly use
|
||||
the ${el("a", { textContent: t`Garbage collection`, ...references.garbage_collection })}.
|
||||
`),
|
||||
el(code, { src: fileURL("./components/examples/scopes/intro.js"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/scopes/intro.js") }),
|
||||
el("p").append(T`The library therefore uses ${el("em", t`scopes`)} to provide these functionalities.`),
|
||||
|
||||
el(h3, t`Understanding Host Elements and Scopes`),
|
||||
@ -83,7 +82,7 @@ export function page({ pkg, info }){
|
||||
el("dd", t`Applies the addons to the host element (and returns the host element)`)
|
||||
)
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/scopes/scopes-and-hosts.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/scopes/scopes-and-hosts.js") }),
|
||||
|
||||
el("div", { className: "tip" }).append(
|
||||
el("p").append(T`
|
||||
@ -95,7 +94,7 @@ export function page({ pkg, info }){
|
||||
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(code, { src: fileURL("./components/examples/scopes/good-practise.js") }),
|
||||
|
||||
el(h3, t`Class-Based Components`),
|
||||
el("p").append(T`
|
||||
@ -103,7 +102,7 @@ export function page({ pkg, info }){
|
||||
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(example, { src: fileURL("./components/examples/scopes/class-component.js") }),
|
||||
|
||||
el(h3, t`Automatic Cleanup with Scopes`),
|
||||
el("p").append(T`
|
||||
@ -123,7 +122,7 @@ export function page({ pkg, info }){
|
||||
- Custom cleanup code (dd<el> and user)
|
||||
` })
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/scopes/cleaning.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/scopes/cleaning.js") }),
|
||||
|
||||
el("div", { className: "note" }).append(
|
||||
el("p").append(T`
|
||||
@ -149,17 +148,17 @@ export function page({ pkg, info }){
|
||||
el("div", { className: "tab", dataTab: "declarative" }).append(
|
||||
el("h4", t`✅ Declarative Approach`),
|
||||
el("p", t`Define what your UI should look like based on state:`),
|
||||
el(code, { src: fileURL("./components/examples/scopes/declarative.js"), page_id })
|
||||
el(code, { src: fileURL("./components/examples/scopes/declarative.js") })
|
||||
),
|
||||
el("div", { className: "tab", dataTab: "imperative" }).append(
|
||||
el("h4", t`⚠️ Imperative Approach`),
|
||||
el("p", t`Manually update the DOM in response to events:`),
|
||||
el(code, { src: fileURL("./components/examples/scopes/imperative.js"), page_id })
|
||||
el(code, { src: fileURL("./components/examples/scopes/imperative.js") })
|
||||
),
|
||||
el("div", { className: "tab", dataTab: "mixed" }).append(
|
||||
el("h4", t`❌ Mixed Approach`),
|
||||
el("p", t`This approach should be avoided:`),
|
||||
el(code, { src: fileURL("./components/examples/scopes/mixed.js"), page_id })
|
||||
el(code, { src: fileURL("./components/examples/scopes/mixed.js") })
|
||||
)
|
||||
),
|
||||
|
||||
|
@ -57,7 +57,6 @@ const references= {
|
||||
};
|
||||
/** @param {import("./types.d.ts").PageAttrs} attrs */
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("p").append(T`
|
||||
dd<el> pairs powerfully with ${el("a", references.mdn_web_components).append(el("strong", t`Web
|
||||
@ -73,7 +72,7 @@ export function page({ pkg, info }){
|
||||
el("li", t`Clean component lifecycle management`),
|
||||
),
|
||||
),
|
||||
el(code, { src: fileURL("./components/examples/customElement/intro.js"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/customElement/intro.js") }),
|
||||
|
||||
el(h3, t`Getting Started: Web Components Basics`),
|
||||
el("p").append(T`
|
||||
@ -95,7 +94,7 @@ export function page({ pkg, info }){
|
||||
el("p").append(T`
|
||||
Let’s start with a basic Custom Element example without dd<el> to establish the foundation:
|
||||
`),
|
||||
el(code, { src: fileURL("./components/examples/customElement/native-basic.js"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/customElement/native-basic.js") }),
|
||||
|
||||
el("div", { className: "note" }).append(
|
||||
el("p").append(T`
|
||||
@ -124,7 +123,7 @@ export function page({ pkg, info }){
|
||||
el("dd", t`Allows using on.connected(), on.disconnected() or S.observedAttributes().`)
|
||||
)
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/customElement/customElementWithDDE.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/customElement/customElementWithDDE.js") }),
|
||||
|
||||
el("div", { className: "tip" }).append(
|
||||
el("p").append(T`
|
||||
@ -156,7 +155,7 @@ export function page({ pkg, info }){
|
||||
el("dd", t`The rendered DOM tree`)
|
||||
)
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/customElement/dde.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/customElement/dde.js") }),
|
||||
|
||||
el("div", { className: "note" }).append(
|
||||
el("p").append(T`
|
||||
@ -188,7 +187,7 @@ export function page({ pkg, info }){
|
||||
Using the ${el("code", "S.observedAttributes")} creates a reactive connection between your element’s
|
||||
attributes and its internal rendering. When attributes change, your component automatically updates!
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/customElement/observedAttributes.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/customElement/observedAttributes.js") }),
|
||||
|
||||
el("div", { className: "callout" }).append(
|
||||
el("h4", t`How S.observedAttributes Works`),
|
||||
@ -221,7 +220,7 @@ export function page({ pkg, info }){
|
||||
<p>Content</p>
|
||||
` })
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/customElement/shadowRoot.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/customElement/shadowRoot.js") }),
|
||||
|
||||
el("p").append(T`
|
||||
For more information on Shadow DOM, see
|
||||
@ -234,7 +233,7 @@ export function page({ pkg, info }){
|
||||
Besides the encapsulation, the Shadow DOM allows for using the ${el("a", references.mdn_shadow_dom_slot).append(
|
||||
el("strong", t`<slot>`), t` element(s)`)}. You can simulate this feature using ${el("code", "simulateSlots")}:
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/customElement/simulateSlots.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/customElement/simulateSlots.js") }),
|
||||
el("div", { className: "function-table" }).append(
|
||||
el("h4", t`simulateSlots`),
|
||||
el("dl").append(
|
||||
|
@ -15,7 +15,6 @@ 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("p").append(T`
|
||||
Debugging is an essential part of application development. This guide provides techniques
|
||||
@ -36,7 +35,7 @@ export function page({ pkg, info }){
|
||||
el(code, { content: `
|
||||
const signal = S(0);
|
||||
console.log('Current value:', signal.valueOf());
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
el("div", { className: "warning" }).append(
|
||||
el("p").append(T`
|
||||
${el("code", "signal.get")} is OK, but in some situations may lead to unexpected results:
|
||||
@ -49,7 +48,7 @@ export function page({ pkg, info }){
|
||||
// but typically this is fine ↓
|
||||
return signal.get() + 1;
|
||||
});
|
||||
` })
|
||||
`, language: "js" })
|
||||
),
|
||||
el("p").append(T`
|
||||
You can also monitor signal changes by adding a listener:
|
||||
@ -57,7 +56,7 @@ export function page({ pkg, info }){
|
||||
el(code, { content: `
|
||||
// Log every time the signal changes
|
||||
S.on(signal, value => console.log('Signal changed:', value));
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("h4", t`Debugging derived signals`),
|
||||
el("p").append(T`
|
||||
@ -69,7 +68,7 @@ export function page({ pkg, info }){
|
||||
el("li", t`Add logging/debugger 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(example, { src: fileURL("./components/examples/debugging/consoleLog.js") }),
|
||||
|
||||
el("h4", t`Examining signal via DevTools`),
|
||||
el("p").append(T`
|
||||
@ -77,11 +76,11 @@ export function page({ pkg, info }){
|
||||
signal objects. It contains the following information:
|
||||
`),
|
||||
el("ul").append(
|
||||
// TODO: value?
|
||||
el("li", t`listeners: A Set of functions called when the signal value changes`),
|
||||
el("li", t`actions: Custom actions that can be performed on the signal`),
|
||||
el("li", t`onclear: Functions to run when the signal is cleared`),
|
||||
el("li", t`host: Reference to the host element/scope in which the signal was created`),
|
||||
el("li", t`readonly: Boolean flag indicating if the signal is read-only`)
|
||||
),
|
||||
el("p").append(T`
|
||||
…to determine the current value of the signal, call ${el("code", "signal.valueOf()")}. Don’t hesitate to
|
||||
@ -121,7 +120,7 @@ export function page({ pkg, info }){
|
||||
"S.el(S(()=> count.get() % 2), odd=> …)")}).
|
||||
`),
|
||||
),
|
||||
el(code, { src: fileURL("./components/examples/debugging/mutations.js"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/debugging/mutations.js") }),
|
||||
|
||||
el("h4", t`Memory leaks with signal listeners`),
|
||||
el("p").append(T`
|
||||
@ -137,7 +136,7 @@ export function page({ pkg, info }){
|
||||
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(code, { src: fileURL("./components/examples/debugging/debouncing.js") }),
|
||||
|
||||
el(h3, t`Browser DevTools tips for components and reactivity`),
|
||||
el("p").append(T`
|
||||
@ -150,7 +149,7 @@ export function page({ pkg, info }){
|
||||
that are automatically updated when signal values change. These elements are wrapped in special
|
||||
comment nodes for debugging (to be true they are also used internally, so please do not edit them by hand):
|
||||
`),
|
||||
el(code, { src: fileURL("./components/examples/debugging/dom-reactive-mark.html"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/debugging/dom-reactive-mark.html") }),
|
||||
el("p").append(T`
|
||||
This is particularly useful when debugging why a reactive section isn’t updating as expected.
|
||||
You can inspect the elements between the comment nodes to see their current state and the
|
||||
@ -187,7 +186,7 @@ export function page({ pkg, info }){
|
||||
so you can see the element and property that changes in the console right away. These properties make it
|
||||
easier to understand the reactive structure of your application when inspecting elements.
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/signals/debugging-dom.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/signals/debugging-dom.js") }),
|
||||
|
||||
el("p", { className: "note" }).append(T`
|
||||
${el("code", "<element>.__dde_reactive")} - An array property on DOM elements that tracks signal-to-element
|
||||
|
@ -14,7 +14,6 @@ const fileURL= url=> new URL(url, import.meta.url);
|
||||
|
||||
/** @param {import("./types.js").PageAttrs} attrs */
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("p").append(T`
|
||||
dd<el> is designed with extensibility in mind. This page covers how to separate
|
||||
@ -49,7 +48,7 @@ export function page({ pkg, info }){
|
||||
|
||||
// Using an addon
|
||||
el("div", { id: "example" }, myAddon({ option: "value" }));
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el(h3, t`Resource Cleanup with Abort Signals`),
|
||||
el("p").append(T`
|
||||
@ -83,7 +82,7 @@ export function page({ pkg, info }){
|
||||
const { signal }= scope;
|
||||
return el("div", null, externalLibraryAddon({ option: "value" }, signal));
|
||||
}
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el(h3, t`Building Library-Independent Extensions`),
|
||||
el("p").append(T`
|
||||
@ -104,7 +103,7 @@ export function page({ pkg, info }){
|
||||
});
|
||||
};
|
||||
}
|
||||
`, page_id })
|
||||
`, language: "js" })
|
||||
),
|
||||
el("div", { className: "tab" }).append(
|
||||
el("h5", t`⚠️ Library-Dependent`),
|
||||
@ -118,7 +117,7 @@ export function page({ pkg, info }){
|
||||
})(element);
|
||||
};
|
||||
}
|
||||
`, page_id })
|
||||
`, language: "js" })
|
||||
)
|
||||
)
|
||||
),
|
||||
@ -177,7 +176,7 @@ export function page({ pkg, info }){
|
||||
textContent: "All"
|
||||
})
|
||||
);
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("div", { className: "callout" }).append(
|
||||
el("h4", t`Benefits of Signal Factories`),
|
||||
@ -217,7 +216,7 @@ export function page({ pkg, info }){
|
||||
const counter = createEnhancedSignal(0);
|
||||
el("button", { textContent: "Increment", onclick: () => counter.increment() });
|
||||
el("div", S.text\`Count: \${counter}\`);
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("div", { className: "tip" }).append(
|
||||
el("p").append(T`
|
||||
@ -259,7 +258,7 @@ export function page({ pkg, info }){
|
||||
// Update signal value
|
||||
count.set(5); // Logs: 5
|
||||
console.log(doubled.get()); // 10
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
el("p").append(T`
|
||||
The independent signals API includes all core functionality (${el("code", "S()")}, ${el("code", "S.on()")},
|
||||
${el("code", "S.action()")}).
|
||||
|
@ -43,7 +43,6 @@ const references= {
|
||||
|
||||
/** @param {import("./types.d.ts").PageAttrs} attrs */
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("p").append(T`
|
||||
As your applications grow, performance becomes increasingly important. dd<el> provides several
|
||||
@ -60,7 +59,7 @@ export function page({ pkg, info }){
|
||||
el("li", t`Simple debugging for performance bottlenecks`)
|
||||
)
|
||||
),
|
||||
el(code, { src: fileURL("./components/examples/optimization/intro.js"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/optimization/intro.js") }),
|
||||
|
||||
el(h3, t`Memoization with memo: Native vs dd<el>`),
|
||||
el("p").append(T`
|
||||
@ -84,7 +83,7 @@ export function page({ pkg, info }){
|
||||
))
|
||||
);
|
||||
}
|
||||
`, page_id })
|
||||
`, language: "js" })
|
||||
),
|
||||
el("div").append(
|
||||
el("h5", t`With dd<el>'s memo`),
|
||||
@ -102,7 +101,7 @@ export function page({ pkg, info }){
|
||||
)))
|
||||
);
|
||||
}
|
||||
`, page_id })
|
||||
`, language: "js" })
|
||||
)
|
||||
)
|
||||
),
|
||||
@ -134,7 +133,7 @@ export function page({ pkg, info }){
|
||||
memo(todo.id, () =>
|
||||
el(TodoItem, todo)
|
||||
))))
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("p").append(T`
|
||||
The ${el("code", "memo")} function in this context:
|
||||
@ -146,7 +145,7 @@ export function page({ pkg, info }){
|
||||
el("li", t`Only calls the generator function when rendering an item with a new key`)
|
||||
),
|
||||
|
||||
el(example, { src: fileURL("./components/examples/optimization/memo.js"), page_id }),
|
||||
el(example, { src: fileURL("./components/examples/optimization/memo.js") }),
|
||||
|
||||
el(h3, t`Creating Memoization Scopes`),
|
||||
el("p").append(T`
|
||||
@ -171,7 +170,7 @@ export function page({ pkg, info }){
|
||||
const container = el("div").append(
|
||||
...items.map(item => renderItem(item))
|
||||
);
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("p").append(T`
|
||||
The scope function accepts options to customize its behavior:
|
||||
@ -188,7 +187,7 @@ export function page({ pkg, info }){
|
||||
// Clear cache when signal is aborted
|
||||
signal: controller.signal
|
||||
});
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
el("p").append(T`
|
||||
You can use custom memo scope as function in (e. g. ${el("code", "S.el(signal, renderList)")}) and as
|
||||
(Abort) signal use ${el("code", "scope.signal")}.
|
||||
@ -317,7 +316,7 @@ export function page({ pkg, info }){
|
||||
|
||||
// On subsequent renders, the cached fragment is empty!
|
||||
container.append(memoizedFragment); // Nothing gets appended
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("p").append(T`
|
||||
This happens because a DocumentFragment is emptied when it's appended to the DOM. When the fragment
|
||||
@ -338,7 +337,7 @@ export function page({ pkg, info }){
|
||||
S.el(itemsSignal, items => items.map(item => el("div", item)))
|
||||
)
|
||||
);
|
||||
`, page_id })
|
||||
`, language: "js" })
|
||||
),
|
||||
|
||||
el("p").append(T`
|
||||
|
@ -43,7 +43,6 @@ const references= {
|
||||
|
||||
/** @param {import("./types.d.ts").PageAttrs} attrs */
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("p").append(T`
|
||||
${el("a", references.todomvc).append("TodoMVC")} is a project that helps developers compare different
|
||||
@ -69,7 +68,7 @@ export function page({ pkg, info }){
|
||||
challenges in a clean, maintainable way.
|
||||
`),
|
||||
|
||||
el(example, { src: fileURL("./components/examples/reallife/todomvc.js"), variant: "big", page_id }),
|
||||
el(example, { src: fileURL("./components/examples/reallife/todomvc.js"), variant: "big" }),
|
||||
|
||||
el(h3, t`Application Architecture Overview`),
|
||||
el("p").append(T`
|
||||
@ -118,7 +117,7 @@ export function page({ pkg, info }){
|
||||
});
|
||||
});
|
||||
const todosRemainingS = S(()=> todosS.get().filter(todo => !todo.completed).length);
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("p").append(T`
|
||||
The ${el("code", "todosSignal")} function creates a custom signal with actions for manipulating the todos:
|
||||
@ -207,7 +206,7 @@ export function page({ pkg, info }){
|
||||
});
|
||||
return out;
|
||||
}
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("div", { className: "note" }).append(
|
||||
el("p").append(T`
|
||||
@ -241,7 +240,7 @@ export function page({ pkg, info }){
|
||||
memo(todo.id, ()=> el(TodoItem, todo, onDelete, onEdit)))
|
||||
)
|
||||
)
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("p").append(T`
|
||||
The derived signal automatically recalculates whenever either the todos list or the current filter changes,
|
||||
@ -263,7 +262,7 @@ export function page({ pkg, info }){
|
||||
type: "checkbox"
|
||||
}, onToggleAll),
|
||||
el("label", { htmlFor: "toggle-all", title: "Mark all as complete" }),
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("p").append(T`
|
||||
The "toggle all" checkbox allows users to mark all todos as completed or active. When the checkbox
|
||||
@ -300,7 +299,7 @@ export function page({ pkg, info }){
|
||||
// Component content...
|
||||
);
|
||||
}
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("p").append(T`
|
||||
The TodoItem component maintains its own local UI state with signals, providing immediate
|
||||
@ -311,16 +310,16 @@ export function page({ pkg, info }){
|
||||
el(code, { content: `
|
||||
// Dynamic class attributes
|
||||
el("a", {
|
||||
textContent: "All",
|
||||
className: S(()=> pageS.get() === "all" ? "selected" : ""),
|
||||
href: "#"
|
||||
textContent,
|
||||
classList: { selected: S(()=> pageS.get() === textContent.toLowerCase()) },
|
||||
href: \`#\${textContent.toLowerCase()}\`
|
||||
})
|
||||
|
||||
// Reactive classList
|
||||
el("li", {
|
||||
classList: { completed: isCompleted, editing: isEditing }
|
||||
})
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("div", { className: "tip" }).append(
|
||||
el("p").append(T`
|
||||
@ -341,7 +340,7 @@ export function page({ pkg, info }){
|
||||
memo(todo.id, ()=> el(TodoItem, todo, onDelete, onEdit)))
|
||||
)
|
||||
)
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("p").append(T`
|
||||
This approach ensures that:
|
||||
@ -355,18 +354,25 @@ export function page({ pkg, info }){
|
||||
|
||||
el("h4", t`Memoizing UI Sections`),
|
||||
el(code, { content: `
|
||||
S.el(todosS, todos => memo(todos.length, length=> length
|
||||
? el("footer", { className: "footer" }).append(
|
||||
// Footer content...
|
||||
S.el(todosS, ({ length }) => !length
|
||||
? el()
|
||||
: el("footer", { className: "footer" }).append(
|
||||
// …
|
||||
memo("filters", ()=>
|
||||
// …
|
||||
el("a", {
|
||||
textContent,
|
||||
classList: { selected: S(()=> pageS.get() === textContent.toLowerCase()) },
|
||||
href: \`#\${textContent.toLowerCase()}\`
|
||||
})
|
||||
// …
|
||||
)
|
||||
: el()
|
||||
))
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("p").append(T`
|
||||
By memoizing based on the todos length, the entire footer component is only re-rendered
|
||||
when todos are added or removed, not when their properties change. This improves performance
|
||||
by avoiding unnecessary DOM operations.
|
||||
We memoize the UI section and uses derived signal for the classList. Re-rendering this part is therefore
|
||||
unnecessary when the number of todos changes.
|
||||
`),
|
||||
|
||||
el("div", { className: "tip" }).append(
|
||||
@ -389,9 +395,11 @@ export function page({ pkg, info }){
|
||||
`),
|
||||
el(code, { content: `
|
||||
// Event handlers in the main component
|
||||
const onDelete = on("todo:delete", ev => S.action(todosS, "delete", ev.detail));
|
||||
const onEdit = on("todo:edit", ev => S.action(todosS, "edit", ev.detail));
|
||||
`, page_id }),
|
||||
const onDelete = on("todo:delete", ev =>
|
||||
S.action(todosS, "delete", /** @type {{ detail: Todo["id"] }} */(ev).detail));
|
||||
const onEdit = on("todo:edit", ev =>
|
||||
S.action(todosS, "edit", /** @type {{ detail: Partial<Todo> & { id: Todo["id"] } }} */(ev).detail));
|
||||
`, language: "js" }),
|
||||
|
||||
el("h4", t`2. The TodoItem Component with Scopes and Local State`),
|
||||
el("p").append(T`
|
||||
@ -434,7 +442,7 @@ export function page({ pkg, info }){
|
||||
|
||||
// Component implementation...
|
||||
}
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("div", { className: "tip" }).append(
|
||||
el("p").append(T`
|
||||
@ -455,7 +463,7 @@ export function page({ pkg, info }){
|
||||
}).append(
|
||||
// Component content...
|
||||
);
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("p").append(T`
|
||||
Benefits of using ${el("code", "classList")}:
|
||||
@ -494,7 +502,7 @@ export function page({ pkg, info }){
|
||||
value: title,
|
||||
"data-id": id
|
||||
}, onBlurEdit, onKeyDown, addFocus)
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("p").append(T`
|
||||
This approach offers several advantages:
|
||||
@ -522,27 +530,26 @@ export function page({ pkg, info }){
|
||||
el("h4", t`Conditional Todo List`),
|
||||
el(code, { content: `
|
||||
S.el(todosS, todos => todos.length
|
||||
? el("main", { className: "main" }).append(
|
||||
? el()
|
||||
: el("main", { className: "main" }).append(
|
||||
// Main content with toggle all and todo list
|
||||
)
|
||||
: el()
|
||||
)
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("h4", t`Conditional Edit Form`),
|
||||
el(code, { content: `
|
||||
S.el(isEditing, editing => editing
|
||||
? el("form", null, onSubmitEdit).append(
|
||||
S.el(isEditing, editing => !editing
|
||||
? el()
|
||||
: el("form", null, onSubmitEdit).append(
|
||||
el("input", {
|
||||
className: "edit",
|
||||
name: "edit",
|
||||
name: formEdit,
|
||||
value: title,
|
||||
"data-id": id
|
||||
}, onBlurEdit, onKeyDown, addFocus)
|
||||
)
|
||||
: el()
|
||||
)
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("h4", t`Conditional Clear Completed Button`),
|
||||
el(code, { content: `
|
||||
@ -553,7 +560,7 @@ export function page({ pkg, info }){
|
||||
{ textContent: "Clear completed", className: "clear-completed" },
|
||||
onClearCompleted)
|
||||
)
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("div", { className: "note" }).append(
|
||||
el("p").append(T`
|
||||
@ -599,7 +606,7 @@ export function page({ pkg, info }){
|
||||
if (event.key !== "Escape") return;
|
||||
isEditing.set(false);
|
||||
});
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("div", { className: "tip" }).append(
|
||||
el("p").append(T`
|
||||
@ -630,7 +637,7 @@ export function page({ pkg, info }){
|
||||
${el("strong", "Declarative Class Management:")} Using the classList property for cleaner class handling
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Focus Management:")} Reliable input focus with setTimeout
|
||||
${el("strong", "Focus Management:")} Reliable input focus with requestAnimationFrame
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Persistent Storage:")} Automatically saving application state with signal listeners
|
||||
|
@ -14,7 +14,6 @@ const fileURL= url=> new URL(url, import.meta.url);
|
||||
|
||||
/** @param {import("./types.js").PageAttrs} attrs */
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("div", { className: "warning" }).append(
|
||||
el("p").append(T`
|
||||
@ -45,7 +44,7 @@ export function page({ pkg, info }){
|
||||
than jsdom
|
||||
`),
|
||||
),
|
||||
el(code, { src: fileURL("./components/examples/ssr/intro.js"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/ssr/intro.js") }),
|
||||
|
||||
el(h3, t`Why Server-Side Rendering?`),
|
||||
el("p").append(T`
|
||||
@ -71,27 +70,27 @@ export function page({ pkg, info }){
|
||||
el("li", t`Provides a promise queue system for managing async operations during rendering`),
|
||||
el("li", t`Handles DOM property/attribute mapping differences between browsers and jsdom`)
|
||||
),
|
||||
el(code, { src: fileURL("./components/examples/ssr/start.js"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/ssr/start.js") }),
|
||||
|
||||
el(h3, t`Basic SSR Example`),
|
||||
el("p").append(T`
|
||||
Here’s a simple example of how to use dd<el> for server-side rendering in a Node.js script:
|
||||
`),
|
||||
el(code, { src: fileURL("./components/examples/ssr/basic-example.js"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/ssr/basic-example.js") }),
|
||||
|
||||
el(h3, t`Building a Static Site Generator`),
|
||||
el("p").append(T`
|
||||
You can build a complete static site generator with dd<el>. In fact, this documentation site
|
||||
is built using dd<el> for server-side rendering! Here’s how the documentation build process works:
|
||||
`),
|
||||
el(code, { src: fileURL("./components/examples/ssr/static-site-generator.js"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/ssr/static-site-generator.js") }),
|
||||
|
||||
el(h3, t`Working with Async Content in SSR`),
|
||||
el("p").append(T`
|
||||
The jsdom export includes a queue system to handle asynchronous operations during rendering.
|
||||
This is crucial for components that fetch data or perform other async tasks.
|
||||
`),
|
||||
el(code, { src: fileURL("./components/examples/ssr/async-data.js"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/ssr/async-data.js") }),
|
||||
|
||||
el(h3, t`Working with Dynamic Imports for SSR`),
|
||||
el("p").append(T`
|
||||
@ -119,7 +118,7 @@ export function page({ pkg, info }){
|
||||
el("p").append(T`
|
||||
Follow this pattern when creating server-side rendered pages:
|
||||
`),
|
||||
el(code, { src: fileURL("./components/examples/ssr/pages.js"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/ssr/pages.js") }),
|
||||
|
||||
el(h3, t`SSR Considerations and Limitations`),
|
||||
el("p").append(T`
|
||||
@ -141,7 +140,7 @@ export function page({ pkg, info }){
|
||||
This documentation site itself is built using dd<el>’s SSR capabilities.
|
||||
The build process collects all page components, renders them with jsdom, and outputs static HTML files.
|
||||
`),
|
||||
el(code, { src: fileURL("./components/examples/ssr/static-site-generator.js"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/ssr/static-site-generator.js") }),
|
||||
|
||||
el("p").append(T`
|
||||
The resulting static files can be deployed to any static hosting service,
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { T, t } from "./utils/index.js";
|
||||
export const info= {
|
||||
title: t`Ireland Components`,
|
||||
fullTitle: t`Interactive Demo Components with Server-Side Pre-Rendering`,
|
||||
description: t`Creating live, interactive component examples in documentation with server-side
|
||||
rendering and client-side hydration.`,
|
||||
fullTitle: t`Server-Side Pre-Rendering and Client-Side Rehydration`,
|
||||
description: t`Using Ireland components for server-side pre-rendering and client-side rehydration`,
|
||||
};
|
||||
|
||||
import { el } from "deka-dom-el";
|
||||
@ -16,7 +15,6 @@ const fileURL= url=> new URL(url, import.meta.url);
|
||||
|
||||
/** @param {import("./types.js").PageAttrs} attrs */
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("div", { className: "warning" }).append(
|
||||
el("p").append(T`
|
||||
@ -67,7 +65,7 @@ export function page({ pkg, info }){
|
||||
src: fileURL("./components/examples/path/to/component.js"),
|
||||
exportName: "NamedExport", // optional, defaults to "default",
|
||||
})
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el("p").append(T`
|
||||
During the build process (${el("code", "bs/docs.js")}), the following happens:
|
||||
@ -119,7 +117,7 @@ export function page({ pkg, info }){
|
||||
|
||||
// Final build step - trigger SSR end event
|
||||
dispatchEvent("onssrend");
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
el("h4", t`File Registration`),
|
||||
el(code, { content: `
|
||||
// From docs/ssr.js - File registration system
|
||||
@ -145,7 +143,7 @@ export function page({ pkg, info }){
|
||||
head[head instanceof HTMLScriptElement ? "src" : "href"] = file_name;
|
||||
document.head.append(head);
|
||||
}
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
el("h4", t`Server-Side Rendering`),
|
||||
el(code, { content: `
|
||||
// From docs/components/ireland.html.js - Server-side component implementation
|
||||
@ -226,7 +224,7 @@ export function page({ pkg, info }){
|
||||
\`.trim())
|
||||
);
|
||||
}
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
el("h4", t`Client-Side Hydration`),
|
||||
el(code, { content: `
|
||||
// From docs/components/ireland.js.js - Client-side hydration
|
||||
@ -250,7 +248,7 @@ export function page({ pkg, info }){
|
||||
});
|
||||
});
|
||||
}
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el(h3, t`Live Example`),
|
||||
el("p").append(T`
|
||||
@ -259,14 +257,10 @@ export function page({ pkg, info }){
|
||||
rendered with the Ireland component system:
|
||||
`),
|
||||
|
||||
el(code, {
|
||||
src: fileURL("./components/examples/ireland-test/counter.js"),
|
||||
page_id
|
||||
}),
|
||||
el(code, { src: fileURL("./components/examples/ireland-test/counter.js") }),
|
||||
el(ireland, {
|
||||
src: fileURL("./components/examples/ireland-test/counter.js"),
|
||||
exportName: "CounterStandard",
|
||||
page_id
|
||||
}),
|
||||
|
||||
el("p").append(T`
|
||||
|
@ -40,7 +40,6 @@ const references= {
|
||||
|
||||
/** @param {import("./types.d.ts").PageAttrs} attrs */
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("p").append(T`
|
||||
This reference guide provides a comprehensive summary of dd<el>’s key concepts, best practices,
|
||||
@ -166,7 +165,7 @@ export function page({ pkg, info }){
|
||||
className: S(() => countS.get() > 10 ? 'warning' : '')
|
||||
})
|
||||
);
|
||||
`, page_id }),
|
||||
`, language: "js" }),
|
||||
|
||||
el(h3, t`Key Concepts Reference`),
|
||||
|
||||
|
@ -13,7 +13,6 @@ import { converter } from "./components/converter.html.js";
|
||||
|
||||
/** @param {import("./types.d.ts").PageAttrs} attrs */
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("p").append(T`
|
||||
Transitioning from HTML to dd<el> is simple with our interactive converter. This tool helps you quickly
|
||||
@ -28,7 +27,7 @@ export function page({ pkg, info }){
|
||||
),
|
||||
|
||||
// The actual converter component
|
||||
el(converter, { page_id }),
|
||||
el(converter),
|
||||
|
||||
el(h3, t`Next Steps`),
|
||||
el("p").append(T`
|
||||
|
@ -14,7 +14,6 @@ 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("p").append(T`
|
||||
Real-world application examples showcasing how to build complete, production-ready interfaces with dd<el>:
|
||||
@ -25,23 +24,21 @@ export function page({ pkg, info }){
|
||||
third-party charting library, data fetching and state management, responsive layout design, and multiple
|
||||
interactive components working together.
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/case-studies/data-dashboard.js"), variant: "big", page_id }),
|
||||
el(example, { src: fileURL("./components/examples/case-studies/data-dashboard.js"), variant: "big" }),
|
||||
|
||||
el(h3, t`Interactive Form`),
|
||||
el("p").append(T`
|
||||
Complete form with real-time validation, conditional rendering, and responsive design. Form handling with
|
||||
real-time validation, reactive UI updates, complex form state management, and clean separation of concerns.
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/case-studies/interactive-form.js"), variant: "big", page_id }),
|
||||
|
||||
el(example, { src: fileURL("./components/examples/case-studies/interactive-form.js"), variant: "big" }),
|
||||
|
||||
el(h3, t`Interactive Image Gallery`),
|
||||
el("p").append(T`
|
||||
Responsive image gallery with lightbox, keyboard navigation, and filtering. Dynamic loading of content,
|
||||
lightbox functionality, animation handling, and keyboard and gesture navigation support.
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/case-studies/image-gallery.js"), variant: "big", page_id }),
|
||||
|
||||
el(example, { src: fileURL("./components/examples/case-studies/image-gallery.js"), variant: "big" }),
|
||||
|
||||
el(h3, t`Task Manager`),
|
||||
el("p").append(T`
|
||||
@ -49,8 +46,29 @@ export function page({ pkg, info }){
|
||||
with signals, drag and drop functionality, local storage persistence, and responsive design for different
|
||||
devices.
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/case-studies/task-manager.js"), variant: "big", page_id }),
|
||||
el(example, { src: fileURL("./components/examples/case-studies/task-manager.js"), variant: "big" }),
|
||||
|
||||
el(h3, t`Product Catalog with asyncSignal`),
|
||||
el("p").append(T`
|
||||
Interactive product catalog with search, sorting, and pagination. Features include dynamic product filtering,
|
||||
responsive UI with detailed view toggles, error handling with retry capability, and proper resource cleanup.
|
||||
Demonstrates advanced signal usage, including derived signals, abortable async data fetching, and optimized
|
||||
rendering patterns.
|
||||
`),
|
||||
el("div", { className: "callout" }).append(
|
||||
el("h4", t`asyncSignal Utility`),
|
||||
el("p").append(T`
|
||||
This example showcases the asyncSignal utility, which is a powerful abstraction for handling async data
|
||||
fetching with proper state management. It provides:
|
||||
`),
|
||||
el("ul").append(
|
||||
el("li", t`Automatic tracking of loading, success, and error states`),
|
||||
el("li", t`AbortController integration for request cancellation`),
|
||||
el("li", t`Error handling and recovery`),
|
||||
el("li", t`Options for caching previous data during loading states`)
|
||||
)
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/case-studies/products.js"), variant: "big" }),
|
||||
|
||||
el(h3, t`TodoMVC`),
|
||||
el("p").append(T`
|
||||
|
@ -1,4 +1,8 @@
|
||||
export { t } from "./utils/index.js";
|
||||
/** @type {string} */
|
||||
export let page_id;
|
||||
/** @param {string} id */
|
||||
export function currentPageId(id){ page_id= id; }
|
||||
export const path_target= {
|
||||
root: "dist/docs/",
|
||||
css: "dist/docs/",
|
||||
|
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "deka-dom-el",
|
||||
"version": "0.9.3-alpha",
|
||||
"version": "0.9.4-alpha",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "deka-dom-el",
|
||||
"version": "0.9.3-alpha",
|
||||
"version": "0.9.4-alpha",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@size-limit/preset-small-lib": "~11.2",
|
||||
|
13
package.json
13
package.json
@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "deka-dom-el",
|
||||
"version": "0.9.3-alpha",
|
||||
"version": "0.9.4-alpha",
|
||||
"description": "A low-code library that simplifies the creation of native DOM elements/components using small wrappers and tweaks.",
|
||||
"author": "Jan Andrle <andrle.jan@centrum.cz>",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/jaandrle/deka-dom-el",
|
||||
"homepage": "https://jaandrle.github.io/deka-dom-el/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+ssh://git@github.com/jaandrle/deka-dom-el.git"
|
||||
@ -52,7 +52,6 @@
|
||||
"maxdepth": 3,
|
||||
"maxcomplexity": 14,
|
||||
"globals": {
|
||||
"requestIdleCallback": false,
|
||||
"AbortController": false,
|
||||
"AbortSignal": false,
|
||||
"FinalizationRegistry": false
|
||||
@ -61,25 +60,25 @@
|
||||
"size-limit": [
|
||||
{
|
||||
"path": "./index.js",
|
||||
"limit": "10.5 kB",
|
||||
"limit": "10 kB",
|
||||
"gzip": false,
|
||||
"brotli": false
|
||||
},
|
||||
{
|
||||
"path": "./signals.js",
|
||||
"limit": "12.5 kB",
|
||||
"limit": "12.2 kB",
|
||||
"gzip": false,
|
||||
"brotli": false
|
||||
},
|
||||
{
|
||||
"path": "./index-with-signals.js",
|
||||
"limit": "15 kB",
|
||||
"limit": "14.75 kB",
|
||||
"gzip": false,
|
||||
"brotli": false
|
||||
},
|
||||
{
|
||||
"path": "./index-with-signals.js",
|
||||
"limit": "5.5 kB"
|
||||
"limit": "5.25 kB"
|
||||
}
|
||||
],
|
||||
"modifyEsbuildConfig": {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { enviroment as env, evc, evd } from './common.js';
|
||||
import { isInstance } from "../helpers.js";
|
||||
import { isInstance, requestIdle } from "../helpers.js";
|
||||
|
||||
/**
|
||||
* Connection changes observer for tracking element connection/disconnection
|
||||
@ -149,15 +149,6 @@ function connectionsChangesObserverConstructor(){
|
||||
observer.disconnect();
|
||||
}
|
||||
|
||||
//TODO: remount support?
|
||||
/**
|
||||
* Schedule a task during browser idle time
|
||||
* @returns {Promise<void>} Promise that resolves when browser is idle
|
||||
*/
|
||||
function requestIdle(){ return new Promise(function(resolve){
|
||||
(requestIdleCallback || requestAnimationFrame)(resolve);
|
||||
}); }
|
||||
|
||||
/**
|
||||
* Collects child elements from the store that are contained by the given element
|
||||
* @param {Element} element - Parent element
|
||||
|
@ -68,3 +68,12 @@ export function observedAttributes(instance, observedAttribute){
|
||||
* @returns {string} The camelCase string
|
||||
*/
|
||||
function kebabToCamel(name){ return name.replace(/-./g, x=> x[1].toUpperCase()); }
|
||||
|
||||
/**
|
||||
* Schedule a task during browser idle time
|
||||
* @returns {Promise<void>} Promise that resolves when browser is idle
|
||||
*/
|
||||
export function requestIdle(){ return new Promise(function(resolve){
|
||||
(globalThis.requestIdleCallback || requestAnimationFrame)(resolve);
|
||||
});
|
||||
}
|
||||
|
@ -1,15 +1,14 @@
|
||||
import { queueSignalWrite, mark } from "./helpers.js";
|
||||
export { mark };
|
||||
import { hasOwn, oCreate, oAssign } from "../helpers.js";
|
||||
import { hasOwn, oCreate, oAssign, requestIdle } from "../helpers.js";
|
||||
|
||||
const Signal = oCreate(null, {
|
||||
const SignalReadOnly= oCreate(null, {
|
||||
get: { value(){ return read(this); } },
|
||||
set: { value(...v){ return write(this, ...v); } },
|
||||
toJSON: { value(){ return read(this); } },
|
||||
valueOf: { value(){ return this[mark] && this[mark].value; } }
|
||||
});
|
||||
const SignalReadOnly= oCreate(Signal, {
|
||||
set: { value(){ return; } },
|
||||
const Signal = oCreate(SignalReadOnly, {
|
||||
set: { value(...v){ return write(this, ...v); } },
|
||||
});
|
||||
/**
|
||||
* Checks if a value is a signal
|
||||
@ -214,7 +213,7 @@ signal.el= function(s, map){
|
||||
*/
|
||||
function requestCleanUpReactives(host){
|
||||
if(!host || !host[key_reactive]) return;
|
||||
(requestIdleCallback || setTimeout)(function(){
|
||||
requestIdle().then(function(){
|
||||
host[key_reactive]= host[key_reactive]
|
||||
.filter(([ s, el ])=> el.isConnected ? true : (removeSignalListener(...s), false));
|
||||
});
|
||||
@ -314,9 +313,14 @@ export const signals_config= {
|
||||
function removeSignalsFromElements(s, listener, ...notes){
|
||||
const { current }= scope;
|
||||
current.host(function(element){
|
||||
if(!element[key_reactive]) element[key_reactive]= [];
|
||||
const is_first= !element[key_reactive];
|
||||
if(is_first) element[key_reactive]= [];
|
||||
element[key_reactive].push([ [ s, listener ], ...notes ]);
|
||||
if(current.prevent) return; // typically document.body, doenst need auto-remove as it should happen on page leave
|
||||
if(
|
||||
!is_first
|
||||
// typically document.body, doenst need auto-remove as it should happen on page leave
|
||||
|| current.prevent
|
||||
) return;
|
||||
on.disconnected(()=>
|
||||
/*! Clears all Signals listeners added in the current scope/host (`S.el`, `assign`, …?).
|
||||
You can investigate the `__dde_reactive` key of the element. */
|
||||
@ -344,7 +348,7 @@ const cleanUpRegistry = new FinalizationRegistry(function(s){
|
||||
*/
|
||||
function create(is_readonly, value, actions){
|
||||
const varS = oCreate(is_readonly ? SignalReadOnly : Signal);
|
||||
const SI= toSignal(varS, value, actions, is_readonly);
|
||||
const SI= toSignal(varS, value, actions);
|
||||
cleanUpRegistry.register(SI, SI[mark]);
|
||||
return SI;
|
||||
}
|
||||
@ -367,11 +371,10 @@ const protoSigal= oAssign(oCreate(), {
|
||||
* @param {Object} s - Object to transform
|
||||
* @param {any} value - Initial value
|
||||
* @param {Object} actions - Custom actions
|
||||
* @param {boolean} [readonly=false] - Whether the signal is readonly
|
||||
* @returns {Object} Signal object with get() and set() methods
|
||||
* @private
|
||||
*/
|
||||
function toSignal(s, value, actions, readonly= false){
|
||||
function toSignal(s, value, actions){
|
||||
const onclear= [];
|
||||
if(typeOf(actions)!=="[object Object]")
|
||||
actions= {};
|
||||
@ -385,7 +388,6 @@ function toSignal(s, value, actions, readonly= false){
|
||||
value: oAssign(oCreate(protoSigal), {
|
||||
value, actions, onclear, host,
|
||||
listeners: new Set(),
|
||||
readonly
|
||||
}),
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
|
Loading…
x
Reference in New Issue
Block a user