mirror of
https://github.com/jaandrle/deka-dom-el
synced 2025-03-31 11:35:52 +02:00
⚡ 🐛 🔤 v0.9.3-alpha (#40)
* 🔤 * ⚡ Replaces defined with name/host * 🐛 __dde_reactive * ⚡ v0.9.3 * 🔤 examples+best paractises * 🐛 📺 fixes npm run docs * ⚡ finalizes v0.9.3-alpha * 🔤 📺 coc tabs * 🔤
This commit is contained in:
parent
f0dfdfde54
commit
5076771410
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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
22
.github/ISSUE_TEMPLATE/documentation.md
vendored
Normal 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 -->
|
29
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
29
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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
39
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
<!--
|
||||
Please use an appropriate git3moji in your PR title: https://robinpokorny.github.io/git3moji/
|
||||
Examples:
|
||||
- :bug: Fix signal update not triggering on nested properties
|
||||
- :zap: Improve event delegation performance
|
||||
- :abc: Add documentation for custom elements
|
||||
-->
|
||||
|
||||
## Description
|
||||
<!-- Describe the changes introduced by this PR -->
|
||||
|
||||
## Related Issues
|
||||
<!-- Link any related issues using the format #ISSUE_NUMBER -->
|
||||
|
||||
## Type of Change
|
||||
- [ ] Bug fix (non-breaking change that fixes an issue)
|
||||
- [ ] New feature (non-breaking change that adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
||||
- [ ] Documentation update
|
||||
- [ ] Code refactoring
|
||||
- [ ] Performance improvement
|
||||
- [ ] Test update
|
||||
|
||||
## Testing Performed
|
||||
<!-- Describe the tests you've done to verify your changes -->
|
||||
|
||||
## Screenshots
|
||||
<!-- If applicable, add screenshots to help explain your changes -->
|
||||
|
||||
## Checklist
|
||||
- [ ] My code follows the code style of this project
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||
- [ ] I have updated the documentation accordingly
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] All existing tests are passing
|
||||
|
||||
## Additional Notes
|
||||
<!-- Any additional information that might be helpful for reviewers -->
|
18
.github/workflows/npm-publish.yml
vendored
Normal file
18
.github/workflows/npm-publish.yml
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
name: Publish Package to npmjs
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [created]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
|
||||
with:
|
||||
node-version: '20.16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- run: npm ci
|
||||
- run: npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
3
.npmrc
Normal file
3
.npmrc
Normal file
@ -0,0 +1,3 @@
|
||||
//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
|
||||
registry=https://registry.npmjs.org/
|
||||
always-auth=true
|
134
CODE_OF_CONDUCT.md
Normal file
134
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,134 @@
|
||||
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
andrle.jan@centrum.cz.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
|
174
CONTRIBUTING.md
Normal file
174
CONTRIBUTING.md
Normal 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
|
||||
[](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.
|
56
README.md
56
README.md
@ -1,6 +1,7 @@
|
||||
**WIP** (the experimentation phase)
|
||||
**Alpha**
|
||||
| [source code on GitHub](https://github.com/jaandrle/deka-dom-el)
|
||||
| [*mirrored* on Gitea](https://gitea.jaandrle.cz/jaandrle/deka-dom-el)
|
||||
| [](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">
|
||||
@ -39,7 +40,7 @@ function EmojiCounter({ initial }) {
|
||||
on("click", () => count.set(count.get() + 1))
|
||||
),
|
||||
|
||||
el("select", null, on.host(el=> el.value= initial),
|
||||
el("select", null, on.defer(el=> el.value= initial),
|
||||
on("change", e => emoji.set(e.target.value))
|
||||
).append(
|
||||
el(Option, "🎉"),
|
||||
@ -60,40 +61,30 @@ 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
|
||||
- ✅ **Optional build-in signals** with support for custom reactive implementations (#39)
|
||||
- ✅ **Server-side rendering** support via [jsdom](https://github.com/jsdom/jsdom)
|
||||
- ✅ **TypeScript support** (work in progress)
|
||||
- ✅ **TypeScript support**
|
||||
- ☑️ **Support for debugging with browser DevTools** without extensions
|
||||
- ☑️ **Enhanced Web Components** support (work in progress)
|
||||
|
||||
## Why Another Library?
|
||||
|
||||
This library bridges the gap between minimal solutions like van/hyperscript and more comprehensive frameworks like
|
||||
[solid-js](https://github.com/solidjs/solid), offering a balanced trade-off between size, complexity, and usability.
|
||||
|
||||
Following functional programming principles, dd\<el\> starts with pure JavaScript (DOM API) and gradually adds
|
||||
auxiliary functions. These range from minor improvements to advanced features for building complete declarative
|
||||
reactive UI templates.
|
||||
|
||||
A key advantage: any internal function (`assign`, `classListDeclarative`, `on`, `dispatchEvent`, `S`, etc.) can be used
|
||||
independently while also working seamlessly together. This modular approach makes it easier to integrate the library
|
||||
into existing projects.
|
||||
- ☑️ **Enhanced Web Components** support
|
||||
|
||||
## 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
|
||||
# TBD
|
||||
# npm install deka-dom-el
|
||||
npm install deka-dom-el --save
|
||||
```
|
||||
|
||||
#### CDN / Direct Script
|
||||
…or via CDN / Direct Script:
|
||||
|
||||
For CDN links and various build formats (ESM/IIFE, with/without signals, minified/unminified), see the [interactive
|
||||
format selector](https://jaandrle.github.io/deka-dom-el/) on the documentation site.
|
||||
@ -113,10 +104,18 @@ format selector](https://jaandrle.github.io/deka-dom-el/) on the documentation s
|
||||
</script>
|
||||
```
|
||||
|
||||
### Documentation
|
||||
## Why Another Library?
|
||||
|
||||
- [**Interactive Guide**](https://jaandrle.github.io/deka-dom-el)
|
||||
- [**Examples**](https://jaandrle.github.io/deka-dom-el/p15-examples.html)
|
||||
This library bridges the gap between minimal solutions like van/hyperscript and more comprehensive frameworks like
|
||||
[solid-js](https://github.com/solidjs/solid), offering a balanced trade-off between size, complexity, and usability.
|
||||
|
||||
Following functional programming principles, dd\<el\> starts with pure JavaScript (DOM API) and gradually adds
|
||||
auxiliary functions. These range from minor improvements to advanced features for building complete declarative
|
||||
reactive UI templates.
|
||||
|
||||
A key advantage: any internal function (`assign`, `classListDeclarative`, `on`, `dispatchEvent`, `S`, etc.) can be used
|
||||
independently while also working seamlessly together. This modular approach makes it easier to integrate the library
|
||||
into existing projects.
|
||||
|
||||
## Understanding Signals
|
||||
|
||||
@ -127,6 +126,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
|
||||
|
@ -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`
|
||||
|
43
dist/esm-with-signals.js
vendored
43
dist/esm-with-signals.js
vendored
@ -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,
|
||||
|
7
dist/esm-with-signals.min.js
vendored
7
dist/esm-with-signals.min.js
vendored
File diff suppressed because one or more lines are too long
14
dist/esm.js
vendored
14
dist/esm.js
vendored
@ -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
2
dist/esm.min.js
vendored
File diff suppressed because one or more lines are too long
43
dist/iife-with-signals.js
vendored
43
dist/iife-with-signals.js
vendored
@ -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,
|
||||
|
7
dist/iife-with-signals.min.js
vendored
7
dist/iife-with-signals.min.js
vendored
File diff suppressed because one or more lines are too long
14
dist/iife.js
vendored
14
dist/iife.js
vendored
@ -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
2
dist/iife.min.js
vendored
File diff suppressed because one or more lines are too long
@ -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" })
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
|
@ -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>-->
|
||||
|
@ -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",
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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)",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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>. It’s based on ${el("a", {
|
||||
approach ${el("strong", "is not something new and/or special to dd<el>")}. It’s 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 # Coming soon", language: "shell", page_id }),
|
||||
el(code, { content: "npm install deka-dom-el --save", 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,6 +154,10 @@ export function page({ pkg, info }){
|
||||
Interactive demos with server-side pre-rendering`),
|
||||
el("li").append(T`${el("a", { href: "p13-appendix.html" }).append(el("strong", "Appendix & Summary"))} —
|
||||
Comprehensive reference and best practices`),
|
||||
el("li").append(T`${el("a", { href: "p14-converter.html" }).append(el("strong", "HTML Converter"))} —
|
||||
Convert HTML to dd<el> JavaScript code`),
|
||||
el("li").append(T`${el("a", { href: "p15-examples.html" }).append(el("strong", "Examples Gallery"))} —
|
||||
Real-world application examples and case studies`),
|
||||
),
|
||||
el("p").append(T`
|
||||
Each section builds on the previous ones, so we recommend following them in order.
|
||||
|
@ -208,6 +208,6 @@ export function page({ pkg, info }){
|
||||
`),
|
||||
),
|
||||
|
||||
el(mnemonic)
|
||||
el(mnemonic),
|
||||
);
|
||||
}
|
||||
|
@ -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`
|
||||
|
@ -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, they’re 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 doesn’t 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 dont’t 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),
|
||||
);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 you’re using signal.set() to update the value, not modifying objects/arrays directly`),
|
||||
el("li", t`For mutable objects, ensure you’re 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 you’re 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 }),
|
||||
|
||||
|
@ -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, they’re 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 doesn’t 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 dont’t 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
4
package-lock.json
generated
@ -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",
|
||||
|
@ -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.js"
|
||||
},
|
||||
"keywords": [
|
||||
"dom",
|
||||
"javascript",
|
||||
|
@ -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]; },
|
||||
|
||||
|
@ -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("/"));
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 wasn’t 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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user