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

5 Commits

Author SHA1 Message Date
330f702409 🔤 examples+best paractises 2025-03-17 12:53:50 +01:00
2f57f115a0 v0.9.3 2025-03-17 11:11:48 +01:00
d1b017c1a1 🐛 __dde_reactive 2025-03-17 10:39:00 +01:00
992370b4e3 Replaces defined with name/host 2025-03-17 10:19:53 +01:00
3301a6600c 🔤 2025-03-16 19:40:05 +01:00
34 changed files with 521 additions and 278 deletions

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

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

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

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

View File

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

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

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

174
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,174 @@
# 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,6 +1,7 @@
**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">
@ -127,6 +128,11 @@ Signals are the reactive backbone of Deka DOM Elements:
- [TC39 Signals Proposal](https://github.com/tc39/proposal-signals) (future standard)
- [Observer pattern](https://en.wikipedia.org/wiki/Observer_pattern) (underlying concept)
## Contributing
We welcome contributions from the community! Please see our [Contributing Guide](CONTRIBUTING.md) for details on how to
get started, coding standards, commit guidelines, and the pull request process.
## Inspiration and Alternatives
- [vanjs-org/van](https://github.com/vanjs-org/van) — World's smallest reactive UI framework

View File

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

View File

@ -41,19 +41,6 @@ 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 = {
@ -303,7 +290,7 @@ var store_abort = /* @__PURE__ */ new WeakMap();
var scope = {
/**
* Gets the current scope
* @returns {Object} Current scope context
* @returns {typeof scopes[number]} Current scope context
*/
get current() {
return scopes[scopes.length - 1];
@ -663,9 +650,9 @@ function memo(key, generator) {
memo.isScope = function(obj) {
return obj[memoMark];
};
memo.scope = function memoScope(fun, { signal: signal2, onlyLast } = {}) {
memo.scope = function memoScopeCreate(fun, { signal: signal2, onlyLast } = {}) {
let cache = oCreate();
function memoScope2(...args) {
function memoScope(...args) {
if (signal2 && signal2.aborted)
return fun.apply(this, args);
let cache_local = onlyLast ? cache : oCreate();
@ -680,10 +667,10 @@ memo.scope = function memoScope(fun, { signal: signal2, onlyLast } = {}) {
cache = cache_local;
return out;
}
memoScope2[memoMark] = true;
memoScope2.clear = () => cache = oCreate();
if (signal2) signal2.addEventListener("abort", memoScope2.clear);
return memoScope2;
memoScope[memoMark] = true;
memoScope.clear = () => cache = oCreate();
if (signal2) signal2.addEventListener("abort", memoScope.clear);
return memoScope;
};
// src/signals-lib/helpers.js
@ -800,17 +787,17 @@ signal.clear = function(...signals2) {
};
var key_reactive = "__dde_reactive";
signal.el = function(s, map) {
map = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const mark_start = createElement.mark({ type: "reactive", source: new Defined().compact }, true);
const mapScoped = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const { current } = scope, { scope: sc } = current;
const mark_start = createElement.mark({ type: "reactive", component: sc && sc.name || "" }, true);
const mark_end = mark_start.end;
const out = enviroment.D.createDocumentFragment();
out.append(mark_start, mark_end);
const { current } = scope;
const reRenderReactiveElement = (v) => {
if (!mark_start.parentNode || !mark_end.parentNode)
return removeSignalListener(s, reRenderReactiveElement);
scope.push(current);
let els = map(v);
let els = mapScoped(v);
scope.pop();
if (!Array.isArray(els))
els = [els];
@ -830,7 +817,7 @@ signal.el = function(s, map) {
current.host(on.disconnected(
() => (
/*! Clears cached elements for reactive element `S.el` */
map.clear()
mapScoped.clear()
)
));
return out;
@ -902,9 +889,8 @@ var signals_config = {
function removeSignalsFromElements(s, listener, ...notes) {
const { current } = scope;
current.host(function(element) {
if (element[key_reactive])
return element[key_reactive].push([[s, listener], ...notes]);
element[key_reactive] = [];
if (!element[key_reactive]) element[key_reactive] = [];
element[key_reactive].push([[s, listener], ...notes]);
if (current.prevent) return;
on.disconnected(
() => (
@ -949,7 +935,6 @@ function toSignal(s, value, actions, readonly = false) {
onclear,
host,
listeners: /* @__PURE__ */ new Set(),
defined: new Defined().stack,
readonly
}),
enumerable: false,

File diff suppressed because one or more lines are too long

14
dist/esm.js vendored
View File

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

2
dist/esm.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -86,19 +86,6 @@ 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 = {
@ -348,7 +335,7 @@ var DDE = (() => {
var scope = {
/**
* Gets the current scope
* @returns {Object} Current scope context
* @returns {typeof scopes[number]} Current scope context
*/
get current() {
return scopes[scopes.length - 1];
@ -708,9 +695,9 @@ var DDE = (() => {
memo.isScope = function(obj) {
return obj[memoMark];
};
memo.scope = function memoScope(fun, { signal: signal2, onlyLast } = {}) {
memo.scope = function memoScopeCreate(fun, { signal: signal2, onlyLast } = {}) {
let cache = oCreate();
function memoScope2(...args) {
function memoScope(...args) {
if (signal2 && signal2.aborted)
return fun.apply(this, args);
let cache_local = onlyLast ? cache : oCreate();
@ -725,10 +712,10 @@ var DDE = (() => {
cache = cache_local;
return out;
}
memoScope2[memoMark] = true;
memoScope2.clear = () => cache = oCreate();
if (signal2) signal2.addEventListener("abort", memoScope2.clear);
return memoScope2;
memoScope[memoMark] = true;
memoScope.clear = () => cache = oCreate();
if (signal2) signal2.addEventListener("abort", memoScope.clear);
return memoScope;
};
// src/signals-lib/helpers.js
@ -845,17 +832,17 @@ var DDE = (() => {
};
var key_reactive = "__dde_reactive";
signal.el = function(s, map) {
map = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const mark_start = createElement.mark({ type: "reactive", source: new Defined().compact }, true);
const mapScoped = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const { current } = scope, { scope: sc } = current;
const mark_start = createElement.mark({ type: "reactive", component: sc && sc.name || "" }, true);
const mark_end = mark_start.end;
const out = enviroment.D.createDocumentFragment();
out.append(mark_start, mark_end);
const { current } = scope;
const reRenderReactiveElement = (v) => {
if (!mark_start.parentNode || !mark_end.parentNode)
return removeSignalListener(s, reRenderReactiveElement);
scope.push(current);
let els = map(v);
let els = mapScoped(v);
scope.pop();
if (!Array.isArray(els))
els = [els];
@ -875,7 +862,7 @@ var DDE = (() => {
current.host(on.disconnected(
() => (
/*! Clears cached elements for reactive element `S.el` */
map.clear()
mapScoped.clear()
)
));
return out;
@ -947,9 +934,8 @@ var DDE = (() => {
function removeSignalsFromElements(s, listener, ...notes) {
const { current } = scope;
current.host(function(element) {
if (element[key_reactive])
return element[key_reactive].push([[s, listener], ...notes]);
element[key_reactive] = [];
if (!element[key_reactive]) element[key_reactive] = [];
element[key_reactive].push([[s, listener], ...notes]);
if (current.prevent) return;
on.disconnected(
() => (
@ -994,7 +980,6 @@ var DDE = (() => {
onclear,
host,
listeners: /* @__PURE__ */ new Set(),
defined: new Defined().stack,
readonly
}),
enumerable: false,

File diff suppressed because one or more lines are too long

14
dist/iife.js vendored
View File

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

2
dist/iife.min.js vendored

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
`),
@ -154,10 +154,14 @@ 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.
Lets get started with the basics of creating elements!
`),
);
}
}

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`)
el("li", t`Capture element references`), // TODO: add example?
)
),
el("p").append(T`

View File

@ -277,7 +277,72 @@ export function page({ pkg, info }){
`),
el("li").append(T`
${el("strong", "Avoid infinite loops")}: Be careful when one signal updates another in a subscription
`)
`),
),
el("p").append(T`
While signals provide powerful reactivity for complex UI interactions, theyre not always necessary.
A good approach is to started with variables/constants and when necessary, convert them to signals.
`),
el("div", { className: "tabs" }).append(
el("div", { className: "tab", dataTab: "events" }).append(
el("h4", t`We can process form events without signals`),
el("p", t`This can be used when the form data doesnt need to be reactive and we just waiting for
results.`),
el(code, { 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(
@ -298,6 +363,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
// 2. access the host element (or other scope related values)
const { host } = scope;
// 3. Add behavior to host

View File

@ -80,8 +80,7 @@ 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`),
el("li", t`defined: Stack trace information for debugging`),
el("li", t`host: Reference to the host element/scope in which the signal was created`),
el("li", t`readonly: Boolean flag indicating if the signal is read-only`)
),
el("p").append(T`
@ -114,7 +113,13 @@ export function page({ pkg, info }){
el("ul").append(
el("li", t`That youre using signal.set() to update the value, not modifying objects/arrays directly`),
el("li", t`For mutable objects, ensure youre using actions or making proper copies before updating`),
el("li", t`That the signal is actually connected to the DOM element (check your S.el or attribute binding code)`)
el("li", t`That the signal is actually connected to the DOM element (check your S.el or attribute binding
code)`),
el("li").append(T`
That youre passing signal corecctly (without using ${el("code", "*.get()")}) and for ${el("code",
"S.el")} that you passing (derived) signals not a function (use ${el("code",
"S.el(S(()=> count.get() % 2), odd=> …)")}).
`),
),
el(code, { src: fileURL("./components/examples/debugging/mutations.js"), page_id }),

View File

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

4
package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { queueSignalWrite, mark } from "./helpers.js";
export { mark };
import { hasOwn, Defined, oCreate, oAssign } from "../helpers.js";
import { hasOwn, oCreate, oAssign } from "../helpers.js";
const Signal = oCreate(null, {
get: { value(){ return read(this); } },
@ -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} map - Function mapping signal value to DOM elements
* @param {Function} mapScoped - Function mapping signal value to DOM elements
* @returns {DocumentFragment} Fragment containing reactive elements
*/
signal.el= function(s, map){
map= memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const mark_start= el.mark({ type: "reactive", source: new Defined().compact }, true);
const mapScoped= memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const { current }= scope, { scope: sc }= current;
const mark_start= el.mark({ type: "reactive", component: sc && sc.name || "" }, true);
const mark_end= mark_start.end;
const out= env.D.createDocumentFragment();
out.append(mark_start, mark_end);
const { current }= scope;
const reRenderReactiveElement= v=> {
if(!mark_start.parentNode || !mark_end.parentNode) // === `isConnected` or wasnt yet rendered
return removeSignalListener(s, reRenderReactiveElement);
scope.push(current);
let els= map(v);
let els= mapScoped(v);
scope.pop();
if(!Array.isArray(els))
els= [ els ];
@ -202,7 +202,7 @@ signal.el= function(s, map){
reRenderReactiveElement(s.get());
current.host(on.disconnected(()=>
/*! Clears cached elements for reactive element `S.el` */
map.clear()
mapScoped.clear()
));
return out;
};
@ -314,9 +314,8 @@ export const signals_config= {
function removeSignalsFromElements(s, listener, ...notes){
const { current }= scope;
current.host(function(element){
if(element[key_reactive])
return element[key_reactive].push([ [ s, listener ], ...notes ]);
element[key_reactive]= [];
if(!element[key_reactive]) element[key_reactive]= [];
element[key_reactive].push([ [ s, listener ], ...notes ]);
if(current.prevent) return; // typically document.body, doenst need auto-remove as it should happen on page leave
on.disconnected(()=>
/*! Clears all Signals listeners added in the current scope/host (`S.el`, `assign`, …?).
@ -386,7 +385,6 @@ function toSignal(s, value, actions, readonly= false){
value: oAssign(oCreate(protoSigal), {
value, actions, onclear, host,
listeners: new Set(),
defined: new Defined().stack,
readonly
}),
enumerable: false,