1
0
mirror of https://github.com/jaandrle/deka-dom-el synced 2025-07-01 20:32:13 +02:00

3 Commits

Author SHA1 Message Date
4c450ae763 🐛 🔤 v0.9.4-alpha (#42)
* 🐛 fixes #41

*  adjust package size limits

* 🔤

* 📺 requestIdleCallback doesn need to be global

* 🔤 corrects irland page headers

* 📺 version

*  Signal ← SignalReadonly

* 🐛 ensures only one disconncetd listener

…for cleanup

*  🔤 Better build and improve texting

* 🐛 logo alignemt (due to gh)

* 🔤 md enhancements

* 🔤  products
2025-03-19 17:10:43 +01:00
04f93345f8 🐛 📺 npm pkg fix 2025-03-17 14:39:15 +01:00
5076771410 🐛 🔤 v0.9.3-alpha (#40)
* 🔤

*  Replaces defined with name/host

* 🐛 __dde_reactive

*  v0.9.3

* 🔤 examples+best paractises

* 🐛 📺 fixes npm run docs

*  finalizes v0.9.3-alpha

* 🔤 📺 coc tabs

* 🔤
2025-03-17 14:21:03 +01:00
59 changed files with 1523 additions and 612 deletions

40
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,40 @@
---
name: Bug report
about: Create a report to help us improve
title: ":bug: "
labels: bug
assignees: ''
---
## Bug Description
<!-- A clear and concise description of what the bug is -->
## Steps to Reproduce
<!-- Steps to reproduce the behavior -->
1.
2.
3.
## Expected Behavior
<!-- A clear and concise description of what you expected to happen -->
## Actual Behavior
<!-- A clear and concise description of what actually happened -->
## Code Sample
<!-- If applicable, add minimal code sample to reproduce the issue -->
```js
// Your code here
```
## Environment
- Browser and version: <!-- e.g. Chrome 120, Firefox 120, Safari 17 -->
- OS: <!-- e.g. Windows 11, macOS Sonoma, Ubuntu 22.04 -->
- dd<el> version: <!-- e.g. 0.9.2 -->
- Other relevant details:
## Screenshots
<!-- If applicable, add screenshots to help explain your problem -->
## Additional Context
<!-- Add any other context about the problem here -->

22
.github/ISSUE_TEMPLATE/documentation.md vendored Normal file
View File

@ -0,0 +1,22 @@
---
name: Documentation improvement
about: Suggest improvements to the documentation
title: ":abc: "
labels: documentation
assignees: ''
---
## Documentation Area
<!-- Which part of the documentation needs improvement? Provide links if applicable -->
## Current Issue
<!-- What's currently unclear, missing, or incorrect in the documentation? -->
## Suggested Improvement
<!-- Describe the improvement or addition you'd like to see -->
## Example Content
<!-- If applicable, provide example content or wording -->
## Additional Context
<!-- Any other context or screenshots about the documentation request -->

View File

@ -0,0 +1,29 @@
---
name: Feature request
about: Suggest an idea for this project
title: ":zap: "
labels: enhancement
assignees: ''
---
<!-- Consider open discussion: https://github.com/jaandrle/deka-dom-el/discussions first -->
## Problem Statement
<!-- A clear and concise description of the problem this feature would solve -->
## Proposed Solution
<!-- A detailed description of the feature you're suggesting -->
## Use Cases
<!-- Describe specific use cases where this feature would be beneficial -->
## Example Implementation
<!-- If possible, provide example code or pseudocode for how this feature might work -->
```js
// Example code
```
## Alternatives Considered
<!-- A description of any alternative solutions or features you've considered -->
## Additional Context
<!-- Any other context, screenshots, or examples that might be helpful -->

39
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,39 @@
<!--
Please use an appropriate git3moji in your PR title: https://robinpokorny.github.io/git3moji/
Examples:
- :bug: Fix signal update not triggering on nested properties
- :zap: Improve event delegation performance
- :abc: Add documentation for custom elements
-->
## Description
<!-- Describe the changes introduced by this PR -->
## Related Issues
<!-- Link any related issues using the format #ISSUE_NUMBER -->
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation update
- [ ] Code refactoring
- [ ] Performance improvement
- [ ] Test update
## Testing Performed
<!-- Describe the tests you've done to verify your changes -->
## Screenshots
<!-- If applicable, add screenshots to help explain your changes -->
## Checklist
- [ ] My code follows the code style of this project
- [ ] I have performed a self-review of my own code
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] I have updated the documentation accordingly
- [ ] My changes generate no new warnings
- [ ] All existing tests are passing
## Additional Notes
<!-- Any additional information that might be helpful for reviewers -->

18
.github/workflows/npm-publish.yml vendored Normal file
View File

@ -0,0 +1,18 @@
name: Publish Package to npmjs
on:
workflow_dispatch:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
with:
node-version: '20.16'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

3
.npmrc Normal file
View File

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

134
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,134 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
andrle.jan@centrum.cz.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

177
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,177 @@
# Contributing to Deka DOM Elements
Thank you for your interest in contributing to Deka DOM Elements (dd<el> or DDE)! This document provides guidelines and
instructions for contributing to the project.
## Table of Contents
- [Code of Conduct](#code-of-conduct)
- [Getting Started](#getting-started)
- [Development Workflow](#development-workflow)
- [Commit Guidelines](#commit-guidelines)
- [Pull Request Process](#pull-request-process)
- [Issue Guidelines](#issue-guidelines)
- [Coding Standards](#coding-standards)
- [Testing](#testing)
- [Documentation](#documentation)
## Code of Conduct
Please be respectful and inclusive in your interactions with other contributors. We aim to foster a welcoming community
where everyone feels comfortable participating.
## Getting Started
1. **Fork the repository**:
- Click the "Fork" button on the GitHub repository
2. **Clone your fork**:
```bash
git clone https://github.com/YOUR-USERNAME/deka-dom-el.git
cd deka-dom-el
```
3. **Set up the development environment**:
```bash
npm ci
```
4. **Add the upstream repository**:
```bash
git remote add upstream https://github.com/jaandrle/deka-dom-el.git
```
## Development Workflow
1. **Create a new branch**:
```bash
git checkout -b your-feature-branch
```
Use descriptive branch names that reflect the changes you're making.
2. **Make your changes**:
- Write clean, modular code
- Follow the project's coding standards (see [Coding Standards](#coding-standards))
- Include relevant tests for your changes
3. ~**Run tests**:~
```bash
#npm test
```
4. **Build the project**:
```bash
npm run build
#or
bs/build.js
```
5. **Preview documentation changes** (if applicable):
```bash
npm run docs
#or
bs/docs.js
```
…see [BS folder](./bs/README.md) for more info.
## Categorizing [![git3moji](https://img.shields.io/badge/git3moji%E2%80%93v1-%E2%9A%A1%EF%B8%8F%F0%9F%90%9B%F0%9F%93%BA%F0%9F%91%AE%F0%9F%94%A4-fffad8.svg?style=flat-square)](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 [git3moji](https://git3moji.netlify.app/) for commit messages. This helps keep the commit history clear and
consistent.
```
:emoji: Short summary of the change
```
…for example:
```
:bug: Fix signal update not triggering on nested properties
:zap: Improve event delegation performance
:abc: Add documentation for custom elements
```
## Pull Request Process
1. **Push your changes**:
```bash
git push origin your-feature-branch
```
2. **Open a Pull Request**:
- Go to the repository on GitHub
- Click "New Pull Request"
- Select your branch
- Provide a clear description of your changes
3. **PR Guidelines**:
- Use a clear, descriptive title with the appropriate git3moji
- Reference any related issues
- Explain what the changes do and why they are needed
- List any dependencies that are required for the change
- Include screenshots or examples if applicable
4. **Code Review**:
- Address any feedback from reviewers
- Make necessary changes and push to your branch
- The PR will be updated automatically
5. **Merge**:
- Once approved, a maintainer will merge your PR
- The main branch is protected, so you cannot push directly to it
## Issue Guidelines
When creating an issue, please use the appropriate template and include as much information as possible:
### Bug Reports
- Use the `:bug:` emoji in the title
- Clearly describe the issue
- Include steps to reproduce
- Mention your environment (browser, OS, etc.)
- Add screenshots if applicable
### Feature Requests
- Use the `:zap:` emoji in the title
- Describe the feature clearly
- Explain why it would be valuable
- Include examples or mockups if possible
### Documentation Improvements
- Use the `:abc:` emoji in the title
- Identify what documentation needs improvement
- Suggest specific changes or additions
## Coding Standards
- Follow the existing code style in the project
- Use meaningful variable and function names
- Keep functions small and focused
- Add comments for complex logic
- Use TypeScript types appropriately
<!--
## Testing
- Add tests for new features
- Update tests for modified code
- Ensure all tests pass before submitting a PR
-->
## Documentation
- Update the documentation when you add or modify features
- Document both API usage and underlying concepts
- Use clear, concise language
- Include examples where appropriate
---
Thank you for contributing to Deka DOM Elements! Your efforts help make the project better for everyone.

109
README.md
View File

@ -1,17 +1,11 @@
**WIP** (the experimentation phase)
| [source code on GitHub](https://github.com/jaandrle/deka-dom-el)
| [*mirrored* on Gitea](https://gitea.jaandrle.cz/jaandrle/deka-dom-el)
<p align="center">
<img src="docs/assets/logo.svg" alt="Deka DOM Elements Logo" width="180" height="180">
</p>
# Deka DOM Elements (dd\<el\> or DDE)
**Alpha**
| [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(
@ -19,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.host(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, "🚀"),
@ -52,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
@ -60,43 +56,35 @@ Creating reactive elements, components, and Web Components using the native
## Features at a Glance
-**No build step required** — use directly in browsers or Node.js
- ☑️ **Lightweight** — ~10-15kB minified (original goal 10kB) with zero/minimal dependencies
- ☑️ **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
- **Server-side rendering** support via [jsdom](https://github.com/jsdom/jsdom)
-**TypeScript support** (work in progress)
- ☑️ **Support for debugging with browser DevTools** without extensions
- ☑️ **Enhanced Web Components** support (work in progress)
## Why Another Library?
This library bridges the gap between minimal solutions like van/hyperscript and more comprehensive frameworks like
[solid-js](https://github.com/solidjs/solid), offering a balanced trade-off between size, complexity, and usability.
Following functional programming principles, dd\<el\> starts with pure JavaScript (DOM API) and gradually adds
auxiliary functions. These range from minor improvements to advanced features for building complete declarative
reactive UI templates.
A key advantage: any internal function (`assign`, `classListDeclarative`, `on`, `dispatchEvent`, `S`, etc.) can be used
independently while also working seamlessly together. This modular approach makes it easier to integrate the library
into existing projects.
- ☑️ **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
- ☑️ **Enhanced Web Components** support
## Getting Started
### 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
#### npm
```bash
# TBD
# npm install deka-dom-el
npm install deka-dom-el --save
```
#### CDN / Direct Script
…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) -->
@ -113,10 +101,18 @@ format selector](https://jaandrle.github.io/deka-dom-el/) on the documentation s
</script>
```
### Documentation
## Why Another Library?
- [**Interactive Guide**](https://jaandrle.github.io/deka-dom-el)
- [**Examples**](https://jaandrle.github.io/deka-dom-el/p15-examples.html)
This library bridges the gap between minimal solutions like van/hyperscript and more comprehensive frameworks like
[solid-js](https://github.com/solidjs/solid), offering a balanced trade-off between size, complexity, and usability.
Following functional programming principles, dd\<el\> starts with pure JavaScript (DOM API) and gradually adds
auxiliary functions. These range from minor improvements to advanced features for building complete declarative
reactive UI templates.
A key advantage: any internal function (`assign`, `classListDeclarative`, `on`, `dispatchEvent`, `S`, etc.) can be used
independently while also working seamlessly together. This modular approach makes it easier to integrate the library
into existing projects.
## Understanding Signals
@ -127,6 +123,11 @@ Signals are the reactive backbone of Deka DOM Elements:
- [TC39 Signals Proposal](https://github.com/tc39/proposal-signals) (future standard)
- [Observer pattern](https://en.wikipedia.org/wiki/Observer_pattern) (underlying concept)
## Contributing
We welcome contributions from the community! Please see our [Contributing Guide](CONTRIBUTING.md) for details on how to
get started, coding standards, commit guidelines, and the pull request process.
## Inspiration and Alternatives
- [vanjs-org/van](https://github.com/vanjs-org/van) — World's smallest reactive UI framework
@ -137,9 +138,9 @@ Signals are the reactive backbone of Deka DOM Elements:
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)

View File

@ -2,15 +2,18 @@
This project uses [jaandrle/bs: The simplest possible build system using executable/bash scripts](
https://github.com/jaandrle/bs).
#### bs/build.js [--minify|--help]
#### bs/build.js [main|signals] [--no-types|--help]
Generates alternative versions of the project (other than native ESM code).
Also generates typescript definitions.
#### bs/docs.js
Generates documentation, from `docs/`. Uses “SSR” technique, using deka-dom-el itself.
For running use `npx serve dist/docs`.
#### bs/lint.sh
Lints size of the project, jshint. See configs:
- `package.json`: key `size-limit`
- `package.json`: key `jshintConfig`
- `.editorconfig`

View File

@ -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 }),
);

View File

@ -41,19 +41,11 @@ function observedAttributes(instance, observedAttribute2) {
function kebabToCamel(name) {
return name.replace(/-./g, (x) => x[1].toUpperCase());
}
var Defined = class extends Error {
constructor() {
super();
const [curr, ...rest] = this.stack.split("\n");
const curr_file = curr.slice(curr.indexOf("@"), curr.indexOf(".js:") + 4);
const curr_lib = curr_file.includes("src/helpers.js") ? "src/" : curr_file;
this.stack = rest.find((l) => !l.includes(curr_lib)) || curr;
}
get compact() {
const { stack } = this;
return stack.slice(0, stack.indexOf("@") + 1) + "\u2026" + stack.slice(stack.lastIndexOf("/"));
}
};
function requestIdle() {
return new Promise(function(resolve) {
(globalThis.requestIdleCallback || requestAnimationFrame)(resolve);
});
}
// src/dom-lib/common.js
var enviroment = {
@ -192,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();
@ -303,7 +290,7 @@ var store_abort = /* @__PURE__ */ new WeakMap();
var scope = {
/**
* Gets the current scope
* @returns {Object} Current scope context
* @returns {typeof scopes[number]} Current scope context
*/
get current() {
return scopes[scopes.length - 1];
@ -663,9 +650,9 @@ function memo(key, generator) {
memo.isScope = function(obj) {
return obj[memoMark];
};
memo.scope = function memoScope(fun, { signal: signal2, onlyLast } = {}) {
memo.scope = function memoScopeCreate(fun, { signal: signal2, onlyLast } = {}) {
let cache = oCreate();
function memoScope2(...args) {
function memoScope(...args) {
if (signal2 && signal2.aborted)
return fun.apply(this, args);
let cache_local = onlyLast ? cache : oCreate();
@ -680,10 +667,10 @@ memo.scope = function memoScope(fun, { signal: signal2, onlyLast } = {}) {
cache = cache_local;
return out;
}
memoScope2[memoMark] = true;
memoScope2.clear = () => cache = oCreate();
if (signal2) signal2.addEventListener("abort", memoScope2.clear);
return memoScope2;
memoScope[memoMark] = true;
memoScope.clear = () => cache = oCreate();
if (signal2) signal2.addEventListener("abort", memoScope.clear);
return memoScope;
};
// src/signals-lib/helpers.js
@ -709,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);
} },
@ -723,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) {
@ -800,17 +784,17 @@ signal.clear = function(...signals2) {
};
var key_reactive = "__dde_reactive";
signal.el = function(s, map) {
map = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const mark_start = createElement.mark({ type: "reactive", source: new Defined().compact }, true);
const mapScoped = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const { current } = scope, { scope: sc } = current;
const mark_start = createElement.mark({ type: "reactive", component: sc && sc.name || "" }, true);
const mark_end = mark_start.end;
const out = enviroment.D.createDocumentFragment();
out.append(mark_start, mark_end);
const { current } = scope;
const reRenderReactiveElement = (v) => {
if (!mark_start.parentNode || !mark_end.parentNode)
return removeSignalListener(s, reRenderReactiveElement);
scope.push(current);
let els = map(v);
let els = mapScoped(v);
scope.pop();
if (!Array.isArray(els))
els = [els];
@ -830,14 +814,14 @@ signal.el = function(s, map) {
current.host(on.disconnected(
() => (
/*! Clears cached elements for reactive element `S.el` */
map.clear()
mapScoped.clear()
)
));
return out;
};
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));
});
}
@ -902,10 +886,10 @@ var signals_config = {
function removeSignalsFromElements(s, listener, ...notes) {
const { current } = scope;
current.host(function(element) {
if (element[key_reactive])
return element[key_reactive].push([[s, listener], ...notes]);
element[key_reactive] = [];
if (current.prevent) return;
const is_first = !element[key_reactive];
if (is_first) element[key_reactive] = [];
element[key_reactive].push([[s, listener], ...notes]);
if (!is_first || current.prevent) return;
on.disconnected(
() => (
/*! Clears all Signals listeners added in the current scope/host (`S.el`, `assign`, …?).
@ -920,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;
}
@ -932,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 = {};
@ -948,9 +932,7 @@ function toSignal(s, value, actions, readonly = false) {
actions,
onclear,
host,
listeners: /* @__PURE__ */ new Set(),
defined: new Defined().stack,
readonly
listeners: /* @__PURE__ */ new Set()
}),
enumerable: false,
writable: false,

File diff suppressed because one or more lines are too long

24
dist/esm.js vendored
View File

@ -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();
@ -274,7 +274,7 @@ var store_abort = /* @__PURE__ */ new WeakMap();
var scope = {
/**
* Gets the current scope
* @returns {Object} Current scope context
* @returns {typeof scopes[number]} Current scope context
*/
get current() {
return scopes[scopes.length - 1];
@ -634,9 +634,9 @@ function memo(key, generator) {
memo.isScope = function(obj) {
return obj[memoMark];
};
memo.scope = function memoScope(fun, { signal, onlyLast } = {}) {
memo.scope = function memoScopeCreate(fun, { signal, onlyLast } = {}) {
let cache = oCreate();
function memoScope2(...args) {
function memoScope(...args) {
if (signal && signal.aborted)
return fun.apply(this, args);
let cache_local = onlyLast ? cache : oCreate();
@ -651,10 +651,10 @@ memo.scope = function memoScope(fun, { signal, onlyLast } = {}) {
cache = cache_local;
return out;
}
memoScope2[memoMark] = true;
memoScope2.clear = () => cache = oCreate();
if (signal) signal.addEventListener("abort", memoScope2.clear);
return memoScope2;
memoScope[memoMark] = true;
memoScope.clear = () => cache = oCreate();
if (signal) signal.addEventListener("abort", memoScope.clear);
return memoScope;
};
export {
assign,

2
dist/esm.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -86,19 +86,11 @@ var DDE = (() => {
function kebabToCamel(name) {
return name.replace(/-./g, (x) => x[1].toUpperCase());
}
var Defined = class extends Error {
constructor() {
super();
const [curr, ...rest] = this.stack.split("\n");
const curr_file = curr.slice(curr.indexOf("@"), curr.indexOf(".js:") + 4);
const curr_lib = curr_file.includes("src/helpers.js") ? "src/" : curr_file;
this.stack = rest.find((l) => !l.includes(curr_lib)) || curr;
}
get compact() {
const { stack } = this;
return stack.slice(0, stack.indexOf("@") + 1) + "\u2026" + stack.slice(stack.lastIndexOf("/"));
}
};
function requestIdle() {
return new Promise(function(resolve) {
(globalThis.requestIdleCallback || requestAnimationFrame)(resolve);
});
}
// src/dom-lib/common.js
var enviroment = {
@ -237,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();
@ -348,7 +335,7 @@ var DDE = (() => {
var scope = {
/**
* Gets the current scope
* @returns {Object} Current scope context
* @returns {typeof scopes[number]} Current scope context
*/
get current() {
return scopes[scopes.length - 1];
@ -708,9 +695,9 @@ var DDE = (() => {
memo.isScope = function(obj) {
return obj[memoMark];
};
memo.scope = function memoScope(fun, { signal: signal2, onlyLast } = {}) {
memo.scope = function memoScopeCreate(fun, { signal: signal2, onlyLast } = {}) {
let cache = oCreate();
function memoScope2(...args) {
function memoScope(...args) {
if (signal2 && signal2.aborted)
return fun.apply(this, args);
let cache_local = onlyLast ? cache : oCreate();
@ -725,10 +712,10 @@ var DDE = (() => {
cache = cache_local;
return out;
}
memoScope2[memoMark] = true;
memoScope2.clear = () => cache = oCreate();
if (signal2) signal2.addEventListener("abort", memoScope2.clear);
return memoScope2;
memoScope[memoMark] = true;
memoScope.clear = () => cache = oCreate();
if (signal2) signal2.addEventListener("abort", memoScope.clear);
return memoScope;
};
// src/signals-lib/helpers.js
@ -754,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);
} },
@ -768,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) {
@ -845,17 +829,17 @@ var DDE = (() => {
};
var key_reactive = "__dde_reactive";
signal.el = function(s, map) {
map = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const mark_start = createElement.mark({ type: "reactive", source: new Defined().compact }, true);
const mapScoped = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const { current } = scope, { scope: sc } = current;
const mark_start = createElement.mark({ type: "reactive", component: sc && sc.name || "" }, true);
const mark_end = mark_start.end;
const out = enviroment.D.createDocumentFragment();
out.append(mark_start, mark_end);
const { current } = scope;
const reRenderReactiveElement = (v) => {
if (!mark_start.parentNode || !mark_end.parentNode)
return removeSignalListener(s, reRenderReactiveElement);
scope.push(current);
let els = map(v);
let els = mapScoped(v);
scope.pop();
if (!Array.isArray(els))
els = [els];
@ -875,14 +859,14 @@ var DDE = (() => {
current.host(on.disconnected(
() => (
/*! Clears cached elements for reactive element `S.el` */
map.clear()
mapScoped.clear()
)
));
return out;
};
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));
});
}
@ -947,10 +931,10 @@ var DDE = (() => {
function removeSignalsFromElements(s, listener, ...notes) {
const { current } = scope;
current.host(function(element) {
if (element[key_reactive])
return element[key_reactive].push([[s, listener], ...notes]);
element[key_reactive] = [];
if (current.prevent) return;
const is_first = !element[key_reactive];
if (is_first) element[key_reactive] = [];
element[key_reactive].push([[s, listener], ...notes]);
if (!is_first || current.prevent) return;
on.disconnected(
() => (
/*! Clears all Signals listeners added in the current scope/host (`S.el`, `assign`, …?).
@ -965,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;
}
@ -977,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 = {};
@ -993,9 +977,7 @@ var DDE = (() => {
actions,
onclear,
host,
listeners: /* @__PURE__ */ new Set(),
defined: new Defined().stack,
readonly
listeners: /* @__PURE__ */ new Set()
}),
enumerable: false,
writable: false,

File diff suppressed because one or more lines are too long

24
dist/iife.js vendored
View File

@ -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();
@ -316,7 +316,7 @@ var DDE = (() => {
var scope = {
/**
* Gets the current scope
* @returns {Object} Current scope context
* @returns {typeof scopes[number]} Current scope context
*/
get current() {
return scopes[scopes.length - 1];
@ -676,9 +676,9 @@ var DDE = (() => {
memo.isScope = function(obj) {
return obj[memoMark];
};
memo.scope = function memoScope(fun, { signal, onlyLast } = {}) {
memo.scope = function memoScopeCreate(fun, { signal, onlyLast } = {}) {
let cache = oCreate();
function memoScope2(...args) {
function memoScope(...args) {
if (signal && signal.aborted)
return fun.apply(this, args);
let cache_local = onlyLast ? cache : oCreate();
@ -693,10 +693,10 @@ var DDE = (() => {
cache = cache_local;
return out;
}
memoScope2[memoMark] = true;
memoScope2.clear = () => cache = oCreate();
if (signal) signal.addEventListener("abort", memoScope2.clear);
return memoScope2;
memoScope[memoMark] = true;
memoScope.clear = () => cache = oCreate();
if (signal) signal.addEventListener("abort", memoScope.clear);
return memoScope;
};
return __toCommonJS(index_exports);
})();

2
dist/iife.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -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,
});
}

View File

@ -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";');

View File

@ -23,14 +23,6 @@ export function DataDashboard() {
conversion: [2.9, 3.5, 3.7, 2.6, 3.4, 3.5, 2.8, 2.8, 2.8, 3.1, 3.0, 2.7],
months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
};
// Application state
const selectedYear = S(2024);
const selectedDataType = S(/** @type {'sales' | 'visitors' | 'conversion'} */ ('sales'));
const isLoading = S(false);
const error = S(null);
// Filter options
const years = [2022, 2023, 2024];
const dataTypes = [
{ id: 'sales', label: 'Sales', unit: 'K' },
@ -38,42 +30,32 @@ export function DataDashboard() {
{ id: 'conversion', label: 'Conversion Rate', unit: '%' }
];
// Computed values
const selectedData = S(() => {
return DATA[selectedDataType.get()];
});
const currentDataType = S(() => {
return dataTypes.find(type => type.id === selectedDataType.get());
});
const totalValue = S(() => {
const data = selectedData.get();
return data.reduce((sum, value) => sum + value, 0);
});
const averageValue = S(() => {
const data = selectedData.get();
return data.reduce((sum, value) => sum + value, 0) / data.length;
});
const highestValue = S(() => {
return Math.max(...selectedData.get());
});
// Event handlers
// Filter options
const selectedYear = S(2024);
const onYearChange = on("change", e => {
selectedYear.set(parseInt(/** @type {HTMLSelectElement} */(e.target).value));
loadData();
});
const selectedDataType = S(/** @type {'sales' | 'visitors' | 'conversion'} */ ('sales'));
const onDataTypeChange = on("click", e => {
const type = /** @type {'sales' | 'visitors' | 'conversion'} */(
/** @type {HTMLButtonElement} */(e.currentTarget).dataset.type);
selectedDataType.set(type);
});
const currentDataType = S(() => dataTypes.find(type => type.id === selectedDataType.get()));
const selectedData = S(() => DATA[selectedDataType.get()]);
// Values based on filters
const totalValue = S(() => selectedData.get().reduce((sum, value) => sum + value, 0));
const averageValue = S(() => {
const data = selectedData.get();
return data.reduce((sum, value) => sum + value, 0) / data.length;
});
const highestValue = S(() => Math.max(...selectedData.get()));
// Simulate data loading
const isLoading = S(false);
const error = S(null);
function loadData() {
isLoading.set(true);
error.set(null);
@ -114,7 +96,7 @@ export function DataDashboard() {
// Draw grid labels
ctx.fillStyle = '#999';
ctx.font = '12px Arial';
ctx.fillText(Math.round(maxValue * (i / 5)), 20, y + 5);
ctx.fillText(Math.round(maxValue * (i / 5)).toString(), 20, y + 5);
}
ctx.stroke();
@ -154,7 +136,6 @@ export function DataDashboard() {
)
),
// Error message (only shown when there's an error)
S.el(error, errorMsg => !errorMsg
? el()
: el("div", { className: "error-message" }).append(
@ -163,7 +144,6 @@ export function DataDashboard() {
),
),
// Loading indicator
S.el(isLoading, loading => !loading
? el()
: el("div", { className: "loading-spinner" })
@ -323,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 {

View File

@ -27,25 +27,23 @@ const imagesSample = (url=> [
* @returns {HTMLElement} Gallery element
*/
export function ImageGallery(images= imagesSample) {
// Application state
const selectedImageId = S(null);
const filterTag = S('all');
const imagesToDisplay = S(() => {
const tag = filterTag.get();
if (tag === 'all') return images;
else return images.filter(img => img.alt.toLowerCase() === tag);
})
const onFilterChange = tag => on("click", () => {
filterTag.set(tag);
});
// Derived state
// Lightbox
const selectedImageId = S(null);
const selectedImage = S(() => {
const id = selectedImageId.get();
return id ? images.find(img => img.id === id) : null;
});
const isLightboxOpen = S(() => selectedImage.get() !== null);
// Event handlers
const onImageClick = id => on("click", () => {
selectedImageId.set(id);
document.body.style.overflow = 'hidden'; // Prevent scrolling when lightbox is open
@ -76,9 +74,6 @@ export function ImageGallery(images= imagesSample) {
const nextIndex = (currentIndex + 1) % images.length;
selectedImageId.set(images[nextIndex].id);
};
const onFilterChange = tag => on("click", () => {
filterTag.set(tag);
});
// Keyboard navigation handler
function handleKeyDown(e) {

View File

@ -73,8 +73,17 @@ export function Form({ initial }) {
this.value[key] = value;
}
});
/**
* Event handler for input events
* @param {"value"|"checked"} prop
* @returns {(ev: Event) => void}
* */
const onChange= prop => ev => {
const input = /** @type {HTMLInputElement} */(ev.target);
S.action(formState, "update", /** @type {keyof FormState} */(input.id), input[prop]);
};
// Derived signals for validation
// Form validate state
const nameValid = S(() => formState.get().name.length >= 3);
const emailValid = S(() => {
const email = formState.get().email;
@ -89,8 +98,6 @@ export function Form({ initial }) {
return password === confirmPassword && confirmPassword !== '';
});
const termsAgreed = S(() => formState.get().agreedToTerms);
// Overall form validity
const formValid = S(() =>
nameValid.get() &&
emailValid.get() &&
@ -99,16 +106,6 @@ export function Form({ initial }) {
termsAgreed.get()
);
// Event handlers
/**
* Event handler for input events
* @param {"value"|"checked"} prop
* @returns {(ev: Event) => void}
* */
const onChange= prop => ev => {
const input = /** @type {HTMLInputElement} */(ev.target);
S.action(formState, "update", /** @type {keyof FormState} */(input.id), input[prop]);
};
const dispatcSubmit = dispatchEvent("form:submit", host);
const onSubmit = on("submit", e => {
e.preventDefault();

View 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));
}
}
`),
);

View File

@ -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;
}

View File

@ -1,4 +1,4 @@
// Example of reactive element marker
<!--<dde:mark type="reactive" source="...">-->
<!--<dde:mark type="reactive" component="<component-name>">-->
<!-- content that updates when signal changes -->
<!--</dde:mark>-->

View File

@ -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);

View File

@ -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"),
),
);

View File

@ -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"
)
}
}
/**

View File

@ -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",

View File

@ -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 = {} }) {

View File

@ -9,7 +9,7 @@ export function mnemonic(){
),
el("li").append(
el("code", "el(<tag-name>, <primitive>)[.append(...)]: <element-from-tag-name>"),
" — simple element containing only text",
" — simple element containing only text (accepts string, number or signal)",
),
el("li").append(
el("code", "el(<tag-name>, <object>)[.append(...)]: <element-from-tag-name>"),
@ -26,6 +26,6 @@ export function mnemonic(){
el("li").append(
el("code", "elNS(<namespace>)(<as-el-see-above>)[.append(...)]: <element-based-on-arguments>"),
" — typically SVG elements",
)
),
);
}

View File

@ -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>]))")

View File

@ -29,7 +29,7 @@ export function mnemonic(){
el("li").append(
el("code", "S.clear(...<signals>)"),
" — off and clear signals (most of the time it is not needed as reactive ",
"attributes and elements are cleared automatically)",
"attributes and elements are handled automatically)",
),
);
}

View File

@ -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: Modelviewviewmodel`,
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 (~1015kB 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") }),
)
)
),
@ -82,7 +85,7 @@ export function page({ pkg, info }){
el("p").append(T`
By separating these concerns, your code becomes more modular, testable, and easier to maintain. This
approach ${el("strong", "is not")} something new and/or special to dd<el>. Its based on ${el("a", {
approach ${el("strong", "is not something new and/or special to dd<el>")}. Its based on ${el("a", {
textContent: "MVC", ...references.w_mvc })} (${el("a", { textContent: "MVVM", ...references.w_mvv })}),
but is there presented in simpler form.
`),
@ -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 # Coming soon", 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>`),
@ -154,6 +161,10 @@ export function page({ pkg, info }){
Interactive demos with server-side pre-rendering`),
el("li").append(T`${el("a", { href: "p13-appendix.html" }).append(el("strong", "Appendix & Summary"))} —
Comprehensive reference and best practices`),
el("li").append(T`${el("a", { href: "p14-converter.html" }).append(el("strong", "HTML Converter"))} —
Convert HTML to dd<el> JavaScript code`),
el("li").append(T`${el("a", { href: "p15-examples.html" }).append(el("strong", "Examples Gallery"))} —
Real-world application examples and case studies`),
),
el("p").append(T`
Each section builds on the previous ones, so we recommend following them in order.

View File

@ -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.
`),
@ -208,6 +207,6 @@ export function page({ pkg, info }){
`),
),
el(mnemonic)
el(mnemonic),
);
}

View File

@ -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 elements 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: "WebReflections detailed analysis", ...references.web_events })}.
`),
@ -136,14 +135,14 @@ export function page({ pkg, info }){
el("li", t`Set up lifecycle behaviors`),
el("li", t`Integrate third-party libraries`),
el("li", t`Create reusable element behaviors`),
el("li", t`Capture element references`)
el("li", t`Capture element references`), // TODO: add example?
)
),
el("p").append(T`
You can use Addons as ≥3rd argument of the ${el("code", "el")} function, making it possible to
extend your templates with additional functionality:
`),
el(example, { src: fileURL("./components/examples/events/templateWithListeners.js"), page_id }),
el(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),
);
}

View File

@ -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 cant call ${el("code", ".set()")} on them. Their value is always
computed from their dependencies. Theyre 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 WONT 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`
Heres 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`
@ -277,7 +276,72 @@ export function page({ pkg, info }){
`),
el("li").append(T`
${el("strong", "Avoid infinite loops")}: Be careful when one signal updates another in a subscription
`)
`),
),
el("p").append(T`
While signals provide powerful reactivity for complex UI interactions, theyre not always necessary.
A good approach is to started with variables/constants and when necessary, convert them to signals.
`),
el("div", { className: "tabs" }).append(
el("div", { className: "tab", dataTab: "events" }).append(
el("h4", t`We can process form events without signals`),
el("p", t`This can be used when the form data doesnt need to be reactive and we just waiting for
results.`),
el(code, { content: `
const onFormSubmit = on("submit", e => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
// this can be sent to a server
// or processed locally
// e.g.: console.log(Object.fromEntries(formData))
});
// …
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 dontt need to reflect changes in the elsewhere (UI).`),
el(code, { content: `
let canSubmit = false;
const onFormSubmit = on("submit", e => {
e.preventDefault();
if(!canSubmit) return; // some message
// …
});
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, { content: `
const canSubmit = S(false);
const onFormSubmit = on("submit", e => {
e.preventDefault();
// …
});
const onAllowSubmit = on("click", e => {
canSubmit.set(true);
});
return el("form", null, onFormSubmit).append(
// ...
el("button", { textContent: "Allow Submit", type: "button" }, onAllowSubmit),
el("button", { disabled: S(()=> !canSubmit), textContent: "Submit" })
);
`, language: "js" }),
),
),
el("div", { className: "troubleshooting" }).append(
@ -298,6 +362,6 @@ export function page({ pkg, info }){
)
),
el(mnemonic)
el(mnemonic),
);
}

View File

@ -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 components life-cycle and provide JavaScript the way to properly use
the ${el("a", { textContent: t`Garbage collection`, ...references.garbage_collection })}.
`),
el(code, { src: fileURL("./components/examples/scopes/intro.js"), 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`),
@ -55,7 +54,7 @@ export function page({ pkg, info }){
el(MyComponent);
function MyComponent() {
// 2. access the host element
// 2. access the host element (or other scope related values)
const { host } = scope;
// 3. Add behavior to host
@ -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") })
)
),

View File

@ -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`
Lets 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 elements
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(

View File

@ -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,12 +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`),
el("li", t`defined: Stack trace information for debugging`),
el("li", t`readonly: Boolean flag indicating if the signal is read-only`)
el("li", t`host: Reference to the host element/scope in which the signal was created`),
),
el("p").append(T`
…to determine the current value of the signal, call ${el("code", "signal.valueOf()")}. Dont hesitate to
@ -114,9 +112,15 @@ export function page({ pkg, info }){
el("ul").append(
el("li", t`That youre using signal.set() to update the value, not modifying objects/arrays directly`),
el("li", t`For mutable objects, ensure youre using actions or making proper copies before updating`),
el("li", t`That the signal is actually connected to the DOM element (check your S.el or attribute binding code)`)
el("li", t`That the signal is actually connected to the DOM element (check your S.el or attribute binding
code)`),
el("li").append(T`
That youre passing signal corecctly (without using ${el("code", "*.get()")}) and for ${el("code",
"S.el")} that you passing (derived) signals not a function (use ${el("code",
"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`
@ -132,7 +136,7 @@ export function page({ pkg, info }){
el("li", t`Make sure derived signals dont 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`
@ -145,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 isnt updating as expected.
You can inspect the elements between the comment nodes to see their current state and the
@ -182,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

View File

@ -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()")}).

View File

@ -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`

View File

@ -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

View File

@ -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`
Heres 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! Heres 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,

View File

@ -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`

View File

@ -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,
@ -129,75 +128,6 @@ export function page({ pkg, info }){
`)
),
el("h4", t`Using Signals Appropriately`),
el("p").append(T`
While signals provide powerful reactivity for complex UI interactions, theyre not always necessary.
`),
el("div", { className: "tabs" }).append(
el("div", { className: "tab", dataTab: "events" }).append(
el("h4", t`We can process form events without signals`),
el("p", t`This can be used when the form data doesnt need to be reactive and we just waiting for
results.`),
el(code, { page_id, content: `
const onFormSubmit = on("submit", e => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
// this can be sent to a server
// or processed locally
// e.g.: console.log(Object.fromEntries(formData))
});
// …
return el("form", null, onFormSubmit).append(
// …
);
` })
),
el("div", { className: "tab", dataTab: "variables" }).append(
el("h4", t`We can use variables without signals`),
el("p", t`We use this when we dontt need to reflect changes in the elsewhere (UI).`),
el(code, { page_id, content: `
let canSubmit = false;
const onFormSubmit = on("submit", e => {
e.preventDefault();
if(!canSubmit) return; // some message
// …
});
const onAllowSubmit = on("click", e => {
canSubmit = true;
});
`}),
),
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: `
const canSubmit = S(false);
const onFormSubmit = on("submit", e => {
e.preventDefault();
// …
});
const onAllowSubmit = on("click", e => {
canSubmit.set(true);
});
return el("form", null, onFormSubmit).append(
// ...
el("button", { textContent: "Allow Submit", type: "button" }, onAllowSubmit),
el("button", { disabled: S(()=> !canSubmit), textContent: "Submit" })
);
`}),
),
el("p").append(T`
A good approach is to started with variables/constants and when necessary, convert them to signals.
`),
),
el("h4", t`Migrating from Traditional Approaches`),
el("p").append(T`
When migrating from traditional DOM manipulation or other frameworks to dd<el>:
@ -235,7 +165,7 @@ export function page({ pkg, info }){
className: S(() => countS.get() > 10 ? 'warning' : '')
})
);
`, page_id }),
`, language: "js" }),
el(h3, t`Key Concepts Reference`),
@ -394,46 +324,46 @@ export function page({ pkg, info }){
el("tr").append(
el("th", "Feature"),
el("th", "dd<el>"),
el("th", "React"),
el("th", "Vue"),
el("th", "Svelte")
el("th", "VanJS"),
el("th", "Solid"),
el("th", "Alpine")
)
),
el("tbody").append(
el("tr").append(
el("td", "No Build Step Required"),
el("td", "✅"),
el("td", "✅"),
el("td", "⚠️ JSX needs transpilation"),
el("td", "⚠️ SFC needs compilation"),
el("td", "❌ Requires compilation")
el("td", "")
),
el("tr").append(
el("td", "Bundle Size (minimal)"),
el("td", "~10-15kb"),
el("td", "~40kb+"),
el("td", "~33kb+"),
el("td", "Minimal runtime")
el("td", "Bundle Size (minified)"),
el("td", "~14kb"),
el("td", "~3kb"),
el("td", "~20kb"),
el("td", "~43kb")
),
el("tr").append(
el("td", "Reactivity Model"),
el("td", "Signal-based"),
el("td", "Virtual DOM diffing"),
el("td", "Proxy-based"),
el("td", "Compile-time reactivity")
el("td", "Signal-based (basics only)"),
el("td", "Signal-based"),
el("td", "MVVM + Proxy")
),
el("tr").append(
el("td", "DOM Interface"),
el("td", "Direct DOM API"),
el("td", "Virtual DOM"),
el("td", "Virtual DOM"),
el("td", "Compiled DOM updates")
el("td", "Direct DOM API"),
el("td", "Compiled DOM updates"),
el("td", "Directive-based")
),
el("tr").append(
el("td", "Server-Side Rendering"),
el("td", "✅ Basic Support"),
el("td", "✅ Basic Support"),
el("td", "✅ Advanced"),
el("td", "✅ Advanced"),
el("td", "✅ Advanced")
el("td", "")
)
)
),

View File

@ -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`

View File

@ -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`

View File

@ -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
View File

@ -1,12 +1,12 @@
{
"name": "deka-dom-el",
"version": "0.9.2-alpha",
"version": "0.9.4-alpha",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "deka-dom-el",
"version": "0.9.2-alpha",
"version": "0.9.4-alpha",
"license": "MIT",
"devDependencies": {
"@size-limit/preset-small-lib": "~11.2",

View File

@ -1,13 +1,13 @@
{
"name": "deka-dom-el",
"version": "0.9.2-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@github.com:jaandrle/deka-dom-el.git"
"url": "git+ssh://git@github.com/jaandrle/deka-dom-el.git"
},
"bugs": {
"url": "https://github.com/jaandrle/deka-dom-el/issues"
@ -52,7 +52,6 @@
"maxdepth": 3,
"maxcomplexity": 14,
"globals": {
"requestIdleCallback": false,
"AbortController": false,
"AbortSignal": false,
"FinalizationRegistry": false
@ -61,31 +60,36 @@
"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": {
"platform": "browser"
},
"scripts": {},
"scripts": {
"test": "echo \"Error: no tests yet\"",
"build": "bs/build.js",
"lint": "bs/lint.sh",
"docs": "bs/docs.js"
},
"keywords": [
"dom",
"javascript",

View File

@ -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

View File

@ -19,7 +19,7 @@ const store_abort= new WeakMap();
export const scope= {
/**
* Gets the current scope
* @returns {Object} Current scope context
* @returns {typeof scopes[number]} Current scope context
*/
get current(){ return scopes[scopes.length-1]; },

View File

@ -70,19 +70,10 @@ export function observedAttributes(instance, observedAttribute){
function kebabToCamel(name){ return name.replace(/-./g, x=> x[1].toUpperCase()); }
/**
* Error class for definition tracking
* Shows the correct stack trace for debugging (signal) creation
* Schedule a task during browser idle time
* @returns {Promise<void>} Promise that resolves when browser is idle
*/
export class Defined extends Error{
constructor(){
super();
const [ curr, ...rest ]= this.stack.split("\n");
const curr_file= curr.slice(curr.indexOf("@"), curr.indexOf(".js:")+4);
const curr_lib= curr_file.includes("src/helpers.js") ? "src/" : curr_file;
this.stack= rest.find(l=> !l.includes(curr_lib)) || curr;
}
get compact(){
const { stack }= this;
return stack.slice(0, stack.indexOf("@")+1)+"…"+stack.slice(stack.lastIndexOf("/"));
}
export function requestIdle(){ return new Promise(function(resolve){
(globalThis.requestIdleCallback || requestAnimationFrame)(resolve);
});
}

View File

@ -27,7 +27,7 @@ memo.isScope= function(obj){ return obj[memoMark]; };
* @param {AbortSignal} options.signal
* @param {boolean} [options.onlyLast=false]
* */
memo.scope= function memoScope(fun, { signal, onlyLast }= {}){
memo.scope= function memoScopeCreate(fun, { signal, onlyLast }= {}){
let cache= oCreate();
function memoScope(...args){
if(signal && signal.aborted)

View File

@ -1,15 +1,14 @@
import { queueSignalWrite, mark } from "./helpers.js";
export { mark };
import { hasOwn, Defined, 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
@ -169,21 +168,21 @@ import { memo } from "../memo.js";
* Creates a reactive DOM element that re-renders when signal changes
*
* @param {Object} s - Signal object to watch
* @param {Function} map - Function mapping signal value to DOM elements
* @param {Function} mapScoped - Function mapping signal value to DOM elements
* @returns {DocumentFragment} Fragment containing reactive elements
*/
signal.el= function(s, map){
map= memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const mark_start= el.mark({ type: "reactive", source: new Defined().compact }, true);
const mapScoped= memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const { current }= scope, { scope: sc }= current;
const mark_start= el.mark({ type: "reactive", component: sc && sc.name || "" }, true);
const mark_end= mark_start.end;
const out= env.D.createDocumentFragment();
out.append(mark_start, mark_end);
const { current }= scope;
const reRenderReactiveElement= v=> {
if(!mark_start.parentNode || !mark_end.parentNode) // === `isConnected` or wasnt yet rendered
return removeSignalListener(s, reRenderReactiveElement);
scope.push(current);
let els= map(v);
let els= mapScoped(v);
scope.pop();
if(!Array.isArray(els))
els= [ els ];
@ -202,7 +201,7 @@ signal.el= function(s, map){
reRenderReactiveElement(s.get());
current.host(on.disconnected(()=>
/*! Clears cached elements for reactive element `S.el` */
map.clear()
mapScoped.clear()
));
return out;
};
@ -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,10 +313,14 @@ export const signals_config= {
function removeSignalsFromElements(s, listener, ...notes){
const { current }= scope;
current.host(function(element){
if(element[key_reactive])
return element[key_reactive].push([ [ s, listener ], ...notes ]);
element[key_reactive]= [];
if(current.prevent) return; // typically document.body, doenst need auto-remove as it should happen on page leave
const is_first= !element[key_reactive];
if(is_first) element[key_reactive]= [];
element[key_reactive].push([ [ s, listener ], ...notes ]);
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. */
@ -345,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;
}
@ -368,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= {};
@ -386,8 +388,6 @@ function toSignal(s, value, actions, readonly= false){
value: oAssign(oCreate(protoSigal), {
value, actions, onclear, host,
listeners: new Set(),
defined: new Defined().stack,
readonly
}),
enumerable: false,
writable: false,