1
0
mirror of https://github.com/jaandrle/deka-dom-el synced 2026-01-10 16:06:28 +01:00

2 Commits

Author SHA1 Message Date
fcef903836 📺 Maintenance update 2026-01-07 15:30:28 +01:00
f2c85ec983 🐛 📺 v0.9.5-alpha (#44)
*  📺 package (version and publint)

* 🔤 docs typos

* 🐛 this address / fixes #43

*  tiny el optimalization

* 🔤 improve docs wording

* 🔤 case-studies/products.js (AbortSignal)

* 🔤 🐛 case-studies/image-gallery.js
2025-03-21 14:43:25 +01:00
21 changed files with 696 additions and 982 deletions

View File

@@ -6,9 +6,10 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: default
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version: '20.16' node-version: '20.16'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'

1
.npmrc
View File

@@ -1,3 +1,2 @@
//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN} //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
registry=https://registry.npmjs.org/ registry=https://registry.npmjs.org/
always-auth=true

View File

@@ -1,5 +1,5 @@
**Alpha** **Alpha**
| [Docs](https://jaandrle.github.io/deka-dom-el "Official documentation and guide site") | [Docs&Examples](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") | [NPM](https://www.npmjs.com/package/deka-dom-el "Official NPM package page")
| [GitHub](https://github.com/jaandrle/deka-dom-el "Official GitHub repository") | [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")) ([*Gitea*](https://gitea.jaandrle.cz/jaandrle/deka-dom-el "GitHub repository mirror on my own Gitea instance"))
@@ -9,7 +9,7 @@
```javascript ```javascript
// 🌟 Reactive component with clear separation of concerns // 🌟 Reactive component with clear separation of concerns
document.body.append( document.body.append(
el(EmojiCounter, { initial: "🚀" }) el(EmojiCounter, { initial: "🚀" }),
); );
function EmojiCounter({ initial }) { function EmojiCounter({ initial }) {
@@ -34,7 +34,7 @@ function EmojiCounter({ initial }) {
el(Option, "🎉"), el(Option, "🎉"),
el(Option, "🚀"), el(Option, "🚀"),
el(Option, "💖"), el(Option, "💖"),
) ),
); );
} }
function Option({ textContent }){ function Option({ textContent }){
@@ -56,10 +56,10 @@ Creating reactive elements, components, and Web Components using the native
## Features at a Glance ## Features at a Glance
-**No build step required** — use directly in browsers or Node.js -**No build step required** — use directly in browsers or Node.js
- ☑️ **Lightweight** — ~10-15kB minified (original goal 10kB) with **zero**/minimal dependencies - **Minimalized footprint** — ~10-15kB minified bundle (original goal 10kB), **zero**/minimal dependencies and
-**Declarative & functional approach** for clean, maintainable code small in-memory size (auto-releasing resources as much as possible)
-**Declarative & functional approach support** for clean, maintainable code
-**Signals and events** for reactive UI -**Signals and events** for reactive UI
-**Auto-releasing resources** for memory management but nice development experience
-**Memoization for performance** — optimize rendering with intelligent caching -**Memoization for performance** — optimize rendering with intelligent caching
- ☑️ **Optional build-in signals** with support for custom reactive implementations (#39) - ☑️ **Optional build-in signals** with support for custom reactive implementations (#39)
- ☑️ **Server-side rendering** support via [jsdom](https://github.com/jsdom/jsdom) - ☑️ **Server-side rendering** support via [jsdom](https://github.com/jsdom/jsdom)

View File

@@ -9,3 +9,4 @@ npx editorconfig-checker -format gcc ${additional}
npx jshint index.js src ${additional} npx jshint index.js src ${additional}
[ "$one" = 'vim' ] && exit 0 [ "$one" = 'vim' ] && exit 0
npx size-limit npx size-limit
npx publint

View File

@@ -430,9 +430,10 @@ function createElement(tag, attributes, ...addons) {
scoped = 1; scoped = 1;
const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0); const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0);
scope.push({ scope: tag, host }); scope.push({ scope: tag, host });
el = tag(attributes || void 0); el = /** @type {Element} */
const is_fragment = isInstance(el, enviroment.F); tag(attributes || void 0);
if (el.nodeName === "#comment") break; if (el.nodeName === "#comment") break;
const is_fragment = isInstance(el, enviroment.F);
const el_mark = createElement.mark({ const el_mark = createElement.mark({
type: "component", type: "component",
name: tag.name, name: tag.name,
@@ -759,7 +760,7 @@ signal.on = function on2(s, listener, options = {}) {
}; };
signal.symbols = { signal.symbols = {
//signal: mark, //signal: mark,
onclear: Symbol.for("Signal.onclear") onclear: /* @__PURE__ */ Symbol.for("Signal.onclear")
}; };
signal.clear = function(...signals2) { signal.clear = function(...signals2) {
for (const s of signals2) { for (const s of signals2) {

File diff suppressed because one or more lines are too long

5
dist/esm.js vendored
View File

@@ -414,9 +414,10 @@ function createElement(tag, attributes, ...addons) {
scoped = 1; scoped = 1;
const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0); const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0);
scope.push({ scope: tag, host }); scope.push({ scope: tag, host });
el = tag(attributes || void 0); el = /** @type {Element} */
const is_fragment = isInstance(el, enviroment.F); tag(attributes || void 0);
if (el.nodeName === "#comment") break; if (el.nodeName === "#comment") break;
const is_fragment = isInstance(el, enviroment.F);
const el_mark = createElement.mark({ const el_mark = createElement.mark({
type: "component", type: "component",
name: tag.name, name: tag.name,

2
dist/esm.min.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -475,9 +475,10 @@ var DDE = (() => {
scoped = 1; scoped = 1;
const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0); const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0);
scope.push({ scope: tag, host }); scope.push({ scope: tag, host });
el = tag(attributes || void 0); el = /** @type {Element} */
const is_fragment = isInstance(el, enviroment.F); tag(attributes || void 0);
if (el.nodeName === "#comment") break; if (el.nodeName === "#comment") break;
const is_fragment = isInstance(el, enviroment.F);
const el_mark = createElement.mark({ const el_mark = createElement.mark({
type: "component", type: "component",
name: tag.name, name: tag.name,
@@ -804,7 +805,7 @@ var DDE = (() => {
}; };
signal.symbols = { signal.symbols = {
//signal: mark, //signal: mark,
onclear: Symbol.for("Signal.onclear") onclear: /* @__PURE__ */ Symbol.for("Signal.onclear")
}; };
signal.clear = function(...signals2) { signal.clear = function(...signals2) {
for (const s of signals2) { for (const s of signals2) {

File diff suppressed because one or more lines are too long

5
dist/iife.js vendored
View File

@@ -456,9 +456,10 @@ var DDE = (() => {
scoped = 1; scoped = 1;
const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0); const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0);
scope.push({ scope: tag, host }); scope.push({ scope: tag, host });
el = tag(attributes || void 0); el = /** @type {Element} */
const is_fragment = isInstance(el, enviroment.F); tag(attributes || void 0);
if (el.nodeName === "#comment") break; if (el.nodeName === "#comment") break;
const is_fragment = isInstance(el, enviroment.F);
const el_mark = createElement.mark({ const el_mark = createElement.mark({
type: "component", type: "component",
name: tag.name, name: tag.name,

2
dist/iife.min.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -82,10 +82,10 @@ export function ImageGallery(images= imagesSample) {
closeLightbox(); closeLightbox();
break; break;
case 'ArrowLeft': case 'ArrowLeft':
document.querySelector('.lightbox-prev-btn').click(); onPrevImage(e);
break; break;
case 'ArrowRight': case 'ArrowRight':
document.querySelector('.lightbox-next-btn').click(); onNextImage(e);
break; break;
} }
} }
@@ -126,12 +126,12 @@ export function ImageGallery(images= imagesSample) {
el("div", { el("div", {
className: "gallery-item", className: "gallery-item",
dataTag: image.alt.toLowerCase() dataTag: image.alt.toLowerCase()
}).append( }, onImageClick(image.id)).append(
el("img", { el("img", {
src: image.src, src: image.src,
alt: image.alt, alt: image.alt,
loading: "lazy" loading: "lazy"
}, onImageClick(image.id)), }),
el("div", { className: "gallery-item-caption" }).append( el("div", { className: "gallery-item-caption" }).append(
el("h3", image.title), el("h3", image.title),
el("p", image.alt) el("p", image.alt)
@@ -146,41 +146,41 @@ export function ImageGallery(images= imagesSample) {
S.el(isLightboxOpen, open => !open S.el(isLightboxOpen, open => !open
? el() ? el()
: el("div", { className: "lightbox-overlay" }, on("click", closeLightbox)).append( : el("div", { className: "lightbox-overlay" }, on("click", closeLightbox)).append(
el("div", { el("div", {
className: "lightbox-content", className: "lightbox-content",
onClick: e => e.stopPropagation() // Prevent closing when clicking inside onClick: e => e.stopPropagation() // Prevent closing when clicking inside
}).append( }).append(
el("button", { el("button", {
className: "lightbox-close-btn", className: "lightbox-close-btn",
"aria-label": "Close lightbox" ariaLabel: "Close lightbox",
}, on("click", closeLightbox)).append("×"), }, on("click", closeLightbox)).append("×"),
el("button", { el("button", {
className: "lightbox-prev-btn", className: "lightbox-prev-btn",
"aria-label": "Previous image" ariaLabel: "Previous image",
}, on("click", onPrevImage)).append(""), }, on("click", onPrevImage)).append(""),
el("button", { el("button", {
className: "lightbox-next-btn", className: "lightbox-next-btn",
"aria-label": "Next image" ariaLabel: "Next image",
}, on("click", onNextImage)).append(""), }, on("click", onNextImage)).append(""),
S.el(selectedImage, img => !img S.el(selectedImage, img => !img
? el() ? el()
: el("div", { className: "lightbox-image-container" }).append( : el("div", { className: "lightbox-image-container" }).append(
el("img", { el("img", {
src: img.src, src: img.src,
alt: img.alt, alt: img.alt,
className: "lightbox-image" className: "lightbox-image",
}), }),
el("div", { className: "lightbox-caption" }).append( el("div", { className: "lightbox-caption" }).append(
el("h2", img.title), el("h2", img.title),
el("p", img.alt) el("p", img.alt),
) ),
)
) )
) ),
) ),
),
), ),
); );
} }

View File

@@ -1,9 +1,13 @@
import { el, on } from "deka-dom-el"; import { el, on, scope } from "deka-dom-el";
import { S } from "deka-dom-el/signals"; import { S } from "deka-dom-el/signals";
export function ProductCatalog() { export function ProductCatalog() {
const { signal }= scope;
const itemsPerPage = 5; const itemsPerPage = 5;
const products = asyncSignal(S, fetchProducts, { initial: [], keepLast: true }); const products = asyncSignal(S,
fetchProducts,
{ initial: [], keepLast: true, signal });
const searchTerm = S(""); const searchTerm = S("");
const handleSearch = (e) => searchTerm.set(e.target.value); const handleSearch = (e) => searchTerm.set(e.target.value);
const sortOrder = S("default"); const sortOrder = S("default");
@@ -220,10 +224,14 @@ function simulateNetworkDelay(min = 300, max = 1200) {
* @template T * @template T
* @param {typeof S} S - Signal constructor * @param {typeof S} S - Signal constructor
* @param {(params: { signal: AbortSignal }) => Promise<T>} invoker - Async function to execute * @param {(params: { signal: AbortSignal }) => Promise<T>} invoker - Async function to execute
* @param {{ initial?: T, keepLast?: boolean }} options - Configuration options * @param {{ initial?: T, keepLast?: boolean, signal?: AbortSignal }} options - Configuration options
* @returns {Object} Status signals and control methods * @returns {Object} Status signals and control methods
*/ */
export function asyncSignal(S, invoker, { initial, keepLast } = {}) { export function asyncSignal(S, invoker, { initial, keepLast, signal } = {}) {
/** @type {(s: AbortSignal) => AbortSignal} */
const anySignal = !signal || !AbortSignal.any // TODO: make better
? s=> s
: s=> AbortSignal.any([s, signal]);
// Status tracking signals // Status tracking signals
const status = S("pending"); const status = S("pending");
const result = S(initial); const result = S(initial);
@@ -242,7 +250,7 @@ export function asyncSignal(S, invoker, { initial, keepLast } = {}) {
try { try {
const data = await invoker({ const data = await invoker({
signal: controller.signal, signal: anySignal(controller.signal),
}); });
if (!controller.signal.aborted) { if (!controller.signal.aborted) {
status.set("resolved"); status.set("resolved");

View File

@@ -74,13 +74,11 @@ export function h3({ textContent, id }){
if(!id) id= "h-"+textContent.toLowerCase().replaceAll(/\s/g, "-").replaceAll(/[^a-z-]/g, ""); if(!id) id= "h-"+textContent.toLowerCase().replaceAll(/\s/g, "-").replaceAll(/[^a-z-]/g, "");
return el("h3", { id }).append( return el("h3", { id }).append(
el("a", { el("a", {
className: "heading-anchor", className: "heading-anchor",
href: "#"+id, href: "#"+id,
textContent: "#", title: `Link to this section: ${textContent}`,
title: `Link to this section: ${textContent}`,
"aria-label": `Link to section ${textContent}`
}), }),
" ", "# ",
textContent, textContent,
); );
} }

View File

@@ -41,7 +41,12 @@ export function page({ pkg, info }){
el("h4", t`Key Benefits of dd<el>`), el("h4", t`Key Benefits of dd<el>`),
el("ul").append( el("ul").append(
el("li", t`No build step required — use directly in the browser`), el("li", t`No build step required — use directly in the browser`),
el("li", t`Lightweight core (~1015kB minified) without unnecessary dependencies (0 at now 😇)`), el("li", t`Minimalized footprint:`),
el("ul").append(
el("li", t`lightweight core (~1015kB minified)`),
el("li", t`…without unnecessary dependencies (0 at now 😇)`),
el("li", t`auto-releasing resources with focus on performance and development experience`),
),
el("li", t`Natural DOM API — work with real DOM nodes, not abstractions`), el("li", t`Natural DOM API — work with real DOM nodes, not abstractions`),
el("li", t`Built-in (but optional) 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("li", t`Clean code organization with the 3PS pattern`)

View File

@@ -151,7 +151,7 @@ export function header({ info: { href, title, description }, pkg }){
), ),
el("span", { el("span", {
className: "version-badge", className: "version-badge",
"aria-label": "Version", ariaLabel: "Version",
textContent: pkg.version || "" textContent: pkg.version || ""
}) })
), ),
@@ -165,13 +165,13 @@ export function header({ info: { href, title, description }, pkg }){
function nav({ href, pkg }){ function nav({ href, pkg }){
return el("nav", { return el("nav", {
role: "navigation", role: "navigation",
"aria-label": "Main navigation", ariaLabel: "Main navigation",
className: nav.name className: nav.name
}).append( }).append(
el("a", { el("a", {
href: pkg.homepage, href: pkg.homepage,
className: "github-link", className: "github-link",
"aria-label": "View on GitHub", ariaLabel: "View on GitHub",
target: "_blank", target: "_blank",
rel: "noopener noreferrer", rel: "noopener noreferrer",
}).append( }).append(
@@ -185,11 +185,11 @@ function nav({ href, pkg }){
return el("a", { return el("a", {
href: isIndex ? "./" : p.href, href: isIndex ? "./" : p.href,
title: p.description || `Go to ${p.title}`, title: p.description || `Go to ${p.title}`,
"aria-current": isCurrent ? "page" : null, ariaCurrent: isCurrent ? "page" : null,
}).append( }).append(
el("span", { el("span", {
className: "nav-number", className: "nav-number",
"aria-hidden": "true", ariaHidden: "true",
textContent: `${i+1}. ` textContent: `${i+1}. `
}), }),
p.title p.title

View File

@@ -168,6 +168,22 @@ export function page({ pkg, info }){
el("li", t`name - The name of the component function`), el("li", t`name - The name of the component function`),
el("li", t`host - Indicates whether the host is "this" (for DocumentFragments) or "parentElement"`), el("li", t`host - Indicates whether the host is "this" (for DocumentFragments) or "parentElement"`),
), ),
el("div", { className: "warning" }).append(
el("p").append(T`
There are edge case when the mark can be missing. For example, the (utility) components with reactive
keys such as ${el("code", ".textContent")}, ${el("code", ".innerText")} or ${el("code", ".innerHTML")}.
As they change the content of the host element.
`),
el(code, { content: `
function Counter() {
const count = S(0);
return el("button",
{ textContent: count, type: "button" },
on("click", () => count.set(count.get() + 1)),
);
}
`, language: "js" }),
),
el("h4", t`Identifying reactive elements in the DOM`), el("h4", t`Identifying reactive elements in the DOM`),
el("p").append(T` el("p").append(T`

1455
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
{ {
"name": "deka-dom-el", "name": "deka-dom-el",
"version": "0.9.4-alpha", "version": "0.9.6-alpha",
"description": "A low-code library that simplifies the creation of native DOM elements/components using small wrappers and tweaks.", "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>", "author": "Jan Andrle <andrle.jan@centrum.cz>",
"license": "MIT", "license": "MIT",
"homepage": "https://jaandrle.github.io/deka-dom-el/", "homepage": "https://github.com/jaandrle/deka-dom-el",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+ssh://git@github.com/jaandrle/deka-dom-el.git" "url": "git+ssh://git@github.com/jaandrle/deka-dom-el.git"
@@ -17,20 +17,20 @@
"type": "module", "type": "module",
"exports": { "exports": {
".": { ".": {
"import": "./index.js", "types": "./index.d.ts",
"types": "./index.d.ts" "import": "./index.js"
}, },
"./signals": { "./signals": {
"import": "./signals.js", "types": "./signals.d.ts",
"types": "./signals.d.ts" "import": "./signals.js"
}, },
"./jsdom": { "./jsdom": {
"import": "./jsdom.js", "types": "./jsdom.d.ts",
"types": "./jsdom.d.ts" "import": "./jsdom.js"
}, },
"./src/signals-lib": { "./src/signals-lib": {
"import": "./src/signals-lib/signals-lib.js", "types": "./src/signals-lib/signals-lib.d.ts",
"types": "./src/signals-lib/signals-lib.d.ts" "import": "./src/signals-lib/signals-lib.js"
} }
}, },
"files": [ "files": [
@@ -97,13 +97,14 @@
"typescript" "typescript"
], ],
"devDependencies": { "devDependencies": {
"@size-limit/preset-small-lib": "~11.2", "@size-limit/preset-small-lib": "~12.0",
"dts-bundle-generator": "~9.5", "dts-bundle-generator": "~9.5",
"editorconfig-checker": "~6.0", "editorconfig-checker": "~6.1",
"esbuild": "~0.25", "esbuild": "~0.27",
"jsdom": "~26.0", "jsdom": "~26.1",
"jshint": "~2.13", "jshint": "~2.13",
"nodejsscript": "^1.0.2", "nodejsscript": "^1.1",
"size-limit-node-esbuild": "~0.3" "publint": "^0.3",
"size-limit-node-esbuild": "~0.4"
} }
} }

View File

@@ -51,9 +51,9 @@ export function createElement(tag, attributes, ...addons){
const host= (...c)=> !c.length ? el_host : const host= (...c)=> !c.length ? el_host :
(scoped===1 ? addons.unshift(...c) : c.forEach(c=> c(el_host)), undefined); (scoped===1 ? addons.unshift(...c) : c.forEach(c=> c(el_host)), undefined);
scope.push({ scope: tag, host }); scope.push({ scope: tag, host });
el= tag(attributes || undefined); el= /** @type {Element} */(tag(attributes || undefined));
const is_fragment= isInstance(el, env.F);
if(el.nodeName==="#comment") break; if(el.nodeName==="#comment") break;
const is_fragment= isInstance(el, env.F);
const el_mark= createElement.mark({ const el_mark= createElement.mark({
type: "component", type: "component",
name: tag.name, name: tag.name,