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

5 Commits

Author SHA1 Message Date
f2c85ec983 🐛 📺 v0.9.5-alpha (#44)
*  📺 package (version and publint)

* 🔤 docs typos

* 🐛 this address / fixes #43

*  tiny el optimalization

* 🔤 improve docs wording

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

* 🔤 🐛 case-studies/image-gallery.js
2025-03-21 14:43:25 +01:00
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
f0dfdfde54 v0.9.2 — 🐛 types, on.defer and other small (#36)
* 🔤  T now uses DocumentFragment

* 🔤

* 🔤 

* 🐛 lint

*  cleanup

*  🔤 lib download

*  🔤 ui

*  reorganize files

*  on.host

* 🐛 on.* types

*  🔤 cdn

* 🔤 converter

* 🐛 signal.set(value, force)

*  🔤

* 🔤  converter - convert also comments

*  bs/build

* 🔤 ui p14

* 🔤

* 🔤 Examples

* 🔤

* 🐛 now only el(..., string|number)

* 🐛 fixes #38

* 🔤

*  on.host → on.defer

* 🔤

* 📺
2025-03-16 11:30:42 +01:00
80 changed files with 3856 additions and 789 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.

132
README.md
View File

@ -1,57 +1,53 @@
**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&Examples](https://jaandrle.github.io/deka-dom-el "Official documentation and guide site")
| [NPM](https://www.npmjs.com/package/deka-dom-el "Official NPM package page")
| [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(
el(EmojiCounter, { initial: "🚀" })
el(EmojiCounter, { initial: "🚀" }),
);
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, "🚀"),
el(Option, "💖"),
)
),
);
}
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
-**Declarative & functional approach** for clean, maintainable code
- **Minimalized footprint** — ~10-15kB minified bundle (original goal 10kB), **zero**/minimal dependencies and
small in-memory size (auto-releasing resources as much as possible)
-**Declarative & functional approach support** for clean, maintainable code
-**Signals and events** for reactive UI
-**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): WIP
- [Examples](./examples/): TBD/WIP
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,12 +123,24 @@ 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
- [adamhaile/S](https://github.com/adamhaile/S) - Simple, clean, fast reactive programming
- [hyperhype/hyperscript](https://github.com/hyperhype/hyperscript) - Create HyperText with JavaScript
- [potch/signals](https://github.com/potch/signals) - A small reactive signals library
- [jaandrle/dollar_dom_component](https://github.com/jaandrle/dollar_dom_component) -
Functional DOM components without JSX/virtual DOM
- [mxjp/rvx: A signal based frontend framework](https://github.com/mxjp/rvx)
- [vanjs-org/van](https://github.com/vanjs-org/van) World's smallest reactive UI framework
- [adamhaile/S](https://github.com/adamhaile/S) Simple, clean, fast reactive programming
- [hyperhype/hyperscript](https://github.com/hyperhype/hyperscript) Create HyperText with JavaScript
- [potch/signals](https://github.com/potch/signals) A small reactive signals library
- [AseasRoa/paintor](https://github.com/AseasRoa/paintor) - JavaScript library for building reactive client-side user
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.
- [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

@ -1,4 +1,5 @@
#!/usr/bin/env -S npx nodejsscript
import { buildSync as esbuildSync } from "esbuild";
const css= echo.css`
.info{ color: gray; }
`;
@ -8,8 +9,7 @@ export function build({ files, filesOut, minify= "partial", iife= true, types= t
const file= file_root+".js";
echo(`Processing ${file} (minified: ${minify})`);
const out= filesOut(file);
const esbuild_output= buildEsbuild({ file, out, minify });
echoVariant(esbuild_output.stderr.split("\n")[1].trim());
esbuild({ file, out, minify });
if(types){
const file_dts= file_root+".d.ts";
@ -31,14 +31,13 @@ export function build({ files, filesOut, minify= "partial", iife= true, types= t
const name= "DDE";
const out= filesOut(file_root+".js", fileMark);
const params= [
"--format=iife",
"--global-name="+name,
];
const dde_output= buildEsbuild({ file, out, minify, params });
echoVariant(`${out} (${name})`)
const params= {
format: "iife",
globalName: name
};
esbuild({ file, out, minify, params });
if(!types) return dde_output;
if(!types) return;
const file_dts= file_root+".d.ts";
const file_dts_out= filesOut(file_dts, fileMark);
echoVariant(file_dts_out, true);
@ -48,8 +47,6 @@ export function build({ files, filesOut, minify= "partial", iife= true, types= t
entry: file_dts,
})
echoVariant(file_dts_out);
return dde_output;
}
}
export function buildDts({ bundle, entry, name }){
@ -64,46 +61,39 @@ export function buildDts({ bundle, entry, name }){
].filter(Boolean).join(" "), { out, entry });
return dts_b_g_output;
}
class ErrorEsbuild extends Error{
constructor({ code, stderr }){
super(stderr);
this.code= code;
this.stderr= stderr;
}
}
function buildEsbuild({ file, out, minify= "partial", params= [] }){
try {
return esbuild({ file, out, minify, params });
} catch(e){
if(e instanceof ErrorEsbuild)
return $.exit(e.code, echo(e.stderr));
throw e;
}
}
export function esbuild({ file, out, minify= "partial", params= [] }){
const esbuild_output= s.$().run([
"npx esbuild '::file::'",
"--platform=neutral",
"--bundle",
minifyOption(minify),
"--legal-comments=inline",
"--packages=external",
...params,
"--outfile='::out::'"
].filter(Boolean).join(" "), { file, out });
if(esbuild_output.code)
throw new ErrorEsbuild(esbuild_output);
export function esbuild({ file, out, minify= "partial", params= {} }){
const esbuild_output= esbuildSync({
entryPoints: [file],
outfile: out,
platform: "neutral",
bundle: true,
legalComments: "inline",
packages: "external",
metafile: true,
...minifyOption(minify),
...params
});
pipe(
f=> f.replace(/^ +/gm, m=> "\t".repeat(m.length/2)),
f=> s.echo(f).to(out)
)(s.cat(out));
echoVariant(metaToLineStatus(esbuild_output.metafile, out));
return esbuild_output;
}
/** @param {"no"|"full"|"partial"} level */
function minifyOption(level= "partial"){
if("no"===level) return undefined;
if("full"===level) return "--minify";
return "--minify-syntax --minify-identifiers";
if("no"===level) return { minify: false };
if("full"===level) return { minify: true };
return { minifySyntax: true, minifyIdentifiers: true };
}
function metaToLineStatus(meta, file){
const status= meta.outputs[file];
if(!status) return `? ${file}: unknown`;
const { bytes }= status;
const kbytes= bytes/1024;
const kbytesR= kbytes.toFixed(2);
return `${file}: ${kbytesR} kB`;
}
function echoVariant(name, todo= false){
if(todo) return echo.use("-R", "~ "+name);

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

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

View File

@ -173,20 +173,22 @@ declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event] & {
target: EL;
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: string, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: Event | CustomEvent) => any, options?: AddEventListenerOptions): EE;
/** Listens to the element is connected to the live DOM. In case of custom elements uses [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
connected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<NoInfer<EL>>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
/** Listens to the element is disconnected from the live DOM. In case of custom elements uses [`disconnectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
disconnected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
/**
* Fires when the host element is "ready", for host element itsel, it is just an alias for `scope.host(listener)`.
* This is handy to apply some property depending on full template such as:
* Fires after the next tick of the Javascript event loop.
* This is handy for example to apply some property depending on the element content:
* ```js
* const selected= "Z";
* //...
* return el("form").append(
* el("select", null, on.host(e=> e.value=selected)).append(
* el("select", null, on.defer(e=> e.value=selected)).append(
* el("option", { value: "A", textContent: "A" }),
* //...
* el("option", { value: "Z", textContent: "Z" }),
@ -194,7 +196,7 @@ export interface On {
* );
* ```
* */
host<EL extends SupportedElement>(listener: (element: EL) => any, host?: Host<SupportedElement>): ddeElementAddon<EL>;
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
}
export const on: On;
export type Scope = {

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();
@ -267,6 +254,7 @@ function on(event, listener, options) {
return element;
};
}
on.defer = (fn) => setTimeout.bind(null, fn, 0);
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
on.connected = function(listener, options) {
options = lifeOptions(options);
@ -302,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];
@ -365,7 +353,6 @@ var scope = {
return scopes.pop();
}
};
on.host = (fn, host = scope.host) => (el) => host(() => fn(el));
// src/signals-lib/common.js
var signals_global = {
@ -435,16 +422,18 @@ function createElement(tag, attributes, ...addons) {
const s = signals(this);
let scoped = 0;
let el, el_host;
if (Object(attributes) !== attributes || s.isSignal(attributes))
const att_type = typeof attributes;
if (att_type === "string" || att_type === "number" || s.isSignal(attributes))
attributes = { textContent: attributes };
switch (true) {
case typeof tag === "function": {
scoped = 1;
const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0);
scope.push({ scope: tag, host });
el = tag(attributes || void 0);
const is_fragment = isInstance(el, enviroment.F);
el = /** @type {Element} */
tag(attributes || void 0);
if (el.nodeName === "#comment") break;
const is_fragment = isInstance(el, enviroment.F);
const el_mark = createElement.mark({
type: "component",
name: tag.name,
@ -662,9 +651,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();
@ -679,28 +668,28 @@ 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
var mark = "__dde_signal";
var queueSignalWrite = /* @__PURE__ */ (() => {
let pendingSignals = /* @__PURE__ */ new Set();
let pendingSignals = /* @__PURE__ */ new Map();
let scheduled = false;
function flushSignals() {
scheduled = false;
const todo = pendingSignals;
pendingSignals = /* @__PURE__ */ new Set();
for (const signal2 of todo) {
pendingSignals = /* @__PURE__ */ new Map();
for (const [signal2, force] of todo) {
const M = signal2[mark];
if (M) M.listeners.forEach((l) => l(M.value));
if (M) M.listeners.forEach((l) => l(M.value, force));
}
}
return function(s) {
pendingSignals.add(s);
return function(s, force = false) {
pendingSignals.set(s, pendingSignals.get(s) || force);
if (scheduled) return;
scheduled = true;
queueMicrotask(flushSignals);
@ -708,13 +697,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);
} },
@ -722,9 +708,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) {
@ -737,11 +723,11 @@ function signal(value, actions) {
return create(false, value, actions);
if (isSignal(value)) return value;
const out = create(true);
function contextReWatch() {
function contextReWatch(_, force) {
const [origin, ...deps_old] = deps.get(contextReWatch);
deps.set(contextReWatch, /* @__PURE__ */ new Set([origin]));
stack_watch.push(contextReWatch);
write(out, value());
write(out, value(), force);
stack_watch.pop();
if (!deps_old.length) return;
const deps_curr = deps.get(contextReWatch);
@ -763,7 +749,7 @@ signal.action = function(s, name, ...a) {
throw new Error(`Action "${name}" not defined. See ${mark}.actions.`);
actions[name].apply(M, a);
if (M.skip) return delete M.skip;
queueSignalWrite(s);
queueSignalWrite(s, true);
};
signal.on = function on2(s, listener, options = {}) {
const { signal: as } = options;
@ -799,17 +785,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];
@ -829,14 +815,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));
});
}
@ -901,10 +887,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`, …?).
@ -919,7 +905,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;
}
@ -931,7 +917,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 = {};
@ -947,9 +933,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,
@ -972,7 +956,7 @@ function write(s, value, force) {
const M = s[mark];
if (!M || !force && M.value === value) return;
M.value = value;
queueSignalWrite(s);
queueSignalWrite(s, force);
return value;
}
function addSignalListener(s, listener) {

View File

@ -173,20 +173,22 @@ declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event] & {
target: EL;
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: string, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: Event | CustomEvent) => any, options?: AddEventListenerOptions): EE;
/** Listens to the element is connected to the live DOM. In case of custom elements uses [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
connected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<NoInfer<EL>>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
/** Listens to the element is disconnected from the live DOM. In case of custom elements uses [`disconnectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
disconnected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
/**
* Fires when the host element is "ready", for host element itsel, it is just an alias for `scope.host(listener)`.
* This is handy to apply some property depending on full template such as:
* Fires after the next tick of the Javascript event loop.
* This is handy for example to apply some property depending on the element content:
* ```js
* const selected= "Z";
* //...
* return el("form").append(
* el("select", null, on.host(e=> e.value=selected)).append(
* el("select", null, on.defer(e=> e.value=selected)).append(
* el("option", { value: "A", textContent: "A" }),
* //...
* el("option", { value: "Z", textContent: "Z" }),
@ -194,7 +196,7 @@ export interface On {
* );
* ```
* */
host<EL extends SupportedElement>(listener: (element: EL) => any, host?: Host<SupportedElement>): ddeElementAddon<EL>;
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
}
export const on: On;
export type Scope = {

File diff suppressed because one or more lines are too long

12
dist/esm.d.ts vendored
View File

@ -172,20 +172,22 @@ declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event] & {
target: EL;
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: string, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: Event | CustomEvent) => any, options?: AddEventListenerOptions): EE;
/** Listens to the element is connected to the live DOM. In case of custom elements uses [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
connected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<NoInfer<EL>>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
/** Listens to the element is disconnected from the live DOM. In case of custom elements uses [`disconnectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
disconnected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
/**
* Fires when the host element is "ready", for host element itsel, it is just an alias for `scope.host(listener)`.
* This is handy to apply some property depending on full template such as:
* Fires after the next tick of the Javascript event loop.
* This is handy for example to apply some property depending on the element content:
* ```js
* const selected= "Z";
* //...
* return el("form").append(
* el("select", null, on.host(e=> e.value=selected)).append(
* el("select", null, on.defer(e=> e.value=selected)).append(
* el("option", { value: "A", textContent: "A" }),
* //...
* el("option", { value: "Z", textContent: "Z" }),
@ -193,7 +195,7 @@ export interface On {
* );
* ```
* */
host<EL extends SupportedElement>(listener: (element: EL) => any, host?: Host<SupportedElement>): ddeElementAddon<EL>;
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
}
export const on: On;
export type Scope = {

34
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();
@ -238,6 +238,7 @@ function on(event, listener, options) {
return element;
};
}
on.defer = (fn) => setTimeout.bind(null, fn, 0);
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
on.connected = function(listener, options) {
options = lifeOptions(options);
@ -273,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];
@ -336,7 +337,6 @@ var scope = {
return scopes.pop();
}
};
on.host = (fn, host = scope.host) => (el) => host(() => fn(el));
// src/signals-lib/common.js
var signals_global = {
@ -406,16 +406,18 @@ function createElement(tag, attributes, ...addons) {
const s = signals(this);
let scoped = 0;
let el, el_host;
if (Object(attributes) !== attributes || s.isSignal(attributes))
const att_type = typeof attributes;
if (att_type === "string" || att_type === "number" || s.isSignal(attributes))
attributes = { textContent: attributes };
switch (true) {
case typeof tag === "function": {
scoped = 1;
const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0);
scope.push({ scope: tag, host });
el = tag(attributes || void 0);
const is_fragment = isInstance(el, enviroment.F);
el = /** @type {Element} */
tag(attributes || void 0);
if (el.nodeName === "#comment") break;
const is_fragment = isInstance(el, enviroment.F);
const el_mark = createElement.mark({
type: "component",
name: tag.name,
@ -633,9 +635,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();
@ -650,10 +652,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,

12
dist/esm.min.d.ts vendored
View File

@ -172,20 +172,22 @@ declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event] & {
target: EL;
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: string, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: Event | CustomEvent) => any, options?: AddEventListenerOptions): EE;
/** Listens to the element is connected to the live DOM. In case of custom elements uses [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
connected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<NoInfer<EL>>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
/** Listens to the element is disconnected from the live DOM. In case of custom elements uses [`disconnectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
disconnected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
/**
* Fires when the host element is "ready", for host element itsel, it is just an alias for `scope.host(listener)`.
* This is handy to apply some property depending on full template such as:
* Fires after the next tick of the Javascript event loop.
* This is handy for example to apply some property depending on the element content:
* ```js
* const selected= "Z";
* //...
* return el("form").append(
* el("select", null, on.host(e=> e.value=selected)).append(
* el("select", null, on.defer(e=> e.value=selected)).append(
* el("option", { value: "A", textContent: "A" }),
* //...
* el("option", { value: "Z", textContent: "Z" }),
@ -193,7 +195,7 @@ export interface On {
* );
* ```
* */
host<EL extends SupportedElement>(listener: (element: EL) => any, host?: Host<SupportedElement>): ddeElementAddon<EL>;
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
}
export const on: On;
export type Scope = {

2
dist/esm.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -173,20 +173,22 @@ declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event] & {
target: EL;
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: string, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: Event | CustomEvent) => any, options?: AddEventListenerOptions): EE;
/** Listens to the element is connected to the live DOM. In case of custom elements uses [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
connected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<NoInfer<EL>>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
/** Listens to the element is disconnected from the live DOM. In case of custom elements uses [`disconnectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
disconnected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
/**
* Fires when the host element is "ready", for host element itsel, it is just an alias for `scope.host(listener)`.
* This is handy to apply some property depending on full template such as:
* Fires after the next tick of the Javascript event loop.
* This is handy for example to apply some property depending on the element content:
* ```js
* const selected= "Z";
* //...
* return el("form").append(
* el("select", null, on.host(e=> e.value=selected)).append(
* el("select", null, on.defer(e=> e.value=selected)).append(
* el("option", { value: "A", textContent: "A" }),
* //...
* el("option", { value: "Z", textContent: "Z" }),
@ -194,7 +196,7 @@ export interface On {
* );
* ```
* */
host<EL extends SupportedElement>(listener: (element: EL) => any, host?: Host<SupportedElement>): ddeElementAddon<EL>;
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
}
export const on: On;
export type Scope = {

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();
@ -312,6 +299,7 @@ var DDE = (() => {
return element;
};
}
on.defer = (fn) => setTimeout.bind(null, fn, 0);
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
on.connected = function(listener, options) {
options = lifeOptions(options);
@ -347,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];
@ -410,7 +398,6 @@ var DDE = (() => {
return scopes.pop();
}
};
on.host = (fn, host = scope.host) => (el) => host(() => fn(el));
// src/signals-lib/common.js
var signals_global = {
@ -480,16 +467,18 @@ var DDE = (() => {
const s = signals(this);
let scoped = 0;
let el, el_host;
if (Object(attributes) !== attributes || s.isSignal(attributes))
const att_type = typeof attributes;
if (att_type === "string" || att_type === "number" || s.isSignal(attributes))
attributes = { textContent: attributes };
switch (true) {
case typeof tag === "function": {
scoped = 1;
const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0);
scope.push({ scope: tag, host });
el = tag(attributes || void 0);
const is_fragment = isInstance(el, enviroment.F);
el = /** @type {Element} */
tag(attributes || void 0);
if (el.nodeName === "#comment") break;
const is_fragment = isInstance(el, enviroment.F);
const el_mark = createElement.mark({
type: "component",
name: tag.name,
@ -707,9 +696,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();
@ -724,28 +713,28 @@ 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
var mark = "__dde_signal";
var queueSignalWrite = /* @__PURE__ */ (() => {
let pendingSignals = /* @__PURE__ */ new Set();
let pendingSignals = /* @__PURE__ */ new Map();
let scheduled = false;
function flushSignals() {
scheduled = false;
const todo = pendingSignals;
pendingSignals = /* @__PURE__ */ new Set();
for (const signal2 of todo) {
pendingSignals = /* @__PURE__ */ new Map();
for (const [signal2, force] of todo) {
const M = signal2[mark];
if (M) M.listeners.forEach((l) => l(M.value));
if (M) M.listeners.forEach((l) => l(M.value, force));
}
}
return function(s) {
pendingSignals.add(s);
return function(s, force = false) {
pendingSignals.set(s, pendingSignals.get(s) || force);
if (scheduled) return;
scheduled = true;
queueMicrotask(flushSignals);
@ -753,13 +742,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);
} },
@ -767,9 +753,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) {
@ -782,11 +768,11 @@ var DDE = (() => {
return create(false, value, actions);
if (isSignal(value)) return value;
const out = create(true);
function contextReWatch() {
function contextReWatch(_, force) {
const [origin, ...deps_old] = deps.get(contextReWatch);
deps.set(contextReWatch, /* @__PURE__ */ new Set([origin]));
stack_watch.push(contextReWatch);
write(out, value());
write(out, value(), force);
stack_watch.pop();
if (!deps_old.length) return;
const deps_curr = deps.get(contextReWatch);
@ -808,7 +794,7 @@ var DDE = (() => {
throw new Error(`Action "${name}" not defined. See ${mark}.actions.`);
actions[name].apply(M, a);
if (M.skip) return delete M.skip;
queueSignalWrite(s);
queueSignalWrite(s, true);
};
signal.on = function on2(s, listener, options = {}) {
const { signal: as } = options;
@ -844,17 +830,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];
@ -874,14 +860,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));
});
}
@ -946,10 +932,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`, …?).
@ -964,7 +950,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;
}
@ -976,7 +962,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 = {};
@ -992,9 +978,7 @@ var DDE = (() => {
actions,
onclear,
host,
listeners: /* @__PURE__ */ new Set(),
defined: new Defined().stack,
readonly
listeners: /* @__PURE__ */ new Set()
}),
enumerable: false,
writable: false,
@ -1017,7 +1001,7 @@ var DDE = (() => {
const M = s[mark];
if (!M || !force && M.value === value) return;
M.value = value;
queueSignalWrite(s);
queueSignalWrite(s, force);
return value;
}
function addSignalListener(s, listener) {

View File

@ -173,20 +173,22 @@ declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event] & {
target: EL;
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: string, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: Event | CustomEvent) => any, options?: AddEventListenerOptions): EE;
/** Listens to the element is connected to the live DOM. In case of custom elements uses [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
connected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<NoInfer<EL>>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
/** Listens to the element is disconnected from the live DOM. In case of custom elements uses [`disconnectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
disconnected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
/**
* Fires when the host element is "ready", for host element itsel, it is just an alias for `scope.host(listener)`.
* This is handy to apply some property depending on full template such as:
* Fires after the next tick of the Javascript event loop.
* This is handy for example to apply some property depending on the element content:
* ```js
* const selected= "Z";
* //...
* return el("form").append(
* el("select", null, on.host(e=> e.value=selected)).append(
* el("select", null, on.defer(e=> e.value=selected)).append(
* el("option", { value: "A", textContent: "A" }),
* //...
* el("option", { value: "Z", textContent: "Z" }),
@ -194,7 +196,7 @@ export interface On {
* );
* ```
* */
host<EL extends SupportedElement>(listener: (element: EL) => any, host?: Host<SupportedElement>): ddeElementAddon<EL>;
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
}
export const on: On;
export type Scope = {

File diff suppressed because one or more lines are too long

12
dist/iife.d.ts vendored
View File

@ -172,20 +172,22 @@ declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event] & {
target: EL;
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: string, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: Event | CustomEvent) => any, options?: AddEventListenerOptions): EE;
/** Listens to the element is connected to the live DOM. In case of custom elements uses [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
connected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<NoInfer<EL>>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
/** Listens to the element is disconnected from the live DOM. In case of custom elements uses [`disconnectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
disconnected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
/**
* Fires when the host element is "ready", for host element itsel, it is just an alias for `scope.host(listener)`.
* This is handy to apply some property depending on full template such as:
* Fires after the next tick of the Javascript event loop.
* This is handy for example to apply some property depending on the element content:
* ```js
* const selected= "Z";
* //...
* return el("form").append(
* el("select", null, on.host(e=> e.value=selected)).append(
* el("select", null, on.defer(e=> e.value=selected)).append(
* el("option", { value: "A", textContent: "A" }),
* //...
* el("option", { value: "Z", textContent: "Z" }),
@ -193,7 +195,7 @@ export interface On {
* );
* ```
* */
host<EL extends SupportedElement>(listener: (element: EL) => any, host?: Host<SupportedElement>): ddeElementAddon<EL>;
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
}
export const on: On;
export type Scope = {

34
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();
@ -280,6 +280,7 @@ var DDE = (() => {
return element;
};
}
on.defer = (fn) => setTimeout.bind(null, fn, 0);
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
on.connected = function(listener, options) {
options = lifeOptions(options);
@ -315,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];
@ -378,7 +379,6 @@ var DDE = (() => {
return scopes.pop();
}
};
on.host = (fn, host = scope.host) => (el) => host(() => fn(el));
// src/signals-lib/common.js
var signals_global = {
@ -448,16 +448,18 @@ var DDE = (() => {
const s = signals(this);
let scoped = 0;
let el, el_host;
if (Object(attributes) !== attributes || s.isSignal(attributes))
const att_type = typeof attributes;
if (att_type === "string" || att_type === "number" || s.isSignal(attributes))
attributes = { textContent: attributes };
switch (true) {
case typeof tag === "function": {
scoped = 1;
const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0);
scope.push({ scope: tag, host });
el = tag(attributes || void 0);
const is_fragment = isInstance(el, enviroment.F);
el = /** @type {Element} */
tag(attributes || void 0);
if (el.nodeName === "#comment") break;
const is_fragment = isInstance(el, enviroment.F);
const el_mark = createElement.mark({
type: "component",
name: tag.name,
@ -675,9 +677,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();
@ -692,10 +694,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);
})();

12
dist/iife.min.d.ts vendored
View File

@ -172,20 +172,22 @@ declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event] & {
target: EL;
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: string, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: Event | CustomEvent) => any, options?: AddEventListenerOptions): EE;
/** Listens to the element is connected to the live DOM. In case of custom elements uses [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
connected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<NoInfer<EL>>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
/** Listens to the element is disconnected from the live DOM. In case of custom elements uses [`disconnectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
disconnected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
/**
* Fires when the host element is "ready", for host element itsel, it is just an alias for `scope.host(listener)`.
* This is handy to apply some property depending on full template such as:
* Fires after the next tick of the Javascript event loop.
* This is handy for example to apply some property depending on the element content:
* ```js
* const selected= "Z";
* //...
* return el("form").append(
* el("select", null, on.host(e=> e.value=selected)).append(
* el("select", null, on.defer(e=> e.value=selected)).append(
* el("option", { value: "A", textContent: "A" }),
* //...
* el("option", { value: "Z", textContent: "Z" }),
@ -193,7 +195,7 @@ export interface On {
* );
* ```
* */
host<EL extends SupportedElement>(listener: (element: EL) => any, host?: Host<SupportedElement>): ddeElementAddon<EL>;
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
}
export const on: On;
export type Scope = {

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 {
@ -70,6 +70,7 @@ styles.css`
background-color: var(--bg);
color: var(--text);
min-height: 200px;
height: 25em;
resize: vertical;
}
@ -148,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

@ -356,11 +356,12 @@ export function converter() {
"dd<el> Output",
el("div", { className: "button-group" }).append(
el("button", {
textContent: "Copy",
type: "button",
className: "copy-button",
title: "Copy to clipboard",
disabled: S(() => !ddeOutput.get())
}, onCopy).append("Copy")
}, onCopy)
)
),
el("textarea", {

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

@ -0,0 +1,375 @@
/**
* Case Study: Data Dashboard with Charts
*
* This example demonstrates:
* - Integration with a third-party charting library
* - Data fetching and state management
* - Responsive layout design
* - Multiple interactive components working together
*/
import { el, on } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
/**
* Data Dashboard Component with Chart Integration
* @returns {HTMLElement} Dashboard element
*/
export function DataDashboard() {
// Mock data for demonstration
const DATA = {
sales: [42, 58, 65, 49, 72, 85, 63, 70, 78, 89, 95, 86],
visitors: [1420, 1620, 1750, 1850, 2100, 2400, 2250, 2500, 2750, 2900, 3100, 3200],
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']
};
const years = [2022, 2023, 2024];
const dataTypes = [
{ id: 'sales', label: 'Sales', unit: 'K' },
{ id: 'visitors', label: 'Visitors', unit: '' },
{ id: 'conversion', label: 'Conversion Rate', unit: '%' }
];
// 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);
// Simulate API call
setTimeout(() => {
if (Math.random() > 0.9) {
// Simulate occasional error
error.set('Failed to load data. Please try again.');
}
isLoading.set(false);
}, 800);
}
// Reactive chart rendering
const chart = S(()=> {
const chart= el("canvas", { id: "chart-canvas", width: 800, height: 400 });
const ctx = chart.getContext('2d');
const data = selectedData.get();
const months = DATA.months;
const width = chart.width;
const height = chart.height;
const maxValue = Math.max(...data) * 1.1;
const barWidth = width / data.length - 10;
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Draw background grid
ctx.beginPath();
ctx.strokeStyle = '#f0f0f0';
ctx.lineWidth = 1;
for(let i = 0; i < 5; i++) {
const y = height - (height * (i / 5)) - 30;
ctx.moveTo(50, y);
ctx.lineTo(width - 20, y);
// Draw grid labels
ctx.fillStyle = '#999';
ctx.font = '12px Arial';
ctx.fillText(Math.round(maxValue * (i / 5)).toString(), 20, y + 5);
}
ctx.stroke();
// Draw bars
data.forEach((value, index) => {
const x = index * (barWidth + 10) + 60;
const barHeight = (value / maxValue) * (height - 60);
// Bar
ctx.fillStyle = '#4a90e2';
ctx.fillRect(x, height - barHeight - 30, barWidth, barHeight);
// Month label
ctx.fillStyle = '#666';
ctx.font = '12px Arial';
ctx.fillText(months[index], x + barWidth/2 - 10, height - 10);
});
// Chart title
ctx.fillStyle = '#333';
ctx.font = 'bold 14px Arial';
ctx.fillText(`${currentDataType.get().label} (${selectedYear.get()})`, width/2 - 80, 20);
return chart;
});
return el("div", { className: "dashboard" }).append(
el("header", { className: "dashboard-header" }).append(
el("h1", "Sales Performance Dashboard"),
el("div", { className: "year-filter" }).append(
el("label", { htmlFor: "yearSelect", textContent: "Select Year:" }),
el("select", { id: "yearSelect" },
on.defer(el=> el.value = selectedYear.get().toString()),
onYearChange
).append(
...years.map(year => el("option", { value: year, textContent: year }))
)
)
),
S.el(error, errorMsg => !errorMsg
? el()
: el("div", { className: "error-message" }).append(
el("p", errorMsg),
el("button", { textContent: "Retry", type: "button" }, on("click", loadData)),
),
),
S.el(isLoading, loading => !loading
? el()
: el("div", { className: "loading-spinner" })
),
// Main dashboard content
el("div", { className: "dashboard-content" }).append(
// Metrics cards
el("div", { className: "metrics-container" }).append(
el("div", { className: "metric-card" }).append(
el("h3", "Total"),
el("#text", S(() => `${totalValue.get().toLocaleString()}${currentDataType.get().unit}`)),
),
el("div", { className: "metric-card" }).append(
el("h3", "Average"),
el("#text", S(() => `${averageValue.get().toFixed(1)}${currentDataType.get().unit}`)),
),
el("div", { className: "metric-card" }).append(
el("h3", "Highest"),
el("#text", S(() => `${highestValue.get()}${currentDataType.get().unit}`)),
),
),
// Data type selection tabs
el("div", { className: "data-type-tabs" }).append(
...dataTypes.map(type =>
el("button", {
type: "button",
className: S(() => selectedDataType.get() === type.id ? 'active' : ''),
dataType: type.id,
textContent: type.label
}, onDataTypeChange)
)
),
// Chart container
el("div", { className: "chart-container" }).append(
S.el(chart, chart => chart)
)
),
);
}
// Render the component
document.body.append(
el("div", { style: "padding: 20px; background: #f5f5f5; min-height: 100vh;" }).append(
el(DataDashboard)
),
el("style", `
.dashboard {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
max-width: 1000px;
margin: 0 auto;
padding: 1rem;
background: #fff;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid #eee;
}
.dashboard-header h1 {
font-size: 1.5rem;
margin: 0;
color: #333;
}
.year-filter {
display: flex;
align-items: center;
gap: 0.5rem;
}
.year-filter select {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.metrics-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-bottom: 1.5rem;
}
.metric-card {
background: #f9f9f9;
border-radius: 8px;
padding: 1rem;
text-align: center;
transition: transform 0.2s ease;
}
.metric-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
}
.metric-card h3 {
margin-top: 0;
color: #666;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.metric-card p {
font-size: 1.5rem;
font-weight: bold;
color: #333;
margin: 0;
}
.data-type-tabs {
display: flex;
border-bottom: 1px solid #eee;
margin-bottom: 1.5rem;
}
.data-type-tabs button {
background: none;
border: none;
padding: 0.75rem 1.5rem;
font-size: 1rem;
cursor: pointer;
color: #666;
position: relative;
}
.data-type-tabs button.active {
color: #4a90e2;
font-weight: 500;
}
.data-type-tabs button.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
width: 100%;
height: 3px;
background: #4a90e2;
border-radius: 3px 3px 0 0;
}
.chart-container {
background: #fff;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
overflow: auto;
}
.loading-spinner {
display: flex;
justify-content: center;
align-items: center;
height: 100px;
}
.loading-spinner::before {
content: '';
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #4a90e2;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
background: #ffecec;
color: #e74c3c;
padding: 1rem;
border-radius: 4px;
margin-bottom: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.error-message p {
margin: 0;
}
.error-message button {
background: #e74c3c;
color: white;
border: none;
border-radius: 4px;
padding: 0.5rem 1rem;
cursor: pointer;
}
@media (max-width: 768px) {
.metrics-container {
grid-template-columns: 1fr;
}
.dashboard-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.year-filter {
width: 100%;
}
.year-filter select {
flex-grow: 1;
}
}
`)
);

View File

@ -0,0 +1,412 @@
/**
* Case Study: Interactive Image Gallery
*
* This example demonstrates:
* - Dynamic loading of content
* - Lightbox functionality
* - Animation handling
* - Keyboard and gesture navigation
*/
import { el, memo, on } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
// Sample image data
const imagesSample = (url=> [
{ id: 1, src: url+'nature', alt: 'Nature', title: 'Beautiful Landscape' },
{ id: 2, src: url+'places', alt: 'City', title: 'Urban Architecture' },
{ id: 3, src: url+'people', alt: 'People', title: 'Street Photography' },
{ id: 4, src: url+'food', alt: 'Food', title: 'Culinary Delights' },
{ id: 5, src: url+'animals', alt: 'Animals', title: 'Wildlife' },
{ id: 6, src: url+'travel', alt: 'Travel', title: 'Adventure Awaits' },
{ id: 7, src: url+'computer', alt: 'Technology', title: 'Modern Tech' },
{ id: 8, src: url+'music', alt: 'Art', title: 'Creative Expression' },
])('https://api.algobook.info/v1/randomimage?category=');
/**
* Interactive Image Gallery Component
* @returns {HTMLElement} Gallery element
*/
export function ImageGallery(images= imagesSample) {
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);
});
// 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);
const onImageClick = id => on("click", () => {
selectedImageId.set(id);
document.body.style.overflow = 'hidden'; // Prevent scrolling when lightbox is open
// Add keyboard event listeners when lightbox opens
document.addEventListener('keydown', handleKeyDown);
});
const closeLightbox = () => {
selectedImageId.set(null);
document.body.style.overflow = ''; // Restore scrolling
// Remove keyboard event listeners when lightbox closes
document.removeEventListener('keydown', handleKeyDown);
};
const onPrevImage = e => {
e.stopPropagation(); // Prevent closing the lightbox
const images = imagesToDisplay.get();
const currentId = selectedImageId.get();
const currentIndex = images.findIndex(img => img.id === currentId);
const prevIndex = (currentIndex - 1 + images.length) % images.length;
selectedImageId.set(images[prevIndex].id);
};
const onNextImage = e => {
e.stopPropagation(); // Prevent closing the lightbox
const images = imagesToDisplay.get();
const currentId = selectedImageId.get();
const currentIndex = images.findIndex(img => img.id === currentId);
const nextIndex = (currentIndex + 1) % images.length;
selectedImageId.set(images[nextIndex].id);
};
// Keyboard navigation handler
function handleKeyDown(e) {
switch(e.key) {
case 'Escape':
closeLightbox();
break;
case 'ArrowLeft':
onPrevImage(e);
break;
case 'ArrowRight':
onNextImage(e);
break;
}
}
// Build the gallery UI
return el("div", { className: "gallery-container" }).append(
// Gallery header
el("header", { className: "gallery-header" }).append(
el("h1", "Interactive Image Gallery"),
el("p", "Click on any image to view it in the lightbox. Use arrow keys for navigation.")
),
// Filter options
el("div", { className: "gallery-filters" }).append(
el("button", {
classList: { active: S(() => filterTag.get() === 'all') },
textContent: "All"
}, onFilterChange('all')),
el("button", {
classList: { active: S(() => filterTag.get() === 'nature') },
textContent: "Nature"
}, onFilterChange('nature')),
el("button", {
classList: { active: S(() => filterTag.get() === 'urban') },
textContent: "Urban"
}, onFilterChange('urban')),
el("button", {
classList: { active: S(() => filterTag.get() === 'people') },
textContent: "People"
}, onFilterChange('people'))
),
// Image grid
el("div", { className: "gallery-grid" }).append(
S.el(imagesToDisplay, images =>
images.map(image =>
memo(image.id, ()=>
el("div", {
className: "gallery-item",
dataTag: image.alt.toLowerCase()
}, onImageClick(image.id)).append(
el("img", {
src: image.src,
alt: image.alt,
loading: "lazy"
}),
el("div", { className: "gallery-item-caption" }).append(
el("h3", image.title),
el("p", image.alt)
)
)
)
)
)
),
// Lightbox (only shown when an image is selected)
S.el(isLightboxOpen, open => !open
? el()
: el("div", { className: "lightbox-overlay" }, on("click", closeLightbox)).append(
el("div", {
className: "lightbox-content",
onClick: e => e.stopPropagation() // Prevent closing when clicking inside
}).append(
el("button", {
className: "lightbox-close-btn",
ariaLabel: "Close lightbox",
}, on("click", closeLightbox)).append("×"),
el("button", {
className: "lightbox-prev-btn",
ariaLabel: "Previous image",
}, on("click", onPrevImage)).append(""),
el("button", {
className: "lightbox-next-btn",
ariaLabel: "Next image",
}, on("click", onNextImage)).append(""),
S.el(selectedImage, img => !img
? el()
: el("div", { className: "lightbox-image-container" }).append(
el("img", {
src: img.src,
alt: img.alt,
className: "lightbox-image",
}),
el("div", { className: "lightbox-caption" }).append(
el("h2", img.title),
el("p", img.alt),
),
)
),
),
),
),
);
}
// Render the component
document.body.append(
el("div", { style: "padding: 20px; background: #f5f5f5; min-height: 100vh;" }).append(
el(ImageGallery)
),
el("style", `
.gallery-container {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.gallery-header {
text-align: center;
margin-bottom: 2rem;
}
.gallery-header h1 {
margin-bottom: 0.5rem;
color: #333;
}
.gallery-header p {
color: #666;
}
.gallery-filters {
display: flex;
justify-content: center;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.gallery-filters button {
background: none;
border: none;
padding: 0.5rem 1.5rem;
margin: 0 0.5rem;
font-size: 1rem;
cursor: pointer;
border-radius: 30px;
transition: all 0.3s ease;
color: #555;
}
.gallery-filters button:hover {
background: #f0f0f0;
}
.gallery-filters button.active {
background: #4a90e2;
color: white;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1.5rem;
}
.gallery-item {
position: relative;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
cursor: pointer;
}
.gallery-item:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15);
}
.gallery-item img {
width: 100%;
height: 200px;
object-fit: cover;
display: block;
transition: transform 0.5s ease;
}
.gallery-item:hover img {
transform: scale(1.05);
}
.gallery-item-caption {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
color: white;
padding: 1rem;
transform: translateY(100%);
transition: transform 0.3s ease;
}
.gallery-item:hover .gallery-item-caption {
transform: translateY(0);
}
.gallery-item-caption h3 {
margin: 0 0 0.5rem;
font-size: 1.2rem;
}
.gallery-item-caption p {
margin: 0;
font-size: 0.9rem;
opacity: 0.8;
}
/* Lightbox styles */
.lightbox-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
padding: 2rem;
}
.lightbox-content {
position: relative;
max-width: 90%;
max-height: 90%;
}
.lightbox-image-container {
overflow: hidden;
border-radius: 4px;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
background: #000;
}
.lightbox-image {
max-width: 100%;
max-height: 80vh;
display: block;
margin: 0 auto;
}
.lightbox-caption {
background: #222;
color: white;
padding: 1rem;
text-align: center;
}
.lightbox-caption h2 {
margin: 0 0 0.5rem;
}
.lightbox-caption p {
margin: 0;
opacity: 0.8;
}
.lightbox-close-btn,
.lightbox-prev-btn,
.lightbox-next-btn {
background: rgba(0, 0, 0, 0.5);
color: white;
border: none;
font-size: 1.5rem;
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: background 0.3s ease;
position: absolute;
}
.lightbox-close-btn:hover,
.lightbox-prev-btn:hover,
.lightbox-next-btn:hover {
background: rgba(0, 0, 0, 0.8);
}
.lightbox-close-btn {
top: -25px;
right: -25px;
}
.lightbox-prev-btn {
left: -25px;
top: 50%;
transform: translateY(-50%);
}
.lightbox-next-btn {
right: -25px;
top: 50%;
transform: translateY(-50%);
}
@media (max-width: 768px) {
.gallery-container {
padding: 1rem;
}
.gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 1rem;
}
.lightbox-prev-btn,
.lightbox-next-btn {
width: 40px;
height: 40px;
font-size: 1.2rem;
}
}
`)
);

View File

@ -0,0 +1,339 @@
/**
* Case Study: Interactive Form with Validation
*
* This example demonstrates:
* - Form handling with real-time validation
* - Reactive UI updates based on input state
* - Complex form state management
* - Clean separation of concerns (data, validation, UI)
*/
import { dispatchEvent, el, on, scope } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
/**
* @typedef {Object} FormState
* @property {string} name
* @property {string} email
* @property {string} password
* @property {string} confirmPassword
* @property {boolean} agreedToTerms
* */
/**
* Interactive Form with Validation Component
* @returns {HTMLElement} Form element
*/
export function InteractiveForm() {
const submitted = S(false);
/** @type {FormState|null} */
let formState = null;
/** @param {CustomEvent<FormState>} event */
const onSubmit = ({ detail }) => {
submitted.set(true);
formState = detail;
};
const onAnotherAccount = () => {
submitted.set(false)
formState = null;
};
return el("div", { className: "form-container" }).append(
S.el(submitted, s => s
? el("div", { className: "success-message" }).append(
el("h3", "Thank you for registering!"),
el("p", `Welcome, ${formState.name}! Your account has been created successfully.`),
el("button", { textContent: "Register another account", type: "button" },
on("click", onAnotherAccount)
),
)
: el(Form, { initial: formState }, on("form:submit", onSubmit))
)
);
}
/**
* Form Component
* @type {(props: { initial: FormState | null }) => HTMLElement}
* */
export function Form({ initial }) {
const { host }= scope;
// Form state management
const formState = S(initial || {
name: '',
email: '',
password: '',
confirmPassword: '',
agreedToTerms: false
}, {
/**
* @template {keyof FormState} K
* @param {K} key
* @param {FormState[K]} value
* */
update(key, value) {
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]);
};
// Form validate state
const nameValid = S(() => formState.get().name.length >= 3);
const emailValid = S(() => {
const email = formState.get().email;
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
});
const passwordValid = S(() => {
const password = formState.get().password;
return password.length >= 8 && /[A-Z]/.test(password) && /[0-9]/.test(password);
});
const passwordsMatch = S(() => {
const { password, confirmPassword } = formState.get();
return password === confirmPassword && confirmPassword !== '';
});
const termsAgreed = S(() => formState.get().agreedToTerms);
const formValid = S(() =>
nameValid.get() &&
emailValid.get() &&
passwordValid.get() &&
passwordsMatch.get() &&
termsAgreed.get()
);
const dispatcSubmit = dispatchEvent("form:submit", host);
const onSubmit = on("submit", e => {
e.preventDefault();
if (!formValid.get()) {
return;
}
dispatcSubmit(formState.get());
});
// Component UI
return el("form", { className: "registration-form" }, onSubmit).append(
el("h2", "Create an Account"),
// Name field
el("div", { classList: {
"form-group": true,
valid: nameValid,
invalid: S(()=> !nameValid.get() && formState.get().name)
}}).append(
el("label", { htmlFor: "name", textContent: "Full Name" }),
el("input", {
id: "name",
type: "text",
value: formState.get().name,
placeholder: "Enter your full name"
}, on("input", onChange("value"))),
el("div", { className: "validation-message", textContent: "Name must be at least 3 characters long" }),
),
// Email field
el("div", { classList: {
"form-group": true,
valid: emailValid,
invalid: S(()=> !emailValid.get() && formState.get().email)
}}).append(
el("label", { htmlFor: "email", textContent: "Email Address" }),
el("input", {
id: "email",
type: "email",
value: formState.get().email,
placeholder: "Enter your email address"
}, on("input", onChange("value"))),
el("div", { className: "validation-message", textContent: "Please enter a valid email address" })
),
// Password field
el("div", { classList: {
"form-group": true,
valid: passwordValid,
invalid: S(()=> !passwordValid.get() && formState.get().password)
}}).append(
el("label", { htmlFor: "password", textContent: "Password" }),
el("input", {
id: "password",
type: "password",
value: formState.get().password,
placeholder: "Create a password"
}, on("input", onChange("value"))),
el("div", {
className: "validation-message",
textContent: "Password must be at least 8 characters with at least one uppercase letter and one number",
}),
),
// Confirm password field
el("div", { classList: {
"form-group": true,
valid: passwordsMatch,
invalid: S(()=> !passwordsMatch.get() && formState.get().confirmPassword)
}}).append(
el("label", { htmlFor: "confirmPassword", textContent: "Confirm Password" }),
el("input", {
id: "confirmPassword",
type: "password",
value: formState.get().confirmPassword,
placeholder: "Confirm your password"
}, on("input", onChange("value"))),
el("div", { className: "validation-message", textContent: "Passwords must match" }),
),
// Terms agreement
el("div", { className: "form-group checkbox-group" }).append(
el("input", {
id: "agreedToTerms",
type: "checkbox",
checked: formState.get().agreedToTerms
}, on("change", onChange("checked"))),
el("label", { htmlFor: "agreedToTerms", textContent: "I agree to the Terms and Conditions" }),
),
// Submit button
el("button", {
textContent: "Create Account",
type: "submit",
className: "submit-button",
disabled: S(() => !formValid.get())
}),
);
}
// Render the component
document.body.append(
el("div", { style: "padding: 20px; background: #f5f5f5; min-height: 100vh;" }).append(
el(InteractiveForm)
),
el("style", `
.form-container {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
max-width: 500px;
margin: 0 auto;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
background: #fff;
}
h2 {
margin-top: 0;
color: #333;
margin-bottom: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
position: relative;
transition: all 0.3s ease;
}
label {
display: block;
margin-bottom: 0.5rem;
color: #555;
font-weight: 500;
}
input[type="text"],
input[type="email"],
input[type="password"] {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
input:focus {
outline: none;
border-color: #4a90e2;
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
}
.checkbox-group {
display: flex;
align-items: center;
}
.checkbox-group label {
margin: 0 0 0 0.5rem;
}
.validation-message {
font-size: 0.85rem;
color: #e74c3c;
margin-top: 0.5rem;
height: 0;
overflow: hidden;
opacity: 0;
transition: all 0.3s ease;
}
.form-group.invalid .validation-message {
height: auto;
opacity: 1;
}
.form-group.valid input {
border-color: #2ecc71;
}
.form-group.invalid input {
border-color: #e74c3c;
}
.submit-button {
background-color: #4a90e2;
color: white;
border: none;
border-radius: 4px;
padding: 0.75rem 1.5rem;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s ease;
width: 100%;
}
.submit-button:hover:not(:disabled) {
background-color: #3a7bc8;
}
.submit-button:disabled {
background-color: #b5b5b5;
cursor: not-allowed;
}
.success-message {
text-align: center;
color: #2ecc71;
}
.success-message h3 {
margin-top: 0;
}
.success-message button {
background-color: #2ecc71;
color: white;
border: none;
border-radius: 4px;
padding: 0.75rem 1.5rem;
font-size: 1rem;
cursor: pointer;
margin-top: 1rem;
}
.success-message button:hover {
background-color: #27ae60;
}
`),
);

View File

@ -0,0 +1,509 @@
import { el, on, scope } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
export function ProductCatalog() {
const { signal }= scope;
const itemsPerPage = 5;
const products = asyncSignal(S,
fetchProducts,
{ initial: [], keepLast: true, signal });
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, signal?: AbortSignal }} options - Configuration options
* @returns {Object} Status signals and control methods
*/
export function asyncSignal(S, invoker, { initial, keepLast, signal } = {}) {
/** @type {(s: AbortSignal) => AbortSignal} */
const anySignal = !signal || !AbortSignal.any // TODO: make better
? s=> s
: s=> AbortSignal.any([s, signal]);
// Status tracking signals
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: anySignal(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

@ -0,0 +1,715 @@
/**
* Case Study: Task Manager Application
*
* This example demonstrates:
* - Complex state management with signals
* - Drag and drop functionality
* - Local storage persistence
* - Responsive design for different devices
*/
import { el, on, dispatchEvent, scope } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
/** @typedef {{ id: number, title: string, description: string, priority: string, status: string }} Task */
/**
* Task Manager Component
* @returns {HTMLElement} Task manager UI
*/
export function TaskManager() {
// <Tasks store>
const STORAGE_KEY = 'dde-task-manager';
const STATUSES = {
TODO: 'todo',
IN_PROGRESS: 'in-progress',
DONE: 'done'
};
/** @type {Task[]} */
let initialTasks = [];
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
initialTasks = JSON.parse(saved);
}
} catch (e) {
console.error('Failed to load tasks from localStorage', e);
}
if (!initialTasks.length) {
initialTasks = [
{ id: 1, title: 'Create project structure', description: 'Set up folders and initial files',
status: STATUSES.DONE, priority: 'high' },
{ id: 2, title: 'Design UI components', description: 'Create mockups for main views',
status: STATUSES.IN_PROGRESS, priority: 'medium' },
{ id: 3, title: 'Implement authentication', description: 'Set up user login and registration',
status: STATUSES.TODO, priority: 'high' },
{ id: 4, title: 'Write documentation', description: 'Document API endpoints and usage examples',
status: STATUSES.TODO, priority: 'low' },
];
}
const tasks = S(initialTasks, {
add(task) { this.value.push(task); },
remove(id) { this.value = this.value.filter(task => task.id !== id); },
update(id, task) {
const current= this.value.find(t => t.id === id);
if (current) Object.assign(current, task);
}
});
S.on(tasks, value => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(value));
} catch (e) {
console.error('Failed to save tasks to localStorage', e);
}
});
// </Tasks store>
const filterPriority = S('all');
const searchQuery = S('');
// Filtered tasks based on priority and search query
const filteredTasks = S(() => {
let filtered = tasks.get();
// Filter by priority
if (filterPriority.get() !== 'all') {
filtered = filtered.filter(task => task.priority === filterPriority.get());
}
// Filter by search query
const query = searchQuery.get().toLowerCase();
if (query) {
filtered = filtered.filter(task =>
task.title.toLowerCase().includes(query) ||
task.description.toLowerCase().includes(query)
);
}
return filtered;
});
/** Tasks grouped by status for display in columns */
const tasksByStatus = S(() => {
const filtered = filteredTasks.get();
return {
[STATUSES.TODO]: filtered.filter(t => t.status === STATUSES.TODO),
[STATUSES.IN_PROGRESS]: filtered.filter(t => t.status === STATUSES.IN_PROGRESS),
[STATUSES.DONE]: filtered.filter(t => t.status === STATUSES.DONE)
};
});
// <Add> signals and handlers for adding new tasks
const newTask = { title: '', description: '', priority: 'medium' };
const onAddTask = e => {
e.preventDefault();
if (!newTask.title) return;
S.action(tasks, "add", {
id: Date.now(),
status: STATUSES.TODO,
...newTask
});
e.target.reset();
};
// </Add>
const onCardEdit= on("card:edit", /** @param {CardEditEvent} ev */({ detail: [ id, task ] })=>
S.action(tasks, "update", id, task));
const onCardDelete= on("card:delete", /** @param {CardDeleteEvent} ev */({ detail: id })=>
S.action(tasks, "remove", id));
const { onDragable, onDragArea }= moveElementAddon(
(id, status) => S.action(tasks, "update", id, { status })
);
// Build the task manager UI
return el("div", { className: "task-manager" }).append(
el("header", { className: "app-header" }).append(
el("h1", "DDE Task Manager"),
el("div", { className: "app-controls" }).append(
el("input", {
type: "text",
placeholder: "Search tasks...",
value: searchQuery.get()
}, on("input", e => searchQuery.set(e.target.value))),
el("select", null,
on.defer(el=> el.value= filterPriority.get()),
on("change", e => filterPriority.set(e.target.value))
).append(
el("option", { value: "all", textContent: "All Priorities" }),
el("option", { value: "low", textContent: "Low Priority" }),
el("option", { value: "medium", textContent: "Medium Priority" }),
el("option", { value: "high", textContent: "High Priority" })
)
)
),
// Add new task form
el("form", { className: "new-task-form" }, on("submit", onAddTask)).append(
el("div", { className: "form-row" }).append(
el("input", {
type: "text",
placeholder: "New task title",
value: newTask.title,
required: true
}, on("input", e => newTask.title= e.target.value.trim())),
el("select", null,
on.defer(el=> el.value= newTask.priority),
on("change", e => newTask.priority= e.target.value)
).append(
el("option", { value: "low", textContent: "Low" }),
el("option", { value: "medium", textContent: "Medium" }),
el("option", { value: "high", textContent: "High" })
),
el("button", { type: "submit", className: "add-btn" }).append("Add Task")
),
el("textarea", {
placeholder: "Task description (optional)",
value: newTask.description
}, on("input", e => newTask.description= e.target.value.trim()))
),
// Task board with columns
el("div", { className: "task-board" }).append(
// Todo column
el("div", {
id: `column-${STATUSES.TODO}`,
className: "task-column"
}, onDragArea(STATUSES.TODO)).append(
el("h2", { className: "column-header" }).append(
"To Do ",
el("span", {
textContent: S(() => tasksByStatus.get()[STATUSES.TODO].length),
className: "task-count"
}),
),
S.el(S(() => tasksByStatus.get()[STATUSES.TODO]), tasks =>
el("div", { className: "column-tasks" }).append(
...tasks.map(task=> el(TaskCard, { task, onDragable }, onCardEdit, onCardDelete))
)
)
),
// In Progress column
el("div", {
id: `column-${STATUSES.IN_PROGRESS}`,
className: "task-column"
}, onDragArea(STATUSES.IN_PROGRESS)).append(
el("h2", { className: "column-header" }).append(
"In Progress ",
el("span", {
textContent: S(() => tasksByStatus.get()[STATUSES.IN_PROGRESS].length),
className: "task-count",
}),
),
S.el(S(() => tasksByStatus.get()[STATUSES.IN_PROGRESS]), tasks =>
el("div", { className: "column-tasks" }).append(
...tasks.map(task=> el(TaskCard, { task, onDragable }, onCardEdit, onCardDelete))
)
)
),
// Done column
el("div", {
id: `column-${STATUSES.DONE}`,
className: "task-column"
}, onDragArea(STATUSES.DONE)).append(
el("h2", { className: "column-header" }).append(
"Done ",
el("span", {
textContent: S(() => tasksByStatus.get()[STATUSES.DONE].length),
className: "task-count",
}),
),
S.el(S(() => tasksByStatus.get()[STATUSES.DONE]), tasks =>
el("div", { className: "column-tasks" }).append(
...tasks.map(task=> el(TaskCard, { task, onDragable }, onCardEdit, onCardDelete))
)
)
)
),
);
}
/** @typedef {CustomEvent<[ string, Task ]>} CardEditEvent */
/** @typedef {CustomEvent<string>} CardDeleteEvent */
/**
* Task Card Component
* @type {(props: { task: Task, onDragable: (id: number) => ddeElementAddon<HTMLDivElement> }) => HTMLElement}
* @fires {CardEditEvent} card:edit
* @fires {CardDeleteEvent} card:delete
* */
function TaskCard({ task, onDragable }){
const { host }= scope;
const isEditing = S(false);
const onEditStart = () => isEditing.set(true);
const dispatchEdit= dispatchEvent("card:edit", host);
const dispatchDelete= dispatchEvent("card:delete", host).bind(null, task.id);
return el("div", {
id: `task-${task.id}`,
className: `task-card priority-${task.priority}`,
draggable: true
}, onDragable(task.id)).append(
S.el(isEditing, editing => editing
? el(EditMode)
: el().append(
el("div", { className: "task-header" }).append(
el("h3", { className: "task-title", textContent: task.title }),
el("div", { className: "task-actions" }).append(
el("button", {
textContent: "✎",
className: "edit-btn",
ariaLabel: "Edit task"
}, on("click", onEditStart)),
el("button", {
textContent: "✕",
className: "delete-btn",
ariaLabel: "Delete task"
}, on("click", dispatchDelete))
)
),
!task.description
? el()
: el("p", { className: "task-description", textContent: task.description }),
el("div", { className: "task-meta" }).append(
el("span", {
className: `priority-badge priority-${task.priority}`,
textContent: task.priority.charAt(0).toUpperCase() + task.priority.slice(1)
})
)
)
)
);
function EditMode(){
const onSubmit = on("submit", e => {
e.preventDefault();
const formData = new FormData(/** @type {HTMLFormElement} */(e.target));
const title = formData.get("title");
const description = formData.get("description");
const priority = formData.get("priority");
isEditing.set(false);
dispatchEdit([ task.id, { title, description, priority } ]);
})
const onEditCancel = () => isEditing.set(false);
return el("form", { className: "task-edit-form" }, onSubmit).append(
el("input", {
name: "title",
className: "task-title-input",
defaultValue: task.title,
placeholder: "Task title",
required: true,
autoFocus: true
}),
el("textarea", {
name: "description",
className: "task-desc-input",
defaultValue: task.description,
placeholder: "Description (optional)"
}),
el("select", {
name: "priority",
}, on.defer(el=> el.value = task.priority)).append(
el("option", { value: "low", textContent: "Low Priority" }),
el("option", { value: "medium", textContent: "Medium Priority" }),
el("option", { value: "high", textContent: "High Priority" })
),
el("div", { className: "task-edit-actions" }).append(
el("button", {
textContent: "Cancel",
type: "button",
className: "cancel-btn"
}, on("click", onEditCancel)),
el("button", {
textContent: "Save",
type: "submit",
className: "save-btn"
})
)
);
}
}
/**
* Helper function to handle move an element
* @param {(id: string, status: string) => void} onMoved
* */
function moveElementAddon(onMoved){
let draggedTaskId = null;
function onDragable(id) {
return element => {
on("dragstart", e => {
draggedTaskId= id;
e.dataTransfer.effectAllowed = 'move';
// Add some styling to the element being dragged
setTimeout(() => {
const el = document.getElementById(`task-${id}`);
if (el) el.classList.add('dragging');
}, 0);
})(element);
on("dragend", () => {
draggedTaskId= null;
// Remove the styling
const el = document.getElementById(`task-${id}`);
if (el) el.classList.remove('dragging');
})(element);
};
}
function onDragArea(status) {
return element => {
on("dragover", e => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
// Add a visual indicator for the drop target
const column = document.getElementById(`column-${status}`);
if (column) column.classList.add('drag-over');
})(element);
on("dragleave", () => {
// Remove the visual indicator
const column = document.getElementById(`column-${status}`);
if (column) column.classList.remove('drag-over');
})(element);
on("drop", e => {
e.preventDefault();
const id = draggedTaskId;
if (id) onMoved(id, status);
// Remove the visual indicator
const column = document.getElementById(`column-${status}`);
if (column) column.classList.remove('drag-over');
})(element);
};
}
return { onDragable, onDragArea };
}
// Render the component
document.body.append(
el("div", { style: "padding: 20px; background: #f5f5f5; min-height: 100vh;" }).append(
el(TaskManager)
),
el("style", `
.task-manager {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
color: #333;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
flex-wrap: wrap;
gap: 1rem;
}
.app-header h1 {
margin: 0;
color: #2d3748;
}
.app-controls {
display: flex;
gap: 1rem;
}
.app-controls input,
.app-controls select {
padding: 0.5rem;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: 0.9rem;
}
.new-task-form {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
margin-bottom: 2rem;
}
.form-row {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.form-row input {
flex-grow: 1;
padding: 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: 1rem;
}
.form-row select {
width: 100px;
padding: 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: 1rem;
}
.add-btn {
background: #4a90e2;
color: white;
border: none;
border-radius: 4px;
padding: 0.75rem 1.5rem;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s ease;
}
.add-btn:hover {
background: #3a7bc8;
}
.new-task-form textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: 1rem;
resize: vertical;
min-height: 80px;
}
.task-board {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.task-column {
background: #f7fafc;
border-radius: 8px;
padding: 1rem;
min-height: 400px;
transition: background 0.2s ease;
}
.column-header {
margin-top: 0;
padding-bottom: 0.75rem;
border-bottom: 2px solid #e2e8f0;
font-size: 1.25rem;
color: #2d3748;
display: flex;
align-items: center;
}
.task-count {
display: inline-flex;
justify-content: center;
align-items: center;
background: #e2e8f0;
color: #4a5568;
border-radius: 50%;
width: 25px;
height: 25px;
font-size: 0.875rem;
margin-left: 0.5rem;
}
.column-tasks {
margin-top: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
min-height: 200px;
}
.task-card {
background: white;
border-radius: 6px;
padding: 1rem;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
cursor: grab;
transition: transform 0.2s ease, box-shadow 0.2s ease;
position: relative;
border-left: 4px solid #ccc;
}
.task-card:hover {
transform: translateY(-3px);
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
}
.task-card.dragging {
opacity: 0.5;
cursor: grabbing;
}
.task-card.priority-low {
border-left-color: #38b2ac;
}
.task-card.priority-medium {
border-left-color: #ecc94b;
}
.task-card.priority-high {
border-left-color: #e53e3e;
}
.task-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.5rem;
}
.task-title {
margin: 0;
font-size: 1.1rem;
color: #2d3748;
word-break: break-word;
}
.task-description {
margin: 0.5rem 0;
font-size: 0.9rem;
color: #4a5568;
word-break: break-word;
}
.task-actions {
display: flex;
gap: 0.5rem;
}
.edit-btn,
.delete-btn {
background: none;
border: none;
font-size: 1rem;
cursor: pointer;
width: 24px;
height: 24px;
display: inline-flex;
justify-content: center;
align-items: center;
border-radius: 50%;
color: #718096;
transition: background 0.2s ease, color 0.2s ease;
}
.edit-btn:hover {
background: #edf2f7;
color: #4a5568;
}
.delete-btn:hover {
background: #fed7d7;
color: #e53e3e;
}
.task-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.75rem;
}
.priority-badge {
font-size: 0.75rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-weight: 500;
}
.priority-badge.priority-low {
background: #e6fffa;
color: #2c7a7b;
}
.priority-badge.priority-medium {
background: #fefcbf;
color: #975a16;
}
.priority-badge.priority-high {
background: #fed7d7;
color: #c53030;
}
.drag-over {
background: #f0f9ff;
border: 2px dashed #4a90e2;
}
.task-edit-form {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.task-title-input,
.task-desc-input {
width: 100%;
padding: 0.5rem;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: 0.9rem;
}
.task-desc-input {
min-height: 60px;
resize: vertical;
}
.task-edit-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 0.5rem;
}
.cancel-btn,
.save-btn {
padding: 0.4rem 0.75rem;
border-radius: 4px;
font-size: 0.9rem;
cursor: pointer;
}
.cancel-btn {
background: #edf2f7;
color: #4a5568;
border: 1px solid #e2e8f0;
}
.save-btn {
background: #4a90e2;
color: white;
border: none;
}
@media (max-width: 768px) {
.app-header {
flex-direction: column;
align-items: flex-start;
}
.app-controls {
width: 100%;
flex-direction: column;
}
.form-row {
flex-direction: column;
}
.task-board {
grid-template-columns: 1fr;
}
}
`)
);

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

@ -5,10 +5,10 @@ function allLifecycleEvents(){
on.connected(log),
on.disconnected(log),
).append(
el("select", { id: "country" }, on.host(select => {
// This runs when the host (select) is ready with all its options
el("select", { id: "country" }, on.defer(select => {
// This runs when the select is ready with all its options
select.value = "cz"; // Pre-select Czechia
log({ type: "dde:on.host", detail: select });
log({ type: "dde:on.defer", detail: select });
})).append(
el("option", { value: "au", textContent: "Australia" }),
el("option", { value: "ca", textContent: "Canada" }),

View File

@ -7,20 +7,20 @@ function HelloWorld({ emoji = "🚀" }) {
const clicks = S(0);
return el().append(
// PART 2: Bind state to UI elements
el("p", {
className: "greeting",
// This paragraph automatically updates when clicks changes
textContent: S(() => `Hello World ${emoji.repeat(clicks.get())}`)
}),
// PART 2: Bind state to UI elements
el("p", {
className: "greeting",
// This paragraph automatically updates when clicks changes
textContent: S(() => `Hello World ${emoji.repeat(clicks.get())}`)
}),
// PART 3: Update state in response to events
el("button", {
type: "button",
textContent: "Add emoji",
// When clicked, update the state
onclick: () => clicks.set(clicks.get() + 1)
})
// PART 3: Update state in response to events
el("button", {
type: "button",
textContent: "Add emoji",
// When clicked, update the state
onclick: () => clicks.set(clicks.get() + 1)
})
);
}

View File

@ -9,7 +9,7 @@ document.body.append(
padding: 1em;
margin: 1em;
}
`.trim())
`.trim())
);
export function CounterStandard() {

View File

@ -13,18 +13,20 @@ import { S } from "deka-dom-el/signals";
* @returns {HTMLElement} The root TodoMVC application element
*/
function Todos(){
const pageS = routerSignal(S);
const { signal } = scope;
const pageS = routerSignal(S, signal);
const todosS = todosSignal();
/** Derived signal that filters todos based on current route */
const filteredTodosS = S(()=> {
const todosFilteredS = S(()=> {
const todos = todosS.get();
const filter = pageS.get();
if (filter === "all") return todos;
return todos.filter(todo => {
if (filter === "active") return !todo.completed;
if (filter === "completed") return todo.completed;
return true; // "all"
});
});
const todosRemainingS = S(()=> todosS.get().filter(todo => !todo.completed).length);
/** @type {ddeElementAddon<HTMLInputElement>} */
const onToggleAll = on("change", event => {
@ -73,17 +75,17 @@ function Todos(){
}, onToggleAll),
el("label", { htmlFor: "toggle-all", title: "Mark all as complete" }),
el("ul", { className: "todo-list" }).append(
S.el(filteredTodosS, filteredTodos => filteredTodos.map(todo =>
S.el(todosFilteredS, filteredTodos => filteredTodos.map(todo =>
memo(todo.id, ()=> el(TodoItem, todo, onDelete, onEdit)))
)
)
)
),
S.el(todosS, todos => !todos.length
S.el(todosS, ({ length }) => !length
? el()
: el("footer", { className: "footer" }).append(
el("span", { className: "todo-count" }).append(
noOfLeft(todos)
el("strong", length + " " + (length === 1 ? "item" : "items")),
),
memo("filters", ()=>
el("ul", { className: "filters" }).append(
@ -98,20 +100,16 @@ function Todos(){
)
),
),
!todos.some(todo => todo.completed)
length - todosRemainingS.get() === 0
? el()
: el("button", { textContent: "Clear completed", className: "clear-completed" }, onClearCompleted)
: memo("delete", () =>
el("button",
{ textContent: "Clear completed", className: "clear-completed" },
onClearCompleted)
)
)
)
);
/** @param {Todo[]} todos */
function noOfLeft(todos){
const { length }= todos.filter(todo => !todo.completed);
return el("strong").append(
length + " ",
length === 1 ? "item left" : "items left"
)
}
}
/**
@ -194,7 +192,7 @@ function TodoItem({ id, title, completed }) {
checked: completed
}, onToggleCompleted),
el("label", { textContent: title }, onStartEdit),
el("button", { className: "destroy" }, onDelete)
el("button", { ariaLabel: "Delete todo", className: "destroy" }, onDelete)
),
S.el(isEditing, editing => !editing
? el()
@ -328,6 +326,7 @@ function todosSignal(){
localStorage.setItem(store_key, JSON.stringify(value));
} catch (e) {
console.error("Failed to save todos to localStorage", e);
// Optionally, provide user feedback
}
});
return out;
@ -336,9 +335,10 @@ function todosSignal(){
/**
* Creates a signal for managing route state
*
* @param {typeof S} signal - The signal constructor
* @param {typeof S} signal - The signal constructor from a library
* @param {AbortSignal} abortSignal
*/
function routerSignal(signal){
function routerSignal(signal, abortSignal){
const initial = location.hash.replace("#", "") || "all";
const out = signal(initial, {
/**
@ -347,15 +347,16 @@ function routerSignal(signal){
*/
set(hash){
location.hash = hash;
this.value = hash;
}
//this.value = hash;
},
});
// Setup hash change listener
window.addEventListener("hashchange", () => {
const hash = location.hash.replace("#", "") || "all";
S.action(out, "set", /** @type {"all"|"active"|"completed"} */(hash));
});
//S.action(out, "set", /** @type {"all"|"active"|"completed"} */(hash));
out.set(hash);
}, { signal: abortSignal });
return out;
}

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

@ -32,19 +32,19 @@ export function getLibraryUrl(){
el("h4", "Select your preferred library format:"),
el("div", { className: "selectors" }).append(
el("select", { name: "module" }, onChangeSubmit,
on.host(select => select.value = lib.get()[0]),
on.defer(select => select.value = lib.get()[0]),
).append(
el("option", { value: "esm", textContent: "ESM — modern JavaScript module" }),
el("option", { value: "iife", textContent: "IIFE — legacy JavaScript with DDE global variable" }),
),
el("select", { name: "what" }, onChangeSubmit,
on.host(select => select.value = lib.get()[1]),
on.defer(select => select.value = lib.get()[1]),
).append(
el("option", { value: "", textContent: "DOM part only" }),
el("option", { value: "-with-signals", textContent: "DOM + signals" }),
),
el("select", { name: "minified" }, onChangeSubmit,
on.host(select => select.value = lib.get()[2]),
on.defer(select => select.value = lib.get()[2]),
).append(
el("option", { value: "", textContent: "Unminified" }),
el("option", { value: ".min", textContent: "Minified" }),

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

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

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

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

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.
@ -155,7 +154,7 @@ export function page({ pkg, info }){
You can think of an Addon as an “oncreate” event handler.
`),
el("p").append(T`
dd<el> provides three additional lifecycle events that correspond to ${el("a", { textContent:
dd<el> provides two additional lifecycle events that correspond to ${el("a", { textContent:
"custom element", ...references.mdn_customElements })} lifecycle callbacks and component patterns:
`),
el("div", { className: "function-table" }).append(
@ -165,13 +164,9 @@ export function page({ pkg, info }){
el("dt", t`on.disconnected(callback)`),
el("dd", t`Fires when the element is removed from the DOM`),
el("dt", t`on.host(callback, host?)`),
el("dd", t`Fires when the host element is "ready" and allows applying properties based on the fully
built template`),
)
),
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`
@ -203,13 +198,26 @@ export function page({ pkg, info }){
)
),
el(h3, t`Utility Helpers`),
el("p").append(T`
You can use the ${el("code", "on.defer")} helper to defer execution to the next event loop.
This is useful for example when you wan to set some element properties based on the current element
body (typically the ${el("code", "<select value=\"...\">")}).
`),
el("div", { className: "function-table" }).append(
el("dl").append(
el("dt", t`on.defer(callback)`),
el("dd", t`Helper that defers function execution to the next event loop (using setTimeout)`),
)
),
el(h3, t`Dispatching Custom Events`),
el("p").append(T`
This makes it easy to implement component communication through events, following standard web platform
patterns. The curried approach allows for easy reuse of event dispatchers throughout your application.
`),
el(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(
@ -242,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
@ -164,6 +168,22 @@ export function page({ pkg, info }){
el("li", t`name - The name of the component function`),
el("li", t`host - Indicates whether the host is "this" (for DocumentFragments) or "parentElement"`),
),
el("div", { className: "warning" }).append(
el("p").append(T`
There are edge case when the mark can be missing. For example, the (utility) components with reactive
keys such as ${el("code", ".textContent")}, ${el("code", ".innerText")} or ${el("code", ".innerHTML")}.
As they change the content of the host element.
`),
el(code, { content: `
function Counter() {
const count = S(0);
return el("button",
{ textContent: count, type: "button" },
on("click", () => count.set(count.get() + 1)),
);
}
`, language: "js" }),
),
el("h4", t`Identifying reactive elements in the DOM`),
el("p").append(T`
@ -182,7 +202,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`
@ -101,22 +100,24 @@ export function page({ pkg, info }){
`),
el(code, { content: `
// Signal for current route (all/active/completed)
const pageS = routerSignal(S);
const { signal } = scope;
const pageS = routerSignal(S, signal);
// Signal for the todos collection with custom actions
const todosS = todosSignal();
// Derived signal that filters todos based on current route
const filteredTodosS = S(()=> {
const todosFilteredS = S(()=> {
const todos = todosS.get();
const filter = pageS.get();
if (filter === "all") return todos;
return todos.filter(todo => {
if (filter === "active") return !todo.completed;
if (filter === "completed") return todo.completed;
return true; // "all"
});
});
`, page_id }),
const todosRemainingS = S(()=> todosS.get().filter(todo => !todo.completed).length);
`, language: "js" }),
el("p").append(T`
The ${el("code", "todosSignal")} function creates a custom signal with actions for manipulating the todos:
@ -200,11 +201,12 @@ export function page({ pkg, info }){
localStorage.setItem(store_key, JSON.stringify(value));
} catch (e) {
console.error("Failed to save todos to localStorage", e);
// Optionally, provide user feedback
}
});
return out;
}
`, page_id }),
`, language: "js" }),
el("div", { className: "note" }).append(
el("p").append(T`
@ -222,23 +224,23 @@ export function page({ pkg, info }){
el("h4", t`1. Derived Signals for Filtering`),
el(code, { content: `
/** Derived signal that filters todos based on current route */
const filteredTodosS = S(()=> {
const todosFilteredS = S(()=> {
const todos = todosS.get();
const filter = pageS.get();
if (filter === "all") return todos;
return todos.filter(todo => {
if (filter === "active") return !todo.completed;
if (filter === "completed") return todo.completed;
return true; // "all"
});
});
// Using the derived signal in the UI
el("ul", { className: "todo-list" }).append(
S.el(filteredTodosS, filteredTodos => filteredTodos.map(todo =>
S.el(todosFilteredS, filteredTodos => filteredTodos.map(todo =>
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,
@ -260,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
@ -297,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
@ -308,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`
@ -334,11 +336,11 @@ export function page({ pkg, info }){
el("h4", t`Memoizing Todo Items`),
el(code, { content: `
el("ul", { className: "todo-list" }).append(
S.el(filteredTodosS, filteredTodos => filteredTodos.map(todo =>
S.el(todosFilteredS, filteredTodos => filteredTodos.map(todo =>
memo(todo.id, ()=> el(TodoItem, todo, onDelete, onEdit)))
)
)
`, page_id }),
`, language: "js" }),
el("p").append(T`
This approach ensures that:
@ -352,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(
@ -386,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`
@ -431,7 +442,7 @@ export function page({ pkg, info }){
// Component implementation...
}
`, page_id }),
`, language: "js" }),
el("div", { className: "tip" }).append(
el("p").append(T`
@ -452,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")}:
@ -491,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:
@ -519,36 +530,37 @@ 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: `
S.el(S(() => todosS.get().some(todo => todo.completed)),
hasTodosCompleted=> hasTodosCompleted
? el("button", { textContent: "Clear completed", className: "clear-completed" }, onClearCompleted)
: el()
)
`, page_id }),
todos.length - todosRemainingS.get() === 0
? el()
: memo("delete", () =>
el("button",
{ textContent: "Clear completed", className: "clear-completed" },
onClearCompleted)
)
`, language: "js" }),
el("div", { className: "note" }).append(
el("p").append(T`
@ -594,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`

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

@ -2,7 +2,7 @@ import { T, t } from "./utils/index.js";
export const info= {
title: t`Appendix & Summary`,
fullTitle: t`dd<el> Comprehensive Reference`,
description: t`A final overview, case studies, key concepts, and best practices for working with deka-dom-el.`,
description: t`A final overview, case studies, key concepts, and best practices for working with deka-dom-el.`,
};
import { el } from "deka-dom-el";
@ -25,16 +25,25 @@ const references= {
performance: {
title: t`Performance Optimization Guide`,
href: "p09-optimization.html",
},
/** Examples gallery */
examples: {
title: t`Examples Gallery`,
href: "p15-examples.html",
},
/** Converter */
converter: {
title: t`HTML to dd<el> Converter`,
href: "p14-converter.html",
}
};
/** @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,
case studies, and advanced techniques. Use it as a quick reference when working with the library
This reference guide provides a comprehensive summary of dd<el>s key concepts, best practices,
case studies, and advanced techniques. Use it as a quick reference when working with the library
or to deepen your understanding of its design principles and patterns.
`),
@ -61,14 +70,18 @@ export function page({ pkg, info }){
${el("strong", "Progressive Enhancement:")} Starting simple and adding complexity only when needed
`),
el("li").append(T`
${el("strong", "Functional Composition:")} Building UIs through function composition rather than
inheritance
${el("strong", "Flexibility:")} Using what you need, whether thats plain DOM elements, event
handling, or signals for reactivity
`),
el("li").append(T`
${el("strong", "Functional Composition:")} Building UIs through function composition
`),
el("li").append(T`
${el("strong", "Clear Patterns:")} Promoting maintainable code organization with the 3PS pattern
`),
el("li").append(T`
${el("strong", "Targeted Reactivity:")} Using signals for efficient, fine-grained updates
${el("strong", "Targeted Reactivity:")} Using signals for efficient, fine-grained updates only when
needed
`),
el("li").append(T`
${el("strong", "Unix Philosophy:")} Doing one thing well and allowing composability with other tools
@ -77,11 +90,15 @@ export function page({ pkg, info }){
),
el(h3, t`Case Studies & Real-World Applications`),
el("p").append(T`
Explore our ${el("a", references.examples).append("Examples Gallery")} to see how dd<el> can be used to build
various real-world applications, from simple components to complex interactive UIs.
`),
el("h4", t`TodoMVC Implementation`),
el("p").append(T`
The ${el("a", references.todomvc).append("TodoMVC implementation")} showcases how dd<el> handles a complete,
real-world application with all standard features of a modern web app:
The ${el("a", references.todomvc).append("TodoMVC implementation")} showcases how dd<el> handles a complete,
real-world application with all standard features of a modern web app:
`),
el("ul").append(
el("li", t`Persistent storage with localStorage`),
@ -117,16 +134,18 @@ export function page({ pkg, info }){
`),
el("ol").append(
el("li").append(T`
${el("strong", "Start with state:")}: Convert global variables or ad-hoc state to signals
${el("strong", "Start with state")}: Convert global variables or ad-hoc state to signals
`),
el("li").append(T`
${el("strong", "Replace query selectors:")}: Replace getElementById/querySelector with direct references to elements
${el("strong", "Replace query selectors")}: Replace getElementById/querySelector with direct references
to elements
`),
el("li").append(T`
${el("strong", "Convert imperative updates:")}: Replace manual DOM updates with declarative signal bindings
${el("strong", "Convert imperative updates")}: Replace manual DOM updates with declarative signal
bindings
`),
el("li").append(T`
${el("strong", "Refactor into components:")}: Organize related UI elements into component functions
${el("strong", "Refactor into components")}: Organize related UI elements into component functions
`)
),
el(code, { content: `
@ -146,7 +165,7 @@ export function page({ pkg, info }){
className: S(() => countS.get() > 10 ? 'warning' : '')
})
);
`, page_id }),
`, language: "js" }),
el(h3, t`Key Concepts Reference`),
@ -157,7 +176,7 @@ export function page({ pkg, info }){
el("dd", t`Core function for creating DOM elements with declarative properties`),
el("dt", t`el().append(...children)`),
el("dd", t`Add child elements to a parent element`),
el("dd", t`Add child elements to a parent element`),
el("dt", t`memo(key, () => element)`),
el("dd", t`Cache and reuse DOM elements for performance optimization`),
@ -171,22 +190,22 @@ export function page({ pkg, info }){
el("h4", t`Signals & Reactivity`),
el("dl").append(
el("dt", t`S(initialValue)`),
el("dd", t`Create a signal with an initial value`),
el("dd", t`Create a signal with an initial value`),
el("dt", t`S(() => computation)`),
el("dd", t`Create a derived signal that updates when dependencies change`),
el("dd", t`Create a derived signal that updates when dependencies change`),
el("dt", t`S.el(signal, data => element)`),
el("dd", t`Create reactive elements that update when a signal changes`),
el("dd", t`Create reactive elements that update when a signal changes`),
el("dt", t`S.action(signal, "method", ...args)`),
el("dd", t`Call custom methods defined on a signal`),
el("dd", t`Call custom methods defined on a signal`),
el("dt", t`signal.get()`),
el("dd", t`Get the current value of a signal`),
el("dd", t`Get the current value of a signal`),
el("dt", t`signal.set(newValue)`),
el("dd", t`Update a signal's value and trigger reactive updates`)
el("dd", t`Update a signals value and trigger reactive updates`)
)
),
@ -200,7 +219,7 @@ export function page({ pkg, info }){
el("dd", t`Provides access to component context, signal, host element`),
el("dt", t`dispatchEvent(type, element)`),
el("dd", t`Creates a function for dispatching custom events`),
el("dd", t`Creates a function for dispatching custom events`),
el("dt", t`Signal Factories`),
el("dd", t`Functions that create and configure signals with domain-specific behavior`)
@ -212,19 +231,45 @@ export function page({ pkg, info }){
el("h4", t`Code Organization`),
el("ul").append(
el("li").append(T`
${el("strong", "Follow the 3PS pattern:")}: Separate state creation, binding to elements, and state updates
${el("strong", "Follow the 3PS pattern")}: Separate state creation, binding to elements, and state updates
`),
el("li").append(T`
${el("strong", "Use component functions:")}: Create reusable UI components as functions
${el("strong", "Use component functions")}: Create reusable UI components as functions
`),
el("li").append(T`
${el("strong", "Create signal factories:")}: Extract reusable signal patterns into factory functions
${el("strong", "Create signal factories")}: Extract reusable signal patterns into factory functions
`),
el("li").append(T`
${el("strong", "Leverage scopes:")}: Use scope for component context and clean resource management
${el("strong", "Leverage scopes")}: Use scope for component context and clean resource management
`),
el("li").append(T`
${el("strong", "Event delegation:")}: Prefer component-level event handlers over many individual handlers
${el("strong", "Event delegation")}: Prefer component-level event handlers over many individual handlers
`)
)
),
el("div").append(
el("h4", t`When to Use Signals vs. Plain DOM`),
el("ul").append(
el("li").append(T`
${el("strong", "Use signals for")}: Data that changes frequently, multiple elements that need to
stay in sync, computed values dependent on other state
`),
el("li").append(T`
${el("strong", "Use plain DOM for")}: Static content, one-time DOM operations, simple toggling of
elements, single-element updates
`),
el("li").append(T`
${el("strong", "Mixed approach")}: Start with plain DOM and events, then add signals only where
needed for reactivity
`),
el("li").append(T`
${el("strong", "Consider derived signals")}: For complex transformations of data rather than manual
updates
`),
el("li").append(T`
${el("strong", "Use event delegation")}: For handling multiple similar interactions without
individual signal bindings
`)
)
),
@ -233,19 +278,19 @@ export function page({ pkg, info }){
el("h4", t`Performance Optimization`),
el("ul").append(
el("li").append(T`
${el("strong", "Memoize list items:")}: Use ${el("code", "memo")} for items in frequently-updated lists
${el("strong", "Memoize list items")}: Use ${el("code", "memo")} for items in frequently-updated lists
`),
el("li").append(T`
${el("strong", "Avoid unnecessary signal updates:")}: Only update signals when values actually change
${el("strong", "Avoid unnecessary signal updates")}: Only update signals when values actually change
`),
el("li").append(T`
${el("strong", "Use AbortSignals:")}: Clean up resources when components are removed
${el("strong", "Use AbortSignals")}: Clean up resources when components are removed
`),
el("li").append(T`
${el("strong", "Prefer derived signals:")}: Use computed values instead of manual updates
${el("strong", "Prefer derived signals")}: Use computed values instead of manual updates
`),
el("li").append(T`
${el("strong", "Avoid memoizing fragments:")}: Never memoize DocumentFragments, only individual elements
${el("strong", "Avoid memoizing fragments")}: Never memoize DocumentFragments, only individual elements
`)
),
el("p").append(T`
@ -263,7 +308,7 @@ export function page({ pkg, info }){
el("dd", t`Use scope.signal or AbortSignals to handle resource cleanup when elements are removed`),
el("dt", t`Circular Signal Dependencies`),
el("dd", t`Avoid signals that depend on each other in a circular way, which can cause infinite update loops`),
el("dd", t`Avoid signals that depend on each other in a circular way, which can cause infinite update loops`),
el("dt", t`Memoizing with Unstable Keys`),
el("dd", t`Always use stable, unique identifiers as memo keys, not array indices or objects`),
@ -279,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", "")
)
)
),
@ -347,7 +392,7 @@ export function page({ pkg, info }){
el(h3, t`Contribution and Community`),
el("p").append(T`
dd<el> is an open-source project that welcomes contributions from the community:
dd<el> is an open-source project that welcomes contributions from the community:
`),
el("ul").append(
el("li").append(T`
@ -367,16 +412,33 @@ export function page({ pkg, info }){
el("div", { className: "callout" }).append(
el("h4", t`Final Thoughts`),
el("p").append(T`
dd<el> provides a lightweight yet powerful approach to building modern web interfaces
dd<el> provides a lightweight yet powerful approach to building modern web interfaces
with minimal overhead and maximal flexibility. By embracing standard web technologies
rather than abstracting them away, it offers a development experience that scales
rather than abstracting them away, it offers a development experience that scales
from simple interactive elements to complex applications while remaining close
to what makes the web platform powerful.
`),
el("p").append(T`
Whether you're building a small interactive component or a full-featured application,
dd<el>'s combination of declarative syntax, targeted reactivity, and pragmatic design
provides the tools you need without the complexity you don't.
Whether youre building a small interactive component or a full-featured application,
dd<el>s combination of declarative syntax, targeted reactivity, and pragmatic design
provides the tools you need without the complexity you dont.
`)
),
el(h3, t`Tools and Resources`),
el("p").append(T`
To help you get started with dd<el>, we provide several tools and resources:
`),
el("ul").append(
el("li").append(T`
${el("a", references.converter).append("HTML to dd<el> Converter")}: Easily convert existing HTML markup
to dd<el> JavaScript code
`),
el("li").append(T`
${el("a", references.examples).append("Examples Gallery")}: Browse real-world examples and code snippets
`),
el("li").append(T`
${el("a", references.github).append("Documentation")}: Comprehensive guides and API reference
`)
),
);

View File

@ -9,67 +9,16 @@ export const info= {
import { el } from "deka-dom-el";
import { simplePage } from "./layout/simplePage.html.js";
import { h3 } from "./components/pageUtils.html.js";
import { code } from "./components/code.html.js";
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
transform existing HTML markup into dd<el> JavaScript code, making it easier to adopt dd<el> in your projects.
`),
el("div", { className: "callout" }).append(
el("h4", t`Features`),
el("ul").append(
el("li", t`Convert any HTML snippet to dd<el> code instantly`),
el("li", t`Choose between different output formats (append vs arrays, style handling)`),
el("li", t`Try pre-built examples or paste your own HTML`),
el("li", t`Copy results to clipboard with one click`)
)
),
el("h3", t`How to Use the Converter`),
el("ol").append(
el("li").append(T`
${el("strong", "Paste your HTML")} into the input box or select one of the example templates
`),
el("li").append(T`
${el("strong", "Configure options")} to match your preferred coding style:
${el("ul").append(
el("li", t`Convert inline styles to JavaScript objects`),
el("li", t`Transform data-attributes/aria-attributes`),
)}
`),
el("li").append(T`
${el("strong", "Click convert")} to generate dd<el> code
`),
el("li").append(T`
${el("strong", "Copy the result")} to your project
`)
),
// The actual converter component
el(converter, { page_id }),
el("h3", t`How the Converter Works`),
el("p").append(T`
The converter uses a three-step process:
`),
el("ol").append(
el("li").append(T`
${el("strong", "Parsing:")} The HTML is parsed into a structured AST (Abstract Syntax Tree)
`),
el("li").append(T`
${el("strong", "Transformation:")} Each HTML node is converted to its dd<el> equivalent
`),
el("li").append(T`
${el("strong", "Code Generation:")} The final JavaScript code is properly formatted and indented
`)
),
el("div", { className: "warning" }).append(
el("p").append(T`
While the converter handles most basic HTML patterns, complex attributes or specialized elements might
@ -77,7 +26,10 @@ export function page({ pkg, info }){
`)
),
el("h3", t`Next Steps`),
// The actual converter component
el(converter),
el(h3, t`Next Steps`),
el("p").append(T`
After converting your HTML to dd<el>, you might want to:
`),
@ -86,14 +38,15 @@ export function page({ pkg, info }){
Add signal bindings for dynamic content (see ${el("a", { href: "p04-signals.html",
textContent: "Signals section" })})
`),
el("li").append(T`
Organize your components with scopes (see ${el("a", { href: "p05-scopes.html",
textContent: "Scopes section" })})
`),
el("li").append(T`
Add event handlers for interactivity (see ${el("a", { href: "p03-events.html",
textContent: "Events section" })})
`)
`),
el("li").append(T`
Organize your components with components (see ${el("a", { href:
"p02-elements.html#h-using-components-to-build-ui-fragments", textContent: "Components section" })}
and ${el("a", { href: "p05-scopes.html", textContent: "Scopes section" })})
`),
)
);
}

81
docs/p15-examples.html.js Normal file
View File

@ -0,0 +1,81 @@
import { T, t } from "./utils/index.js";
export const info= {
title: t`Examples Gallery`,
fullTitle: t`DDE Examples & Code Snippets`,
description: t`A comprehensive collection of examples and code snippets for working with Deka DOM Elements.`,
};
import { el } from "deka-dom-el";
import { simplePage } from "./layout/simplePage.html.js";
import { h3 } from "./components/pageUtils.html.js";
import { example } from "./components/example.html.js";
/** @param {string} url */
const fileURL= url=> new URL(url, import.meta.url);
/** @param {import("./types.d.ts").PageAttrs} attrs */
export function page({ pkg, info }){
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>:
`),
el(h3, t`Data Dashboard`),
el("p").append(T`
Data visualization dashboard with charts, filters, and responsive layout. Integration with a
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" }),
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" }),
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" }),
el(h3, t`Task Manager`),
el("p").append(T`
Kanban-style task management app with drag-and-drop and localStorage persistence. Complex state management
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" }),
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`
Complete TodoMVC implementation with local storage and routing. TodoMVC implementation showing routing,
local storage persistence, filtering, and component architecture patterns. For commented code, see the
dedicated page ${el("a", { href: "./p10-todomvc.html" }).append(T`TodoMVC`)}.
`),
);
}

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

11
index.d.ts vendored
View File

@ -187,7 +187,7 @@ interface On{
EL extends SupportedElement
>(
type: Event,
listener: (this: EL, ev: DocumentEventMap[Event]) => any,
listener: (this: EL, ev: DocumentEventMap[Event] & { target: EL }) => any,
options?: AddEventListenerOptions
) : ddeElementAddon<EL>;
<
@ -212,13 +212,13 @@ interface On{
options?: AddEventListenerOptions
) : ddeElementAddon<EL>;
/**
* Fires when the host element is "ready", for host element itsel, it is just an alias for `scope.host(listener)`.
* This is handy to apply some property depending on full template such as:
* Fires after the next tick of the Javascript event loop.
* This is handy for example to apply some property depending on the element content:
* ```js
* const selected= "Z";
* //...
* return el("form").append(
* el("select", null, on.host(e=> e.value=selected)).append(
* el("select", null, on.defer(e=> e.value=selected)).append(
* el("option", { value: "A", textContent: "A" }),
* //...
* el("option", { value: "Z", textContent: "Z" }),
@ -226,11 +226,10 @@ interface On{
* );
* ```
* */
host<
defer<
EL extends SupportedElement
>(
listener: (element: EL) => any,
host?: Host<SupportedElement>
) : ddeElementAddon<EL>;
}
export const on: On;

69
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "deka-dom-el",
"version": "0.9.1-alpha",
"version": "0.9.4-alpha",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "deka-dom-el",
"version": "0.9.1-alpha",
"version": "0.9.4-alpha",
"license": "MIT",
"devDependencies": {
"@size-limit/preset-small-lib": "~11.2",
@ -15,7 +15,8 @@
"esbuild": "~0.25",
"jsdom": "~26.0",
"jshint": "~2.13",
"nodejsscript": "^1.0.2",
"nodejsscript": "^1.0",
"publint": "^0.3",
"size-limit-node-esbuild": "~0.3"
},
"engines": {
@ -614,6 +615,19 @@
"node": ">= 8"
}
},
"node_modules/@publint/pack": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@publint/pack/-/pack-0.1.2.tgz",
"integrity": "sha512-S+9ANAvUmjutrshV4jZjaiG8XQyuJIZ8a4utWmN/vW1sgQ9IfBnPndwkmQYw53QmouOIytT874u65HEmu6H5jw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://bjornlu.com/sponsor"
}
},
"node_modules/@sindresorhus/merge-streams": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz",
@ -2229,6 +2243,16 @@
"node": ">=4"
}
},
"node_modules/package-manager-detector": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz",
"integrity": "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"quansync": "^0.2.7"
}
},
"node_modules/parse5": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
@ -2306,6 +2330,28 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/publint": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/publint/-/publint-0.3.9.tgz",
"integrity": "sha512-irTwfRfYW38vomkxxoiZQtFtUOQKpz5m0p9Z60z4xpXrl1KmvSrX1OMARvnnolB5usOXeNfvLj6d/W3rwXKfBQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@publint/pack": "^0.1.2",
"package-manager-detector": "^0.2.9",
"picocolors": "^1.1.1",
"sade": "^1.8.1"
},
"bin": {
"publint": "src/cli.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://bjornlu.com/sponsor"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -2316,6 +2362,23 @@
"node": ">=6"
}
},
"node_modules/quansync": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz",
"integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/antfu"
},
{
"type": "individual",
"url": "https://github.com/sponsors/sxzz"
}
],
"license": "MIT"
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",

View File

@ -1,13 +1,13 @@
{
"name": "deka-dom-el",
"version": "0.9.1-alpha",
"version": "0.9.5-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",
"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"
@ -17,20 +17,20 @@
"type": "module",
"exports": {
".": {
"import": "./index.js",
"types": "./index.d.ts"
"types": "./index.d.ts",
"import": "./index.js"
},
"./signals": {
"import": "./signals.js",
"types": "./signals.d.ts"
"types": "./signals.d.ts",
"import": "./signals.js"
},
"./jsdom": {
"import": "./jsdom.js",
"types": "./jsdom.d.ts"
"types": "./jsdom.d.ts",
"import": "./jsdom.js"
},
"./src/signals-lib": {
"import": "./src/signals-lib/signals-lib.js",
"types": "./src/signals-lib/signals-lib.d.ts"
"types": "./src/signals-lib/signals-lib.d.ts",
"import": "./src/signals-lib/signals-lib.js"
}
},
"files": [
@ -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",
@ -99,7 +103,8 @@
"esbuild": "~0.25",
"jsdom": "~26.0",
"jshint": "~2.13",
"nodejsscript": "^1.0.2",
"nodejsscript": "^1.0",
"publint": "^0.3",
"size-limit-node-esbuild": "~0.3"
}
}

View File

@ -38,11 +38,12 @@ import { scope } from "./scopes.js";
* @returns {Element|DocumentFragment} Created element
*/
export function createElement(tag, attributes, ...addons){
/* jshint maxcomplexity: 15 */
/* jshint maxcomplexity: 16 */
const s= signals(this);
let scoped= 0;
let el, el_host;
if(Object(attributes)!==attributes || s.isSignal(attributes))
const att_type= typeof attributes;
if(att_type==="string" || att_type==="number" || s.isSignal(attributes))
attributes= { textContent: attributes };
switch(true){
case typeof tag==="function": {
@ -50,9 +51,9 @@ export function createElement(tag, attributes, ...addons){
const host= (...c)=> !c.length ? el_host :
(scoped===1 ? addons.unshift(...c) : c.forEach(c=> c(el_host)), undefined);
scope.push({ scope: tag, host });
el= tag(attributes || undefined);
const is_fragment= isInstance(el, env.F);
el= /** @type {Element} */(tag(attributes || undefined));
if(el.nodeName==="#comment") break;
const is_fragment= isInstance(el, env.F);
const el_mark= createElement.mark({
type: "component",
name: tag.name,

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

@ -38,6 +38,8 @@ export function on(event, listener, options){
};
}
on.defer= fn=> setTimeout.bind(null, fn, 0);
import { c_ch_o } from "./events-observer.js";
/**

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]; },
@ -80,5 +80,3 @@ export const scope= {
return scopes.pop();
},
};
// TODO: better place while no cross-import?
on.host= (fn, host= scope.host)=> el=> host(()=> fn(el));

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

@ -9,7 +9,8 @@ export const mark= "__dde_signal";
* @type {Function}
*/
export const queueSignalWrite= (()=> {
let pendingSignals= new Set();
/** @type {Map<ddeSignal, boolean>} */
let pendingSignals= new Map();
let scheduled= false;
/**
@ -19,19 +20,20 @@ export const queueSignalWrite= (()=> {
function flushSignals() {
scheduled = false;
const todo= pendingSignals;
pendingSignals= new Set();
for(const signal of todo){
pendingSignals= new Map();
for(const [ signal, force ] of todo){
const M = signal[mark];
if(M) M.listeners.forEach(l => l(M.value));
if(M) M.listeners.forEach(l => l(M.value, force));
}
}
/**
* Queues a signal for update
* @param {Object} s - Signal to queue
* @param {ddeSignal} s - Signal to queue
* @param {boolean} force - Forced update
*/
return function(s){
pendingSignals.add(s);
return function(s, force= false){
pendingSignals.set(s, pendingSignals.get(s) || force);
if(scheduled) return;
scheduled = true;
queueMicrotask(flushSignals);

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
@ -57,12 +56,12 @@ export function signal(value, actions){
* Updates the derived signal when dependencies change
* @private
*/
function contextReWatch(){
function contextReWatch(_, force){
const [ origin, ...deps_old ]= deps.get(contextReWatch);
deps.set(contextReWatch, new Set([ origin ]));
stack_watch.push(contextReWatch);
write(out, value());
write(out, value(), force);
stack_watch.pop();
if(!deps_old.length) return;
@ -95,7 +94,7 @@ signal.action= function(s, name, ...a){
throw new Error(`Action "${name}" not defined. See ${mark}.actions.`);
actions[name].apply(M, a);
if(M.skip) return (delete M.skip);
queueSignalWrite(s);
queueSignalWrite(s, true);
};
/**
@ -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,
@ -434,7 +434,7 @@ function write(s, value, force){
if(!M || (!force && M.value===value)) return;
M.value= value;
queueSignalWrite(s);
queueSignalWrite(s, force);
return value;
}