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

17 Commits

Author SHA1 Message Date
6d550a50ef 🔤 ui p14 2025-03-15 12:08:57 +01:00
d54b6a65ef bs/build 2025-03-15 11:45:39 +01:00
64566f17af 🔤 converter - convert also comments 2025-03-14 21:58:40 +01:00
2fcec0551c 🔤 2025-03-14 20:43:30 +01:00
7ed2856298 🐛 signal.set(value, force) 2025-03-14 19:24:17 +01:00
a2b0223c4f 🔤 converter 2025-03-14 19:15:26 +01:00
19b0e4666e 🔤 cdn 2025-03-14 14:54:38 +01:00
3823b66003 🐛 on.* types 2025-03-14 13:54:02 +01:00
ba13055d7d on.host 2025-03-14 13:49:01 +01:00
36fab5276d reorganize files 2025-03-14 13:15:31 +01:00
93b905e677 🔤 ui 2025-03-13 22:11:02 +01:00
2fdd8ebea9 🔤 lib download 2025-03-13 17:48:29 +01:00
7cee9a4a14 cleanup 2025-03-13 17:48:23 +01:00
3c6cad5648 🐛 lint 2025-03-13 16:24:47 +01:00
9251e70015 🔤 2025-03-13 16:07:16 +01:00
8756dabc55 🔤 2025-03-13 15:20:48 +01:00
0a2d17ac6f 🔤 T now uses DocumentFragment 2025-03-13 12:58:38 +01:00
58 changed files with 365 additions and 2875 deletions

View File

@ -1,40 +0,0 @@
---
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 -->

View File

@ -1,22 +0,0 @@
---
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

@ -1,29 +0,0 @@
---
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 -->

View File

@ -1,39 +0,0 @@
<!--
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 -->

View File

@ -1,18 +0,0 @@
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
View File

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

View File

@ -1,134 +0,0 @@
# 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

View File

@ -1,174 +0,0 @@
# 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.
## Commit Guidelines
We use
[![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 -->
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.

View File

@ -1,7 +1,6 @@
**Alpha**
**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)
| [![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 -->
<p align="center">
<img src="docs/assets/logo.svg" alt="Deka DOM Elements Logo" width="180" height="180">
@ -40,7 +39,7 @@ function EmojiCounter({ initial }) {
on("click", () => count.set(count.get() + 1))
),
el("select", null, on.defer(el=> el.value= initial),
el("select", null, on.host(el=> el.value= initial),
on("change", e => emoji.set(e.target.value))
).append(
el(Option, "🎉"),
@ -61,30 +60,40 @@ Creating reactive elements, components, and Web Components using the native
## Features at a Glance
-**No build step required** — use directly in browsers or Node.js
- ☑️ **Lightweight** — ~10-15kB minified (original goal 10kB) with **zero**/minimal dependencies
- ☑️ **Lightweight** — ~10-15kB minified (original goal 10kB) with zero/minimal dependencies
-**Declarative & functional approach** for clean, maintainable code
-**Signals and events** for reactive UI
-**Memoization for performance** — optimize rendering with intelligent caching
-**Optional build-in signals** with support for custom reactive implementations (#39)
-**Optional build-in signals** with support for custom reactive implementations
-**Server-side rendering** support via [jsdom](https://github.com/jsdom/jsdom)
-**TypeScript support**
-**TypeScript support** (work in progress)
- ☑️ **Support for debugging with browser DevTools** without extensions
- ☑️ **Enhanced Web Components** support
- ☑️ **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.
## Getting Started
### Documentation
- [**Documentation and Guide**](https://jaandrle.github.io/deka-dom-el)
- [**Examples**](https://jaandrle.github.io/deka-dom-el/p15-examples.html)
### Installation
#### npm
```bash
npm install deka-dom-el --save
# TBD
# npm install deka-dom-el
```
…or via CDN / Direct Script:
#### 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.
@ -104,18 +113,10 @@ format selector](https://jaandrle.github.io/deka-dom-el/) on the documentation s
</script>
```
## Why Another Library?
### Documentation
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.
- [**Interactive Guide**](https://jaandrle.github.io/deka-dom-el): WIP
- [Examples](./examples/): TBD/WIP
## Understanding Signals
@ -126,24 +127,12 @@ 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
- [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.
- [jaandrle/dollar_dom_component](https://github.com/jaandrle/dollar_dom_component) —
- [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
- [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
- [mxjp/rvx: A signal based frontend framework](https://github.com/mxjp/rvx)

View File

@ -2,18 +2,15 @@
This project uses [jaandrle/bs: The simplest possible build system using executable/bash scripts](
https://github.com/jaandrle/bs).
#### bs/build.js [main|signals] [--no-types|--help]
#### bs/build.js [--minify|--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,5 +1,5 @@
#!/usr/bin/env -S npx nodejsscript
import { buildSync as esbuildSync } from "esbuild";
import { analyzeMetafileSync, buildSync as esbuildSync } from "esbuild";
const css= echo.css`
.info{ color: gray; }
`;
@ -93,7 +93,7 @@ function metaToLineStatus(meta, file){
const { bytes }= status;
const kbytes= bytes/1024;
const kbytesR= kbytes.toFixed(2);
return `${file}: ${kbytesR} kB`;
return `${file}: ${kbytesR} KiB`;
}
function echoVariant(name, todo= false){
if(todo) return echo.use("-R", "~ "+name);

View File

@ -173,22 +173,20 @@ 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] & {
target: EL;
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event]) => 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 after the next tick of the Javascript event loop.
* This is handy for example to apply some property depending on the element content:
* 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:
* ```js
* const selected= "Z";
* //...
* return el("form").append(
* el("select", null, on.defer(e=> e.value=selected)).append(
* el("select", null, on.host(e=> e.value=selected)).append(
* el("option", { value: "A", textContent: "A" }),
* //...
* el("option", { value: "Z", textContent: "Z" }),
@ -196,7 +194,7 @@ export interface On {
* );
* ```
* */
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
host<EL extends SupportedElement>(listener: (element: EL) => any, host?: Host<SupportedElement>): ddeElementAddon<EL>;
}
export const on: On;
export type Scope = {

View File

@ -41,6 +41,19 @@ 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("/"));
}
};
// src/dom-lib/common.js
var enviroment = {
@ -254,7 +267,6 @@ 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);
@ -290,7 +302,7 @@ var store_abort = /* @__PURE__ */ new WeakMap();
var scope = {
/**
* Gets the current scope
* @returns {typeof scopes[number]} Current scope context
* @returns {Object} Current scope context
*/
get current() {
return scopes[scopes.length - 1];
@ -353,6 +365,7 @@ var scope = {
return scopes.pop();
}
};
on.host = (fn, host = scope.host) => (el) => host(() => fn(el));
// src/signals-lib/common.js
var signals_global = {
@ -422,8 +435,7 @@ function createElement(tag, attributes, ...addons) {
const s = signals(this);
let scoped = 0;
let el, el_host;
const att_type = typeof attributes;
if (att_type === "string" || att_type === "number" || s.isSignal(attributes))
if (Object(attributes) !== attributes || s.isSignal(attributes))
attributes = { textContent: attributes };
switch (true) {
case typeof tag === "function": {
@ -650,9 +662,9 @@ function memo(key, generator) {
memo.isScope = function(obj) {
return obj[memoMark];
};
memo.scope = function memoScopeCreate(fun, { signal: signal2, onlyLast } = {}) {
memo.scope = function memoScope(fun, { signal: signal2, onlyLast } = {}) {
let cache = oCreate();
function memoScope(...args) {
function memoScope2(...args) {
if (signal2 && signal2.aborted)
return fun.apply(this, args);
let cache_local = onlyLast ? cache : oCreate();
@ -667,28 +679,28 @@ memo.scope = function memoScopeCreate(fun, { signal: signal2, onlyLast } = {}) {
cache = cache_local;
return out;
}
memoScope[memoMark] = true;
memoScope.clear = () => cache = oCreate();
if (signal2) signal2.addEventListener("abort", memoScope.clear);
return memoScope;
memoScope2[memoMark] = true;
memoScope2.clear = () => cache = oCreate();
if (signal2) signal2.addEventListener("abort", memoScope2.clear);
return memoScope2;
};
// src/signals-lib/helpers.js
var mark = "__dde_signal";
var queueSignalWrite = /* @__PURE__ */ (() => {
let pendingSignals = /* @__PURE__ */ new Map();
let pendingSignals = /* @__PURE__ */ new Set();
let scheduled = false;
function flushSignals() {
scheduled = false;
const todo = pendingSignals;
pendingSignals = /* @__PURE__ */ new Map();
for (const [signal2, force] of todo) {
pendingSignals = /* @__PURE__ */ new Set();
for (const signal2 of todo) {
const M = signal2[mark];
if (M) M.listeners.forEach((l) => l(M.value, force));
if (M) M.listeners.forEach((l) => l(M.value));
}
}
return function(s, force = false) {
pendingSignals.set(s, pendingSignals.get(s) || force);
return function(s) {
pendingSignals.add(s);
if (scheduled) return;
scheduled = true;
queueMicrotask(flushSignals);
@ -725,11 +737,11 @@ function signal(value, actions) {
return create(false, value, actions);
if (isSignal(value)) return value;
const out = create(true);
function contextReWatch(_, force) {
function contextReWatch() {
const [origin, ...deps_old] = deps.get(contextReWatch);
deps.set(contextReWatch, /* @__PURE__ */ new Set([origin]));
stack_watch.push(contextReWatch);
write(out, value(), force);
write(out, value());
stack_watch.pop();
if (!deps_old.length) return;
const deps_curr = deps.get(contextReWatch);
@ -751,7 +763,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, true);
queueSignalWrite(s);
};
signal.on = function on2(s, listener, options = {}) {
const { signal: as } = options;
@ -787,17 +799,17 @@ signal.clear = function(...signals2) {
};
var key_reactive = "__dde_reactive";
signal.el = function(s, map) {
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);
map = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const mark_start = createElement.mark({ type: "reactive", source: new Defined().compact }, 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 = mapScoped(v);
let els = map(v);
scope.pop();
if (!Array.isArray(els))
els = [els];
@ -817,7 +829,7 @@ signal.el = function(s, map) {
current.host(on.disconnected(
() => (
/*! Clears cached elements for reactive element `S.el` */
mapScoped.clear()
map.clear()
)
));
return out;
@ -889,8 +901,9 @@ var signals_config = {
function removeSignalsFromElements(s, listener, ...notes) {
const { current } = scope;
current.host(function(element) {
if (!element[key_reactive]) element[key_reactive] = [];
element[key_reactive].push([[s, listener], ...notes]);
if (element[key_reactive])
return element[key_reactive].push([[s, listener], ...notes]);
element[key_reactive] = [];
if (current.prevent) return;
on.disconnected(
() => (
@ -935,6 +948,7 @@ function toSignal(s, value, actions, readonly = false) {
onclear,
host,
listeners: /* @__PURE__ */ new Set(),
defined: new Defined().stack,
readonly
}),
enumerable: false,
@ -958,7 +972,7 @@ function write(s, value, force) {
const M = s[mark];
if (!M || !force && M.value === value) return;
M.value = value;
queueSignalWrite(s, force);
queueSignalWrite(s);
return value;
}
function addSignalListener(s, listener) {

View File

@ -173,22 +173,20 @@ 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] & {
target: EL;
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event]) => 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 after the next tick of the Javascript event loop.
* This is handy for example to apply some property depending on the element content:
* 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:
* ```js
* const selected= "Z";
* //...
* return el("form").append(
* el("select", null, on.defer(e=> e.value=selected)).append(
* el("select", null, on.host(e=> e.value=selected)).append(
* el("option", { value: "A", textContent: "A" }),
* //...
* el("option", { value: "Z", textContent: "Z" }),
@ -196,7 +194,7 @@ export interface On {
* );
* ```
* */
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
host<EL extends SupportedElement>(listener: (element: EL) => any, host?: Host<SupportedElement>): 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,22 +172,20 @@ 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] & {
target: EL;
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event]) => 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 after the next tick of the Javascript event loop.
* This is handy for example to apply some property depending on the element content:
* 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:
* ```js
* const selected= "Z";
* //...
* return el("form").append(
* el("select", null, on.defer(e=> e.value=selected)).append(
* el("select", null, on.host(e=> e.value=selected)).append(
* el("option", { value: "A", textContent: "A" }),
* //...
* el("option", { value: "Z", textContent: "Z" }),
@ -195,7 +193,7 @@ export interface On {
* );
* ```
* */
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
host<EL extends SupportedElement>(listener: (element: EL) => any, host?: Host<SupportedElement>): ddeElementAddon<EL>;
}
export const on: On;
export type Scope = {

19
dist/esm.js vendored
View File

@ -238,7 +238,6 @@ 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);
@ -274,7 +273,7 @@ var store_abort = /* @__PURE__ */ new WeakMap();
var scope = {
/**
* Gets the current scope
* @returns {typeof scopes[number]} Current scope context
* @returns {Object} Current scope context
*/
get current() {
return scopes[scopes.length - 1];
@ -337,6 +336,7 @@ var scope = {
return scopes.pop();
}
};
on.host = (fn, host = scope.host) => (el) => host(() => fn(el));
// src/signals-lib/common.js
var signals_global = {
@ -406,8 +406,7 @@ function createElement(tag, attributes, ...addons) {
const s = signals(this);
let scoped = 0;
let el, el_host;
const att_type = typeof attributes;
if (att_type === "string" || att_type === "number" || s.isSignal(attributes))
if (Object(attributes) !== attributes || s.isSignal(attributes))
attributes = { textContent: attributes };
switch (true) {
case typeof tag === "function": {
@ -634,9 +633,9 @@ function memo(key, generator) {
memo.isScope = function(obj) {
return obj[memoMark];
};
memo.scope = function memoScopeCreate(fun, { signal, onlyLast } = {}) {
memo.scope = function memoScope(fun, { signal, onlyLast } = {}) {
let cache = oCreate();
function memoScope(...args) {
function memoScope2(...args) {
if (signal && signal.aborted)
return fun.apply(this, args);
let cache_local = onlyLast ? cache : oCreate();
@ -651,10 +650,10 @@ memo.scope = function memoScopeCreate(fun, { signal, onlyLast } = {}) {
cache = cache_local;
return out;
}
memoScope[memoMark] = true;
memoScope.clear = () => cache = oCreate();
if (signal) signal.addEventListener("abort", memoScope.clear);
return memoScope;
memoScope2[memoMark] = true;
memoScope2.clear = () => cache = oCreate();
if (signal) signal.addEventListener("abort", memoScope2.clear);
return memoScope2;
};
export {
assign,

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

@ -172,22 +172,20 @@ 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] & {
target: EL;
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event]) => 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 after the next tick of the Javascript event loop.
* This is handy for example to apply some property depending on the element content:
* 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:
* ```js
* const selected= "Z";
* //...
* return el("form").append(
* el("select", null, on.defer(e=> e.value=selected)).append(
* el("select", null, on.host(e=> e.value=selected)).append(
* el("option", { value: "A", textContent: "A" }),
* //...
* el("option", { value: "Z", textContent: "Z" }),
@ -195,7 +193,7 @@ export interface On {
* );
* ```
* */
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
host<EL extends SupportedElement>(listener: (element: EL) => any, host?: Host<SupportedElement>): 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,22 +173,20 @@ 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] & {
target: EL;
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event]) => 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 after the next tick of the Javascript event loop.
* This is handy for example to apply some property depending on the element content:
* 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:
* ```js
* const selected= "Z";
* //...
* return el("form").append(
* el("select", null, on.defer(e=> e.value=selected)).append(
* el("select", null, on.host(e=> e.value=selected)).append(
* el("option", { value: "A", textContent: "A" }),
* //...
* el("option", { value: "Z", textContent: "Z" }),
@ -196,7 +194,7 @@ export interface On {
* );
* ```
* */
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
host<EL extends SupportedElement>(listener: (element: EL) => any, host?: Host<SupportedElement>): ddeElementAddon<EL>;
}
export const on: On;
export type Scope = {

View File

@ -86,6 +86,19 @@ 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("/"));
}
};
// src/dom-lib/common.js
var enviroment = {
@ -299,7 +312,6 @@ 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);
@ -335,7 +347,7 @@ var DDE = (() => {
var scope = {
/**
* Gets the current scope
* @returns {typeof scopes[number]} Current scope context
* @returns {Object} Current scope context
*/
get current() {
return scopes[scopes.length - 1];
@ -398,6 +410,7 @@ var DDE = (() => {
return scopes.pop();
}
};
on.host = (fn, host = scope.host) => (el) => host(() => fn(el));
// src/signals-lib/common.js
var signals_global = {
@ -467,8 +480,7 @@ var DDE = (() => {
const s = signals(this);
let scoped = 0;
let el, el_host;
const att_type = typeof attributes;
if (att_type === "string" || att_type === "number" || s.isSignal(attributes))
if (Object(attributes) !== attributes || s.isSignal(attributes))
attributes = { textContent: attributes };
switch (true) {
case typeof tag === "function": {
@ -695,9 +707,9 @@ var DDE = (() => {
memo.isScope = function(obj) {
return obj[memoMark];
};
memo.scope = function memoScopeCreate(fun, { signal: signal2, onlyLast } = {}) {
memo.scope = function memoScope(fun, { signal: signal2, onlyLast } = {}) {
let cache = oCreate();
function memoScope(...args) {
function memoScope2(...args) {
if (signal2 && signal2.aborted)
return fun.apply(this, args);
let cache_local = onlyLast ? cache : oCreate();
@ -712,28 +724,28 @@ var DDE = (() => {
cache = cache_local;
return out;
}
memoScope[memoMark] = true;
memoScope.clear = () => cache = oCreate();
if (signal2) signal2.addEventListener("abort", memoScope.clear);
return memoScope;
memoScope2[memoMark] = true;
memoScope2.clear = () => cache = oCreate();
if (signal2) signal2.addEventListener("abort", memoScope2.clear);
return memoScope2;
};
// src/signals-lib/helpers.js
var mark = "__dde_signal";
var queueSignalWrite = /* @__PURE__ */ (() => {
let pendingSignals = /* @__PURE__ */ new Map();
let pendingSignals = /* @__PURE__ */ new Set();
let scheduled = false;
function flushSignals() {
scheduled = false;
const todo = pendingSignals;
pendingSignals = /* @__PURE__ */ new Map();
for (const [signal2, force] of todo) {
pendingSignals = /* @__PURE__ */ new Set();
for (const signal2 of todo) {
const M = signal2[mark];
if (M) M.listeners.forEach((l) => l(M.value, force));
if (M) M.listeners.forEach((l) => l(M.value));
}
}
return function(s, force = false) {
pendingSignals.set(s, pendingSignals.get(s) || force);
return function(s) {
pendingSignals.add(s);
if (scheduled) return;
scheduled = true;
queueMicrotask(flushSignals);
@ -770,11 +782,11 @@ var DDE = (() => {
return create(false, value, actions);
if (isSignal(value)) return value;
const out = create(true);
function contextReWatch(_, force) {
function contextReWatch() {
const [origin, ...deps_old] = deps.get(contextReWatch);
deps.set(contextReWatch, /* @__PURE__ */ new Set([origin]));
stack_watch.push(contextReWatch);
write(out, value(), force);
write(out, value());
stack_watch.pop();
if (!deps_old.length) return;
const deps_curr = deps.get(contextReWatch);
@ -796,7 +808,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, true);
queueSignalWrite(s);
};
signal.on = function on2(s, listener, options = {}) {
const { signal: as } = options;
@ -832,17 +844,17 @@ var DDE = (() => {
};
var key_reactive = "__dde_reactive";
signal.el = function(s, map) {
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);
map = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const mark_start = createElement.mark({ type: "reactive", source: new Defined().compact }, 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 = mapScoped(v);
let els = map(v);
scope.pop();
if (!Array.isArray(els))
els = [els];
@ -862,7 +874,7 @@ var DDE = (() => {
current.host(on.disconnected(
() => (
/*! Clears cached elements for reactive element `S.el` */
mapScoped.clear()
map.clear()
)
));
return out;
@ -934,8 +946,9 @@ var DDE = (() => {
function removeSignalsFromElements(s, listener, ...notes) {
const { current } = scope;
current.host(function(element) {
if (!element[key_reactive]) element[key_reactive] = [];
element[key_reactive].push([[s, listener], ...notes]);
if (element[key_reactive])
return element[key_reactive].push([[s, listener], ...notes]);
element[key_reactive] = [];
if (current.prevent) return;
on.disconnected(
() => (
@ -980,6 +993,7 @@ var DDE = (() => {
onclear,
host,
listeners: /* @__PURE__ */ new Set(),
defined: new Defined().stack,
readonly
}),
enumerable: false,
@ -1003,7 +1017,7 @@ var DDE = (() => {
const M = s[mark];
if (!M || !force && M.value === value) return;
M.value = value;
queueSignalWrite(s, force);
queueSignalWrite(s);
return value;
}
function addSignalListener(s, listener) {

View File

@ -173,22 +173,20 @@ 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] & {
target: EL;
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event]) => 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 after the next tick of the Javascript event loop.
* This is handy for example to apply some property depending on the element content:
* 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:
* ```js
* const selected= "Z";
* //...
* return el("form").append(
* el("select", null, on.defer(e=> e.value=selected)).append(
* el("select", null, on.host(e=> e.value=selected)).append(
* el("option", { value: "A", textContent: "A" }),
* //...
* el("option", { value: "Z", textContent: "Z" }),
@ -196,7 +194,7 @@ export interface On {
* );
* ```
* */
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
host<EL extends SupportedElement>(listener: (element: EL) => any, host?: Host<SupportedElement>): 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,22 +172,20 @@ 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] & {
target: EL;
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event]) => 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 after the next tick of the Javascript event loop.
* This is handy for example to apply some property depending on the element content:
* 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:
* ```js
* const selected= "Z";
* //...
* return el("form").append(
* el("select", null, on.defer(e=> e.value=selected)).append(
* el("select", null, on.host(e=> e.value=selected)).append(
* el("option", { value: "A", textContent: "A" }),
* //...
* el("option", { value: "Z", textContent: "Z" }),
@ -195,7 +193,7 @@ export interface On {
* );
* ```
* */
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
host<EL extends SupportedElement>(listener: (element: EL) => any, host?: Host<SupportedElement>): ddeElementAddon<EL>;
}
export const on: On;
export type Scope = {

19
dist/iife.js vendored
View File

@ -280,7 +280,6 @@ 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);
@ -316,7 +315,7 @@ var DDE = (() => {
var scope = {
/**
* Gets the current scope
* @returns {typeof scopes[number]} Current scope context
* @returns {Object} Current scope context
*/
get current() {
return scopes[scopes.length - 1];
@ -379,6 +378,7 @@ var DDE = (() => {
return scopes.pop();
}
};
on.host = (fn, host = scope.host) => (el) => host(() => fn(el));
// src/signals-lib/common.js
var signals_global = {
@ -448,8 +448,7 @@ var DDE = (() => {
const s = signals(this);
let scoped = 0;
let el, el_host;
const att_type = typeof attributes;
if (att_type === "string" || att_type === "number" || s.isSignal(attributes))
if (Object(attributes) !== attributes || s.isSignal(attributes))
attributes = { textContent: attributes };
switch (true) {
case typeof tag === "function": {
@ -676,9 +675,9 @@ var DDE = (() => {
memo.isScope = function(obj) {
return obj[memoMark];
};
memo.scope = function memoScopeCreate(fun, { signal, onlyLast } = {}) {
memo.scope = function memoScope(fun, { signal, onlyLast } = {}) {
let cache = oCreate();
function memoScope(...args) {
function memoScope2(...args) {
if (signal && signal.aborted)
return fun.apply(this, args);
let cache_local = onlyLast ? cache : oCreate();
@ -693,10 +692,10 @@ var DDE = (() => {
cache = cache_local;
return out;
}
memoScope[memoMark] = true;
memoScope.clear = () => cache = oCreate();
if (signal) signal.addEventListener("abort", memoScope.clear);
return memoScope;
memoScope2[memoMark] = true;
memoScope2.clear = () => cache = oCreate();
if (signal) signal.addEventListener("abort", memoScope2.clear);
return memoScope2;
};
return __toCommonJS(index_exports);
})();

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

@ -172,22 +172,20 @@ 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] & {
target: EL;
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event]) => 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 after the next tick of the Javascript event loop.
* This is handy for example to apply some property depending on the element content:
* 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:
* ```js
* const selected= "Z";
* //...
* return el("form").append(
* el("select", null, on.defer(e=> e.value=selected)).append(
* el("select", null, on.host(e=> e.value=selected)).append(
* el("option", { value: "A", textContent: "A" }),
* //...
* el("option", { value: "Z", textContent: "Z" }),
@ -195,7 +193,7 @@ export interface On {
* );
* ```
* */
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
host<EL extends SupportedElement>(listener: (element: EL) => any, host?: Host<SupportedElement>): 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,374 +0,0 @@
/**
* 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);
}
.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

@ -1,412 +0,0 @@
/**
* 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':
document.querySelector('.lightbox-prev-btn').click();
break;
case 'ArrowRight':
document.querySelector('.lightbox-next-btn').click();
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()
}).append(
el("img", {
src: image.src,
alt: image.alt,
loading: "lazy"
}, onImageClick(image.id)),
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",
"aria-label": "Close lightbox"
}, on("click", closeLightbox)).append("×"),
el("button", {
className: "lightbox-prev-btn",
"aria-label": "Previous image"
}, on("click", onPrevImage)).append(""),
el("button", {
className: "lightbox-next-btn",
"aria-label": "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

@ -1,339 +0,0 @@
/**
* 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

@ -1,715 +0,0 @@
/**
* 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(300px, 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" component="<component-name>">-->
<!--<dde:mark type="reactive" source="...">-->
<!-- content that updates when signal changes -->
<!--</dde:mark>-->

View File

@ -5,10 +5,10 @@ function allLifecycleEvents(){
on.connected(log),
on.disconnected(log),
).append(
el("select", { id: "country" }, on.defer(select => {
// This runs when the select is ready with all its options
el("select", { id: "country" }, on.host(select => {
// This runs when the host (select) is ready with all its options
select.value = "cz"; // Pre-select Czechia
log({ type: "dde:on.defer", detail: select });
log({ type: "dde:on.host", 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,20 +13,18 @@ import { S } from "deka-dom-el/signals";
* @returns {HTMLElement} The root TodoMVC application element
*/
function Todos(){
const { signal } = scope;
const pageS = routerSignal(S, signal);
const pageS = routerSignal(S);
const todosS = todosSignal();
/** Derived signal that filters todos based on current route */
const todosFilteredS = S(()=> {
const filteredTodosS = 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 => {
@ -75,7 +73,7 @@ function Todos(){
}, onToggleAll),
el("label", { htmlFor: "toggle-all", title: "Mark all as complete" }),
el("ul", { className: "todo-list" }).append(
S.el(todosFilteredS, filteredTodos => filteredTodos.map(todo =>
S.el(filteredTodosS, filteredTodos => filteredTodos.map(todo =>
memo(todo.id, ()=> el(TodoItem, todo, onDelete, onEdit)))
)
)
@ -85,7 +83,7 @@ function Todos(){
? el()
: el("footer", { className: "footer" }).append(
el("span", { className: "todo-count" }).append(
noOfLeft()
noOfLeft(todos)
),
memo("filters", ()=>
el("ul", { className: "filters" }).append(
@ -100,18 +98,15 @@ function Todos(){
)
),
),
todos.length - todosRemainingS.get() === 0
!todos.some(todo => todo.completed)
? el()
: memo("delete", () =>
el("button",
{ textContent: "Clear completed", className: "clear-completed" },
onClearCompleted)
)
: el("button", { textContent: "Clear completed", className: "clear-completed" }, onClearCompleted)
)
)
);
function noOfLeft(){
const length = todosRemainingS.get();
/** @param {Todo[]} todos */
function noOfLeft(todos){
const { length }= todos.filter(todo => !todo.completed);
return el("strong").append(
length + " ",
length === 1 ? "item left" : "items left"
@ -199,7 +194,7 @@ function TodoItem({ id, title, completed }) {
checked: completed
}, onToggleCompleted),
el("label", { textContent: title }, onStartEdit),
el("button", { ariaLabel: "Delete todo", className: "destroy" }, onDelete)
el("button", { className: "destroy" }, onDelete)
),
S.el(isEditing, editing => !editing
? el()
@ -333,7 +328,6 @@ 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;
@ -342,10 +336,9 @@ function todosSignal(){
/**
* Creates a signal for managing route state
*
* @param {typeof S} signal - The signal constructor from a library
* @param {AbortSignal} abortSignal
* @param {typeof S} signal - The signal constructor
*/
function routerSignal(signal, abortSignal){
function routerSignal(signal){
const initial = location.hash.replace("#", "") || "all";
const out = signal(initial, {
/**
@ -354,16 +347,15 @@ function routerSignal(signal, abortSignal){
*/
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));
out.set(hash);
}, { signal: abortSignal });
S.action(out, "set", /** @type {"all"|"active"|"completed"} */(hash));
});
return out;
}

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.defer(select => select.value = lib.get()[0]),
on.host(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.defer(select => select.value = lib.get()[1]),
on.host(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.defer(select => select.value = lib.get()[2]),
on.host(select => select.value = lib.get()[2]),
).append(
el("option", { value: "", textContent: "Unminified" }),
el("option", { value: ".min", textContent: "Minified" }),

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 (accepts string, number or signal)",
" — simple element containing only text",
),
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

@ -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 handled automatically)",
"attributes and elements are cleared automatically)",
),
);
}

View File

@ -82,7 +82,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.
`),
@ -105,7 +105,7 @@ export function page({ pkg, info }){
or directly include it from a CDN for quick prototyping.
`),
el("h4", "npm installation"),
el(code, { content: "npm install deka-dom-el --save", language: "shell", page_id }),
el(code, { content: "npm install deka-dom-el # Coming soon", language: "shell", page_id }),
el("h4", "CDN / Direct Script Usage"),
el("p").append(T`
Use the interactive selector below to choose your preferred format:
@ -154,10 +154,6 @@ 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

@ -208,6 +208,6 @@ export function page({ pkg, info }){
`),
),
el(mnemonic),
el(mnemonic)
);
}

View File

@ -136,7 +136,7 @@ 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`), // TODO: add example?
el("li", t`Capture element references`)
)
),
el("p").append(T`
@ -155,7 +155,7 @@ export function page({ pkg, info }){
You can think of an Addon as an “oncreate” event handler.
`),
el("p").append(T`
dd<el> provides two additional lifecycle events that correspond to ${el("a", { textContent:
dd<el> provides three 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,6 +165,10 @@ 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 }),
@ -199,19 +203,6 @@ 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

View File

@ -277,72 +277,7 @@ 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, { page_id, content: `
const onFormSubmit = on("submit", e => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
// this can be sent to a server
// or processed locally
// e.g.: console.log(Object.fromEntries(formData))
});
// …
return el("form", null, onFormSubmit).append(
// …
);
` })
),
el("div", { className: "tab", dataTab: "variables" }).append(
el("h4", t`We can use variables without signals`),
el("p", t`We use this when we dontt need to reflect changes in the elsewhere (UI).`),
el(code, { page_id, content: `
let canSubmit = false;
const onFormSubmit = on("submit", e => {
e.preventDefault();
if(!canSubmit) return; // some message
// …
});
const onAllowSubmit = on("click", e => {
canSubmit = true;
});
`}),
),
el("div", { className: "tab", dataTab: "state" }).append(
el("h4", t`Using signals`),
el("p", t`We use this when we need to reflect changes for example in the UI (e.g. enable/disable
buttons).`),
el(code, { page_id, content: `
const canSubmit = S(false);
const onFormSubmit = on("submit", e => {
e.preventDefault();
// …
});
const onAllowSubmit = on("click", e => {
canSubmit.set(true);
});
return el("form", null, onFormSubmit).append(
// ...
el("button", { textContent: "Allow Submit", type: "button" }, onAllowSubmit),
el("button", { disabled: S(()=> !canSubmit), textContent: "Submit" })
);
`}),
),
`)
),
el("div", { className: "troubleshooting" }).append(
@ -363,6 +298,6 @@ export function page({ pkg, info }){
)
),
el(mnemonic),
el(mnemonic)
);
}

View File

@ -55,7 +55,7 @@ export function page({ pkg, info }){
el(MyComponent);
function MyComponent() {
// 2. access the host element (or other scope related values)
// 2. access the host element
const { host } = scope;
// 3. Add behavior to host

View File

@ -80,7 +80,8 @@ export function page({ pkg, info }){
el("li", t`listeners: A Set of functions called when the signal value changes`),
el("li", t`actions: Custom actions that can be performed on the signal`),
el("li", t`onclear: Functions to run when the signal is cleared`),
el("li", t`host: Reference to the host element/scope in which the signal was created`),
el("li", t`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("p").append(T`
@ -113,13 +114,7 @@ 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").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("li", t`That the signal is actually connected to the DOM element (check your S.el or attribute binding code)`)
),
el(code, { src: fileURL("./components/examples/debugging/mutations.js"), page_id }),

View File

@ -101,23 +101,21 @@ export function page({ pkg, info }){
`),
el(code, { content: `
// Signal for current route (all/active/completed)
const { signal } = scope;
const pageS = routerSignal(S, signal);
const pageS = routerSignal(S);
// Signal for the todos collection with custom actions
const todosS = todosSignal();
// Derived signal that filters todos based on current route
const todosFilteredS = S(()=> {
const filteredTodosS = 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);
`, page_id }),
el("p").append(T`
@ -202,7 +200,6 @@ 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;
@ -225,19 +222,19 @@ 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 todosFilteredS = S(()=> {
const filteredTodosS = 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(todosFilteredS, filteredTodos => filteredTodos.map(todo =>
S.el(filteredTodosS, filteredTodos => filteredTodos.map(todo =>
memo(todo.id, ()=> el(TodoItem, todo, onDelete, onEdit)))
)
)
@ -337,7 +334,7 @@ export function page({ pkg, info }){
el("h4", t`Memoizing Todo Items`),
el(code, { content: `
el("ul", { className: "todo-list" }).append(
S.el(todosFilteredS, filteredTodos => filteredTodos.map(todo =>
S.el(filteredTodosS, filteredTodos => filteredTodos.map(todo =>
memo(todo.id, ()=> el(TodoItem, todo, onDelete, onEdit)))
)
)
@ -546,13 +543,11 @@ export function page({ pkg, info }){
el("h4", t`Conditional Clear Completed Button`),
el(code, { content: `
todos.length - todosRemainingS.get() === 0
? el()
: memo("delete", () =>
el("button",
{ textContent: "Clear completed", className: "clear-completed" },
onClearCompleted)
)
S.el(S(() => todosS.get().some(todo => todo.completed)),
hasTodosCompleted=> hasTodosCompleted
? el("button", { textContent: "Clear completed", className: "clear-completed" }, onClearCompleted)
: el()
)
`, page_id }),
el("div", { className: "note" }).append(
@ -630,7 +625,7 @@ export function page({ pkg, info }){
${el("strong", "Declarative Class Management:")} Using the classList property for cleaner class handling
`),
el("li").append(T`
${el("strong", "Focus Management:")} Reliable input focus with setTimeout
${el("strong", "Focus Management:")} Reliable input focus with requestAnimationFrame
`),
el("li").append(T`
${el("strong", "Persistent Storage:")} Automatically saving application state with signal listeners

View File

@ -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,6 @@ 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",
}
};
@ -43,8 +33,8 @@ 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.
`),
@ -71,18 +61,14 @@ export function page({ pkg, info }){
${el("strong", "Progressive Enhancement:")} Starting simple and adding complexity only when needed
`),
el("li").append(T`
${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("strong", "Functional Composition:")} Building UIs through function composition rather than
inheritance
`),
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 only when
needed
${el("strong", "Targeted Reactivity:")} Using signals for efficient, fine-grained updates
`),
el("li").append(T`
${el("strong", "Unix Philosophy:")} Doing one thing well and allowing composability with other tools
@ -91,15 +77,11 @@ 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`),
@ -135,18 +117,16 @@ 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: `
@ -177,7 +157,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`),
@ -191,22 +171,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 signals value and trigger reactive updates`)
el("dd", t`Update a signal's value and trigger reactive updates`)
)
),
@ -220,7 +200,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`)
@ -232,45 +212,19 @@ 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("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
${el("strong", "Event delegation:")}: Prefer component-level event handlers over many individual handlers
`)
)
),
@ -279,19 +233,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`
@ -309,7 +263,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`),
@ -325,46 +279,46 @@ export function page({ pkg, info }){
el("tr").append(
el("th", "Feature"),
el("th", "dd<el>"),
el("th", "VanJS"),
el("th", "Solid"),
el("th", "Alpine")
el("th", "React"),
el("th", "Vue"),
el("th", "Svelte")
)
),
el("tbody").append(
el("tr").append(
el("td", "No Build Step Required"),
el("td", "✅"),
el("td", "✅"),
el("td", "⚠️ JSX needs transpilation"),
el("td", "")
el("td", "⚠️ SFC needs compilation"),
el("td", "❌ Requires compilation")
),
el("tr").append(
el("td", "Bundle Size (minified)"),
el("td", "~14kb"),
el("td", "~3kb"),
el("td", "~20kb"),
el("td", "~43kb")
el("td", "Bundle Size (minimal)"),
el("td", "~10-15kb"),
el("td", "~40kb+"),
el("td", "~33kb+"),
el("td", "Minimal runtime")
),
el("tr").append(
el("td", "Reactivity Model"),
el("td", "Signal-based"),
el("td", "Signal-based (basics only)"),
el("td", "Signal-based"),
el("td", "MVVM + Proxy")
el("td", "Virtual DOM diffing"),
el("td", "Proxy-based"),
el("td", "Compile-time reactivity")
),
el("tr").append(
el("td", "DOM Interface"),
el("td", "Direct DOM API"),
el("td", "Direct DOM API"),
el("td", "Compiled DOM updates"),
el("td", "Directive-based")
el("td", "Virtual DOM"),
el("td", "Virtual DOM"),
el("td", "Compiled DOM updates")
),
el("tr").append(
el("td", "Server-Side Rendering"),
el("td", "✅ Basic Support"),
el("td", "✅ Basic Support"),
el("td", "✅ Advanced"),
el("td", "")
el("td", "✅ Advanced"),
el("td", "✅ Advanced")
)
)
),
@ -393,7 +347,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`
@ -413,33 +367,16 @@ 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 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
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.
`)
),
);

View File

@ -1,63 +0,0 @@
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 }){
const page_id= info.id;
return el(simplePage, { info, pkg }).append(
el("p").append(T`
Real-world application examples showcasing how to build complete, production-ready interfaces with dd<el>:
`),
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", page_id }),
el(h3, t`Interactive Form`),
el("p").append(T`
Complete form with real-time validation, conditional rendering, and responsive design. Form handling with
real-time validation, reactive UI updates, complex form state management, and clean separation of concerns.
`),
el(example, { src: fileURL("./components/examples/case-studies/interactive-form.js"), variant: "big", page_id }),
el(h3, t`Interactive Image Gallery`),
el("p").append(T`
Responsive image gallery with lightbox, keyboard navigation, and filtering. Dynamic loading of content,
lightbox functionality, animation handling, and keyboard and gesture navigation support.
`),
el(example, { src: fileURL("./components/examples/case-studies/image-gallery.js"), variant: "big", page_id }),
el(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", page_id }),
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`)}.
`),
);
}

11
index.d.ts vendored
View File

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

4
package-lock.json generated
View File

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

View File

@ -1,6 +1,6 @@
{
"name": "deka-dom-el",
"version": "0.9.3-alpha",
"version": "0.9.1-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",
@ -85,12 +85,7 @@
"modifyEsbuildConfig": {
"platform": "browser"
},
"scripts": {
"test": "echo \"Error: no tests yet\"",
"build": "bs/build.js",
"lint": "bs/lint.sh",
"docs": "bs/docs.js"
},
"scripts": {},
"keywords": [
"dom",
"javascript",

View File

@ -38,12 +38,11 @@ import { scope } from "./scopes.js";
* @returns {Element|DocumentFragment} Created element
*/
export function createElement(tag, attributes, ...addons){
/* jshint maxcomplexity: 16 */
/* jshint maxcomplexity: 15 */
const s= signals(this);
let scoped= 0;
let el, el_host;
const att_type= typeof attributes;
if(att_type==="string" || att_type==="number" || s.isSignal(attributes))
if(Object(attributes)!==attributes || s.isSignal(attributes))
attributes= { textContent: attributes };
switch(true){
case typeof tag==="function": {

View File

@ -38,8 +38,6 @@ 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 {typeof scopes[number]} Current scope context
* @returns {Object} Current scope context
*/
get current(){ return scopes[scopes.length-1]; },
@ -80,3 +80,5 @@ 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

@ -68,3 +68,21 @@ export function observedAttributes(instance, observedAttribute){
* @returns {string} The camelCase string
*/
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
*/
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("/"));
}
}

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 memoScopeCreate(fun, { signal, onlyLast }= {}){
memo.scope= function memoScope(fun, { signal, onlyLast }= {}){
let cache= oCreate();
function memoScope(...args){
if(signal && signal.aborted)

View File

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

View File

@ -1,6 +1,6 @@
import { queueSignalWrite, mark } from "./helpers.js";
export { mark };
import { hasOwn, oCreate, oAssign } from "../helpers.js";
import { hasOwn, Defined, oCreate, oAssign } from "../helpers.js";
const Signal = oCreate(null, {
get: { value(){ return read(this); } },
@ -57,12 +57,12 @@ export function signal(value, actions){
* Updates the derived signal when dependencies change
* @private
*/
function contextReWatch(_, force){
function contextReWatch(){
const [ origin, ...deps_old ]= deps.get(contextReWatch);
deps.set(contextReWatch, new Set([ origin ]));
stack_watch.push(contextReWatch);
write(out, value(), force);
write(out, value());
stack_watch.pop();
if(!deps_old.length) return;
@ -95,7 +95,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, true);
queueSignalWrite(s);
};
/**
@ -169,21 +169,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} mapScoped - Function mapping signal value to DOM elements
* @param {Function} map - Function mapping signal value to DOM elements
* @returns {DocumentFragment} Fragment containing reactive elements
*/
signal.el= function(s, map){
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);
map= memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const mark_start= el.mark({ type: "reactive", source: new Defined().compact }, 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= mapScoped(v);
let els= map(v);
scope.pop();
if(!Array.isArray(els))
els= [ els ];
@ -202,7 +202,7 @@ signal.el= function(s, map){
reRenderReactiveElement(s.get());
current.host(on.disconnected(()=>
/*! Clears cached elements for reactive element `S.el` */
mapScoped.clear()
map.clear()
));
return out;
};
@ -314,8 +314,9 @@ export const signals_config= {
function removeSignalsFromElements(s, listener, ...notes){
const { current }= scope;
current.host(function(element){
if(!element[key_reactive]) element[key_reactive]= [];
element[key_reactive].push([ [ s, listener ], ...notes ]);
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
on.disconnected(()=>
/*! Clears all Signals listeners added in the current scope/host (`S.el`, `assign`, …?).
@ -385,6 +386,7 @@ 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,
@ -432,7 +434,7 @@ function write(s, value, force){
if(!M || (!force && M.value===value)) return;
M.value= value;
queueSignalWrite(s, force);
queueSignalWrite(s);
return value;
}