mirror of
https://github.com/jaandrle/deka-dom-el
synced 2025-07-02 04:32:14 +02:00
Compare commits
7 Commits
v0.9.3-alp
...
93b905e677
Author | SHA1 | Date | |
---|---|---|---|
93b905e677
|
|||
2fdd8ebea9
|
|||
7cee9a4a14
|
|||
3c6cad5648
|
|||
9251e70015
|
|||
8756dabc55
|
|||
0a2d17ac6f
|
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,40 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ":bug: "
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## Bug Description
|
||||
<!-- A clear and concise description of what the bug is -->
|
||||
|
||||
## Steps to Reproduce
|
||||
<!-- Steps to reproduce the behavior -->
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## Expected Behavior
|
||||
<!-- A clear and concise description of what you expected to happen -->
|
||||
|
||||
## Actual Behavior
|
||||
<!-- A clear and concise description of what actually happened -->
|
||||
|
||||
## Code Sample
|
||||
<!-- If applicable, add minimal code sample to reproduce the issue -->
|
||||
```js
|
||||
// Your code here
|
||||
```
|
||||
|
||||
## Environment
|
||||
- Browser and version: <!-- e.g. Chrome 120, Firefox 120, Safari 17 -->
|
||||
- OS: <!-- e.g. Windows 11, macOS Sonoma, Ubuntu 22.04 -->
|
||||
- dd<el> version: <!-- e.g. 0.9.2 -->
|
||||
- Other relevant details:
|
||||
|
||||
## Screenshots
|
||||
<!-- If applicable, add screenshots to help explain your problem -->
|
||||
|
||||
## Additional Context
|
||||
<!-- Add any other context about the problem here -->
|
22
.github/ISSUE_TEMPLATE/documentation.md
vendored
22
.github/ISSUE_TEMPLATE/documentation.md
vendored
@ -1,22 +0,0 @@
|
||||
---
|
||||
name: Documentation improvement
|
||||
about: Suggest improvements to the documentation
|
||||
title: ":abc: "
|
||||
labels: documentation
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## Documentation Area
|
||||
<!-- Which part of the documentation needs improvement? Provide links if applicable -->
|
||||
|
||||
## Current Issue
|
||||
<!-- What's currently unclear, missing, or incorrect in the documentation? -->
|
||||
|
||||
## Suggested Improvement
|
||||
<!-- Describe the improvement or addition you'd like to see -->
|
||||
|
||||
## Example Content
|
||||
<!-- If applicable, provide example content or wording -->
|
||||
|
||||
## Additional Context
|
||||
<!-- Any other context or screenshots about the documentation request -->
|
29
.github/ISSUE_TEMPLATE/feature_request.md
vendored
29
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,29 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ":zap: "
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
---
|
||||
<!-- Consider open discussion: https://github.com/jaandrle/deka-dom-el/discussions first -->
|
||||
|
||||
## Problem Statement
|
||||
<!-- A clear and concise description of the problem this feature would solve -->
|
||||
|
||||
## Proposed Solution
|
||||
<!-- A detailed description of the feature you're suggesting -->
|
||||
|
||||
## Use Cases
|
||||
<!-- Describe specific use cases where this feature would be beneficial -->
|
||||
|
||||
## Example Implementation
|
||||
<!-- If possible, provide example code or pseudocode for how this feature might work -->
|
||||
```js
|
||||
// Example code
|
||||
```
|
||||
|
||||
## Alternatives Considered
|
||||
<!-- A description of any alternative solutions or features you've considered -->
|
||||
|
||||
## Additional Context
|
||||
<!-- Any other context, screenshots, or examples that might be helpful -->
|
39
.github/pull_request_template.md
vendored
39
.github/pull_request_template.md
vendored
@ -1,39 +0,0 @@
|
||||
<!--
|
||||
Please use an appropriate git3moji in your PR title: https://robinpokorny.github.io/git3moji/
|
||||
Examples:
|
||||
- :bug: Fix signal update not triggering on nested properties
|
||||
- :zap: Improve event delegation performance
|
||||
- :abc: Add documentation for custom elements
|
||||
-->
|
||||
|
||||
## Description
|
||||
<!-- Describe the changes introduced by this PR -->
|
||||
|
||||
## Related Issues
|
||||
<!-- Link any related issues using the format #ISSUE_NUMBER -->
|
||||
|
||||
## Type of Change
|
||||
- [ ] Bug fix (non-breaking change that fixes an issue)
|
||||
- [ ] New feature (non-breaking change that adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
||||
- [ ] Documentation update
|
||||
- [ ] Code refactoring
|
||||
- [ ] Performance improvement
|
||||
- [ ] Test update
|
||||
|
||||
## Testing Performed
|
||||
<!-- Describe the tests you've done to verify your changes -->
|
||||
|
||||
## Screenshots
|
||||
<!-- If applicable, add screenshots to help explain your changes -->
|
||||
|
||||
## Checklist
|
||||
- [ ] My code follows the code style of this project
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||
- [ ] I have updated the documentation accordingly
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] All existing tests are passing
|
||||
|
||||
## Additional Notes
|
||||
<!-- Any additional information that might be helpful for reviewers -->
|
18
.github/workflows/npm-publish.yml
vendored
18
.github/workflows/npm-publish.yml
vendored
@ -1,18 +0,0 @@
|
||||
name: Publish Package to npmjs
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [created]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
|
||||
with:
|
||||
node-version: '20.16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- run: npm ci
|
||||
- run: npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
3
.npmrc
3
.npmrc
@ -1,3 +0,0 @@
|
||||
//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
|
||||
registry=https://registry.npmjs.org/
|
||||
always-auth=true
|
@ -1,134 +0,0 @@
|
||||
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
andrle.jan@centrum.cz.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
|
174
CONTRIBUTING.md
174
CONTRIBUTING.md
@ -1,174 +0,0 @@
|
||||
# Contributing to Deka DOM Elements
|
||||
|
||||
Thank you for your interest in contributing to Deka DOM Elements (dd<el> or DDE)! This document provides guidelines and
|
||||
instructions for contributing to the project.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Development Workflow](#development-workflow)
|
||||
- [Commit Guidelines](#commit-guidelines)
|
||||
- [Pull Request Process](#pull-request-process)
|
||||
- [Issue Guidelines](#issue-guidelines)
|
||||
- [Coding Standards](#coding-standards)
|
||||
- [Testing](#testing)
|
||||
- [Documentation](#documentation)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Please be respectful and inclusive in your interactions with other contributors. We aim to foster a welcoming community
|
||||
where everyone feels comfortable participating.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. **Fork the repository**:
|
||||
- Click the "Fork" button on the GitHub repository
|
||||
|
||||
2. **Clone your fork**:
|
||||
```bash
|
||||
git clone https://github.com/YOUR-USERNAME/deka-dom-el.git
|
||||
cd deka-dom-el
|
||||
```
|
||||
|
||||
3. **Set up the development environment**:
|
||||
```bash
|
||||
npm ci
|
||||
```
|
||||
|
||||
4. **Add the upstream repository**:
|
||||
```bash
|
||||
git remote add upstream https://github.com/jaandrle/deka-dom-el.git
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Create a new branch**:
|
||||
```bash
|
||||
git checkout -b your-feature-branch
|
||||
```
|
||||
Use descriptive branch names that reflect the changes you're making.
|
||||
|
||||
2. **Make your changes**:
|
||||
- Write clean, modular code
|
||||
- Follow the project's coding standards (see [Coding Standards](#coding-standards))
|
||||
- Include relevant tests for your changes
|
||||
|
||||
3. ~**Run tests**:~
|
||||
```bash
|
||||
#npm test
|
||||
```
|
||||
|
||||
4. **Build the project**:
|
||||
```bash
|
||||
npm run build
|
||||
#or
|
||||
bs/build.js
|
||||
```
|
||||
|
||||
5. **Preview documentation changes** (if applicable):
|
||||
```bash
|
||||
npm run docs
|
||||
#or
|
||||
bs/docs.js
|
||||
```
|
||||
|
||||
…see [BS folder](./bs/README.md) for more info.
|
||||
|
||||
## Commit Guidelines
|
||||
|
||||
We use
|
||||
[](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.
|
101
README.md
101
README.md
@ -1,7 +1,6 @@
|
||||
**Alpha**
|
||||
**WIP** (the experimentation phase)
|
||||
| [source code on GitHub](https://github.com/jaandrle/deka-dom-el)
|
||||
| [*mirrored* on Gitea](https://gitea.jaandrle.cz/jaandrle/deka-dom-el)
|
||||
| [](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">
|
||||
@ -32,7 +31,7 @@ function EmojiCounter({ initial }) {
|
||||
el("p", {
|
||||
className: "output",
|
||||
textContent: S(() =>
|
||||
`Hello World ${emoji.get().repeat(count.get())}`),
|
||||
`Hello World ${emoji.get().repeat(clicks.get())}`),
|
||||
}),
|
||||
|
||||
// 🎮 Controls - Update state on events
|
||||
@ -40,12 +39,12 @@ function EmojiCounter({ initial }) {
|
||||
on("click", () => count.set(count.get() + 1))
|
||||
),
|
||||
|
||||
el("select", null, on.defer(el=> el.value= initial),
|
||||
el("select", null,
|
||||
on("change", e => emoji.set(e.target.value))
|
||||
).append(
|
||||
el(Option, "🎉"),
|
||||
el(Option, "🚀"),
|
||||
el(Option, "💖"),
|
||||
el(Option, "🎉", isSelected),
|
||||
el(Option, "🚀", isSelected),
|
||||
el(Option, "💖", isSelected),
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -61,48 +60,15 @@ Creating reactive elements, components, and Web Components using the native
|
||||
## Features at a Glance
|
||||
|
||||
- ✅ **No build step required** — use directly in browsers or Node.js
|
||||
- ☑️ **Lightweight** — ~10-15kB minified (original goal 10kB) with **zero**/minimal dependencies
|
||||
- ☑️ **Lightweight** — ~10-15kB minified (original goal 10kB) with zero/minimal dependencies
|
||||
- ✅ **Declarative & functional approach** for clean, maintainable code
|
||||
- ✅ **Signals and events** for reactive UI
|
||||
- ✅ **Memoization for performance** — optimize rendering with intelligent caching
|
||||
- ✅ **Optional build-in signals** with support for custom reactive implementations (#39)
|
||||
- ✅ **Optional build-in signals** with support for custom reactive implementations
|
||||
- ✅ **Server-side rendering** support via [jsdom](https://github.com/jsdom/jsdom)
|
||||
- ✅ **TypeScript support**
|
||||
- ✅ **TypeScript support** (work in progress)
|
||||
- ☑️ **Support for debugging with browser DevTools** without extensions
|
||||
- ☑️ **Enhanced Web Components** support
|
||||
|
||||
## 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
|
||||
|
||||
```bash
|
||||
npm install deka-dom-el --save
|
||||
```
|
||||
|
||||
…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.
|
||||
|
||||
```html
|
||||
<!-- Example with IIFE build (creates a global DDE object) -->
|
||||
<script src="https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/iife-with-signals.min.js"></script>
|
||||
<script>
|
||||
const { el, S } = DDE;
|
||||
// Your code here
|
||||
</script>
|
||||
|
||||
<!-- Or with ES modules -->
|
||||
<script type="module">
|
||||
import { el, S } from "https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-signals.min.js";
|
||||
// Your code here
|
||||
</script>
|
||||
```
|
||||
- ☑️ **Enhanced Web Components** support (work in progress)
|
||||
|
||||
## Why Another Library?
|
||||
|
||||
@ -117,6 +83,29 @@ A key advantage: any internal function (`assign`, `classListDeclarative`, `on`,
|
||||
independently while also working seamlessly together. This modular approach makes it easier to integrate the library
|
||||
into existing projects.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installation
|
||||
|
||||
#### npm
|
||||
```bash
|
||||
# TBD
|
||||
# npm install deka-dom-el
|
||||
```
|
||||
|
||||
#### Direct Script
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/iife-with-signals.min.js"></script>
|
||||
<script type="module">
|
||||
const { el, S } = DDE;
|
||||
</script>
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
- [**Interactive Guide**](https://jaandrle.github.io/deka-dom-el): WIP
|
||||
- [Examples](./examples/): TBD/WIP
|
||||
|
||||
## Understanding Signals
|
||||
|
||||
Signals are the reactive backbone of Deka DOM Elements:
|
||||
@ -126,24 +115,12 @@ Signals are the reactive backbone of Deka DOM Elements:
|
||||
- [TC39 Signals Proposal](https://github.com/tc39/proposal-signals) (future standard)
|
||||
- [Observer pattern](https://en.wikipedia.org/wiki/Observer_pattern) (underlying concept)
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions from the community! Please see our [Contributing Guide](CONTRIBUTING.md) for details on how to
|
||||
get started, coding standards, commit guidelines, and the pull request process.
|
||||
|
||||
## Inspiration and Alternatives
|
||||
|
||||
- [vanjs-org/van](https://github.com/vanjs-org/van) — World's smallest reactive UI framework
|
||||
- [adamhaile/S](https://github.com/adamhaile/S) — Simple, clean, fast reactive programming
|
||||
- [hyperhype/hyperscript](https://github.com/hyperhype/hyperscript) — Create HyperText with JavaScript
|
||||
- [potch/signals](https://github.com/potch/signals) — A small reactive signals library
|
||||
- [AseasRoa/paintor](https://github.com/AseasRoa/paintor) - JavaScript library for building reactive client-side user
|
||||
interfaces or HTML code.
|
||||
- [pota](https://pota.quack.uy/) — small and pluggable Reactive Web Renderer. It's compiler-less, includes an html
|
||||
function, and a optimized babel preset in case you fancy JSX.
|
||||
- [jaandrle/dollar_dom_component](https://github.com/jaandrle/dollar_dom_component) —
|
||||
- [vanjs-org/van](https://github.com/vanjs-org/van) - World's smallest reactive UI framework
|
||||
- [adamhaile/S](https://github.com/adamhaile/S) - Simple, clean, fast reactive programming
|
||||
- [hyperhype/hyperscript](https://github.com/hyperhype/hyperscript) - Create HyperText with JavaScript
|
||||
- [potch/signals](https://github.com/potch/signals) - A small reactive signals library
|
||||
- [jaandrle/dollar_dom_component](https://github.com/jaandrle/dollar_dom_component) -
|
||||
Functional DOM components without JSX/virtual DOM
|
||||
- [TarekRaafat/eleva](https://github.com/TarekRaafat/eleva) — A minimalist, lightweight, pure vanilla JavaScript
|
||||
frontend runtime framework.
|
||||
- [didi/mpx](https://github.com/didi/mpx) — Mpx,一款具有优秀开发体验和深度性能优化的增强型跨端小程序框架
|
||||
- [mxjp/rvx](https://github.com/mxjp/rvx) — A signal based frontend framework
|
||||
- [mxjp/rvx: A signal based frontend framework](https://github.com/mxjp/rvx)
|
||||
|
@ -2,18 +2,15 @@
|
||||
This project uses [jaandrle/bs: The simplest possible build system using executable/bash scripts](
|
||||
https://github.com/jaandrle/bs).
|
||||
|
||||
#### bs/build.js [main|signals] [--no-types|--help]
|
||||
#### bs/build.js [--minify|--help]
|
||||
Generates alternative versions of the project (other than native ESM code).
|
||||
Also generates typescript definitions.
|
||||
|
||||
#### bs/docs.js
|
||||
Generates documentation, from `docs/`. Uses “SSR” technique, using deka-dom-el itself.
|
||||
|
||||
For running use `npx serve dist/docs`.
|
||||
|
||||
#### bs/lint.sh
|
||||
Lints size of the project, jshint. See configs:
|
||||
|
||||
- `package.json`: key `size-limit`
|
||||
- `package.json`: key `jshintConfig`
|
||||
- `.editorconfig`
|
||||
|
@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env -S npx nodejsscript
|
||||
import { buildSync as esbuildSync } from "esbuild";
|
||||
const css= echo.css`
|
||||
.info{ color: gray; }
|
||||
`;
|
||||
@ -9,7 +8,8 @@ export function build({ files, filesOut, minify= "partial", iife= true, types= t
|
||||
const file= file_root+".js";
|
||||
echo(`Processing ${file} (minified: ${minify})`);
|
||||
const out= filesOut(file);
|
||||
esbuild({ file, out, minify });
|
||||
const esbuild_output= buildEsbuild({ file, out, minify });
|
||||
echoVariant(esbuild_output.stderr.split("\n")[1].trim());
|
||||
|
||||
if(types){
|
||||
const file_dts= file_root+".d.ts";
|
||||
@ -31,13 +31,14 @@ export function build({ files, filesOut, minify= "partial", iife= true, types= t
|
||||
const name= "DDE";
|
||||
const out= filesOut(file_root+".js", fileMark);
|
||||
|
||||
const params= {
|
||||
format: "iife",
|
||||
globalName: name
|
||||
};
|
||||
esbuild({ file, out, minify, params });
|
||||
const params= [
|
||||
"--format=iife",
|
||||
"--global-name="+name,
|
||||
];
|
||||
const dde_output= buildEsbuild({ file, out, minify, params });
|
||||
echoVariant(`${out} (${name})`)
|
||||
|
||||
if(!types) return;
|
||||
if(!types) return dde_output;
|
||||
const file_dts= file_root+".d.ts";
|
||||
const file_dts_out= filesOut(file_dts, fileMark);
|
||||
echoVariant(file_dts_out, true);
|
||||
@ -47,6 +48,8 @@ export function build({ files, filesOut, minify= "partial", iife= true, types= t
|
||||
entry: file_dts,
|
||||
})
|
||||
echoVariant(file_dts_out);
|
||||
|
||||
return dde_output;
|
||||
}
|
||||
}
|
||||
export function buildDts({ bundle, entry, name }){
|
||||
@ -61,39 +64,46 @@ export function buildDts({ bundle, entry, name }){
|
||||
].filter(Boolean).join(" "), { out, entry });
|
||||
return dts_b_g_output;
|
||||
}
|
||||
export function esbuild({ file, out, minify= "partial", params= {} }){
|
||||
const esbuild_output= esbuildSync({
|
||||
entryPoints: [file],
|
||||
outfile: out,
|
||||
platform: "neutral",
|
||||
bundle: true,
|
||||
legalComments: "inline",
|
||||
packages: "external",
|
||||
metafile: true,
|
||||
...minifyOption(minify),
|
||||
...params
|
||||
});
|
||||
class ErrorEsbuild extends Error{
|
||||
constructor({ code, stderr }){
|
||||
super(stderr);
|
||||
this.code= code;
|
||||
this.stderr= stderr;
|
||||
}
|
||||
}
|
||||
function buildEsbuild({ file, out, minify= "partial", params= [] }){
|
||||
try {
|
||||
return esbuild({ file, out, minify, params });
|
||||
} catch(e){
|
||||
if(e instanceof ErrorEsbuild)
|
||||
return $.exit(e.code, echo(e.stderr));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
export function esbuild({ file, out, minify= "partial", params= [] }){
|
||||
const esbuild_output= s.$().run([
|
||||
"npx esbuild '::file::'",
|
||||
"--platform=neutral",
|
||||
"--bundle",
|
||||
minifyOption(minify),
|
||||
"--legal-comments=inline",
|
||||
"--packages=external",
|
||||
...params,
|
||||
"--outfile='::out::'"
|
||||
].filter(Boolean).join(" "), { file, out });
|
||||
if(esbuild_output.code)
|
||||
throw new ErrorEsbuild(esbuild_output);
|
||||
pipe(
|
||||
f=> f.replace(/^ +/gm, m=> "\t".repeat(m.length/2)),
|
||||
f=> s.echo(f).to(out)
|
||||
)(s.cat(out));
|
||||
|
||||
echoVariant(metaToLineStatus(esbuild_output.metafile, out));
|
||||
return esbuild_output;
|
||||
}
|
||||
/** @param {"no"|"full"|"partial"} level */
|
||||
function minifyOption(level= "partial"){
|
||||
if("no"===level) return { minify: false };
|
||||
if("full"===level) return { minify: true };
|
||||
return { minifySyntax: true, minifyIdentifiers: true };
|
||||
}
|
||||
function metaToLineStatus(meta, file){
|
||||
const status= meta.outputs[file];
|
||||
if(!status) return `? ${file}: unknown`;
|
||||
const { bytes }= status;
|
||||
const kbytes= bytes/1024;
|
||||
const kbytesR= kbytes.toFixed(2);
|
||||
return `${file}: ${kbytesR} kB`;
|
||||
if("no"===level) return undefined;
|
||||
if("full"===level) return "--minify";
|
||||
return "--minify-syntax --minify-identifiers";
|
||||
}
|
||||
function echoVariant(name, todo= false){
|
||||
if(todo) return echo.use("-R", "~ "+name);
|
||||
|
31
dist/esm-with-signals.d.ts
vendored
31
dist/esm-with-signals.d.ts
vendored
@ -4,7 +4,7 @@ export interface Signal<V, A> {
|
||||
/** The current value of the signal */
|
||||
get(): V;
|
||||
/** Set new value of the signal */
|
||||
set(value: V, force?: boolean): V;
|
||||
set(value: V): V;
|
||||
toJSON(): V;
|
||||
valueOf(): V;
|
||||
}
|
||||
@ -173,30 +173,17 @@ declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
|
||||
export interface On {
|
||||
/** Listens to the DOM event. See {@link Document.addEventListener} */
|
||||
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event] & {
|
||||
target: EL;
|
||||
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
|
||||
<EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: string, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: Event | CustomEvent) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element is connected to the live DOM. In case of custom elements uses [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
connected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<NoInfer<EL>>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
connected<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<El>) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element is disconnected from the live DOM. In case of custom elements uses [`disconnectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
disconnected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
/**
|
||||
* Fires after the next tick of the Javascript event loop.
|
||||
* This is handy for example to apply some property depending on the element content:
|
||||
* ```js
|
||||
* const selected= "Z";
|
||||
* //...
|
||||
* return el("form").append(
|
||||
* el("select", null, on.defer(e=> e.value=selected)).append(
|
||||
* el("option", { value: "A", textContent: "A" }),
|
||||
* //...
|
||||
* el("option", { value: "Z", textContent: "Z" }),
|
||||
* ),
|
||||
* );
|
||||
* ```
|
||||
* */
|
||||
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
|
||||
disconnected<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element attribute changes. In case of custom elements uses [`attributeChangedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
attributeChanged<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<[
|
||||
string,
|
||||
string
|
||||
]>) => any, options?: AddEventListenerOptions): EE;
|
||||
}
|
||||
export const on: On;
|
||||
export type Scope = {
|
||||
|
246
dist/esm-with-signals.js
vendored
246
dist/esm-with-signals.js
vendored
@ -41,8 +41,52 @@ 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
|
||||
// src/signals-lib/common.js
|
||||
var signals_global = {
|
||||
/**
|
||||
* Checks if a value is a signal
|
||||
* @param {any} attributes - Value to check
|
||||
* @returns {boolean} Whether the value is a signal
|
||||
*/
|
||||
isSignal(attributes) {
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* Processes an attribute that might be reactive
|
||||
* @param {Element} obj - Element that owns the attribute
|
||||
* @param {string} key - Attribute name
|
||||
* @param {any} attr - Attribute value
|
||||
* @param {Function} set - Function to set the attribute
|
||||
* @returns {any} Processed attribute value
|
||||
*/
|
||||
processReactiveAttribute(obj, key, attr, set) {
|
||||
return attr;
|
||||
}
|
||||
};
|
||||
function registerReactivity(def, global = true) {
|
||||
if (global) return oAssign(signals_global, def);
|
||||
Object.setPrototypeOf(def, signals_global);
|
||||
return def;
|
||||
}
|
||||
function signals(_this) {
|
||||
return isProtoFrom(_this, signals_global) && _this !== signals_global ? _this : signals_global;
|
||||
}
|
||||
|
||||
// src/dom-common.js
|
||||
var enviroment = {
|
||||
setDeleteAttr,
|
||||
ssr: "",
|
||||
@ -68,7 +112,7 @@ var evc = "dde:connected";
|
||||
var evd = "dde:disconnected";
|
||||
var eva = "dde:attributeChanged";
|
||||
|
||||
// src/dom-lib/events-observer.js
|
||||
// src/events-observer.js
|
||||
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
|
||||
get() {
|
||||
return () => {
|
||||
@ -232,7 +276,7 @@ function connectionsChangesObserverConstructor() {
|
||||
}
|
||||
}
|
||||
|
||||
// src/dom-lib/events.js
|
||||
// src/events.js
|
||||
function dispatchEvent(name, options, host) {
|
||||
if (typeof options === "function") {
|
||||
host = options;
|
||||
@ -254,7 +298,6 @@ function on(event, listener, options) {
|
||||
return element;
|
||||
};
|
||||
}
|
||||
on.defer = (fn) => setTimeout.bind(null, fn, 0);
|
||||
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
|
||||
on.connected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
@ -278,7 +321,10 @@ on.disconnected = function(listener, options) {
|
||||
};
|
||||
};
|
||||
|
||||
// src/dom-lib/scopes.js
|
||||
// src/dom.js
|
||||
function queue(promise) {
|
||||
return enviroment.q(promise);
|
||||
}
|
||||
var scopes = [{
|
||||
get scope() {
|
||||
return enviroment.D.body;
|
||||
@ -290,7 +336,7 @@ var store_abort = /* @__PURE__ */ new WeakMap();
|
||||
var scope = {
|
||||
/**
|
||||
* Gets the current scope
|
||||
* @returns {typeof scopes[number]} Current scope context
|
||||
* @returns {Object} Current scope context
|
||||
*/
|
||||
get current() {
|
||||
return scopes[scopes.length - 1];
|
||||
@ -353,60 +399,6 @@ var scope = {
|
||||
return scopes.pop();
|
||||
}
|
||||
};
|
||||
|
||||
// src/signals-lib/common.js
|
||||
var signals_global = {
|
||||
/**
|
||||
* Checks if a value is a signal
|
||||
* @param {any} attributes - Value to check
|
||||
* @returns {boolean} Whether the value is a signal
|
||||
*/
|
||||
isSignal(attributes) {
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* Processes an attribute that might be reactive
|
||||
* @param {Element} obj - Element that owns the attribute
|
||||
* @param {string} key - Attribute name
|
||||
* @param {any} attr - Attribute value
|
||||
* @param {Function} set - Function to set the attribute
|
||||
* @returns {any} Processed attribute value
|
||||
*/
|
||||
processReactiveAttribute(obj, key, attr, set) {
|
||||
return attr;
|
||||
}
|
||||
};
|
||||
function registerReactivity(def, global = true) {
|
||||
if (global) return oAssign(signals_global, def);
|
||||
Object.setPrototypeOf(def, signals_global);
|
||||
return def;
|
||||
}
|
||||
function signals(_this) {
|
||||
return isProtoFrom(_this, signals_global) && _this !== signals_global ? _this : signals_global;
|
||||
}
|
||||
|
||||
// src/dom-lib/helpers.js
|
||||
function setRemove(obj, prop, key, val) {
|
||||
return obj[(isUndef(val) ? "remove" : "set") + prop](key, val);
|
||||
}
|
||||
function setRemoveNS(obj, prop, key, val, ns = null) {
|
||||
return obj[(isUndef(val) ? "remove" : "set") + prop + "NS"](ns, key, val);
|
||||
}
|
||||
function setDelete(obj, key, val) {
|
||||
Reflect.set(obj, key, val);
|
||||
if (!isUndef(val)) return;
|
||||
return Reflect.deleteProperty(obj, key);
|
||||
}
|
||||
function elementAttribute(element, op, key, value) {
|
||||
if (isInstance(element, enviroment.H))
|
||||
return element[op + "Attribute"](key, value);
|
||||
return element[op + "AttributeNS"](null, key, value);
|
||||
}
|
||||
|
||||
// src/dom-lib/el.js
|
||||
function queue(promise) {
|
||||
return enviroment.q(promise);
|
||||
}
|
||||
function append(...els) {
|
||||
this.appendOriginal(...els);
|
||||
return this;
|
||||
@ -422,8 +414,7 @@ function createElement(tag, attributes, ...addons) {
|
||||
const s = signals(this);
|
||||
let scoped = 0;
|
||||
let el, el_host;
|
||||
const att_type = typeof attributes;
|
||||
if (att_type === "string" || att_type === "number" || s.isSignal(attributes))
|
||||
if (Object(attributes) !== attributes || s.isSignal(attributes))
|
||||
attributes = { textContent: attributes };
|
||||
switch (true) {
|
||||
case typeof tag === "function": {
|
||||
@ -477,6 +468,38 @@ function createElementNS(ns) {
|
||||
return el;
|
||||
};
|
||||
}
|
||||
function simulateSlots(element, root = element) {
|
||||
const mark_e = "\xB9\u2070", mark_s = "\u2713";
|
||||
const slots = Object.fromEntries(
|
||||
Array.from(root.querySelectorAll("slot")).filter((s) => !s.name.endsWith(mark_e)).map((s) => [s.name += mark_e, s])
|
||||
);
|
||||
element.append = new Proxy(element.append, {
|
||||
apply(orig, _, els) {
|
||||
if (els[0] === root) return orig.apply(element, els);
|
||||
for (const el of els) {
|
||||
const name = (el.slot || "") + mark_e;
|
||||
try {
|
||||
elementAttribute(el, "remove", "slot");
|
||||
} catch (_error) {
|
||||
}
|
||||
const slot = slots[name];
|
||||
if (!slot) return;
|
||||
if (!slot.name.startsWith(mark_s)) {
|
||||
slot.childNodes.forEach((c) => c.remove());
|
||||
slot.name = mark_s + name;
|
||||
}
|
||||
slot.append(el);
|
||||
}
|
||||
element.append = orig;
|
||||
return element;
|
||||
}
|
||||
});
|
||||
if (element !== root) {
|
||||
const els = Array.from(element.childNodes);
|
||||
element.append(...els);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
var assign_context = /* @__PURE__ */ new WeakMap();
|
||||
var { setDeleteAttr: setDeleteAttr2 } = enviroment;
|
||||
function assign(element, ...attributes) {
|
||||
@ -539,6 +562,11 @@ function classListDeclarative(element, toggle) {
|
||||
);
|
||||
return element;
|
||||
}
|
||||
function elementAttribute(element, op, key, value) {
|
||||
if (isInstance(element, enviroment.H))
|
||||
return element[op + "Attribute"](key, value);
|
||||
return element[op + "AttributeNS"](null, key, value);
|
||||
}
|
||||
function isPropSetter(el, key) {
|
||||
if (!(key in el)) return false;
|
||||
const des = getPropDescriptor(el, key);
|
||||
@ -562,40 +590,19 @@ function forEachEntries(s, target, element, obj, cb) {
|
||||
cb(key, val);
|
||||
});
|
||||
}
|
||||
|
||||
// src/dom-lib/customElement.js
|
||||
function simulateSlots(element, root = element) {
|
||||
const mark_e = "\xB9\u2070", mark_s = "\u2713";
|
||||
const slots = Object.fromEntries(
|
||||
Array.from(root.querySelectorAll("slot")).filter((s) => !s.name.endsWith(mark_e)).map((s) => [s.name += mark_e, s])
|
||||
);
|
||||
element.append = new Proxy(element.append, {
|
||||
apply(orig, _, els) {
|
||||
if (els[0] === root) return orig.apply(element, els);
|
||||
for (const el of els) {
|
||||
const name = (el.slot || "") + mark_e;
|
||||
try {
|
||||
elementAttribute(el, "remove", "slot");
|
||||
} catch (_error) {
|
||||
}
|
||||
const slot = slots[name];
|
||||
if (!slot) return;
|
||||
if (!slot.name.startsWith(mark_s)) {
|
||||
slot.childNodes.forEach((c) => c.remove());
|
||||
slot.name = mark_s + name;
|
||||
}
|
||||
slot.append(el);
|
||||
}
|
||||
element.append = orig;
|
||||
return element;
|
||||
}
|
||||
});
|
||||
if (element !== root) {
|
||||
const els = Array.from(element.childNodes);
|
||||
element.append(...els);
|
||||
}
|
||||
return root;
|
||||
function setRemove(obj, prop, key, val) {
|
||||
return obj[(isUndef(val) ? "remove" : "set") + prop](key, val);
|
||||
}
|
||||
function setRemoveNS(obj, prop, key, val, ns = null) {
|
||||
return obj[(isUndef(val) ? "remove" : "set") + prop + "NS"](ns, key, val);
|
||||
}
|
||||
function setDelete(obj, key, val) {
|
||||
Reflect.set(obj, key, val);
|
||||
if (!isUndef(val)) return;
|
||||
return Reflect.deleteProperty(obj, key);
|
||||
}
|
||||
|
||||
// src/customElement.js
|
||||
function customElementRender(target, render, props = {}) {
|
||||
const custom_element = target.host || target;
|
||||
scope.push({
|
||||
@ -650,9 +657,9 @@ function memo(key, generator) {
|
||||
memo.isScope = function(obj) {
|
||||
return obj[memoMark];
|
||||
};
|
||||
memo.scope = function memoScopeCreate(fun, { signal: signal2, onlyLast } = {}) {
|
||||
memo.scope = function memoScope(fun, { signal: signal2, onlyLast } = {}) {
|
||||
let cache = oCreate();
|
||||
function memoScope(...args) {
|
||||
function memoScope2(...args) {
|
||||
if (signal2 && signal2.aborted)
|
||||
return fun.apply(this, args);
|
||||
let cache_local = onlyLast ? cache : oCreate();
|
||||
@ -667,28 +674,28 @@ memo.scope = function memoScopeCreate(fun, { signal: signal2, onlyLast } = {}) {
|
||||
cache = cache_local;
|
||||
return out;
|
||||
}
|
||||
memoScope[memoMark] = true;
|
||||
memoScope.clear = () => cache = oCreate();
|
||||
if (signal2) signal2.addEventListener("abort", memoScope.clear);
|
||||
return memoScope;
|
||||
memoScope2[memoMark] = true;
|
||||
memoScope2.clear = () => cache = oCreate();
|
||||
if (signal2) signal2.addEventListener("abort", memoScope2.clear);
|
||||
return memoScope2;
|
||||
};
|
||||
|
||||
// src/signals-lib/helpers.js
|
||||
var mark = "__dde_signal";
|
||||
var queueSignalWrite = /* @__PURE__ */ (() => {
|
||||
let pendingSignals = /* @__PURE__ */ new Map();
|
||||
let pendingSignals = /* @__PURE__ */ new Set();
|
||||
let scheduled = false;
|
||||
function flushSignals() {
|
||||
scheduled = false;
|
||||
const todo = pendingSignals;
|
||||
pendingSignals = /* @__PURE__ */ new Map();
|
||||
for (const [signal2, force] of todo) {
|
||||
pendingSignals = /* @__PURE__ */ new Set();
|
||||
for (const signal2 of todo) {
|
||||
const M = signal2[mark];
|
||||
if (M) M.listeners.forEach((l) => l(M.value, force));
|
||||
if (M) M.listeners.forEach((l) => l(M.value));
|
||||
}
|
||||
}
|
||||
return function(s, force = false) {
|
||||
pendingSignals.set(s, pendingSignals.get(s) || force);
|
||||
return function(s) {
|
||||
pendingSignals.add(s);
|
||||
if (scheduled) return;
|
||||
scheduled = true;
|
||||
queueMicrotask(flushSignals);
|
||||
@ -725,11 +732,11 @@ function signal(value, actions) {
|
||||
return create(false, value, actions);
|
||||
if (isSignal(value)) return value;
|
||||
const out = create(true);
|
||||
function contextReWatch(_, force) {
|
||||
function contextReWatch() {
|
||||
const [origin, ...deps_old] = deps.get(contextReWatch);
|
||||
deps.set(contextReWatch, /* @__PURE__ */ new Set([origin]));
|
||||
stack_watch.push(contextReWatch);
|
||||
write(out, value(), force);
|
||||
write(out, value());
|
||||
stack_watch.pop();
|
||||
if (!deps_old.length) return;
|
||||
const deps_curr = deps.get(contextReWatch);
|
||||
@ -751,7 +758,7 @@ signal.action = function(s, name, ...a) {
|
||||
throw new Error(`Action "${name}" not defined. See ${mark}.actions.`);
|
||||
actions[name].apply(M, a);
|
||||
if (M.skip) return delete M.skip;
|
||||
queueSignalWrite(s, true);
|
||||
queueSignalWrite(s);
|
||||
};
|
||||
signal.on = function on2(s, listener, options = {}) {
|
||||
const { signal: as } = options;
|
||||
@ -787,17 +794,17 @@ signal.clear = function(...signals2) {
|
||||
};
|
||||
var key_reactive = "__dde_reactive";
|
||||
signal.el = function(s, map) {
|
||||
const mapScoped = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
|
||||
const { current } = scope, { scope: sc } = current;
|
||||
const mark_start = createElement.mark({ type: "reactive", component: sc && sc.name || "" }, true);
|
||||
map = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
|
||||
const mark_start = createElement.mark({ type: "reactive", source: new Defined().compact }, true);
|
||||
const mark_end = mark_start.end;
|
||||
const out = enviroment.D.createDocumentFragment();
|
||||
out.append(mark_start, mark_end);
|
||||
const { current } = scope;
|
||||
const reRenderReactiveElement = (v) => {
|
||||
if (!mark_start.parentNode || !mark_end.parentNode)
|
||||
return removeSignalListener(s, reRenderReactiveElement);
|
||||
scope.push(current);
|
||||
let els = mapScoped(v);
|
||||
let els = map(v);
|
||||
scope.pop();
|
||||
if (!Array.isArray(els))
|
||||
els = [els];
|
||||
@ -817,7 +824,7 @@ signal.el = function(s, map) {
|
||||
current.host(on.disconnected(
|
||||
() => (
|
||||
/*! Clears cached elements for reactive element `S.el` */
|
||||
mapScoped.clear()
|
||||
map.clear()
|
||||
)
|
||||
));
|
||||
return out;
|
||||
@ -889,8 +896,9 @@ var signals_config = {
|
||||
function removeSignalsFromElements(s, listener, ...notes) {
|
||||
const { current } = scope;
|
||||
current.host(function(element) {
|
||||
if (!element[key_reactive]) element[key_reactive] = [];
|
||||
element[key_reactive].push([[s, listener], ...notes]);
|
||||
if (element[key_reactive])
|
||||
return element[key_reactive].push([[s, listener], ...notes]);
|
||||
element[key_reactive] = [];
|
||||
if (current.prevent) return;
|
||||
on.disconnected(
|
||||
() => (
|
||||
@ -935,6 +943,7 @@ function toSignal(s, value, actions, readonly = false) {
|
||||
onclear,
|
||||
host,
|
||||
listeners: /* @__PURE__ */ new Set(),
|
||||
defined: new Defined().stack,
|
||||
readonly
|
||||
}),
|
||||
enumerable: false,
|
||||
@ -958,7 +967,7 @@ function write(s, value, force) {
|
||||
const M = s[mark];
|
||||
if (!M || !force && M.value === value) return;
|
||||
M.value = value;
|
||||
queueSignalWrite(s, force);
|
||||
queueSignalWrite(s);
|
||||
return value;
|
||||
}
|
||||
function addSignalListener(s, listener) {
|
||||
@ -995,6 +1004,7 @@ export {
|
||||
dispatchEvent,
|
||||
createElement as el,
|
||||
createElementNS as elNS,
|
||||
elementAttribute,
|
||||
isSignal,
|
||||
lifecyclesToEvents,
|
||||
memo,
|
||||
|
31
dist/esm-with-signals.min.d.ts
vendored
31
dist/esm-with-signals.min.d.ts
vendored
@ -4,7 +4,7 @@ export interface Signal<V, A> {
|
||||
/** The current value of the signal */
|
||||
get(): V;
|
||||
/** Set new value of the signal */
|
||||
set(value: V, force?: boolean): V;
|
||||
set(value: V): V;
|
||||
toJSON(): V;
|
||||
valueOf(): V;
|
||||
}
|
||||
@ -173,30 +173,17 @@ declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
|
||||
export interface On {
|
||||
/** Listens to the DOM event. See {@link Document.addEventListener} */
|
||||
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event] & {
|
||||
target: EL;
|
||||
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
|
||||
<EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: string, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: Event | CustomEvent) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element is connected to the live DOM. In case of custom elements uses [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
connected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<NoInfer<EL>>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
connected<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<El>) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element is disconnected from the live DOM. In case of custom elements uses [`disconnectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
disconnected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
/**
|
||||
* Fires after the next tick of the Javascript event loop.
|
||||
* This is handy for example to apply some property depending on the element content:
|
||||
* ```js
|
||||
* const selected= "Z";
|
||||
* //...
|
||||
* return el("form").append(
|
||||
* el("select", null, on.defer(e=> e.value=selected)).append(
|
||||
* el("option", { value: "A", textContent: "A" }),
|
||||
* //...
|
||||
* el("option", { value: "Z", textContent: "Z" }),
|
||||
* ),
|
||||
* );
|
||||
* ```
|
||||
* */
|
||||
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
|
||||
disconnected<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element attribute changes. In case of custom elements uses [`attributeChangedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
attributeChanged<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<[
|
||||
string,
|
||||
string
|
||||
]>) => any, options?: AddEventListenerOptions): EE;
|
||||
}
|
||||
export const on: On;
|
||||
export type Scope = {
|
||||
|
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
31
dist/esm.d.ts
vendored
31
dist/esm.d.ts
vendored
@ -4,7 +4,7 @@ export interface Signal<V, A> {
|
||||
/** The current value of the signal */
|
||||
get(): V;
|
||||
/** Set new value of the signal */
|
||||
set(value: V, force?: boolean): V;
|
||||
set(value: V): V;
|
||||
toJSON(): V;
|
||||
valueOf(): V;
|
||||
}
|
||||
@ -172,30 +172,17 @@ declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
|
||||
export interface On {
|
||||
/** Listens to the DOM event. See {@link Document.addEventListener} */
|
||||
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event] & {
|
||||
target: EL;
|
||||
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
|
||||
<EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: string, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: Event | CustomEvent) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element is connected to the live DOM. In case of custom elements uses [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
connected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<NoInfer<EL>>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
connected<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<El>) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element is disconnected from the live DOM. In case of custom elements uses [`disconnectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
disconnected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
/**
|
||||
* Fires after the next tick of the Javascript event loop.
|
||||
* This is handy for example to apply some property depending on the element content:
|
||||
* ```js
|
||||
* const selected= "Z";
|
||||
* //...
|
||||
* return el("form").append(
|
||||
* el("select", null, on.defer(e=> e.value=selected)).append(
|
||||
* el("option", { value: "A", textContent: "A" }),
|
||||
* //...
|
||||
* el("option", { value: "Z", textContent: "Z" }),
|
||||
* ),
|
||||
* );
|
||||
* ```
|
||||
* */
|
||||
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
|
||||
disconnected<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element attribute changes. In case of custom elements uses [`attributeChangedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
attributeChanged<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<[
|
||||
string,
|
||||
string
|
||||
]>) => any, options?: AddEventListenerOptions): EE;
|
||||
}
|
||||
export const on: On;
|
||||
export type Scope = {
|
||||
|
197
dist/esm.js
vendored
197
dist/esm.js
vendored
@ -26,7 +26,38 @@ function onAbort(signal, listener) {
|
||||
};
|
||||
}
|
||||
|
||||
// src/dom-lib/common.js
|
||||
// src/signals-lib/common.js
|
||||
var signals_global = {
|
||||
/**
|
||||
* Checks if a value is a signal
|
||||
* @param {any} attributes - Value to check
|
||||
* @returns {boolean} Whether the value is a signal
|
||||
*/
|
||||
isSignal(attributes) {
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* Processes an attribute that might be reactive
|
||||
* @param {Element} obj - Element that owns the attribute
|
||||
* @param {string} key - Attribute name
|
||||
* @param {any} attr - Attribute value
|
||||
* @param {Function} set - Function to set the attribute
|
||||
* @returns {any} Processed attribute value
|
||||
*/
|
||||
processReactiveAttribute(obj, key, attr, set) {
|
||||
return attr;
|
||||
}
|
||||
};
|
||||
function registerReactivity(def, global = true) {
|
||||
if (global) return oAssign(signals_global, def);
|
||||
Object.setPrototypeOf(def, signals_global);
|
||||
return def;
|
||||
}
|
||||
function signals(_this) {
|
||||
return isProtoFrom(_this, signals_global) && _this !== signals_global ? _this : signals_global;
|
||||
}
|
||||
|
||||
// src/dom-common.js
|
||||
var enviroment = {
|
||||
setDeleteAttr,
|
||||
ssr: "",
|
||||
@ -52,7 +83,7 @@ var evc = "dde:connected";
|
||||
var evd = "dde:disconnected";
|
||||
var eva = "dde:attributeChanged";
|
||||
|
||||
// src/dom-lib/events-observer.js
|
||||
// src/events-observer.js
|
||||
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
|
||||
get() {
|
||||
return () => {
|
||||
@ -216,7 +247,7 @@ function connectionsChangesObserverConstructor() {
|
||||
}
|
||||
}
|
||||
|
||||
// src/dom-lib/events.js
|
||||
// src/events.js
|
||||
function dispatchEvent(name, options, host) {
|
||||
if (typeof options === "function") {
|
||||
host = options;
|
||||
@ -238,7 +269,6 @@ function on(event, listener, options) {
|
||||
return element;
|
||||
};
|
||||
}
|
||||
on.defer = (fn) => setTimeout.bind(null, fn, 0);
|
||||
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
|
||||
on.connected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
@ -262,7 +292,10 @@ on.disconnected = function(listener, options) {
|
||||
};
|
||||
};
|
||||
|
||||
// src/dom-lib/scopes.js
|
||||
// src/dom.js
|
||||
function queue(promise) {
|
||||
return enviroment.q(promise);
|
||||
}
|
||||
var scopes = [{
|
||||
get scope() {
|
||||
return enviroment.D.body;
|
||||
@ -274,7 +307,7 @@ var store_abort = /* @__PURE__ */ new WeakMap();
|
||||
var scope = {
|
||||
/**
|
||||
* Gets the current scope
|
||||
* @returns {typeof scopes[number]} Current scope context
|
||||
* @returns {Object} Current scope context
|
||||
*/
|
||||
get current() {
|
||||
return scopes[scopes.length - 1];
|
||||
@ -337,60 +370,6 @@ var scope = {
|
||||
return scopes.pop();
|
||||
}
|
||||
};
|
||||
|
||||
// src/signals-lib/common.js
|
||||
var signals_global = {
|
||||
/**
|
||||
* Checks if a value is a signal
|
||||
* @param {any} attributes - Value to check
|
||||
* @returns {boolean} Whether the value is a signal
|
||||
*/
|
||||
isSignal(attributes) {
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* Processes an attribute that might be reactive
|
||||
* @param {Element} obj - Element that owns the attribute
|
||||
* @param {string} key - Attribute name
|
||||
* @param {any} attr - Attribute value
|
||||
* @param {Function} set - Function to set the attribute
|
||||
* @returns {any} Processed attribute value
|
||||
*/
|
||||
processReactiveAttribute(obj, key, attr, set) {
|
||||
return attr;
|
||||
}
|
||||
};
|
||||
function registerReactivity(def, global = true) {
|
||||
if (global) return oAssign(signals_global, def);
|
||||
Object.setPrototypeOf(def, signals_global);
|
||||
return def;
|
||||
}
|
||||
function signals(_this) {
|
||||
return isProtoFrom(_this, signals_global) && _this !== signals_global ? _this : signals_global;
|
||||
}
|
||||
|
||||
// src/dom-lib/helpers.js
|
||||
function setRemove(obj, prop, key, val) {
|
||||
return obj[(isUndef(val) ? "remove" : "set") + prop](key, val);
|
||||
}
|
||||
function setRemoveNS(obj, prop, key, val, ns = null) {
|
||||
return obj[(isUndef(val) ? "remove" : "set") + prop + "NS"](ns, key, val);
|
||||
}
|
||||
function setDelete(obj, key, val) {
|
||||
Reflect.set(obj, key, val);
|
||||
if (!isUndef(val)) return;
|
||||
return Reflect.deleteProperty(obj, key);
|
||||
}
|
||||
function elementAttribute(element, op, key, value) {
|
||||
if (isInstance(element, enviroment.H))
|
||||
return element[op + "Attribute"](key, value);
|
||||
return element[op + "AttributeNS"](null, key, value);
|
||||
}
|
||||
|
||||
// src/dom-lib/el.js
|
||||
function queue(promise) {
|
||||
return enviroment.q(promise);
|
||||
}
|
||||
function append(...els) {
|
||||
this.appendOriginal(...els);
|
||||
return this;
|
||||
@ -406,8 +385,7 @@ function createElement(tag, attributes, ...addons) {
|
||||
const s = signals(this);
|
||||
let scoped = 0;
|
||||
let el, el_host;
|
||||
const att_type = typeof attributes;
|
||||
if (att_type === "string" || att_type === "number" || s.isSignal(attributes))
|
||||
if (Object(attributes) !== attributes || s.isSignal(attributes))
|
||||
attributes = { textContent: attributes };
|
||||
switch (true) {
|
||||
case typeof tag === "function": {
|
||||
@ -461,6 +439,38 @@ function createElementNS(ns) {
|
||||
return el;
|
||||
};
|
||||
}
|
||||
function simulateSlots(element, root = element) {
|
||||
const mark_e = "\xB9\u2070", mark_s = "\u2713";
|
||||
const slots = Object.fromEntries(
|
||||
Array.from(root.querySelectorAll("slot")).filter((s) => !s.name.endsWith(mark_e)).map((s) => [s.name += mark_e, s])
|
||||
);
|
||||
element.append = new Proxy(element.append, {
|
||||
apply(orig, _, els) {
|
||||
if (els[0] === root) return orig.apply(element, els);
|
||||
for (const el of els) {
|
||||
const name = (el.slot || "") + mark_e;
|
||||
try {
|
||||
elementAttribute(el, "remove", "slot");
|
||||
} catch (_error) {
|
||||
}
|
||||
const slot = slots[name];
|
||||
if (!slot) return;
|
||||
if (!slot.name.startsWith(mark_s)) {
|
||||
slot.childNodes.forEach((c) => c.remove());
|
||||
slot.name = mark_s + name;
|
||||
}
|
||||
slot.append(el);
|
||||
}
|
||||
element.append = orig;
|
||||
return element;
|
||||
}
|
||||
});
|
||||
if (element !== root) {
|
||||
const els = Array.from(element.childNodes);
|
||||
element.append(...els);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
var assign_context = /* @__PURE__ */ new WeakMap();
|
||||
var { setDeleteAttr: setDeleteAttr2 } = enviroment;
|
||||
function assign(element, ...attributes) {
|
||||
@ -523,6 +533,11 @@ function classListDeclarative(element, toggle) {
|
||||
);
|
||||
return element;
|
||||
}
|
||||
function elementAttribute(element, op, key, value) {
|
||||
if (isInstance(element, enviroment.H))
|
||||
return element[op + "Attribute"](key, value);
|
||||
return element[op + "AttributeNS"](null, key, value);
|
||||
}
|
||||
function isPropSetter(el, key) {
|
||||
if (!(key in el)) return false;
|
||||
const des = getPropDescriptor(el, key);
|
||||
@ -546,40 +561,19 @@ function forEachEntries(s, target, element, obj, cb) {
|
||||
cb(key, val);
|
||||
});
|
||||
}
|
||||
|
||||
// src/dom-lib/customElement.js
|
||||
function simulateSlots(element, root = element) {
|
||||
const mark_e = "\xB9\u2070", mark_s = "\u2713";
|
||||
const slots = Object.fromEntries(
|
||||
Array.from(root.querySelectorAll("slot")).filter((s) => !s.name.endsWith(mark_e)).map((s) => [s.name += mark_e, s])
|
||||
);
|
||||
element.append = new Proxy(element.append, {
|
||||
apply(orig, _, els) {
|
||||
if (els[0] === root) return orig.apply(element, els);
|
||||
for (const el of els) {
|
||||
const name = (el.slot || "") + mark_e;
|
||||
try {
|
||||
elementAttribute(el, "remove", "slot");
|
||||
} catch (_error) {
|
||||
}
|
||||
const slot = slots[name];
|
||||
if (!slot) return;
|
||||
if (!slot.name.startsWith(mark_s)) {
|
||||
slot.childNodes.forEach((c) => c.remove());
|
||||
slot.name = mark_s + name;
|
||||
}
|
||||
slot.append(el);
|
||||
}
|
||||
element.append = orig;
|
||||
return element;
|
||||
}
|
||||
});
|
||||
if (element !== root) {
|
||||
const els = Array.from(element.childNodes);
|
||||
element.append(...els);
|
||||
}
|
||||
return root;
|
||||
function setRemove(obj, prop, key, val) {
|
||||
return obj[(isUndef(val) ? "remove" : "set") + prop](key, val);
|
||||
}
|
||||
function setRemoveNS(obj, prop, key, val, ns = null) {
|
||||
return obj[(isUndef(val) ? "remove" : "set") + prop + "NS"](ns, key, val);
|
||||
}
|
||||
function setDelete(obj, key, val) {
|
||||
Reflect.set(obj, key, val);
|
||||
if (!isUndef(val)) return;
|
||||
return Reflect.deleteProperty(obj, key);
|
||||
}
|
||||
|
||||
// src/customElement.js
|
||||
function customElementRender(target, render, props = {}) {
|
||||
const custom_element = target.host || target;
|
||||
scope.push({
|
||||
@ -634,9 +628,9 @@ function memo(key, generator) {
|
||||
memo.isScope = function(obj) {
|
||||
return obj[memoMark];
|
||||
};
|
||||
memo.scope = function memoScopeCreate(fun, { signal, onlyLast } = {}) {
|
||||
memo.scope = function memoScope(fun, { signal, onlyLast } = {}) {
|
||||
let cache = oCreate();
|
||||
function memoScope(...args) {
|
||||
function memoScope2(...args) {
|
||||
if (signal && signal.aborted)
|
||||
return fun.apply(this, args);
|
||||
let cache_local = onlyLast ? cache : oCreate();
|
||||
@ -651,10 +645,10 @@ memo.scope = function memoScopeCreate(fun, { signal, onlyLast } = {}) {
|
||||
cache = cache_local;
|
||||
return out;
|
||||
}
|
||||
memoScope[memoMark] = true;
|
||||
memoScope.clear = () => cache = oCreate();
|
||||
if (signal) signal.addEventListener("abort", memoScope.clear);
|
||||
return memoScope;
|
||||
memoScope2[memoMark] = true;
|
||||
memoScope2.clear = () => cache = oCreate();
|
||||
if (signal) signal.addEventListener("abort", memoScope2.clear);
|
||||
return memoScope2;
|
||||
};
|
||||
export {
|
||||
assign,
|
||||
@ -668,6 +662,7 @@ export {
|
||||
dispatchEvent,
|
||||
createElement as el,
|
||||
createElementNS as elNS,
|
||||
elementAttribute,
|
||||
lifecyclesToEvents,
|
||||
memo,
|
||||
on,
|
||||
|
31
dist/esm.min.d.ts
vendored
31
dist/esm.min.d.ts
vendored
@ -4,7 +4,7 @@ export interface Signal<V, A> {
|
||||
/** The current value of the signal */
|
||||
get(): V;
|
||||
/** Set new value of the signal */
|
||||
set(value: V, force?: boolean): V;
|
||||
set(value: V): V;
|
||||
toJSON(): V;
|
||||
valueOf(): V;
|
||||
}
|
||||
@ -172,30 +172,17 @@ declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
|
||||
export interface On {
|
||||
/** Listens to the DOM event. See {@link Document.addEventListener} */
|
||||
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event] & {
|
||||
target: EL;
|
||||
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
|
||||
<EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: string, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: Event | CustomEvent) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element is connected to the live DOM. In case of custom elements uses [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
connected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<NoInfer<EL>>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
connected<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<El>) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element is disconnected from the live DOM. In case of custom elements uses [`disconnectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
disconnected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
/**
|
||||
* Fires after the next tick of the Javascript event loop.
|
||||
* This is handy for example to apply some property depending on the element content:
|
||||
* ```js
|
||||
* const selected= "Z";
|
||||
* //...
|
||||
* return el("form").append(
|
||||
* el("select", null, on.defer(e=> e.value=selected)).append(
|
||||
* el("option", { value: "A", textContent: "A" }),
|
||||
* //...
|
||||
* el("option", { value: "Z", textContent: "Z" }),
|
||||
* ),
|
||||
* );
|
||||
* ```
|
||||
* */
|
||||
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
|
||||
disconnected<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element attribute changes. In case of custom elements uses [`attributeChangedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
attributeChanged<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<[
|
||||
string,
|
||||
string
|
||||
]>) => any, options?: AddEventListenerOptions): EE;
|
||||
}
|
||||
export const on: On;
|
||||
export type Scope = {
|
||||
|
2
dist/esm.min.js
vendored
2
dist/esm.min.js
vendored
File diff suppressed because one or more lines are too long
31
dist/iife-with-signals.d.ts
vendored
31
dist/iife-with-signals.d.ts
vendored
@ -4,7 +4,7 @@ export interface Signal<V, A> {
|
||||
/** The current value of the signal */
|
||||
get(): V;
|
||||
/** Set new value of the signal */
|
||||
set(value: V, force?: boolean): V;
|
||||
set(value: V): V;
|
||||
toJSON(): V;
|
||||
valueOf(): V;
|
||||
}
|
||||
@ -173,30 +173,17 @@ declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
|
||||
export interface On {
|
||||
/** Listens to the DOM event. See {@link Document.addEventListener} */
|
||||
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event] & {
|
||||
target: EL;
|
||||
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
|
||||
<EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: string, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: Event | CustomEvent) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element is connected to the live DOM. In case of custom elements uses [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
connected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<NoInfer<EL>>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
connected<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<El>) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element is disconnected from the live DOM. In case of custom elements uses [`disconnectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
disconnected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
/**
|
||||
* Fires after the next tick of the Javascript event loop.
|
||||
* This is handy for example to apply some property depending on the element content:
|
||||
* ```js
|
||||
* const selected= "Z";
|
||||
* //...
|
||||
* return el("form").append(
|
||||
* el("select", null, on.defer(e=> e.value=selected)).append(
|
||||
* el("option", { value: "A", textContent: "A" }),
|
||||
* //...
|
||||
* el("option", { value: "Z", textContent: "Z" }),
|
||||
* ),
|
||||
* );
|
||||
* ```
|
||||
* */
|
||||
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
|
||||
disconnected<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element attribute changes. In case of custom elements uses [`attributeChangedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
attributeChanged<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<[
|
||||
string,
|
||||
string
|
||||
]>) => any, options?: AddEventListenerOptions): EE;
|
||||
}
|
||||
export const on: On;
|
||||
export type Scope = {
|
||||
|
246
dist/iife-with-signals.js
vendored
246
dist/iife-with-signals.js
vendored
@ -32,6 +32,7 @@ var DDE = (() => {
|
||||
dispatchEvent: () => dispatchEvent,
|
||||
el: () => createElement,
|
||||
elNS: () => createElementNS,
|
||||
elementAttribute: () => elementAttribute,
|
||||
isSignal: () => isSignal,
|
||||
lifecyclesToEvents: () => lifecyclesToEvents,
|
||||
memo: () => memo,
|
||||
@ -86,8 +87,52 @@ 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
|
||||
// src/signals-lib/common.js
|
||||
var signals_global = {
|
||||
/**
|
||||
* Checks if a value is a signal
|
||||
* @param {any} attributes - Value to check
|
||||
* @returns {boolean} Whether the value is a signal
|
||||
*/
|
||||
isSignal(attributes) {
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* Processes an attribute that might be reactive
|
||||
* @param {Element} obj - Element that owns the attribute
|
||||
* @param {string} key - Attribute name
|
||||
* @param {any} attr - Attribute value
|
||||
* @param {Function} set - Function to set the attribute
|
||||
* @returns {any} Processed attribute value
|
||||
*/
|
||||
processReactiveAttribute(obj, key, attr, set) {
|
||||
return attr;
|
||||
}
|
||||
};
|
||||
function registerReactivity(def, global = true) {
|
||||
if (global) return oAssign(signals_global, def);
|
||||
Object.setPrototypeOf(def, signals_global);
|
||||
return def;
|
||||
}
|
||||
function signals(_this) {
|
||||
return isProtoFrom(_this, signals_global) && _this !== signals_global ? _this : signals_global;
|
||||
}
|
||||
|
||||
// src/dom-common.js
|
||||
var enviroment = {
|
||||
setDeleteAttr,
|
||||
ssr: "",
|
||||
@ -113,7 +158,7 @@ var DDE = (() => {
|
||||
var evd = "dde:disconnected";
|
||||
var eva = "dde:attributeChanged";
|
||||
|
||||
// src/dom-lib/events-observer.js
|
||||
// src/events-observer.js
|
||||
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
|
||||
get() {
|
||||
return () => {
|
||||
@ -277,7 +322,7 @@ var DDE = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
// src/dom-lib/events.js
|
||||
// src/events.js
|
||||
function dispatchEvent(name, options, host) {
|
||||
if (typeof options === "function") {
|
||||
host = options;
|
||||
@ -299,7 +344,6 @@ var DDE = (() => {
|
||||
return element;
|
||||
};
|
||||
}
|
||||
on.defer = (fn) => setTimeout.bind(null, fn, 0);
|
||||
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
|
||||
on.connected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
@ -323,7 +367,10 @@ var DDE = (() => {
|
||||
};
|
||||
};
|
||||
|
||||
// src/dom-lib/scopes.js
|
||||
// src/dom.js
|
||||
function queue(promise) {
|
||||
return enviroment.q(promise);
|
||||
}
|
||||
var scopes = [{
|
||||
get scope() {
|
||||
return enviroment.D.body;
|
||||
@ -335,7 +382,7 @@ var DDE = (() => {
|
||||
var scope = {
|
||||
/**
|
||||
* Gets the current scope
|
||||
* @returns {typeof scopes[number]} Current scope context
|
||||
* @returns {Object} Current scope context
|
||||
*/
|
||||
get current() {
|
||||
return scopes[scopes.length - 1];
|
||||
@ -398,60 +445,6 @@ var DDE = (() => {
|
||||
return scopes.pop();
|
||||
}
|
||||
};
|
||||
|
||||
// src/signals-lib/common.js
|
||||
var signals_global = {
|
||||
/**
|
||||
* Checks if a value is a signal
|
||||
* @param {any} attributes - Value to check
|
||||
* @returns {boolean} Whether the value is a signal
|
||||
*/
|
||||
isSignal(attributes) {
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* Processes an attribute that might be reactive
|
||||
* @param {Element} obj - Element that owns the attribute
|
||||
* @param {string} key - Attribute name
|
||||
* @param {any} attr - Attribute value
|
||||
* @param {Function} set - Function to set the attribute
|
||||
* @returns {any} Processed attribute value
|
||||
*/
|
||||
processReactiveAttribute(obj, key, attr, set) {
|
||||
return attr;
|
||||
}
|
||||
};
|
||||
function registerReactivity(def, global = true) {
|
||||
if (global) return oAssign(signals_global, def);
|
||||
Object.setPrototypeOf(def, signals_global);
|
||||
return def;
|
||||
}
|
||||
function signals(_this) {
|
||||
return isProtoFrom(_this, signals_global) && _this !== signals_global ? _this : signals_global;
|
||||
}
|
||||
|
||||
// src/dom-lib/helpers.js
|
||||
function setRemove(obj, prop, key, val) {
|
||||
return obj[(isUndef(val) ? "remove" : "set") + prop](key, val);
|
||||
}
|
||||
function setRemoveNS(obj, prop, key, val, ns = null) {
|
||||
return obj[(isUndef(val) ? "remove" : "set") + prop + "NS"](ns, key, val);
|
||||
}
|
||||
function setDelete(obj, key, val) {
|
||||
Reflect.set(obj, key, val);
|
||||
if (!isUndef(val)) return;
|
||||
return Reflect.deleteProperty(obj, key);
|
||||
}
|
||||
function elementAttribute(element, op, key, value) {
|
||||
if (isInstance(element, enviroment.H))
|
||||
return element[op + "Attribute"](key, value);
|
||||
return element[op + "AttributeNS"](null, key, value);
|
||||
}
|
||||
|
||||
// src/dom-lib/el.js
|
||||
function queue(promise) {
|
||||
return enviroment.q(promise);
|
||||
}
|
||||
function append(...els) {
|
||||
this.appendOriginal(...els);
|
||||
return this;
|
||||
@ -467,8 +460,7 @@ var DDE = (() => {
|
||||
const s = signals(this);
|
||||
let scoped = 0;
|
||||
let el, el_host;
|
||||
const att_type = typeof attributes;
|
||||
if (att_type === "string" || att_type === "number" || s.isSignal(attributes))
|
||||
if (Object(attributes) !== attributes || s.isSignal(attributes))
|
||||
attributes = { textContent: attributes };
|
||||
switch (true) {
|
||||
case typeof tag === "function": {
|
||||
@ -522,6 +514,38 @@ var DDE = (() => {
|
||||
return el;
|
||||
};
|
||||
}
|
||||
function simulateSlots(element, root = element) {
|
||||
const mark_e = "\xB9\u2070", mark_s = "\u2713";
|
||||
const slots = Object.fromEntries(
|
||||
Array.from(root.querySelectorAll("slot")).filter((s) => !s.name.endsWith(mark_e)).map((s) => [s.name += mark_e, s])
|
||||
);
|
||||
element.append = new Proxy(element.append, {
|
||||
apply(orig, _, els) {
|
||||
if (els[0] === root) return orig.apply(element, els);
|
||||
for (const el of els) {
|
||||
const name = (el.slot || "") + mark_e;
|
||||
try {
|
||||
elementAttribute(el, "remove", "slot");
|
||||
} catch (_error) {
|
||||
}
|
||||
const slot = slots[name];
|
||||
if (!slot) return;
|
||||
if (!slot.name.startsWith(mark_s)) {
|
||||
slot.childNodes.forEach((c) => c.remove());
|
||||
slot.name = mark_s + name;
|
||||
}
|
||||
slot.append(el);
|
||||
}
|
||||
element.append = orig;
|
||||
return element;
|
||||
}
|
||||
});
|
||||
if (element !== root) {
|
||||
const els = Array.from(element.childNodes);
|
||||
element.append(...els);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
var assign_context = /* @__PURE__ */ new WeakMap();
|
||||
var { setDeleteAttr: setDeleteAttr2 } = enviroment;
|
||||
function assign(element, ...attributes) {
|
||||
@ -584,6 +608,11 @@ var DDE = (() => {
|
||||
);
|
||||
return element;
|
||||
}
|
||||
function elementAttribute(element, op, key, value) {
|
||||
if (isInstance(element, enviroment.H))
|
||||
return element[op + "Attribute"](key, value);
|
||||
return element[op + "AttributeNS"](null, key, value);
|
||||
}
|
||||
function isPropSetter(el, key) {
|
||||
if (!(key in el)) return false;
|
||||
const des = getPropDescriptor(el, key);
|
||||
@ -607,40 +636,19 @@ var DDE = (() => {
|
||||
cb(key, val);
|
||||
});
|
||||
}
|
||||
|
||||
// src/dom-lib/customElement.js
|
||||
function simulateSlots(element, root = element) {
|
||||
const mark_e = "\xB9\u2070", mark_s = "\u2713";
|
||||
const slots = Object.fromEntries(
|
||||
Array.from(root.querySelectorAll("slot")).filter((s) => !s.name.endsWith(mark_e)).map((s) => [s.name += mark_e, s])
|
||||
);
|
||||
element.append = new Proxy(element.append, {
|
||||
apply(orig, _, els) {
|
||||
if (els[0] === root) return orig.apply(element, els);
|
||||
for (const el of els) {
|
||||
const name = (el.slot || "") + mark_e;
|
||||
try {
|
||||
elementAttribute(el, "remove", "slot");
|
||||
} catch (_error) {
|
||||
}
|
||||
const slot = slots[name];
|
||||
if (!slot) return;
|
||||
if (!slot.name.startsWith(mark_s)) {
|
||||
slot.childNodes.forEach((c) => c.remove());
|
||||
slot.name = mark_s + name;
|
||||
}
|
||||
slot.append(el);
|
||||
}
|
||||
element.append = orig;
|
||||
return element;
|
||||
}
|
||||
});
|
||||
if (element !== root) {
|
||||
const els = Array.from(element.childNodes);
|
||||
element.append(...els);
|
||||
}
|
||||
return root;
|
||||
function setRemove(obj, prop, key, val) {
|
||||
return obj[(isUndef(val) ? "remove" : "set") + prop](key, val);
|
||||
}
|
||||
function setRemoveNS(obj, prop, key, val, ns = null) {
|
||||
return obj[(isUndef(val) ? "remove" : "set") + prop + "NS"](ns, key, val);
|
||||
}
|
||||
function setDelete(obj, key, val) {
|
||||
Reflect.set(obj, key, val);
|
||||
if (!isUndef(val)) return;
|
||||
return Reflect.deleteProperty(obj, key);
|
||||
}
|
||||
|
||||
// src/customElement.js
|
||||
function customElementRender(target, render, props = {}) {
|
||||
const custom_element = target.host || target;
|
||||
scope.push({
|
||||
@ -695,9 +703,9 @@ var DDE = (() => {
|
||||
memo.isScope = function(obj) {
|
||||
return obj[memoMark];
|
||||
};
|
||||
memo.scope = function memoScopeCreate(fun, { signal: signal2, onlyLast } = {}) {
|
||||
memo.scope = function memoScope(fun, { signal: signal2, onlyLast } = {}) {
|
||||
let cache = oCreate();
|
||||
function memoScope(...args) {
|
||||
function memoScope2(...args) {
|
||||
if (signal2 && signal2.aborted)
|
||||
return fun.apply(this, args);
|
||||
let cache_local = onlyLast ? cache : oCreate();
|
||||
@ -712,28 +720,28 @@ var DDE = (() => {
|
||||
cache = cache_local;
|
||||
return out;
|
||||
}
|
||||
memoScope[memoMark] = true;
|
||||
memoScope.clear = () => cache = oCreate();
|
||||
if (signal2) signal2.addEventListener("abort", memoScope.clear);
|
||||
return memoScope;
|
||||
memoScope2[memoMark] = true;
|
||||
memoScope2.clear = () => cache = oCreate();
|
||||
if (signal2) signal2.addEventListener("abort", memoScope2.clear);
|
||||
return memoScope2;
|
||||
};
|
||||
|
||||
// src/signals-lib/helpers.js
|
||||
var mark = "__dde_signal";
|
||||
var queueSignalWrite = /* @__PURE__ */ (() => {
|
||||
let pendingSignals = /* @__PURE__ */ new Map();
|
||||
let pendingSignals = /* @__PURE__ */ new Set();
|
||||
let scheduled = false;
|
||||
function flushSignals() {
|
||||
scheduled = false;
|
||||
const todo = pendingSignals;
|
||||
pendingSignals = /* @__PURE__ */ new Map();
|
||||
for (const [signal2, force] of todo) {
|
||||
pendingSignals = /* @__PURE__ */ new Set();
|
||||
for (const signal2 of todo) {
|
||||
const M = signal2[mark];
|
||||
if (M) M.listeners.forEach((l) => l(M.value, force));
|
||||
if (M) M.listeners.forEach((l) => l(M.value));
|
||||
}
|
||||
}
|
||||
return function(s, force = false) {
|
||||
pendingSignals.set(s, pendingSignals.get(s) || force);
|
||||
return function(s) {
|
||||
pendingSignals.add(s);
|
||||
if (scheduled) return;
|
||||
scheduled = true;
|
||||
queueMicrotask(flushSignals);
|
||||
@ -770,11 +778,11 @@ var DDE = (() => {
|
||||
return create(false, value, actions);
|
||||
if (isSignal(value)) return value;
|
||||
const out = create(true);
|
||||
function contextReWatch(_, force) {
|
||||
function contextReWatch() {
|
||||
const [origin, ...deps_old] = deps.get(contextReWatch);
|
||||
deps.set(contextReWatch, /* @__PURE__ */ new Set([origin]));
|
||||
stack_watch.push(contextReWatch);
|
||||
write(out, value(), force);
|
||||
write(out, value());
|
||||
stack_watch.pop();
|
||||
if (!deps_old.length) return;
|
||||
const deps_curr = deps.get(contextReWatch);
|
||||
@ -796,7 +804,7 @@ var DDE = (() => {
|
||||
throw new Error(`Action "${name}" not defined. See ${mark}.actions.`);
|
||||
actions[name].apply(M, a);
|
||||
if (M.skip) return delete M.skip;
|
||||
queueSignalWrite(s, true);
|
||||
queueSignalWrite(s);
|
||||
};
|
||||
signal.on = function on2(s, listener, options = {}) {
|
||||
const { signal: as } = options;
|
||||
@ -832,17 +840,17 @@ var DDE = (() => {
|
||||
};
|
||||
var key_reactive = "__dde_reactive";
|
||||
signal.el = function(s, map) {
|
||||
const mapScoped = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
|
||||
const { current } = scope, { scope: sc } = current;
|
||||
const mark_start = createElement.mark({ type: "reactive", component: sc && sc.name || "" }, true);
|
||||
map = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
|
||||
const mark_start = createElement.mark({ type: "reactive", source: new Defined().compact }, true);
|
||||
const mark_end = mark_start.end;
|
||||
const out = enviroment.D.createDocumentFragment();
|
||||
out.append(mark_start, mark_end);
|
||||
const { current } = scope;
|
||||
const reRenderReactiveElement = (v) => {
|
||||
if (!mark_start.parentNode || !mark_end.parentNode)
|
||||
return removeSignalListener(s, reRenderReactiveElement);
|
||||
scope.push(current);
|
||||
let els = mapScoped(v);
|
||||
let els = map(v);
|
||||
scope.pop();
|
||||
if (!Array.isArray(els))
|
||||
els = [els];
|
||||
@ -862,7 +870,7 @@ var DDE = (() => {
|
||||
current.host(on.disconnected(
|
||||
() => (
|
||||
/*! Clears cached elements for reactive element `S.el` */
|
||||
mapScoped.clear()
|
||||
map.clear()
|
||||
)
|
||||
));
|
||||
return out;
|
||||
@ -934,8 +942,9 @@ var DDE = (() => {
|
||||
function removeSignalsFromElements(s, listener, ...notes) {
|
||||
const { current } = scope;
|
||||
current.host(function(element) {
|
||||
if (!element[key_reactive]) element[key_reactive] = [];
|
||||
element[key_reactive].push([[s, listener], ...notes]);
|
||||
if (element[key_reactive])
|
||||
return element[key_reactive].push([[s, listener], ...notes]);
|
||||
element[key_reactive] = [];
|
||||
if (current.prevent) return;
|
||||
on.disconnected(
|
||||
() => (
|
||||
@ -980,6 +989,7 @@ var DDE = (() => {
|
||||
onclear,
|
||||
host,
|
||||
listeners: /* @__PURE__ */ new Set(),
|
||||
defined: new Defined().stack,
|
||||
readonly
|
||||
}),
|
||||
enumerable: false,
|
||||
@ -1003,7 +1013,7 @@ var DDE = (() => {
|
||||
const M = s[mark];
|
||||
if (!M || !force && M.value === value) return;
|
||||
M.value = value;
|
||||
queueSignalWrite(s, force);
|
||||
queueSignalWrite(s);
|
||||
return value;
|
||||
}
|
||||
function addSignalListener(s, listener) {
|
||||
|
31
dist/iife-with-signals.min.d.ts
vendored
31
dist/iife-with-signals.min.d.ts
vendored
@ -4,7 +4,7 @@ export interface Signal<V, A> {
|
||||
/** The current value of the signal */
|
||||
get(): V;
|
||||
/** Set new value of the signal */
|
||||
set(value: V, force?: boolean): V;
|
||||
set(value: V): V;
|
||||
toJSON(): V;
|
||||
valueOf(): V;
|
||||
}
|
||||
@ -173,30 +173,17 @@ declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
|
||||
export interface On {
|
||||
/** Listens to the DOM event. See {@link Document.addEventListener} */
|
||||
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event] & {
|
||||
target: EL;
|
||||
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
|
||||
<EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: string, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: Event | CustomEvent) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element is connected to the live DOM. In case of custom elements uses [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
connected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<NoInfer<EL>>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
connected<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<El>) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element is disconnected from the live DOM. In case of custom elements uses [`disconnectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
disconnected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
/**
|
||||
* Fires after the next tick of the Javascript event loop.
|
||||
* This is handy for example to apply some property depending on the element content:
|
||||
* ```js
|
||||
* const selected= "Z";
|
||||
* //...
|
||||
* return el("form").append(
|
||||
* el("select", null, on.defer(e=> e.value=selected)).append(
|
||||
* el("option", { value: "A", textContent: "A" }),
|
||||
* //...
|
||||
* el("option", { value: "Z", textContent: "Z" }),
|
||||
* ),
|
||||
* );
|
||||
* ```
|
||||
* */
|
||||
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
|
||||
disconnected<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element attribute changes. In case of custom elements uses [`attributeChangedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
attributeChanged<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<[
|
||||
string,
|
||||
string
|
||||
]>) => any, options?: AddEventListenerOptions): EE;
|
||||
}
|
||||
export const on: On;
|
||||
export type Scope = {
|
||||
|
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
31
dist/iife.d.ts
vendored
31
dist/iife.d.ts
vendored
@ -4,7 +4,7 @@ export interface Signal<V, A> {
|
||||
/** The current value of the signal */
|
||||
get(): V;
|
||||
/** Set new value of the signal */
|
||||
set(value: V, force?: boolean): V;
|
||||
set(value: V): V;
|
||||
toJSON(): V;
|
||||
valueOf(): V;
|
||||
}
|
||||
@ -172,30 +172,17 @@ declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
|
||||
export interface On {
|
||||
/** Listens to the DOM event. See {@link Document.addEventListener} */
|
||||
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event] & {
|
||||
target: EL;
|
||||
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
|
||||
<EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: string, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: Event | CustomEvent) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element is connected to the live DOM. In case of custom elements uses [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
connected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<NoInfer<EL>>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
connected<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<El>) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element is disconnected from the live DOM. In case of custom elements uses [`disconnectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
disconnected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
/**
|
||||
* Fires after the next tick of the Javascript event loop.
|
||||
* This is handy for example to apply some property depending on the element content:
|
||||
* ```js
|
||||
* const selected= "Z";
|
||||
* //...
|
||||
* return el("form").append(
|
||||
* el("select", null, on.defer(e=> e.value=selected)).append(
|
||||
* el("option", { value: "A", textContent: "A" }),
|
||||
* //...
|
||||
* el("option", { value: "Z", textContent: "Z" }),
|
||||
* ),
|
||||
* );
|
||||
* ```
|
||||
* */
|
||||
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
|
||||
disconnected<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element attribute changes. In case of custom elements uses [`attributeChangedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
attributeChanged<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<[
|
||||
string,
|
||||
string
|
||||
]>) => any, options?: AddEventListenerOptions): EE;
|
||||
}
|
||||
export const on: On;
|
||||
export type Scope = {
|
||||
|
197
dist/iife.js
vendored
197
dist/iife.js
vendored
@ -31,6 +31,7 @@ var DDE = (() => {
|
||||
dispatchEvent: () => dispatchEvent,
|
||||
el: () => createElement,
|
||||
elNS: () => createElementNS,
|
||||
elementAttribute: () => elementAttribute,
|
||||
lifecyclesToEvents: () => lifecyclesToEvents,
|
||||
memo: () => memo,
|
||||
on: () => on,
|
||||
@ -68,7 +69,38 @@ var DDE = (() => {
|
||||
};
|
||||
}
|
||||
|
||||
// src/dom-lib/common.js
|
||||
// src/signals-lib/common.js
|
||||
var signals_global = {
|
||||
/**
|
||||
* Checks if a value is a signal
|
||||
* @param {any} attributes - Value to check
|
||||
* @returns {boolean} Whether the value is a signal
|
||||
*/
|
||||
isSignal(attributes) {
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* Processes an attribute that might be reactive
|
||||
* @param {Element} obj - Element that owns the attribute
|
||||
* @param {string} key - Attribute name
|
||||
* @param {any} attr - Attribute value
|
||||
* @param {Function} set - Function to set the attribute
|
||||
* @returns {any} Processed attribute value
|
||||
*/
|
||||
processReactiveAttribute(obj, key, attr, set) {
|
||||
return attr;
|
||||
}
|
||||
};
|
||||
function registerReactivity(def, global = true) {
|
||||
if (global) return oAssign(signals_global, def);
|
||||
Object.setPrototypeOf(def, signals_global);
|
||||
return def;
|
||||
}
|
||||
function signals(_this) {
|
||||
return isProtoFrom(_this, signals_global) && _this !== signals_global ? _this : signals_global;
|
||||
}
|
||||
|
||||
// src/dom-common.js
|
||||
var enviroment = {
|
||||
setDeleteAttr,
|
||||
ssr: "",
|
||||
@ -94,7 +126,7 @@ var DDE = (() => {
|
||||
var evd = "dde:disconnected";
|
||||
var eva = "dde:attributeChanged";
|
||||
|
||||
// src/dom-lib/events-observer.js
|
||||
// src/events-observer.js
|
||||
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
|
||||
get() {
|
||||
return () => {
|
||||
@ -258,7 +290,7 @@ var DDE = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
// src/dom-lib/events.js
|
||||
// src/events.js
|
||||
function dispatchEvent(name, options, host) {
|
||||
if (typeof options === "function") {
|
||||
host = options;
|
||||
@ -280,7 +312,6 @@ var DDE = (() => {
|
||||
return element;
|
||||
};
|
||||
}
|
||||
on.defer = (fn) => setTimeout.bind(null, fn, 0);
|
||||
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
|
||||
on.connected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
@ -304,7 +335,10 @@ var DDE = (() => {
|
||||
};
|
||||
};
|
||||
|
||||
// src/dom-lib/scopes.js
|
||||
// src/dom.js
|
||||
function queue(promise) {
|
||||
return enviroment.q(promise);
|
||||
}
|
||||
var scopes = [{
|
||||
get scope() {
|
||||
return enviroment.D.body;
|
||||
@ -316,7 +350,7 @@ var DDE = (() => {
|
||||
var scope = {
|
||||
/**
|
||||
* Gets the current scope
|
||||
* @returns {typeof scopes[number]} Current scope context
|
||||
* @returns {Object} Current scope context
|
||||
*/
|
||||
get current() {
|
||||
return scopes[scopes.length - 1];
|
||||
@ -379,60 +413,6 @@ var DDE = (() => {
|
||||
return scopes.pop();
|
||||
}
|
||||
};
|
||||
|
||||
// src/signals-lib/common.js
|
||||
var signals_global = {
|
||||
/**
|
||||
* Checks if a value is a signal
|
||||
* @param {any} attributes - Value to check
|
||||
* @returns {boolean} Whether the value is a signal
|
||||
*/
|
||||
isSignal(attributes) {
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* Processes an attribute that might be reactive
|
||||
* @param {Element} obj - Element that owns the attribute
|
||||
* @param {string} key - Attribute name
|
||||
* @param {any} attr - Attribute value
|
||||
* @param {Function} set - Function to set the attribute
|
||||
* @returns {any} Processed attribute value
|
||||
*/
|
||||
processReactiveAttribute(obj, key, attr, set) {
|
||||
return attr;
|
||||
}
|
||||
};
|
||||
function registerReactivity(def, global = true) {
|
||||
if (global) return oAssign(signals_global, def);
|
||||
Object.setPrototypeOf(def, signals_global);
|
||||
return def;
|
||||
}
|
||||
function signals(_this) {
|
||||
return isProtoFrom(_this, signals_global) && _this !== signals_global ? _this : signals_global;
|
||||
}
|
||||
|
||||
// src/dom-lib/helpers.js
|
||||
function setRemove(obj, prop, key, val) {
|
||||
return obj[(isUndef(val) ? "remove" : "set") + prop](key, val);
|
||||
}
|
||||
function setRemoveNS(obj, prop, key, val, ns = null) {
|
||||
return obj[(isUndef(val) ? "remove" : "set") + prop + "NS"](ns, key, val);
|
||||
}
|
||||
function setDelete(obj, key, val) {
|
||||
Reflect.set(obj, key, val);
|
||||
if (!isUndef(val)) return;
|
||||
return Reflect.deleteProperty(obj, key);
|
||||
}
|
||||
function elementAttribute(element, op, key, value) {
|
||||
if (isInstance(element, enviroment.H))
|
||||
return element[op + "Attribute"](key, value);
|
||||
return element[op + "AttributeNS"](null, key, value);
|
||||
}
|
||||
|
||||
// src/dom-lib/el.js
|
||||
function queue(promise) {
|
||||
return enviroment.q(promise);
|
||||
}
|
||||
function append(...els) {
|
||||
this.appendOriginal(...els);
|
||||
return this;
|
||||
@ -448,8 +428,7 @@ var DDE = (() => {
|
||||
const s = signals(this);
|
||||
let scoped = 0;
|
||||
let el, el_host;
|
||||
const att_type = typeof attributes;
|
||||
if (att_type === "string" || att_type === "number" || s.isSignal(attributes))
|
||||
if (Object(attributes) !== attributes || s.isSignal(attributes))
|
||||
attributes = { textContent: attributes };
|
||||
switch (true) {
|
||||
case typeof tag === "function": {
|
||||
@ -503,6 +482,38 @@ var DDE = (() => {
|
||||
return el;
|
||||
};
|
||||
}
|
||||
function simulateSlots(element, root = element) {
|
||||
const mark_e = "\xB9\u2070", mark_s = "\u2713";
|
||||
const slots = Object.fromEntries(
|
||||
Array.from(root.querySelectorAll("slot")).filter((s) => !s.name.endsWith(mark_e)).map((s) => [s.name += mark_e, s])
|
||||
);
|
||||
element.append = new Proxy(element.append, {
|
||||
apply(orig, _, els) {
|
||||
if (els[0] === root) return orig.apply(element, els);
|
||||
for (const el of els) {
|
||||
const name = (el.slot || "") + mark_e;
|
||||
try {
|
||||
elementAttribute(el, "remove", "slot");
|
||||
} catch (_error) {
|
||||
}
|
||||
const slot = slots[name];
|
||||
if (!slot) return;
|
||||
if (!slot.name.startsWith(mark_s)) {
|
||||
slot.childNodes.forEach((c) => c.remove());
|
||||
slot.name = mark_s + name;
|
||||
}
|
||||
slot.append(el);
|
||||
}
|
||||
element.append = orig;
|
||||
return element;
|
||||
}
|
||||
});
|
||||
if (element !== root) {
|
||||
const els = Array.from(element.childNodes);
|
||||
element.append(...els);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
var assign_context = /* @__PURE__ */ new WeakMap();
|
||||
var { setDeleteAttr: setDeleteAttr2 } = enviroment;
|
||||
function assign(element, ...attributes) {
|
||||
@ -565,6 +576,11 @@ var DDE = (() => {
|
||||
);
|
||||
return element;
|
||||
}
|
||||
function elementAttribute(element, op, key, value) {
|
||||
if (isInstance(element, enviroment.H))
|
||||
return element[op + "Attribute"](key, value);
|
||||
return element[op + "AttributeNS"](null, key, value);
|
||||
}
|
||||
function isPropSetter(el, key) {
|
||||
if (!(key in el)) return false;
|
||||
const des = getPropDescriptor(el, key);
|
||||
@ -588,40 +604,19 @@ var DDE = (() => {
|
||||
cb(key, val);
|
||||
});
|
||||
}
|
||||
|
||||
// src/dom-lib/customElement.js
|
||||
function simulateSlots(element, root = element) {
|
||||
const mark_e = "\xB9\u2070", mark_s = "\u2713";
|
||||
const slots = Object.fromEntries(
|
||||
Array.from(root.querySelectorAll("slot")).filter((s) => !s.name.endsWith(mark_e)).map((s) => [s.name += mark_e, s])
|
||||
);
|
||||
element.append = new Proxy(element.append, {
|
||||
apply(orig, _, els) {
|
||||
if (els[0] === root) return orig.apply(element, els);
|
||||
for (const el of els) {
|
||||
const name = (el.slot || "") + mark_e;
|
||||
try {
|
||||
elementAttribute(el, "remove", "slot");
|
||||
} catch (_error) {
|
||||
}
|
||||
const slot = slots[name];
|
||||
if (!slot) return;
|
||||
if (!slot.name.startsWith(mark_s)) {
|
||||
slot.childNodes.forEach((c) => c.remove());
|
||||
slot.name = mark_s + name;
|
||||
}
|
||||
slot.append(el);
|
||||
}
|
||||
element.append = orig;
|
||||
return element;
|
||||
}
|
||||
});
|
||||
if (element !== root) {
|
||||
const els = Array.from(element.childNodes);
|
||||
element.append(...els);
|
||||
}
|
||||
return root;
|
||||
function setRemove(obj, prop, key, val) {
|
||||
return obj[(isUndef(val) ? "remove" : "set") + prop](key, val);
|
||||
}
|
||||
function setRemoveNS(obj, prop, key, val, ns = null) {
|
||||
return obj[(isUndef(val) ? "remove" : "set") + prop + "NS"](ns, key, val);
|
||||
}
|
||||
function setDelete(obj, key, val) {
|
||||
Reflect.set(obj, key, val);
|
||||
if (!isUndef(val)) return;
|
||||
return Reflect.deleteProperty(obj, key);
|
||||
}
|
||||
|
||||
// src/customElement.js
|
||||
function customElementRender(target, render, props = {}) {
|
||||
const custom_element = target.host || target;
|
||||
scope.push({
|
||||
@ -676,9 +671,9 @@ var DDE = (() => {
|
||||
memo.isScope = function(obj) {
|
||||
return obj[memoMark];
|
||||
};
|
||||
memo.scope = function memoScopeCreate(fun, { signal, onlyLast } = {}) {
|
||||
memo.scope = function memoScope(fun, { signal, onlyLast } = {}) {
|
||||
let cache = oCreate();
|
||||
function memoScope(...args) {
|
||||
function memoScope2(...args) {
|
||||
if (signal && signal.aborted)
|
||||
return fun.apply(this, args);
|
||||
let cache_local = onlyLast ? cache : oCreate();
|
||||
@ -693,10 +688,10 @@ var DDE = (() => {
|
||||
cache = cache_local;
|
||||
return out;
|
||||
}
|
||||
memoScope[memoMark] = true;
|
||||
memoScope.clear = () => cache = oCreate();
|
||||
if (signal) signal.addEventListener("abort", memoScope.clear);
|
||||
return memoScope;
|
||||
memoScope2[memoMark] = true;
|
||||
memoScope2.clear = () => cache = oCreate();
|
||||
if (signal) signal.addEventListener("abort", memoScope2.clear);
|
||||
return memoScope2;
|
||||
};
|
||||
return __toCommonJS(index_exports);
|
||||
})();
|
||||
|
31
dist/iife.min.d.ts
vendored
31
dist/iife.min.d.ts
vendored
@ -4,7 +4,7 @@ export interface Signal<V, A> {
|
||||
/** The current value of the signal */
|
||||
get(): V;
|
||||
/** Set new value of the signal */
|
||||
set(value: V, force?: boolean): V;
|
||||
set(value: V): V;
|
||||
toJSON(): V;
|
||||
valueOf(): V;
|
||||
}
|
||||
@ -172,30 +172,17 @@ declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
|
||||
export interface On {
|
||||
/** Listens to the DOM event. See {@link Document.addEventListener} */
|
||||
<Event extends keyof DocumentEventMap, EL extends SupportedElement>(type: Event, listener: (this: EL, ev: DocumentEventMap[Event] & {
|
||||
target: EL;
|
||||
}) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
|
||||
<EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: string, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: Event | CustomEvent) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element is connected to the live DOM. In case of custom elements uses [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
connected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<NoInfer<EL>>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
connected<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<El>) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element is disconnected from the live DOM. In case of custom elements uses [`disconnectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
disconnected<EL extends SupportedElement>(listener: (this: EL, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): ddeElementAddon<EL>;
|
||||
/**
|
||||
* Fires after the next tick of the Javascript event loop.
|
||||
* This is handy for example to apply some property depending on the element content:
|
||||
* ```js
|
||||
* const selected= "Z";
|
||||
* //...
|
||||
* return el("form").append(
|
||||
* el("select", null, on.defer(e=> e.value=selected)).append(
|
||||
* el("option", { value: "A", textContent: "A" }),
|
||||
* //...
|
||||
* el("option", { value: "Z", textContent: "Z" }),
|
||||
* ),
|
||||
* );
|
||||
* ```
|
||||
* */
|
||||
defer<EL extends SupportedElement>(listener: (element: EL) => any): ddeElementAddon<EL>;
|
||||
disconnected<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<void>) => any, options?: AddEventListenerOptions): EE;
|
||||
/** Listens to the element attribute changes. In case of custom elements uses [`attributeChangedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */ // editorconfig-checker-disable-line
|
||||
attributeChanged<EE extends ddeElementAddon<SupportedElement>, El extends (EE extends ddeElementAddon<infer El> ? El : never)>(listener: (this: El, event: CustomEvent<[
|
||||
string,
|
||||
string
|
||||
]>) => any, options?: AddEventListenerOptions): EE;
|
||||
}
|
||||
export const on: On;
|
||||
export type Scope = {
|
||||
|
2
dist/iife.min.js
vendored
2
dist/iife.min.js
vendored
File diff suppressed because one or more lines are too long
@ -184,7 +184,7 @@ import { el } from "deka-dom-el";
|
||||
* @param {string} [attrs.className]
|
||||
* @param {URL} [attrs.src] Example code file path
|
||||
* @param {string} [attrs.content] Example code
|
||||
* @param {"js"|"ts"|"html"|"css"|"shell"} [attrs.language="js"] Language of the code
|
||||
* @param {"js"|"ts"|"html"|"css"} [attrs.language="js"] Language of the code
|
||||
* @param {string} [attrs.page_id] ID of the page, if setted it registers shiki
|
||||
* */
|
||||
export function code({ id, src, content, language= "js", className= host.slice(1), page_id }){
|
||||
|
@ -1,176 +0,0 @@
|
||||
import { styles } from "../ssr.js";
|
||||
|
||||
styles.css`
|
||||
#html-to-dde-converter {
|
||||
grid-column: full-main;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--bg-sidebar);
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
#html-to-dde-converter h3 {
|
||||
margin-top: 0;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
#html-to-dde-converter .description {
|
||||
color: var(--text-light);
|
||||
font-size: 0.95rem;
|
||||
margin-top: -1rem;
|
||||
}
|
||||
|
||||
#html-to-dde-converter .converter-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
#html-to-dde-converter .input-group,
|
||||
#html-to-dde-converter .output-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
#html-to-dde-converter [type="number"]{
|
||||
width: 3em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#html-to-dde-converter label {
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#html-to-dde-converter .options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#html-to-dde-converter .option-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
#html-to-dde-converter textarea {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9rem;
|
||||
padding: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--border);
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 200px;
|
||||
height: 25em;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
#html-to-dde-converter textarea:focus {
|
||||
outline: 2px solid var(--primary-light);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
#html-to-dde-converter .button-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#html-to-dde-converter button {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
border: none;
|
||||
background-color: var(--primary);
|
||||
color: var(--button-text);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
#html-to-dde-converter button:hover {
|
||||
background-color: var(--primary-dark);
|
||||
}
|
||||
|
||||
#html-to-dde-converter button.secondary {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
#html-to-dde-converter button.secondary:hover {
|
||||
background-color: var(--bg);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
#html-to-dde-converter .copy-button {
|
||||
background-color: var(--secondary);
|
||||
}
|
||||
|
||||
#html-to-dde-converter .copy-button:hover {
|
||||
background-color: var(--secondary-dark);
|
||||
}
|
||||
|
||||
#html-to-dde-converter .status {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
#html-to-dde-converter .error {
|
||||
color: hsl(0, 100%, 60%);
|
||||
font-size: 0.9rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Sample HTML examples list */
|
||||
#html-to-dde-converter .examples-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
#html-to-dde-converter .example-button {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
import { ireland } from "./ireland.html.js";
|
||||
import { el } from "deka-dom-el";
|
||||
const fileURL= url=> new URL(url, import.meta.url);
|
||||
|
||||
export function converter({ page_id }){
|
||||
registerClientPart(page_id);
|
||||
return el(ireland, {
|
||||
src: fileURL("./converter.js.js"),
|
||||
exportName: "converter",
|
||||
page_id,
|
||||
});
|
||||
}
|
||||
|
||||
let is_registered= {};
|
||||
/** @param {string} page_id */
|
||||
function registerClientPart(page_id){
|
||||
if(is_registered[page_id]) return;
|
||||
|
||||
document.head.append(
|
||||
el("script", {
|
||||
// src: "https://unpkg.com/@beforesemicolon/html-parser/dist/client.js",
|
||||
src: "https://cdn.jsdelivr.net/npm/@beforesemicolon/html-parser/dist/client.js",
|
||||
type: "text/javascript",
|
||||
charset: "utf-8",
|
||||
defer: true
|
||||
}),
|
||||
);
|
||||
is_registered[page_id]= true;
|
||||
}
|
@ -1,384 +0,0 @@
|
||||
import { el, on } from "deka-dom-el";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
const { parse }= globalThis.BFS || { parse(){ return { children: [ "not implemented" ] } } };
|
||||
// Example HTML snippets
|
||||
const examples = [
|
||||
{
|
||||
name: "Simple Component",
|
||||
html: `<div class="card">
|
||||
<img src="image.jpg" alt="Card Image" class="card-image">
|
||||
<h2 class="card-title">Card Title</h2>
|
||||
<p class="card-text">This is a simple card component</p>
|
||||
<button aria-pressed="mixed" type="button" class="card-button">Click Me</button>
|
||||
</div>`
|
||||
},
|
||||
{
|
||||
name: "Navigation",
|
||||
html: `<nav class="main-nav">
|
||||
<ul>
|
||||
<li><a href="/" class="active">Home</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
<li><a href="/services">Services</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
</ul>
|
||||
</nav>`
|
||||
},
|
||||
{
|
||||
name: "Form",
|
||||
html: `<form class="contact-form" onsubmit="submitForm(event)">
|
||||
<div class="form-group">
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" id="name" name="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email:</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="message">Message:</label>
|
||||
<textarea id="message" name="message" rows="4" required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="submit-btn">Send Message</button>
|
||||
</form>`
|
||||
}
|
||||
];
|
||||
|
||||
// Convert HTML to dd<el> code
|
||||
function convertHTMLtoDDE(html, options = {}) {
|
||||
|
||||
try {
|
||||
const parsed = parse(html);
|
||||
const content = parsed.children[0] || parsed.childNodes[0];
|
||||
return !content ? "" : nodeToDDE(content, options);
|
||||
} catch (error) {
|
||||
console.error("Parsing error:", error);
|
||||
return `// Error parsing HTML: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Node types based on standard DOM nodeType values
|
||||
const NODE_TYPE = {
|
||||
ELEMENT: 1, // Standard element node (equivalent to node.type === "element")
|
||||
TEXT: 3, // Text node (equivalent to node.type === "text")
|
||||
COMMENT: 8 // Comment node (equivalent to node.type === "comment")
|
||||
};
|
||||
|
||||
// Convert a parsed node to dd<el> code
|
||||
function nodeToDDE(node, options = {}, level = 0) {
|
||||
const tab= options.indent === "-1" ? "\t" : " ".repeat(options.indent);
|
||||
const indent = tab.repeat(level);
|
||||
const nextIndent = tab.repeat(level + 1);
|
||||
|
||||
const { nodeType } = node;
|
||||
// Handle text nodes
|
||||
if (nodeType === NODE_TYPE.TEXT) {
|
||||
const text = el("i", { innerText: node.nodeValue }).textContent;
|
||||
if (!text.trim()) return null;
|
||||
|
||||
// Return as plain text or template string for longer text
|
||||
return text.includes("\n") || text.includes('"')
|
||||
? `\`${text}\``
|
||||
: `"${text}"`;
|
||||
}
|
||||
|
||||
// Handle comment nodes
|
||||
if (nodeType === NODE_TYPE.COMMENT) {
|
||||
const text = node.nodeValue;
|
||||
if (!text.trim()) return null;
|
||||
return text.includes("\n")
|
||||
? [ "/*", ...text.trim().split("\n").map(l=> tab+l), "*/" ]
|
||||
: [ `// ${text}` ];
|
||||
}
|
||||
|
||||
// For element nodes
|
||||
if (nodeType === NODE_TYPE.ELEMENT) {
|
||||
// Special case for SVG elements
|
||||
const isNS = node.tagName === "svg";
|
||||
const elFunction = isNS ? "elNS" : "el";
|
||||
|
||||
// Get tag name
|
||||
let tagStr = `"${node.tagName}"`;
|
||||
|
||||
// Process attributes
|
||||
const attrs = [];
|
||||
const sets = {
|
||||
aria: {},
|
||||
data: {},
|
||||
}
|
||||
|
||||
for (const { name: key, value } of node.attributes) {
|
||||
// Handle class attribute
|
||||
if (key === "class") {
|
||||
attrs.push(`className: "${value}"`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle style attribute
|
||||
if (key === "style") {
|
||||
if (options.styleAsObject) {
|
||||
// Convert inline style to object
|
||||
const styleObj = {};
|
||||
value.split(";").forEach(part => {
|
||||
const [propRaw, valueRaw] = part.split(":");
|
||||
if (propRaw && valueRaw) {
|
||||
const prop = propRaw.trim();
|
||||
const propCamel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
||||
styleObj[propCamel] = valueRaw.trim();
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(styleObj).length > 0) {
|
||||
const styleStr = JSON.stringify(styleObj).replace(/"([^"]+)":/g, "$1:");
|
||||
attrs.push(`style: ${styleStr}`);
|
||||
}
|
||||
} else {
|
||||
// Keep as string
|
||||
attrs.push(`style: "${value}"`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle boolean attributes
|
||||
if (value === "" || value === key) {
|
||||
attrs.push(`${key}: true`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle data/aria attributes
|
||||
if (key.startsWith("data-") || key.startsWith("aria-")) {
|
||||
const keyName = key.startsWith("aria-") ? "aria" : "data";
|
||||
const keyCamel = key.slice(keyName.length + 1).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
||||
sets[keyName][keyCamel] = value;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular attributes
|
||||
const keyRegular = key==="for"
|
||||
? "htmlFor"
|
||||
: key.startsWith("on")
|
||||
? `"=${key}"`
|
||||
: key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
||||
attrs.push(`${keyRegular}: "${value}"`);
|
||||
}
|
||||
|
||||
// Process sets
|
||||
for (const [name, set] of Object.entries(sets)) {
|
||||
if(options.dataAttrsAsCamel)
|
||||
for (const [key, value] of Object.entries(set))
|
||||
attrs.push(`${name}${key[0].toUpperCase() + key.substring(1)}: "${value}"`);
|
||||
else {
|
||||
const setStr= Object.entries(set).map(([key, value]) => `${key}: "${value}"`).join(",");
|
||||
if (setStr !== "")
|
||||
attrs.push(`${name}set: { ${setStr} }`);
|
||||
}
|
||||
}
|
||||
|
||||
// Process children
|
||||
const children = [];
|
||||
for (const child of node.childNodes) {
|
||||
const childCode = nodeToDDE(child, options, level + 1);
|
||||
if (!childCode) continue;
|
||||
|
||||
children.push(childCode);
|
||||
}
|
||||
if(node.childNodes.length===1 && node.childNodes[0].nodeType===NODE_TYPE.TEXT){
|
||||
const textContent= children.pop().slice(1, -1);
|
||||
attrs.unshift(`textContent: "${textContent}"`);
|
||||
}
|
||||
|
||||
// Build the element creation code
|
||||
let result = `${elFunction}("${node.tagName.toLowerCase()}"`;
|
||||
|
||||
// Add attributes if any
|
||||
if (attrs.length > 0) {
|
||||
const tooLong= attrs.join(``).length+result.length > 55;
|
||||
if(options.expaned || tooLong || attrs.length > 3)
|
||||
result += `, {\n${nextIndent}${attrs.join(`,\n${nextIndent}`)},\n${indent}}`;
|
||||
else
|
||||
result += `, { ${attrs.join(", ")} }`;
|
||||
}
|
||||
|
||||
// Add children if any
|
||||
if (children.length > 0) {
|
||||
const chs= children.map(ch=>
|
||||
Array.isArray(ch) ? ch.map(l=> nextIndent + l).join("\n") :
|
||||
nextIndent + ch + ",");
|
||||
result += `).append(\n${chs.join("\n")}\n${indent})`;
|
||||
} else {
|
||||
result += ")";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function converter() {
|
||||
// State for the converter
|
||||
const htmlInput = S(examples[0].html);
|
||||
const error = S("");
|
||||
|
||||
const status = S("");
|
||||
const showStatus= msg => {
|
||||
status.set(msg);
|
||||
// Clear status after 3 seconds
|
||||
setTimeout(() => status.set(""), 3000);
|
||||
};
|
||||
|
||||
// Options state
|
||||
const options = {
|
||||
styleAsObject: {
|
||||
title: "Convert style to object",
|
||||
value: S(true),
|
||||
},
|
||||
dataAttrsAsCamel: {
|
||||
title: "dataKey/ariaKey (or dataset/ariaset)",
|
||||
value: S(true),
|
||||
},
|
||||
indent: {
|
||||
title: "Indentation (-1 for tabs)",
|
||||
value: S("-1"),
|
||||
type: "number",
|
||||
},
|
||||
expaned: {
|
||||
title: "Force multiline",
|
||||
value: S(false),
|
||||
}
|
||||
};
|
||||
const getOptions = ()=> Object.fromEntries(Object.entries(options)
|
||||
.map(([key, option]) => ([
|
||||
key,
|
||||
option.value.get()
|
||||
]))
|
||||
);
|
||||
|
||||
// Update the dd<el> output when input or options change
|
||||
const ddeOutput = S(() => {
|
||||
try {
|
||||
const result = convertHTMLtoDDE(htmlInput.get(), getOptions());
|
||||
error.set("");
|
||||
return result;
|
||||
} catch (err) {
|
||||
error.set(`Error: ${err.message}`);
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
// Event handlers
|
||||
const onConvert = on("submit", e => {
|
||||
e.preventDefault();
|
||||
htmlInput.set(htmlInput.get(), true);
|
||||
showStatus("Converted!");
|
||||
});
|
||||
|
||||
const onCopy = on("click", async () => {
|
||||
if (!ddeOutput.get()) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(ddeOutput.get());
|
||||
showStatus("Copied to clipboard!");
|
||||
} catch (err) {
|
||||
error.set(`Could not copy: ${err.message}`);
|
||||
}
|
||||
});
|
||||
const onClear = on("click", () => {
|
||||
htmlInput.set("");
|
||||
showStatus("Input cleared");
|
||||
});
|
||||
const onExampleLoad = (example) => on("click", () => {
|
||||
htmlInput.set(example.html);
|
||||
showStatus(`Loaded "${example.name}" example`);
|
||||
});
|
||||
|
||||
const optionsElements = () => Object.entries(options)
|
||||
.map(([key, option]) =>
|
||||
el("label", { className: "option-group" }).append(
|
||||
option.type==="number"
|
||||
? el("input", {
|
||||
type: option.type || "checkbox",
|
||||
name: key,
|
||||
value: option.value.get(),
|
||||
max: 10,
|
||||
}, on("change", e => option.value.set(e.target.value)))
|
||||
: el("input", {
|
||||
type: option.type || "checkbox",
|
||||
name: key,
|
||||
checked: option.value.get(),
|
||||
}, on("change", e => option.value.set(e.target.checked))),
|
||||
option.title,
|
||||
)
|
||||
);
|
||||
const exampleButtons = examples.map(example =>
|
||||
el("button", {
|
||||
type: "button",
|
||||
className: "secondary example-button"
|
||||
}, onExampleLoad(example)).append(example.name)
|
||||
);
|
||||
|
||||
return el("div", { id: "html-to-dde-converter" }).append(
|
||||
el("h3", "HTML to dd<el> Converter"),
|
||||
el("p", { className: "description" }).append(
|
||||
"Convert HTML markup to dd<el> JavaScript code. Paste your HTML below or choose from an example."
|
||||
),
|
||||
|
||||
el("form", { className: "converter-form" }, onConvert).append(
|
||||
el("div", { className: "options" }).append(...optionsElements()),
|
||||
|
||||
el("div", { className: "examples-list" }).append(
|
||||
el("label", "Examples: "),
|
||||
...exampleButtons
|
||||
),
|
||||
|
||||
el("div", { className: "editor-container" }).append(
|
||||
el("div", { className: "input-group" }).append(
|
||||
el("label", { htmlFor: "html-input" }).append(
|
||||
"HTML Input",
|
||||
el("div", { className: "button-group" }).append(
|
||||
el("button", {
|
||||
type: "button",
|
||||
className: "secondary",
|
||||
title: "Clear input"
|
||||
}, onClear).append("Clear")
|
||||
)
|
||||
),
|
||||
el("textarea", {
|
||||
id: "html-input",
|
||||
spellcheck: false,
|
||||
value: htmlInput,
|
||||
placeholder: "Paste your HTML here or choose an example",
|
||||
oninput: e => htmlInput.set(e.target.value)
|
||||
})
|
||||
),
|
||||
|
||||
el("div", { className: "output-group" }).append(
|
||||
el("label", { htmlFor: "dde-output" }).append(
|
||||
"dd<el> Output",
|
||||
el("div", { className: "button-group" }).append(
|
||||
el("button", {
|
||||
textContent: "Copy",
|
||||
type: "button",
|
||||
className: "copy-button",
|
||||
title: "Copy to clipboard",
|
||||
disabled: S(() => !ddeOutput.get())
|
||||
}, onCopy)
|
||||
)
|
||||
),
|
||||
el("textarea", {
|
||||
id: "dde-output",
|
||||
readonly: true,
|
||||
spellcheck: false,
|
||||
placeholder: "The converted dd<el> code will appear here",
|
||||
value: S(() => ddeOutput.get() || "// Convert HTML to see results here")
|
||||
})
|
||||
)
|
||||
),
|
||||
|
||||
el("div", { className: "button-group" }).append(
|
||||
S.el(error, error => !error ? el() : el("div", { className: "error" }).append(error)),
|
||||
el("div", { className: "status", textContent: status }),
|
||||
el("button", { type: "submit" }).append("Convert")
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
@ -1,374 +0,0 @@
|
||||
/**
|
||||
* Case Study: Data Dashboard with Charts
|
||||
*
|
||||
* This example demonstrates:
|
||||
* - Integration with a third-party charting library
|
||||
* - Data fetching and state management
|
||||
* - Responsive layout design
|
||||
* - Multiple interactive components working together
|
||||
*/
|
||||
|
||||
import { el, on } from "deka-dom-el";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
|
||||
/**
|
||||
* Data Dashboard Component with Chart Integration
|
||||
* @returns {HTMLElement} Dashboard element
|
||||
*/
|
||||
export function DataDashboard() {
|
||||
// Mock data for demonstration
|
||||
const DATA = {
|
||||
sales: [42, 58, 65, 49, 72, 85, 63, 70, 78, 89, 95, 86],
|
||||
visitors: [1420, 1620, 1750, 1850, 2100, 2400, 2250, 2500, 2750, 2900, 3100, 3200],
|
||||
conversion: [2.9, 3.5, 3.7, 2.6, 3.4, 3.5, 2.8, 2.8, 2.8, 3.1, 3.0, 2.7],
|
||||
months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||||
};
|
||||
const years = [2022, 2023, 2024];
|
||||
const dataTypes = [
|
||||
{ id: 'sales', label: 'Sales', unit: 'K' },
|
||||
{ id: 'visitors', label: 'Visitors', unit: '' },
|
||||
{ id: 'conversion', label: 'Conversion Rate', unit: '%' }
|
||||
];
|
||||
|
||||
// Filter options
|
||||
const selectedYear = S(2024);
|
||||
const onYearChange = on("change", e => {
|
||||
selectedYear.set(parseInt(/** @type {HTMLSelectElement} */(e.target).value));
|
||||
loadData();
|
||||
});
|
||||
const selectedDataType = S(/** @type {'sales' | 'visitors' | 'conversion'} */ ('sales'));
|
||||
const onDataTypeChange = on("click", e => {
|
||||
const type = /** @type {'sales' | 'visitors' | 'conversion'} */(
|
||||
/** @type {HTMLButtonElement} */(e.currentTarget).dataset.type);
|
||||
selectedDataType.set(type);
|
||||
});
|
||||
const currentDataType = S(() => dataTypes.find(type => type.id === selectedDataType.get()));
|
||||
const selectedData = S(() => DATA[selectedDataType.get()]);
|
||||
|
||||
// Values based on filters
|
||||
const totalValue = S(() => selectedData.get().reduce((sum, value) => sum + value, 0));
|
||||
const averageValue = S(() => {
|
||||
const data = selectedData.get();
|
||||
return data.reduce((sum, value) => sum + value, 0) / data.length;
|
||||
});
|
||||
const highestValue = S(() => Math.max(...selectedData.get()));
|
||||
|
||||
// Simulate data loading
|
||||
const isLoading = S(false);
|
||||
const error = S(null);
|
||||
function loadData() {
|
||||
isLoading.set(true);
|
||||
error.set(null);
|
||||
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
if (Math.random() > 0.9) {
|
||||
// Simulate occasional error
|
||||
error.set('Failed to load data. Please try again.');
|
||||
}
|
||||
isLoading.set(false);
|
||||
}, 800);
|
||||
}
|
||||
|
||||
// Reactive chart rendering
|
||||
const chart = S(()=> {
|
||||
const chart= el("canvas", { id: "chart-canvas", width: 800, height: 400 });
|
||||
const ctx = chart.getContext('2d');
|
||||
const data = selectedData.get();
|
||||
const months = DATA.months;
|
||||
const width = chart.width;
|
||||
const height = chart.height;
|
||||
const maxValue = Math.max(...data) * 1.1;
|
||||
const barWidth = width / data.length - 10;
|
||||
|
||||
// Clear canvas
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
// Draw background grid
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#f0f0f0';
|
||||
ctx.lineWidth = 1;
|
||||
for(let i = 0; i < 5; i++) {
|
||||
const y = height - (height * (i / 5)) - 30;
|
||||
ctx.moveTo(50, y);
|
||||
ctx.lineTo(width - 20, y);
|
||||
|
||||
// Draw grid labels
|
||||
ctx.fillStyle = '#999';
|
||||
ctx.font = '12px Arial';
|
||||
ctx.fillText(Math.round(maxValue * (i / 5)).toString(), 20, y + 5);
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
// Draw bars
|
||||
data.forEach((value, index) => {
|
||||
const x = index * (barWidth + 10) + 60;
|
||||
const barHeight = (value / maxValue) * (height - 60);
|
||||
|
||||
// Bar
|
||||
ctx.fillStyle = '#4a90e2';
|
||||
ctx.fillRect(x, height - barHeight - 30, barWidth, barHeight);
|
||||
|
||||
// Month label
|
||||
ctx.fillStyle = '#666';
|
||||
ctx.font = '12px Arial';
|
||||
ctx.fillText(months[index], x + barWidth/2 - 10, height - 10);
|
||||
});
|
||||
|
||||
// Chart title
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.font = 'bold 14px Arial';
|
||||
ctx.fillText(`${currentDataType.get().label} (${selectedYear.get()})`, width/2 - 80, 20);
|
||||
return chart;
|
||||
});
|
||||
|
||||
return el("div", { className: "dashboard" }).append(
|
||||
el("header", { className: "dashboard-header" }).append(
|
||||
el("h1", "Sales Performance Dashboard"),
|
||||
el("div", { className: "year-filter" }).append(
|
||||
el("label", { htmlFor: "yearSelect", textContent: "Select Year:" }),
|
||||
el("select", { id: "yearSelect" },
|
||||
on.defer(el=> el.value = selectedYear.get().toString()),
|
||||
onYearChange
|
||||
).append(
|
||||
...years.map(year => el("option", { value: year, textContent: year }))
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
S.el(error, errorMsg => !errorMsg
|
||||
? el()
|
||||
: el("div", { className: "error-message" }).append(
|
||||
el("p", errorMsg),
|
||||
el("button", { textContent: "Retry", type: "button" }, on("click", loadData)),
|
||||
),
|
||||
),
|
||||
|
||||
S.el(isLoading, loading => !loading
|
||||
? el()
|
||||
: el("div", { className: "loading-spinner" })
|
||||
),
|
||||
|
||||
// Main dashboard content
|
||||
el("div", { className: "dashboard-content" }).append(
|
||||
// Metrics cards
|
||||
el("div", { className: "metrics-container" }).append(
|
||||
el("div", { className: "metric-card" }).append(
|
||||
el("h3", "Total"),
|
||||
el("#text", S(() => `${totalValue.get().toLocaleString()}${currentDataType.get().unit}`)),
|
||||
),
|
||||
el("div", { className: "metric-card" }).append(
|
||||
el("h3", "Average"),
|
||||
el("#text", S(() => `${averageValue.get().toFixed(1)}${currentDataType.get().unit}`)),
|
||||
),
|
||||
el("div", { className: "metric-card" }).append(
|
||||
el("h3", "Highest"),
|
||||
el("#text", S(() => `${highestValue.get()}${currentDataType.get().unit}`)),
|
||||
),
|
||||
),
|
||||
|
||||
// Data type selection tabs
|
||||
el("div", { className: "data-type-tabs" }).append(
|
||||
...dataTypes.map(type =>
|
||||
el("button", {
|
||||
type: "button",
|
||||
className: S(() => selectedDataType.get() === type.id ? 'active' : ''),
|
||||
dataType: type.id,
|
||||
textContent: type.label
|
||||
}, onDataTypeChange)
|
||||
)
|
||||
),
|
||||
|
||||
// Chart container
|
||||
el("div", { className: "chart-container" }).append(
|
||||
S.el(chart, chart => chart)
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Render the component
|
||||
document.body.append(
|
||||
el("div", { style: "padding: 20px; background: #f5f5f5; min-height: 100vh;" }).append(
|
||||
el(DataDashboard)
|
||||
),
|
||||
el("style", `
|
||||
.dashboard {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.dashboard-header h1 {
|
||||
font-size: 1.5rem;
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.year-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.year-filter select {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.metrics-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.metric-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.metric-card h3 {
|
||||
margin-top: 0;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.metric-card p {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.data-type-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.data-type-tabs button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.data-type-tabs button.active {
|
||||
color: #4a90e2;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.data-type-tabs button.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: #4a90e2;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.loading-spinner::before {
|
||||
content: '';
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #4a90e2;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #ffecec;
|
||||
color: #e74c3c;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.error-message p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error-message button {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.metrics-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.year-filter {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.year-filter select {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
`)
|
||||
);
|
@ -1,412 +0,0 @@
|
||||
/**
|
||||
* Case Study: Interactive Image Gallery
|
||||
*
|
||||
* This example demonstrates:
|
||||
* - Dynamic loading of content
|
||||
* - Lightbox functionality
|
||||
* - Animation handling
|
||||
* - Keyboard and gesture navigation
|
||||
*/
|
||||
|
||||
import { el, memo, on } from "deka-dom-el";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
|
||||
// Sample image data
|
||||
const imagesSample = (url=> [
|
||||
{ id: 1, src: url+'nature', alt: 'Nature', title: 'Beautiful Landscape' },
|
||||
{ id: 2, src: url+'places', alt: 'City', title: 'Urban Architecture' },
|
||||
{ id: 3, src: url+'people', alt: 'People', title: 'Street Photography' },
|
||||
{ id: 4, src: url+'food', alt: 'Food', title: 'Culinary Delights' },
|
||||
{ id: 5, src: url+'animals', alt: 'Animals', title: 'Wildlife' },
|
||||
{ id: 6, src: url+'travel', alt: 'Travel', title: 'Adventure Awaits' },
|
||||
{ id: 7, src: url+'computer', alt: 'Technology', title: 'Modern Tech' },
|
||||
{ id: 8, src: url+'music', alt: 'Art', title: 'Creative Expression' },
|
||||
])('https://api.algobook.info/v1/randomimage?category=');
|
||||
/**
|
||||
* Interactive Image Gallery Component
|
||||
* @returns {HTMLElement} Gallery element
|
||||
*/
|
||||
export function ImageGallery(images= imagesSample) {
|
||||
const filterTag = S('all');
|
||||
const imagesToDisplay = S(() => {
|
||||
const tag = filterTag.get();
|
||||
if (tag === 'all') return images;
|
||||
else return images.filter(img => img.alt.toLowerCase() === tag);
|
||||
})
|
||||
const onFilterChange = tag => on("click", () => {
|
||||
filterTag.set(tag);
|
||||
});
|
||||
|
||||
// Lightbox
|
||||
const selectedImageId = S(null);
|
||||
const selectedImage = S(() => {
|
||||
const id = selectedImageId.get();
|
||||
return id ? images.find(img => img.id === id) : null;
|
||||
});
|
||||
const isLightboxOpen = S(() => selectedImage.get() !== null);
|
||||
const onImageClick = id => on("click", () => {
|
||||
selectedImageId.set(id);
|
||||
document.body.style.overflow = 'hidden'; // Prevent scrolling when lightbox is open
|
||||
|
||||
// Add keyboard event listeners when lightbox opens
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
});
|
||||
const closeLightbox = () => {
|
||||
selectedImageId.set(null);
|
||||
document.body.style.overflow = ''; // Restore scrolling
|
||||
|
||||
// Remove keyboard event listeners when lightbox closes
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
const onPrevImage = e => {
|
||||
e.stopPropagation(); // Prevent closing the lightbox
|
||||
const images = imagesToDisplay.get();
|
||||
const currentId = selectedImageId.get();
|
||||
const currentIndex = images.findIndex(img => img.id === currentId);
|
||||
const prevIndex = (currentIndex - 1 + images.length) % images.length;
|
||||
selectedImageId.set(images[prevIndex].id);
|
||||
};
|
||||
const onNextImage = e => {
|
||||
e.stopPropagation(); // Prevent closing the lightbox
|
||||
const images = imagesToDisplay.get();
|
||||
const currentId = selectedImageId.get();
|
||||
const currentIndex = images.findIndex(img => img.id === currentId);
|
||||
const nextIndex = (currentIndex + 1) % images.length;
|
||||
selectedImageId.set(images[nextIndex].id);
|
||||
};
|
||||
|
||||
// Keyboard navigation handler
|
||||
function handleKeyDown(e) {
|
||||
switch(e.key) {
|
||||
case 'Escape':
|
||||
closeLightbox();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
document.querySelector('.lightbox-prev-btn').click();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
document.querySelector('.lightbox-next-btn').click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Build the gallery UI
|
||||
return el("div", { className: "gallery-container" }).append(
|
||||
// Gallery header
|
||||
el("header", { className: "gallery-header" }).append(
|
||||
el("h1", "Interactive Image Gallery"),
|
||||
el("p", "Click on any image to view it in the lightbox. Use arrow keys for navigation.")
|
||||
),
|
||||
|
||||
// Filter options
|
||||
el("div", { className: "gallery-filters" }).append(
|
||||
el("button", {
|
||||
classList: { active: S(() => filterTag.get() === 'all') },
|
||||
textContent: "All"
|
||||
}, onFilterChange('all')),
|
||||
el("button", {
|
||||
classList: { active: S(() => filterTag.get() === 'nature') },
|
||||
textContent: "Nature"
|
||||
}, onFilterChange('nature')),
|
||||
el("button", {
|
||||
classList: { active: S(() => filterTag.get() === 'urban') },
|
||||
textContent: "Urban"
|
||||
}, onFilterChange('urban')),
|
||||
el("button", {
|
||||
classList: { active: S(() => filterTag.get() === 'people') },
|
||||
textContent: "People"
|
||||
}, onFilterChange('people'))
|
||||
),
|
||||
|
||||
// Image grid
|
||||
el("div", { className: "gallery-grid" }).append(
|
||||
S.el(imagesToDisplay, images =>
|
||||
images.map(image =>
|
||||
memo(image.id, ()=>
|
||||
el("div", {
|
||||
className: "gallery-item",
|
||||
dataTag: image.alt.toLowerCase()
|
||||
}).append(
|
||||
el("img", {
|
||||
src: image.src,
|
||||
alt: image.alt,
|
||||
loading: "lazy"
|
||||
}, onImageClick(image.id)),
|
||||
el("div", { className: "gallery-item-caption" }).append(
|
||||
el("h3", image.title),
|
||||
el("p", image.alt)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// Lightbox (only shown when an image is selected)
|
||||
S.el(isLightboxOpen, open => !open
|
||||
? el()
|
||||
: el("div", { className: "lightbox-overlay" }, on("click", closeLightbox)).append(
|
||||
el("div", {
|
||||
className: "lightbox-content",
|
||||
onClick: e => e.stopPropagation() // Prevent closing when clicking inside
|
||||
}).append(
|
||||
el("button", {
|
||||
className: "lightbox-close-btn",
|
||||
"aria-label": "Close lightbox"
|
||||
}, on("click", closeLightbox)).append("×"),
|
||||
|
||||
el("button", {
|
||||
className: "lightbox-prev-btn",
|
||||
"aria-label": "Previous image"
|
||||
}, on("click", onPrevImage)).append("❮"),
|
||||
|
||||
el("button", {
|
||||
className: "lightbox-next-btn",
|
||||
"aria-label": "Next image"
|
||||
}, on("click", onNextImage)).append("❯"),
|
||||
|
||||
S.el(selectedImage, img => !img
|
||||
? el()
|
||||
: el("div", { className: "lightbox-image-container" }).append(
|
||||
el("img", {
|
||||
src: img.src,
|
||||
alt: img.alt,
|
||||
className: "lightbox-image"
|
||||
}),
|
||||
el("div", { className: "lightbox-caption" }).append(
|
||||
el("h2", img.title),
|
||||
el("p", img.alt)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Render the component
|
||||
document.body.append(
|
||||
el("div", { style: "padding: 20px; background: #f5f5f5; min-height: 100vh;" }).append(
|
||||
el(ImageGallery)
|
||||
),
|
||||
el("style", `
|
||||
.gallery-container {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.gallery-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.gallery-header h1 {
|
||||
margin-bottom: 0.5rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.gallery-header p {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.gallery-filters {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.gallery-filters button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.5rem 1.5rem;
|
||||
margin: 0 0.5rem;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
border-radius: 30px;
|
||||
transition: all 0.3s ease;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.gallery-filters button:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.gallery-filters button.active {
|
||||
background: #4a90e2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.gallery-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.gallery-item {
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gallery-item:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.gallery-item img {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
|
||||
.gallery-item:hover img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.gallery-item-caption {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.gallery-item:hover .gallery-item-caption {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.gallery-item-caption h3 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.gallery-item-caption p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Lightbox styles */
|
||||
.lightbox-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.lightbox-content {
|
||||
position: relative;
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
}
|
||||
|
||||
.lightbox-image-container {
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.lightbox-image {
|
||||
max-width: 100%;
|
||||
max-height: 80vh;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.lightbox-caption {
|
||||
background: #222;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.lightbox-caption h2 {
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.lightbox-caption p {
|
||||
margin: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.lightbox-close-btn,
|
||||
.lightbox-prev-btn,
|
||||
.lightbox-next-btn {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.lightbox-close-btn:hover,
|
||||
.lightbox-prev-btn:hover,
|
||||
.lightbox-next-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.lightbox-close-btn {
|
||||
top: -25px;
|
||||
right: -25px;
|
||||
}
|
||||
|
||||
.lightbox-prev-btn {
|
||||
left: -25px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.lightbox-next-btn {
|
||||
right: -25px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.gallery-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.gallery-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.lightbox-prev-btn,
|
||||
.lightbox-next-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
`)
|
||||
);
|
@ -1,339 +0,0 @@
|
||||
/**
|
||||
* Case Study: Interactive Form with Validation
|
||||
*
|
||||
* This example demonstrates:
|
||||
* - Form handling with real-time validation
|
||||
* - Reactive UI updates based on input state
|
||||
* - Complex form state management
|
||||
* - Clean separation of concerns (data, validation, UI)
|
||||
*/
|
||||
|
||||
import { dispatchEvent, el, on, scope } from "deka-dom-el";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
|
||||
/**
|
||||
* @typedef {Object} FormState
|
||||
* @property {string} name
|
||||
* @property {string} email
|
||||
* @property {string} password
|
||||
* @property {string} confirmPassword
|
||||
* @property {boolean} agreedToTerms
|
||||
* */
|
||||
/**
|
||||
* Interactive Form with Validation Component
|
||||
* @returns {HTMLElement} Form element
|
||||
*/
|
||||
export function InteractiveForm() {
|
||||
const submitted = S(false);
|
||||
/** @type {FormState|null} */
|
||||
let formState = null;
|
||||
/** @param {CustomEvent<FormState>} event */
|
||||
const onSubmit = ({ detail }) => {
|
||||
submitted.set(true);
|
||||
formState = detail;
|
||||
};
|
||||
const onAnotherAccount = () => {
|
||||
submitted.set(false)
|
||||
formState = null;
|
||||
};
|
||||
|
||||
return el("div", { className: "form-container" }).append(
|
||||
S.el(submitted, s => s
|
||||
? el("div", { className: "success-message" }).append(
|
||||
el("h3", "Thank you for registering!"),
|
||||
el("p", `Welcome, ${formState.name}! Your account has been created successfully.`),
|
||||
el("button", { textContent: "Register another account", type: "button" },
|
||||
on("click", onAnotherAccount)
|
||||
),
|
||||
)
|
||||
: el(Form, { initial: formState }, on("form:submit", onSubmit))
|
||||
)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Form Component
|
||||
* @type {(props: { initial: FormState | null }) => HTMLElement}
|
||||
* */
|
||||
export function Form({ initial }) {
|
||||
const { host }= scope;
|
||||
// Form state management
|
||||
const formState = S(initial || {
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
agreedToTerms: false
|
||||
}, {
|
||||
/**
|
||||
* @template {keyof FormState} K
|
||||
* @param {K} key
|
||||
* @param {FormState[K]} value
|
||||
* */
|
||||
update(key, value) {
|
||||
this.value[key] = value;
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Event handler for input events
|
||||
* @param {"value"|"checked"} prop
|
||||
* @returns {(ev: Event) => void}
|
||||
* */
|
||||
const onChange= prop => ev => {
|
||||
const input = /** @type {HTMLInputElement} */(ev.target);
|
||||
S.action(formState, "update", /** @type {keyof FormState} */(input.id), input[prop]);
|
||||
};
|
||||
|
||||
// Form validate state
|
||||
const nameValid = S(() => formState.get().name.length >= 3);
|
||||
const emailValid = S(() => {
|
||||
const email = formState.get().email;
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
});
|
||||
const passwordValid = S(() => {
|
||||
const password = formState.get().password;
|
||||
return password.length >= 8 && /[A-Z]/.test(password) && /[0-9]/.test(password);
|
||||
});
|
||||
const passwordsMatch = S(() => {
|
||||
const { password, confirmPassword } = formState.get();
|
||||
return password === confirmPassword && confirmPassword !== '';
|
||||
});
|
||||
const termsAgreed = S(() => formState.get().agreedToTerms);
|
||||
const formValid = S(() =>
|
||||
nameValid.get() &&
|
||||
emailValid.get() &&
|
||||
passwordValid.get() &&
|
||||
passwordsMatch.get() &&
|
||||
termsAgreed.get()
|
||||
);
|
||||
|
||||
const dispatcSubmit = dispatchEvent("form:submit", host);
|
||||
const onSubmit = on("submit", e => {
|
||||
e.preventDefault();
|
||||
if (!formValid.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatcSubmit(formState.get());
|
||||
});
|
||||
|
||||
// Component UI
|
||||
return el("form", { className: "registration-form" }, onSubmit).append(
|
||||
el("h2", "Create an Account"),
|
||||
|
||||
// Name field
|
||||
el("div", { classList: {
|
||||
"form-group": true,
|
||||
valid: nameValid,
|
||||
invalid: S(()=> !nameValid.get() && formState.get().name)
|
||||
}}).append(
|
||||
el("label", { htmlFor: "name", textContent: "Full Name" }),
|
||||
el("input", {
|
||||
id: "name",
|
||||
type: "text",
|
||||
value: formState.get().name,
|
||||
placeholder: "Enter your full name"
|
||||
}, on("input", onChange("value"))),
|
||||
el("div", { className: "validation-message", textContent: "Name must be at least 3 characters long" }),
|
||||
),
|
||||
|
||||
// Email field
|
||||
el("div", { classList: {
|
||||
"form-group": true,
|
||||
valid: emailValid,
|
||||
invalid: S(()=> !emailValid.get() && formState.get().email)
|
||||
}}).append(
|
||||
el("label", { htmlFor: "email", textContent: "Email Address" }),
|
||||
el("input", {
|
||||
id: "email",
|
||||
type: "email",
|
||||
value: formState.get().email,
|
||||
placeholder: "Enter your email address"
|
||||
}, on("input", onChange("value"))),
|
||||
el("div", { className: "validation-message", textContent: "Please enter a valid email address" })
|
||||
),
|
||||
|
||||
// Password field
|
||||
el("div", { classList: {
|
||||
"form-group": true,
|
||||
valid: passwordValid,
|
||||
invalid: S(()=> !passwordValid.get() && formState.get().password)
|
||||
}}).append(
|
||||
el("label", { htmlFor: "password", textContent: "Password" }),
|
||||
el("input", {
|
||||
id: "password",
|
||||
type: "password",
|
||||
value: formState.get().password,
|
||||
placeholder: "Create a password"
|
||||
}, on("input", onChange("value"))),
|
||||
el("div", {
|
||||
className: "validation-message",
|
||||
textContent: "Password must be at least 8 characters with at least one uppercase letter and one number",
|
||||
}),
|
||||
),
|
||||
|
||||
// Confirm password field
|
||||
el("div", { classList: {
|
||||
"form-group": true,
|
||||
valid: passwordsMatch,
|
||||
invalid: S(()=> !passwordsMatch.get() && formState.get().confirmPassword)
|
||||
}}).append(
|
||||
el("label", { htmlFor: "confirmPassword", textContent: "Confirm Password" }),
|
||||
el("input", {
|
||||
id: "confirmPassword",
|
||||
type: "password",
|
||||
value: formState.get().confirmPassword,
|
||||
placeholder: "Confirm your password"
|
||||
}, on("input", onChange("value"))),
|
||||
el("div", { className: "validation-message", textContent: "Passwords must match" }),
|
||||
),
|
||||
|
||||
// Terms agreement
|
||||
el("div", { className: "form-group checkbox-group" }).append(
|
||||
el("input", {
|
||||
id: "agreedToTerms",
|
||||
type: "checkbox",
|
||||
checked: formState.get().agreedToTerms
|
||||
}, on("change", onChange("checked"))),
|
||||
el("label", { htmlFor: "agreedToTerms", textContent: "I agree to the Terms and Conditions" }),
|
||||
),
|
||||
|
||||
// Submit button
|
||||
el("button", {
|
||||
textContent: "Create Account",
|
||||
type: "submit",
|
||||
className: "submit-button",
|
||||
disabled: S(() => !formValid.get())
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Render the component
|
||||
document.body.append(
|
||||
el("div", { style: "padding: 20px; background: #f5f5f5; min-height: 100vh;" }).append(
|
||||
el(InteractiveForm)
|
||||
),
|
||||
el("style", `
|
||||
.form-container {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #4a90e2;
|
||||
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
margin: 0 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
font-size: 0.85rem;
|
||||
color: #e74c3c;
|
||||
margin-top: 0.5rem;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group.invalid .validation-message {
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.form-group.valid input {
|
||||
border-color: #2ecc71;
|
||||
}
|
||||
|
||||
.form-group.invalid input {
|
||||
border-color: #e74c3c;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
background-color: #4a90e2;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.submit-button:hover:not(:disabled) {
|
||||
background-color: #3a7bc8;
|
||||
}
|
||||
|
||||
.submit-button:disabled {
|
||||
background-color: #b5b5b5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
text-align: center;
|
||||
color: #2ecc71;
|
||||
}
|
||||
|
||||
.success-message h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.success-message button {
|
||||
background-color: #2ecc71;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.success-message button:hover {
|
||||
background-color: #27ae60;
|
||||
}
|
||||
`),
|
||||
);
|
@ -1,715 +0,0 @@
|
||||
/**
|
||||
* Case Study: Task Manager Application
|
||||
*
|
||||
* This example demonstrates:
|
||||
* - Complex state management with signals
|
||||
* - Drag and drop functionality
|
||||
* - Local storage persistence
|
||||
* - Responsive design for different devices
|
||||
*/
|
||||
|
||||
import { el, on, dispatchEvent, scope } from "deka-dom-el";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
|
||||
/** @typedef {{ id: number, title: string, description: string, priority: string, status: string }} Task */
|
||||
/**
|
||||
* Task Manager Component
|
||||
* @returns {HTMLElement} Task manager UI
|
||||
*/
|
||||
export function TaskManager() {
|
||||
// <Tasks store>
|
||||
const STORAGE_KEY = 'dde-task-manager';
|
||||
const STATUSES = {
|
||||
TODO: 'todo',
|
||||
IN_PROGRESS: 'in-progress',
|
||||
DONE: 'done'
|
||||
};
|
||||
/** @type {Task[]} */
|
||||
let initialTasks = [];
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved) {
|
||||
initialTasks = JSON.parse(saved);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load tasks from localStorage', e);
|
||||
}
|
||||
if (!initialTasks.length) {
|
||||
initialTasks = [
|
||||
{ id: 1, title: 'Create project structure', description: 'Set up folders and initial files',
|
||||
status: STATUSES.DONE, priority: 'high' },
|
||||
{ id: 2, title: 'Design UI components', description: 'Create mockups for main views',
|
||||
status: STATUSES.IN_PROGRESS, priority: 'medium' },
|
||||
{ id: 3, title: 'Implement authentication', description: 'Set up user login and registration',
|
||||
status: STATUSES.TODO, priority: 'high' },
|
||||
{ id: 4, title: 'Write documentation', description: 'Document API endpoints and usage examples',
|
||||
status: STATUSES.TODO, priority: 'low' },
|
||||
];
|
||||
}
|
||||
const tasks = S(initialTasks, {
|
||||
add(task) { this.value.push(task); },
|
||||
remove(id) { this.value = this.value.filter(task => task.id !== id); },
|
||||
update(id, task) {
|
||||
const current= this.value.find(t => t.id === id);
|
||||
if (current) Object.assign(current, task);
|
||||
}
|
||||
});
|
||||
S.on(tasks, value => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(value));
|
||||
} catch (e) {
|
||||
console.error('Failed to save tasks to localStorage', e);
|
||||
}
|
||||
});
|
||||
// </Tasks store>
|
||||
|
||||
const filterPriority = S('all');
|
||||
const searchQuery = S('');
|
||||
// Filtered tasks based on priority and search query
|
||||
const filteredTasks = S(() => {
|
||||
let filtered = tasks.get();
|
||||
|
||||
// Filter by priority
|
||||
if (filterPriority.get() !== 'all') {
|
||||
filtered = filtered.filter(task => task.priority === filterPriority.get());
|
||||
}
|
||||
|
||||
// Filter by search query
|
||||
const query = searchQuery.get().toLowerCase();
|
||||
if (query) {
|
||||
filtered = filtered.filter(task =>
|
||||
task.title.toLowerCase().includes(query) ||
|
||||
task.description.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
});
|
||||
/** Tasks grouped by status for display in columns */
|
||||
const tasksByStatus = S(() => {
|
||||
const filtered = filteredTasks.get();
|
||||
return {
|
||||
[STATUSES.TODO]: filtered.filter(t => t.status === STATUSES.TODO),
|
||||
[STATUSES.IN_PROGRESS]: filtered.filter(t => t.status === STATUSES.IN_PROGRESS),
|
||||
[STATUSES.DONE]: filtered.filter(t => t.status === STATUSES.DONE)
|
||||
};
|
||||
});
|
||||
|
||||
// <Add> signals and handlers for adding new tasks
|
||||
const newTask = { title: '', description: '', priority: 'medium' };
|
||||
const onAddTask = e => {
|
||||
e.preventDefault();
|
||||
if (!newTask.title) return;
|
||||
|
||||
S.action(tasks, "add", {
|
||||
id: Date.now(),
|
||||
status: STATUSES.TODO,
|
||||
...newTask
|
||||
});
|
||||
e.target.reset();
|
||||
};
|
||||
// </Add>
|
||||
const onCardEdit= on("card:edit", /** @param {CardEditEvent} ev */({ detail: [ id, task ] })=>
|
||||
S.action(tasks, "update", id, task));
|
||||
const onCardDelete= on("card:delete", /** @param {CardDeleteEvent} ev */({ detail: id })=>
|
||||
S.action(tasks, "remove", id));
|
||||
|
||||
const { onDragable, onDragArea }= moveElementAddon(
|
||||
(id, status) => S.action(tasks, "update", id, { status })
|
||||
);
|
||||
|
||||
// Build the task manager UI
|
||||
return el("div", { className: "task-manager" }).append(
|
||||
el("header", { className: "app-header" }).append(
|
||||
el("h1", "DDE Task Manager"),
|
||||
el("div", { className: "app-controls" }).append(
|
||||
el("input", {
|
||||
type: "text",
|
||||
placeholder: "Search tasks...",
|
||||
value: searchQuery.get()
|
||||
}, on("input", e => searchQuery.set(e.target.value))),
|
||||
el("select", null,
|
||||
on.defer(el=> el.value= filterPriority.get()),
|
||||
on("change", e => filterPriority.set(e.target.value))
|
||||
).append(
|
||||
el("option", { value: "all", textContent: "All Priorities" }),
|
||||
el("option", { value: "low", textContent: "Low Priority" }),
|
||||
el("option", { value: "medium", textContent: "Medium Priority" }),
|
||||
el("option", { value: "high", textContent: "High Priority" })
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// Add new task form
|
||||
el("form", { className: "new-task-form" }, on("submit", onAddTask)).append(
|
||||
el("div", { className: "form-row" }).append(
|
||||
el("input", {
|
||||
type: "text",
|
||||
placeholder: "New task title",
|
||||
value: newTask.title,
|
||||
required: true
|
||||
}, on("input", e => newTask.title= e.target.value.trim())),
|
||||
el("select", null,
|
||||
on.defer(el=> el.value= newTask.priority),
|
||||
on("change", e => newTask.priority= e.target.value)
|
||||
).append(
|
||||
el("option", { value: "low", textContent: "Low" }),
|
||||
el("option", { value: "medium", textContent: "Medium" }),
|
||||
el("option", { value: "high", textContent: "High" })
|
||||
),
|
||||
el("button", { type: "submit", className: "add-btn" }).append("Add Task")
|
||||
),
|
||||
el("textarea", {
|
||||
placeholder: "Task description (optional)",
|
||||
value: newTask.description
|
||||
}, on("input", e => newTask.description= e.target.value.trim()))
|
||||
),
|
||||
|
||||
// Task board with columns
|
||||
el("div", { className: "task-board" }).append(
|
||||
// Todo column
|
||||
el("div", {
|
||||
id: `column-${STATUSES.TODO}`,
|
||||
className: "task-column"
|
||||
}, onDragArea(STATUSES.TODO)).append(
|
||||
el("h2", { className: "column-header" }).append(
|
||||
"To Do ",
|
||||
el("span", {
|
||||
textContent: S(() => tasksByStatus.get()[STATUSES.TODO].length),
|
||||
className: "task-count"
|
||||
}),
|
||||
),
|
||||
S.el(S(() => tasksByStatus.get()[STATUSES.TODO]), tasks =>
|
||||
el("div", { className: "column-tasks" }).append(
|
||||
...tasks.map(task=> el(TaskCard, { task, onDragable }, onCardEdit, onCardDelete))
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// In Progress column
|
||||
el("div", {
|
||||
id: `column-${STATUSES.IN_PROGRESS}`,
|
||||
className: "task-column"
|
||||
}, onDragArea(STATUSES.IN_PROGRESS)).append(
|
||||
el("h2", { className: "column-header" }).append(
|
||||
"In Progress ",
|
||||
el("span", {
|
||||
textContent: S(() => tasksByStatus.get()[STATUSES.IN_PROGRESS].length),
|
||||
className: "task-count",
|
||||
}),
|
||||
),
|
||||
S.el(S(() => tasksByStatus.get()[STATUSES.IN_PROGRESS]), tasks =>
|
||||
el("div", { className: "column-tasks" }).append(
|
||||
...tasks.map(task=> el(TaskCard, { task, onDragable }, onCardEdit, onCardDelete))
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// Done column
|
||||
el("div", {
|
||||
id: `column-${STATUSES.DONE}`,
|
||||
className: "task-column"
|
||||
}, onDragArea(STATUSES.DONE)).append(
|
||||
el("h2", { className: "column-header" }).append(
|
||||
"Done ",
|
||||
el("span", {
|
||||
textContent: S(() => tasksByStatus.get()[STATUSES.DONE].length),
|
||||
className: "task-count",
|
||||
}),
|
||||
),
|
||||
S.el(S(() => tasksByStatus.get()[STATUSES.DONE]), tasks =>
|
||||
el("div", { className: "column-tasks" }).append(
|
||||
...tasks.map(task=> el(TaskCard, { task, onDragable }, onCardEdit, onCardDelete))
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
/** @typedef {CustomEvent<[ string, Task ]>} CardEditEvent */
|
||||
/** @typedef {CustomEvent<string>} CardDeleteEvent */
|
||||
/**
|
||||
* Task Card Component
|
||||
* @type {(props: { task: Task, onDragable: (id: number) => ddeElementAddon<HTMLDivElement> }) => HTMLElement}
|
||||
* @fires {CardEditEvent} card:edit
|
||||
* @fires {CardDeleteEvent} card:delete
|
||||
* */
|
||||
function TaskCard({ task, onDragable }){
|
||||
const { host }= scope;
|
||||
const isEditing = S(false);
|
||||
const onEditStart = () => isEditing.set(true);
|
||||
|
||||
const dispatchEdit= dispatchEvent("card:edit", host);
|
||||
const dispatchDelete= dispatchEvent("card:delete", host).bind(null, task.id);
|
||||
|
||||
return el("div", {
|
||||
id: `task-${task.id}`,
|
||||
className: `task-card priority-${task.priority}`,
|
||||
draggable: true
|
||||
}, onDragable(task.id)).append(
|
||||
S.el(isEditing, editing => editing
|
||||
? el(EditMode)
|
||||
: el().append(
|
||||
el("div", { className: "task-header" }).append(
|
||||
el("h3", { className: "task-title", textContent: task.title }),
|
||||
el("div", { className: "task-actions" }).append(
|
||||
el("button", {
|
||||
textContent: "✎",
|
||||
className: "edit-btn",
|
||||
ariaLabel: "Edit task"
|
||||
}, on("click", onEditStart)),
|
||||
el("button", {
|
||||
textContent: "✕",
|
||||
className: "delete-btn",
|
||||
ariaLabel: "Delete task"
|
||||
}, on("click", dispatchDelete))
|
||||
)
|
||||
),
|
||||
!task.description
|
||||
? el()
|
||||
: el("p", { className: "task-description", textContent: task.description }),
|
||||
el("div", { className: "task-meta" }).append(
|
||||
el("span", {
|
||||
className: `priority-badge priority-${task.priority}`,
|
||||
textContent: task.priority.charAt(0).toUpperCase() + task.priority.slice(1)
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
function EditMode(){
|
||||
const onSubmit = on("submit", e => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(/** @type {HTMLFormElement} */(e.target));
|
||||
const title = formData.get("title");
|
||||
const description = formData.get("description");
|
||||
const priority = formData.get("priority");
|
||||
isEditing.set(false);
|
||||
dispatchEdit([ task.id, { title, description, priority } ]);
|
||||
})
|
||||
const onEditCancel = () => isEditing.set(false);
|
||||
|
||||
return el("form", { className: "task-edit-form" }, onSubmit).append(
|
||||
el("input", {
|
||||
name: "title",
|
||||
className: "task-title-input",
|
||||
defaultValue: task.title,
|
||||
placeholder: "Task title",
|
||||
required: true,
|
||||
autoFocus: true
|
||||
}),
|
||||
el("textarea", {
|
||||
name: "description",
|
||||
className: "task-desc-input",
|
||||
defaultValue: task.description,
|
||||
placeholder: "Description (optional)"
|
||||
}),
|
||||
el("select", {
|
||||
name: "priority",
|
||||
}, on.defer(el=> el.value = task.priority)).append(
|
||||
el("option", { value: "low", textContent: "Low Priority" }),
|
||||
el("option", { value: "medium", textContent: "Medium Priority" }),
|
||||
el("option", { value: "high", textContent: "High Priority" })
|
||||
),
|
||||
el("div", { className: "task-edit-actions" }).append(
|
||||
el("button", {
|
||||
textContent: "Cancel",
|
||||
type: "button",
|
||||
className: "cancel-btn"
|
||||
}, on("click", onEditCancel)),
|
||||
el("button", {
|
||||
textContent: "Save",
|
||||
type: "submit",
|
||||
className: "save-btn"
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to handle move an element
|
||||
* @param {(id: string, status: string) => void} onMoved
|
||||
* */
|
||||
function moveElementAddon(onMoved){
|
||||
let draggedTaskId = null;
|
||||
function onDragable(id) {
|
||||
return element => {
|
||||
on("dragstart", e => {
|
||||
draggedTaskId= id;
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
|
||||
// Add some styling to the element being dragged
|
||||
setTimeout(() => {
|
||||
const el = document.getElementById(`task-${id}`);
|
||||
if (el) el.classList.add('dragging');
|
||||
}, 0);
|
||||
})(element);
|
||||
|
||||
on("dragend", () => {
|
||||
draggedTaskId= null;
|
||||
|
||||
// Remove the styling
|
||||
const el = document.getElementById(`task-${id}`);
|
||||
if (el) el.classList.remove('dragging');
|
||||
})(element);
|
||||
};
|
||||
}
|
||||
function onDragArea(status) {
|
||||
return element => {
|
||||
on("dragover", e => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
|
||||
// Add a visual indicator for the drop target
|
||||
const column = document.getElementById(`column-${status}`);
|
||||
if (column) column.classList.add('drag-over');
|
||||
})(element);
|
||||
|
||||
on("dragleave", () => {
|
||||
// Remove the visual indicator
|
||||
const column = document.getElementById(`column-${status}`);
|
||||
if (column) column.classList.remove('drag-over');
|
||||
})(element);
|
||||
|
||||
on("drop", e => {
|
||||
e.preventDefault();
|
||||
const id = draggedTaskId;
|
||||
if (id) onMoved(id, status);
|
||||
// Remove the visual indicator
|
||||
const column = document.getElementById(`column-${status}`);
|
||||
if (column) column.classList.remove('drag-over');
|
||||
})(element);
|
||||
};
|
||||
}
|
||||
return { onDragable, onDragArea };
|
||||
}
|
||||
|
||||
// Render the component
|
||||
document.body.append(
|
||||
el("div", { style: "padding: 20px; background: #f5f5f5; min-height: 100vh;" }).append(
|
||||
el(TaskManager)
|
||||
),
|
||||
el("style", `
|
||||
.task-manager {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.app-header h1 {
|
||||
margin: 0;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.app-controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.app-controls input,
|
||||
.app-controls select {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.new-task-form {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-row input {
|
||||
flex-grow: 1;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-row select {
|
||||
width: 100px;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
background: #4a90e2;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.add-btn:hover {
|
||||
background: #3a7bc8;
|
||||
}
|
||||
|
||||
.new-task-form textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.task-board {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.task-column {
|
||||
background: #f7fafc;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
min-height: 400px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.column-header {
|
||||
margin-top: 0;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
font-size: 1.25rem;
|
||||
color: #2d3748;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-count {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #e2e8f0;
|
||||
color: #4a5568;
|
||||
border-radius: 50%;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
font-size: 0.875rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.column-tasks {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
||||
cursor: grab;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
position: relative;
|
||||
border-left: 4px solid #ccc;
|
||||
}
|
||||
|
||||
.task-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.task-card.dragging {
|
||||
opacity: 0.5;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.task-card.priority-low {
|
||||
border-left-color: #38b2ac;
|
||||
}
|
||||
|
||||
.task-card.priority-medium {
|
||||
border-left-color: #ecc94b;
|
||||
}
|
||||
|
||||
.task-card.priority-high {
|
||||
border-left-color: #e53e3e;
|
||||
}
|
||||
|
||||
.task-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.task-title {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
color: #2d3748;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.task-description {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 0.9rem;
|
||||
color: #4a5568;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.edit-btn,
|
||||
.delete-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
color: #718096;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.edit-btn:hover {
|
||||
background: #edf2f7;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background: #fed7d7;
|
||||
color: #e53e3e;
|
||||
}
|
||||
|
||||
.task-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.priority-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.priority-badge.priority-low {
|
||||
background: #e6fffa;
|
||||
color: #2c7a7b;
|
||||
}
|
||||
|
||||
.priority-badge.priority-medium {
|
||||
background: #fefcbf;
|
||||
color: #975a16;
|
||||
}
|
||||
|
||||
.priority-badge.priority-high {
|
||||
background: #fed7d7;
|
||||
color: #c53030;
|
||||
}
|
||||
|
||||
.drag-over {
|
||||
background: #f0f9ff;
|
||||
border: 2px dashed #4a90e2;
|
||||
}
|
||||
|
||||
.task-edit-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.task-title-input,
|
||||
.task-desc-input {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.task-desc-input {
|
||||
min-height: 60px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.task-edit-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.cancel-btn,
|
||||
.save-btn {
|
||||
padding: 0.4rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: #edf2f7;
|
||||
color: #4a5568;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
background: #4a90e2;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.app-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.app-controls {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.task-board {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
`)
|
||||
);
|
@ -1,4 +1,4 @@
|
||||
// Example of reactive element marker
|
||||
<!--<dde:mark type="reactive" component="<component-name>">-->
|
||||
<!--<dde:mark type="reactive" source="...">-->
|
||||
<!-- content that updates when signal changes -->
|
||||
<!--</dde:mark>-->
|
||||
|
@ -1,28 +1,13 @@
|
||||
import { el, on } from "deka-dom-el";
|
||||
function allLifecycleEvents(){
|
||||
return el("form", null,
|
||||
el=> log({ type: "dde:created", detail: el }),
|
||||
on.connected(log),
|
||||
on.disconnected(log),
|
||||
).append(
|
||||
el("select", { id: "country" }, on.defer(select => {
|
||||
// This runs when the select is ready with all its options
|
||||
select.value = "cz"; // Pre-select Czechia
|
||||
log({ type: "dde:on.defer", detail: select });
|
||||
})).append(
|
||||
el("option", { value: "au", textContent: "Australia" }),
|
||||
el("option", { value: "ca", textContent: "Canada" }),
|
||||
el("option", { value: "cz", textContent: "Czechia" }),
|
||||
),
|
||||
el("p", "See lifecycle events in console."),
|
||||
);
|
||||
}
|
||||
const paragraph= el("p", "See lifecycle events in console.",
|
||||
el=> log({ type: "dde:created", detail: el }),
|
||||
on.connected(log),
|
||||
on.disconnected(log),
|
||||
);
|
||||
|
||||
document.body.append(
|
||||
el(allLifecycleEvents),
|
||||
el("button", "Remove Element", on("click", function(){
|
||||
this.previousSibling.remove();
|
||||
}))
|
||||
paragraph,
|
||||
el("button", "Remove", on("click", ()=> paragraph.remove()))
|
||||
);
|
||||
|
||||
/** @param {Partial<CustomEvent>} event */
|
||||
|
@ -7,20 +7,20 @@ function HelloWorld({ emoji = "🚀" }) {
|
||||
const clicks = S(0);
|
||||
|
||||
return el().append(
|
||||
// PART 2: Bind state to UI elements
|
||||
el("p", {
|
||||
className: "greeting",
|
||||
// This paragraph automatically updates when clicks changes
|
||||
textContent: S(() => `Hello World ${emoji.repeat(clicks.get())}`)
|
||||
}),
|
||||
// PART 2: Bind state to UI elements
|
||||
el("p", {
|
||||
className: "greeting",
|
||||
// This paragraph automatically updates when clicks changes
|
||||
textContent: S(() => `Hello World ${emoji.repeat(clicks.get())}`)
|
||||
}),
|
||||
|
||||
// PART 3: Update state in response to events
|
||||
el("button", {
|
||||
type: "button",
|
||||
textContent: "Add emoji",
|
||||
// When clicked, update the state
|
||||
onclick: () => clicks.set(clicks.get() + 1)
|
||||
})
|
||||
// PART 3: Update state in response to events
|
||||
el("button", {
|
||||
type: "button",
|
||||
textContent: "Add emoji",
|
||||
// When clicked, update the state
|
||||
onclick: () => clicks.set(clicks.get() + 1)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ document.body.append(
|
||||
padding: 1em;
|
||||
margin: 1em;
|
||||
}
|
||||
`.trim())
|
||||
`.trim())
|
||||
);
|
||||
|
||||
export function CounterStandard() {
|
||||
|
@ -13,20 +13,18 @@ import { S } from "deka-dom-el/signals";
|
||||
* @returns {HTMLElement} The root TodoMVC application element
|
||||
*/
|
||||
function Todos(){
|
||||
const { signal } = scope;
|
||||
const pageS = routerSignal(S, signal);
|
||||
const pageS = routerSignal(S);
|
||||
const todosS = todosSignal();
|
||||
/** Derived signal that filters todos based on current route */
|
||||
const todosFilteredS = S(()=> {
|
||||
const filteredTodosS = S(()=> {
|
||||
const todos = todosS.get();
|
||||
const filter = pageS.get();
|
||||
if (filter === "all") return todos;
|
||||
return todos.filter(todo => {
|
||||
if (filter === "active") return !todo.completed;
|
||||
if (filter === "completed") return todo.completed;
|
||||
return true; // "all"
|
||||
});
|
||||
});
|
||||
const todosRemainingS = S(()=> todosS.get().filter(todo => !todo.completed).length);
|
||||
|
||||
/** @type {ddeElementAddon<HTMLInputElement>} */
|
||||
const onToggleAll = on("change", event => {
|
||||
@ -75,7 +73,7 @@ function Todos(){
|
||||
}, onToggleAll),
|
||||
el("label", { htmlFor: "toggle-all", title: "Mark all as complete" }),
|
||||
el("ul", { className: "todo-list" }).append(
|
||||
S.el(todosFilteredS, filteredTodos => filteredTodos.map(todo =>
|
||||
S.el(filteredTodosS, filteredTodos => filteredTodos.map(todo =>
|
||||
memo(todo.id, ()=> el(TodoItem, todo, onDelete, onEdit)))
|
||||
)
|
||||
)
|
||||
@ -85,7 +83,7 @@ function Todos(){
|
||||
? el()
|
||||
: el("footer", { className: "footer" }).append(
|
||||
el("span", { className: "todo-count" }).append(
|
||||
noOfLeft()
|
||||
noOfLeft(todos)
|
||||
),
|
||||
memo("filters", ()=>
|
||||
el("ul", { className: "filters" }).append(
|
||||
@ -100,18 +98,15 @@ function Todos(){
|
||||
)
|
||||
),
|
||||
),
|
||||
todos.length - todosRemainingS.get() === 0
|
||||
!todos.some(todo => todo.completed)
|
||||
? el()
|
||||
: memo("delete", () =>
|
||||
el("button",
|
||||
{ textContent: "Clear completed", className: "clear-completed" },
|
||||
onClearCompleted)
|
||||
)
|
||||
: el("button", { textContent: "Clear completed", className: "clear-completed" }, onClearCompleted)
|
||||
)
|
||||
)
|
||||
);
|
||||
function noOfLeft(){
|
||||
const length = todosRemainingS.get();
|
||||
/** @param {Todo[]} todos */
|
||||
function noOfLeft(todos){
|
||||
const { length }= todos.filter(todo => !todo.completed);
|
||||
return el("strong").append(
|
||||
length + " ",
|
||||
length === 1 ? "item left" : "items left"
|
||||
@ -199,7 +194,7 @@ function TodoItem({ id, title, completed }) {
|
||||
checked: completed
|
||||
}, onToggleCompleted),
|
||||
el("label", { textContent: title }, onStartEdit),
|
||||
el("button", { ariaLabel: "Delete todo", className: "destroy" }, onDelete)
|
||||
el("button", { className: "destroy" }, onDelete)
|
||||
),
|
||||
S.el(isEditing, editing => !editing
|
||||
? el()
|
||||
@ -333,7 +328,6 @@ function todosSignal(){
|
||||
localStorage.setItem(store_key, JSON.stringify(value));
|
||||
} catch (e) {
|
||||
console.error("Failed to save todos to localStorage", e);
|
||||
// Optionally, provide user feedback
|
||||
}
|
||||
});
|
||||
return out;
|
||||
@ -342,10 +336,9 @@ function todosSignal(){
|
||||
/**
|
||||
* Creates a signal for managing route state
|
||||
*
|
||||
* @param {typeof S} signal - The signal constructor from a library
|
||||
* @param {AbortSignal} abortSignal
|
||||
* @param {typeof S} signal - The signal constructor
|
||||
*/
|
||||
function routerSignal(signal, abortSignal){
|
||||
function routerSignal(signal){
|
||||
const initial = location.hash.replace("#", "") || "all";
|
||||
const out = signal(initial, {
|
||||
/**
|
||||
@ -354,16 +347,15 @@ function routerSignal(signal, abortSignal){
|
||||
*/
|
||||
set(hash){
|
||||
location.hash = hash;
|
||||
//this.value = hash;
|
||||
},
|
||||
this.value = hash;
|
||||
}
|
||||
});
|
||||
|
||||
// Setup hash change listener
|
||||
window.addEventListener("hashchange", () => {
|
||||
const hash = location.hash.replace("#", "") || "all";
|
||||
//S.action(out, "set", /** @type {"all"|"active"|"completed"} */(hash));
|
||||
out.set(hash);
|
||||
}, { signal: abortSignal });
|
||||
S.action(out, "set", /** @type {"all"|"active"|"completed"} */(hash));
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
8
docs/components/getLibraryUrl.css.js
Normal file
8
docs/components/getLibraryUrl.css.js
Normal file
@ -0,0 +1,8 @@
|
||||
import { styles } from "../ssr.js";
|
||||
|
||||
styles.css`
|
||||
#library-url-form {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
`;
|
@ -1,83 +0,0 @@
|
||||
import { styles } from "../ssr.js";
|
||||
|
||||
styles.css`
|
||||
#library-url-form {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--bg-sidebar);
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
#library-url-form .selectors {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
#library-url-form output {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
#library-url-form output p {
|
||||
font-weight: 500;
|
||||
margin: 0.25rem 0;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
#library-url-form .url-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: -0.25rem;
|
||||
}
|
||||
|
||||
#library-url-form .url-title strong {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
#library-url-form .url-title span {
|
||||
color: var(--text-light);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
#library-url-form .code {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#library-url-form .info-text {
|
||||
font-size: 0.9rem;
|
||||
font-style: italic;
|
||||
margin-top: 1rem;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#library-url-form .selectors {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#library-url-form select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
import { el } from "deka-dom-el";
|
||||
import { ireland } from "./ireland.html.js";
|
||||
|
||||
export function getLibraryUrl({ page_id }){
|
||||
return el(ireland, {
|
||||
src: new URL("./getLibraryUrl.js.js", import.meta.url),
|
||||
exportName: "getLibraryUrl",
|
||||
page_id,
|
||||
});
|
||||
}
|
@ -8,13 +8,6 @@ const url_base= {
|
||||
export function getLibraryUrl(){
|
||||
const lib= S([ "esm", "-with-signals", ".min" ]);
|
||||
const url= S(()=> url_base.jsdeka+lib.get().join(""));
|
||||
const urlLabel= S(() => {
|
||||
const [format, signalsPart, minified] = lib.get();
|
||||
const formatText = format === "esm" ? "ES Module" : "IIFE";
|
||||
const signalsText = signalsPart ? " with signals" : "";
|
||||
const minText = minified ? " (minified)" : "";
|
||||
return `${formatText}${signalsText}${minText}`;
|
||||
})
|
||||
const onSubmit= on("submit", ev => {
|
||||
ev.preventDefault();
|
||||
const form= new FormData(/** @type {HTMLFormElement} */ (ev.target));
|
||||
@ -27,66 +20,56 @@ export function getLibraryUrl(){
|
||||
const onChangeSubmit= on("change",
|
||||
ev=> /** @type {HTMLSelectElement} */(ev.target).form.requestSubmit()
|
||||
);
|
||||
const onCopy= on("click", ev => {
|
||||
const code= /** @type {HTMLDivElement} */ (ev.target).previousElementSibling;
|
||||
navigator.clipboard.writeText(code.textContent);
|
||||
});
|
||||
|
||||
return el("form", { id: "library-url-form" }, onSubmit).append(
|
||||
el("h4", "Select your preferred library format:"),
|
||||
el("div", { className: "selectors" }).append(
|
||||
el("select", { name: "module" }, onChangeSubmit,
|
||||
on.defer(select => select.value = lib.get()[0]),
|
||||
).append(
|
||||
el("option", { value: "esm", textContent: "ESM — modern JavaScript module" }),
|
||||
el("option", { value: "iife", textContent: "IIFE — legacy JavaScript with DDE global variable" }),
|
||||
),
|
||||
el("select", { name: "what" }, onChangeSubmit,
|
||||
on.defer(select => select.value = lib.get()[1]),
|
||||
).append(
|
||||
el("option", { value: "", textContent: "DOM part only" }),
|
||||
el("option", { value: "-with-signals", textContent: "DOM + signals" }),
|
||||
),
|
||||
el("select", { name: "minified" }, onChangeSubmit,
|
||||
on.defer(select => select.value = lib.get()[2]),
|
||||
).append(
|
||||
el("option", { value: "", textContent: "Unminified" }),
|
||||
el("option", { value: ".min", textContent: "Minified" }),
|
||||
),
|
||||
el("select", { name: "module" }, onChangeSubmit).append(
|
||||
el("option", { value: "esm", textContent: "ESM — modern JavaScript module" },
|
||||
isSelected(lib.get()[0])),
|
||||
el("option", { value: "iife", textContent: "IIFE — legacy JavaScript with DDE global variable" },
|
||||
isSelected(lib.get()[0])),
|
||||
),
|
||||
el("select", { name: "what" }, onChangeSubmit).append(
|
||||
el("option", { value: "", textContent: "only DOM part" },
|
||||
isSelected(lib.get()[1])),
|
||||
el("option", { value: "-with-signals", textContent: "DOM part and signals library" },
|
||||
isSelected(lib.get()[1])),
|
||||
),
|
||||
el("select", { name: "minified" }, onChangeSubmit).append(
|
||||
el("option", { value: "", textContent: "unminified" },
|
||||
isSelected(lib.get()[2])),
|
||||
el("option", { value: ".min", textContent: "minified" },
|
||||
isSelected(lib.get()[2])),
|
||||
),
|
||||
el("output").append(
|
||||
el("div", { className: "url-title" }).append(
|
||||
el("strong", "JavaScript:"),
|
||||
el("span", urlLabel),
|
||||
el("p", "Library URL:"),
|
||||
el("div", { className: "code", dataJs: "done", tabIndex: 0 }).append(
|
||||
el("code").append(
|
||||
el("pre", S(()=> url.get()+".js")),
|
||||
),
|
||||
el("button", {
|
||||
className: "copy-button",
|
||||
textContent: "Copy",
|
||||
ariaLabel: "Copy code to clipboard",
|
||||
}, onCopy),
|
||||
),
|
||||
el(code, { value: S(()=> url.get()+".js") }),
|
||||
el("div", { className: "url-title" }).append(
|
||||
el("strong", "TypeScript definition:")
|
||||
el("p", "Library type definition:"),
|
||||
el("div", { className: "code", dataJs: "done", tabIndex: 0 }).append(
|
||||
el("code").append(
|
||||
el("pre", S(()=> url.get()+".d.ts")),
|
||||
),
|
||||
el("button", {
|
||||
className: "copy-button",
|
||||
textContent: "Copy",
|
||||
ariaLabel: "Copy code to clipboard",
|
||||
}, onCopy),
|
||||
),
|
||||
el(code, { value: S(()=> url.get()+".d.ts") }),
|
||||
el("p", { className: "info-text",
|
||||
textContent: "Use the CDN URL in your HTML or import it in your JavaScript files."
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
/** @param {{ value: ddeSignal<string> }} props */
|
||||
function code({ value }){
|
||||
/** @type {ddeSignal<"Copy"|"Copied!">} */
|
||||
const textContent= S("Copy");
|
||||
const onCopy= on("click", () => {
|
||||
navigator.clipboard.writeText(value.get());
|
||||
|
||||
textContent.set("Copied!");
|
||||
setTimeout(() => {
|
||||
textContent.set("Copy");
|
||||
}, 1500);
|
||||
});
|
||||
return el("div", { className: "code", dataJs: "done", tabIndex: 0 }).append(
|
||||
el("code").append(
|
||||
el("pre", value),
|
||||
),
|
||||
el("button", {
|
||||
className: "copy-button",
|
||||
textContent,
|
||||
ariaLabel: "Copy code to clipboard",
|
||||
}, onCopy)
|
||||
)
|
||||
;
|
||||
function isSelected(value){
|
||||
return element=> element.selected= element.value===value;
|
||||
}
|
||||
|
@ -1,33 +1,3 @@
|
||||
import { styles } from "../ssr.js";
|
||||
styles.css`
|
||||
[data-dde-mark] {
|
||||
opacity: .5;
|
||||
filter: grayscale();
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
animation: fadein 2s infinite ease forwards;;
|
||||
}
|
||||
|
||||
position: relative;
|
||||
&::after {
|
||||
content: "Loading Ireland…";
|
||||
background-color: rgba(0, 0, 0, .5);
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
padding: 5px 10px;
|
||||
position: absolute;
|
||||
top: 3%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
@keyframes fadein {
|
||||
from { opacity: .5; }
|
||||
to { opacity: .85; }
|
||||
}
|
||||
`;
|
||||
|
||||
import { el, queue } from "deka-dom-el";
|
||||
import { addEventListener, registerClientFile } from "../ssr.js";
|
||||
import { relative } from "node:path";
|
||||
@ -51,17 +21,10 @@ export function ireland({ src, exportName = "default", props = {} }) {
|
||||
const path= "./"+relative(dir, src.pathname);
|
||||
const id = "ireland-" + generateComponentId(src);
|
||||
const element = el.mark({ type: "later", name: ireland.name });
|
||||
queue(
|
||||
import(path)
|
||||
.then(module => {
|
||||
const component = module[exportName];
|
||||
const content= el(component, props, mark(id));
|
||||
element.replaceWith(content);
|
||||
content.querySelectorAll("input, textarea, button")
|
||||
.forEach(el=> el.disabled= true);
|
||||
})
|
||||
.catch(console.error)
|
||||
);
|
||||
queue(import(path).then(module => {
|
||||
const component = module[exportName];
|
||||
element.replaceWith(el(component, props, mark(id)));
|
||||
}));
|
||||
|
||||
if(!componentsRegistry.size)
|
||||
addEventListener("oneachrender", registerClientPart);
|
||||
|
@ -9,7 +9,7 @@ export function mnemonic(){
|
||||
),
|
||||
el("li").append(
|
||||
el("code", "el(<tag-name>, <primitive>)[.append(...)]: <element-from-tag-name>"),
|
||||
" — simple element containing only text (accepts string, number or signal)",
|
||||
" — simple element containing only text",
|
||||
),
|
||||
el("li").append(
|
||||
el("code", "el(<tag-name>, <object>)[.append(...)]: <element-from-tag-name>"),
|
||||
@ -26,6 +26,6 @@ export function mnemonic(){
|
||||
el("li").append(
|
||||
el("code", "elNS(<namespace>)(<as-el-see-above>)[.append(...)]: <element-based-on-arguments>"),
|
||||
" — typically SVG elements",
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ export function mnemonic(){
|
||||
el("li").append(
|
||||
el("code", "S.clear(...<signals>)"),
|
||||
" — off and clear signals (most of the time it is not needed as reactive ",
|
||||
"attributes and elements are handled automatically)",
|
||||
"attributes and elements are cleared automatically)",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -37,14 +37,3 @@ styles.css`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
import { el } from "deka-dom-el";
|
||||
import { ireland } from "./ireland.html.js";
|
||||
|
||||
export function scrollTop(){
|
||||
return el(ireland, {
|
||||
src: new URL("./scrollTop.js.js", import.meta.url),
|
||||
exportName: "scrollTop",
|
||||
page_id: "*",
|
||||
});
|
||||
}
|
@ -86,7 +86,7 @@ html {
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 3px solid var(--primary-light);
|
||||
outline: 3px solid hsl(231, 48%, 70%);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@ -207,20 +207,6 @@ figure {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
select {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--border);
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
font-family: var(--font-main);
|
||||
}
|
||||
|
||||
select:hover {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
body {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import "./components/getLibraryUrl.html.js";
|
||||
import "./components/getLibraryUrl.css.js";
|
||||
import { t, T } from "./utils/index.js";
|
||||
export const info= {
|
||||
href: "./",
|
||||
@ -12,7 +12,7 @@ import { simplePage } from "./layout/simplePage.html.js";
|
||||
import { h3 } from "./components/pageUtils.html.js";
|
||||
import { example } from "./components/example.html.js";
|
||||
import { code } from "./components/code.html.js";
|
||||
import { getLibraryUrl } from "./components/getLibraryUrl.html.js";
|
||||
import { ireland } from "./components/ireland.html.js";
|
||||
/** @param {string} url */
|
||||
const fileURL= url=> new URL(url, import.meta.url);
|
||||
const references= {
|
||||
@ -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.
|
||||
`),
|
||||
@ -101,29 +101,13 @@ export function page({ pkg, info }){
|
||||
|
||||
el(h3, t`Getting Started`),
|
||||
el("p").append(T`
|
||||
There are multiple ways to include dd<el> in your project. You can use npm for a full development setup,
|
||||
or directly include it from a CDN for quick prototyping.
|
||||
To get builded dd<el> to be used immediately in your browser, you can download the latest version from:
|
||||
`),
|
||||
el("h4", "npm installation"),
|
||||
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:
|
||||
`),
|
||||
el(getLibraryUrl, { page_id }),
|
||||
el("div", { className: "note" }).append(
|
||||
el("p").append(T`
|
||||
Based on your selection, you can use dd<el> in your project like this:
|
||||
`),
|
||||
el(code, { content: `
|
||||
// ESM format (modern JavaScript with import/export)
|
||||
import { el, on } from "https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-signals.min.js";
|
||||
|
||||
// Or with IIFE format (creates a global DDE object)
|
||||
// <script src="https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/iife-with-signals.min.js"></script>
|
||||
const { el, on } = DDE;
|
||||
`, language: "js", page_id }),
|
||||
),
|
||||
el(ireland, {
|
||||
src: fileURL("./components/getLibraryUrl.js.js"),
|
||||
exportName: "getLibraryUrl",
|
||||
page_id,
|
||||
}),
|
||||
|
||||
el(h3, t`How to Use This Documentation`),
|
||||
el("p").append(T`
|
||||
@ -154,10 +138,6 @@ export function page({ pkg, info }){
|
||||
Interactive demos with server-side pre-rendering`),
|
||||
el("li").append(T`${el("a", { href: "p13-appendix.html" }).append(el("strong", "Appendix & Summary"))} —
|
||||
Comprehensive reference and best practices`),
|
||||
el("li").append(T`${el("a", { href: "p14-converter.html" }).append(el("strong", "HTML Converter"))} —
|
||||
Convert HTML to dd<el> JavaScript code`),
|
||||
el("li").append(T`${el("a", { href: "p15-examples.html" }).append(el("strong", "Examples Gallery"))} —
|
||||
Real-world application examples and case studies`),
|
||||
),
|
||||
el("p").append(T`
|
||||
Each section builds on the previous ones, so we recommend following them in order.
|
||||
|
@ -3,7 +3,10 @@ import { el, simulateSlots } from "deka-dom-el";
|
||||
|
||||
import { header } from "./head.html.js";
|
||||
import { prevNext } from "../components/pageUtils.html.js";
|
||||
import { scrollTop } from "../components/scrollTop.html.js";
|
||||
import { ireland } from "../components/ireland.html.js";
|
||||
import "../components/scrollTop.css.js";
|
||||
/** @param {string} url */
|
||||
const fileURL= url=> new URL(url, import.meta.url);
|
||||
|
||||
/** @param {Pick<import("../types.d.ts").PageAttrs, "pkg" | "info">} attrs */
|
||||
export function simplePage({ pkg, info }){
|
||||
@ -30,6 +33,6 @@ export function simplePage({ pkg, info }){
|
||||
),
|
||||
|
||||
// Scroll to top button
|
||||
el(scrollTop),
|
||||
el(ireland, { src: fileURL("../components/scrollTop.js.js"), exportName: "scrollTop" })
|
||||
));
|
||||
}
|
||||
|
@ -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`), // TODO: add example?
|
||||
el("li", t`Capture element references`)
|
||||
)
|
||||
),
|
||||
el("p").append(T`
|
||||
@ -156,7 +156,7 @@ export function page({ pkg, info }){
|
||||
`),
|
||||
el("p").append(T`
|
||||
dd<el> provides two additional lifecycle events that correspond to ${el("a", { textContent:
|
||||
"custom element", ...references.mdn_customElements })} lifecycle callbacks and component patterns:
|
||||
"custom element", ...references.mdn_customElements })} lifecycle callbacks:
|
||||
`),
|
||||
el("div", { className: "function-table" }).append(
|
||||
el("dl").append(
|
||||
@ -199,19 +199,6 @@ export function page({ pkg, info }){
|
||||
)
|
||||
),
|
||||
|
||||
el(h3, t`Utility Helpers`),
|
||||
el("p").append(T`
|
||||
You can use the ${el("code", "on.defer")} helper to defer execution to the next event loop.
|
||||
This is useful for example when you wan to set some element properties based on the current element
|
||||
body (typically the ${el("code", "<select value=\"...\">")}).
|
||||
`),
|
||||
el("div", { className: "function-table" }).append(
|
||||
el("dl").append(
|
||||
el("dt", t`on.defer(callback)`),
|
||||
el("dd", t`Helper that defers function execution to the next event loop (using setTimeout)`),
|
||||
)
|
||||
),
|
||||
|
||||
el(h3, t`Dispatching Custom Events`),
|
||||
el("p").append(T`
|
||||
This makes it easy to implement component communication through events, following standard web platform
|
||||
|
@ -277,72 +277,7 @@ export function page({ pkg, info }){
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Avoid infinite loops")}: Be careful when one signal updates another in a subscription
|
||||
`),
|
||||
),
|
||||
el("p").append(T`
|
||||
While signals provide powerful reactivity for complex UI interactions, 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(
|
||||
@ -363,6 +298,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 (or other scope related values)
|
||||
// 2. access the host element
|
||||
const { host } = scope;
|
||||
|
||||
// 3. Add behavior to host
|
||||
|
@ -80,7 +80,8 @@ export function page({ pkg, info }){
|
||||
el("li", t`listeners: A Set of functions called when the signal value changes`),
|
||||
el("li", t`actions: Custom actions that can be performed on the signal`),
|
||||
el("li", t`onclear: Functions to run when the signal is cleared`),
|
||||
el("li", t`host: Reference to the host element/scope in which the signal was created`),
|
||||
el("li", t`host: Reference to the host element/scope`),
|
||||
el("li", t`defined: Stack trace information for debugging`),
|
||||
el("li", t`readonly: Boolean flag indicating if the signal is read-only`)
|
||||
),
|
||||
el("p").append(T`
|
||||
@ -113,13 +114,7 @@ export function page({ pkg, info }){
|
||||
el("ul").append(
|
||||
el("li", t`That 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").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("li", t`That the signal is actually connected to the DOM element (check your S.el or attribute binding code)`)
|
||||
),
|
||||
el(code, { src: fileURL("./components/examples/debugging/mutations.js"), page_id }),
|
||||
|
||||
|
@ -101,23 +101,21 @@ export function page({ pkg, info }){
|
||||
`),
|
||||
el(code, { content: `
|
||||
// Signal for current route (all/active/completed)
|
||||
const { signal } = scope;
|
||||
const pageS = routerSignal(S, signal);
|
||||
const pageS = routerSignal(S);
|
||||
|
||||
// Signal for the todos collection with custom actions
|
||||
const todosS = todosSignal();
|
||||
|
||||
// Derived signal that filters todos based on current route
|
||||
const todosFilteredS = S(()=> {
|
||||
const filteredTodosS = S(()=> {
|
||||
const todos = todosS.get();
|
||||
const filter = pageS.get();
|
||||
if (filter === "all") return todos;
|
||||
return todos.filter(todo => {
|
||||
if (filter === "active") return !todo.completed;
|
||||
if (filter === "completed") return todo.completed;
|
||||
return true; // "all"
|
||||
});
|
||||
});
|
||||
const todosRemainingS = S(()=> todosS.get().filter(todo => !todo.completed).length);
|
||||
`, page_id }),
|
||||
|
||||
el("p").append(T`
|
||||
@ -202,7 +200,6 @@ export function page({ pkg, info }){
|
||||
localStorage.setItem(store_key, JSON.stringify(value));
|
||||
} catch (e) {
|
||||
console.error("Failed to save todos to localStorage", e);
|
||||
// Optionally, provide user feedback
|
||||
}
|
||||
});
|
||||
return out;
|
||||
@ -225,19 +222,19 @@ export function page({ pkg, info }){
|
||||
el("h4", t`1. Derived Signals for Filtering`),
|
||||
el(code, { content: `
|
||||
/** Derived signal that filters todos based on current route */
|
||||
const todosFilteredS = S(()=> {
|
||||
const filteredTodosS = S(()=> {
|
||||
const todos = todosS.get();
|
||||
const filter = pageS.get();
|
||||
if (filter === "all") return todos;
|
||||
return todos.filter(todo => {
|
||||
if (filter === "active") return !todo.completed;
|
||||
if (filter === "completed") return todo.completed;
|
||||
return true; // "all"
|
||||
});
|
||||
});
|
||||
|
||||
// Using the derived signal in the UI
|
||||
el("ul", { className: "todo-list" }).append(
|
||||
S.el(todosFilteredS, filteredTodos => filteredTodos.map(todo =>
|
||||
S.el(filteredTodosS, filteredTodos => filteredTodos.map(todo =>
|
||||
memo(todo.id, ()=> el(TodoItem, todo, onDelete, onEdit)))
|
||||
)
|
||||
)
|
||||
@ -337,7 +334,7 @@ export function page({ pkg, info }){
|
||||
el("h4", t`Memoizing Todo Items`),
|
||||
el(code, { content: `
|
||||
el("ul", { className: "todo-list" }).append(
|
||||
S.el(todosFilteredS, filteredTodos => filteredTodos.map(todo =>
|
||||
S.el(filteredTodosS, filteredTodos => filteredTodos.map(todo =>
|
||||
memo(todo.id, ()=> el(TodoItem, todo, onDelete, onEdit)))
|
||||
)
|
||||
)
|
||||
@ -546,13 +543,11 @@ export function page({ pkg, info }){
|
||||
|
||||
el("h4", t`Conditional Clear Completed Button`),
|
||||
el(code, { content: `
|
||||
todos.length - todosRemainingS.get() === 0
|
||||
? el()
|
||||
: memo("delete", () =>
|
||||
el("button",
|
||||
{ textContent: "Clear completed", className: "clear-completed" },
|
||||
onClearCompleted)
|
||||
)
|
||||
S.el(S(() => todosS.get().some(todo => todo.completed)),
|
||||
hasTodosCompleted=> hasTodosCompleted
|
||||
? el("button", { textContent: "Clear completed", className: "clear-completed" }, onClearCompleted)
|
||||
: el()
|
||||
)
|
||||
`, page_id }),
|
||||
|
||||
el("div", { className: "note" }).append(
|
||||
@ -630,7 +625,7 @@ export function page({ pkg, info }){
|
||||
${el("strong", "Declarative Class Management:")} Using the classList property for cleaner class handling
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Focus Management:")} Reliable input focus with setTimeout
|
||||
${el("strong", "Focus Management:")} Reliable input focus with requestAnimationFrame
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Persistent Storage:")} Automatically saving application state with signal listeners
|
||||
|
@ -2,7 +2,7 @@ import { T, t } from "./utils/index.js";
|
||||
export const info= {
|
||||
title: t`Appendix & Summary`,
|
||||
fullTitle: t`dd<el> Comprehensive Reference`,
|
||||
description: t`A final overview, case studies, key concepts, and best practices for working with deka-dom-el.`,
|
||||
description: t`A final overview, case studies, key concepts, and best practices for working with deka-dom-el.`,
|
||||
};
|
||||
|
||||
import { el } from "deka-dom-el";
|
||||
@ -25,16 +25,6 @@ const references= {
|
||||
performance: {
|
||||
title: t`Performance Optimization Guide`,
|
||||
href: "p09-optimization.html",
|
||||
},
|
||||
/** Examples gallery */
|
||||
examples: {
|
||||
title: t`Examples Gallery`,
|
||||
href: "p15-examples.html",
|
||||
},
|
||||
/** Converter */
|
||||
converter: {
|
||||
title: t`HTML to dd<el> Converter`,
|
||||
href: "p14-converter.html",
|
||||
}
|
||||
};
|
||||
|
||||
@ -43,8 +33,8 @@ export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("p").append(T`
|
||||
This reference guide provides a comprehensive summary of dd<el>’s key concepts, best practices,
|
||||
case studies, and advanced techniques. Use it as a quick reference when working with the library
|
||||
This reference guide provides a comprehensive summary of dd<el>'s key concepts, best practices,
|
||||
case studies, and advanced techniques. Use it as a quick reference when working with the library
|
||||
or to deepen your understanding of its design principles and patterns.
|
||||
`),
|
||||
|
||||
@ -71,18 +61,14 @@ export function page({ pkg, info }){
|
||||
${el("strong", "Progressive Enhancement:")} Starting simple and adding complexity only when needed
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Flexibility:")} Using what you need, whether that’s plain DOM elements, event
|
||||
handling, or signals for reactivity
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Functional Composition:")} Building UIs through function composition
|
||||
${el("strong", "Functional Composition:")} Building UIs through function composition rather than
|
||||
inheritance
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Clear Patterns:")} Promoting maintainable code organization with the 3PS pattern
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Targeted Reactivity:")} Using signals for efficient, fine-grained updates only when
|
||||
needed
|
||||
${el("strong", "Targeted Reactivity:")} Using signals for efficient, fine-grained updates
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Unix Philosophy:")} Doing one thing well and allowing composability with other tools
|
||||
@ -91,15 +77,11 @@ export function page({ pkg, info }){
|
||||
),
|
||||
|
||||
el(h3, t`Case Studies & Real-World Applications`),
|
||||
el("p").append(T`
|
||||
Explore our ${el("a", references.examples).append("Examples Gallery")} to see how dd<el> can be used to build
|
||||
various real-world applications, from simple components to complex interactive UIs.
|
||||
`),
|
||||
|
||||
el("h4", t`TodoMVC Implementation`),
|
||||
el("p").append(T`
|
||||
The ${el("a", references.todomvc).append("TodoMVC implementation")} showcases how dd<el> handles a complete,
|
||||
real-world application with all standard features of a modern web app:
|
||||
The ${el("a", references.todomvc).append("TodoMVC implementation")} showcases how dd<el> handles a complete,
|
||||
real-world application with all standard features of a modern web app:
|
||||
`),
|
||||
el("ul").append(
|
||||
el("li", t`Persistent storage with localStorage`),
|
||||
@ -135,18 +117,16 @@ export function page({ pkg, info }){
|
||||
`),
|
||||
el("ol").append(
|
||||
el("li").append(T`
|
||||
${el("strong", "Start with state")}: Convert global variables or ad-hoc state to signals
|
||||
${el("strong", "Start with state:")}: Convert global variables or ad-hoc state to signals
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Replace query selectors")}: Replace getElementById/querySelector with direct references
|
||||
to elements
|
||||
${el("strong", "Replace query selectors:")}: Replace getElementById/querySelector with direct references to elements
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Convert imperative updates")}: Replace manual DOM updates with declarative signal
|
||||
bindings
|
||||
${el("strong", "Convert imperative updates:")}: Replace manual DOM updates with declarative signal bindings
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Refactor into components")}: Organize related UI elements into component functions
|
||||
${el("strong", "Refactor into components:")}: Organize related UI elements into component functions
|
||||
`)
|
||||
),
|
||||
el(code, { content: `
|
||||
@ -177,7 +157,7 @@ export function page({ pkg, info }){
|
||||
el("dd", t`Core function for creating DOM elements with declarative properties`),
|
||||
|
||||
el("dt", t`el().append(...children)`),
|
||||
el("dd", t`Add child elements to a parent element`),
|
||||
el("dd", t`Add child elements to a parent element`),
|
||||
|
||||
el("dt", t`memo(key, () => element)`),
|
||||
el("dd", t`Cache and reuse DOM elements for performance optimization`),
|
||||
@ -191,22 +171,22 @@ export function page({ pkg, info }){
|
||||
el("h4", t`Signals & Reactivity`),
|
||||
el("dl").append(
|
||||
el("dt", t`S(initialValue)`),
|
||||
el("dd", t`Create a signal with an initial value`),
|
||||
el("dd", t`Create a signal with an initial value`),
|
||||
|
||||
el("dt", t`S(() => computation)`),
|
||||
el("dd", t`Create a derived signal that updates when dependencies change`),
|
||||
el("dd", t`Create a derived signal that updates when dependencies change`),
|
||||
|
||||
el("dt", t`S.el(signal, data => element)`),
|
||||
el("dd", t`Create reactive elements that update when a signal changes`),
|
||||
el("dd", t`Create reactive elements that update when a signal changes`),
|
||||
|
||||
el("dt", t`S.action(signal, "method", ...args)`),
|
||||
el("dd", t`Call custom methods defined on a signal`),
|
||||
el("dd", t`Call custom methods defined on a signal`),
|
||||
|
||||
el("dt", t`signal.get()`),
|
||||
el("dd", t`Get the current value of a signal`),
|
||||
el("dd", t`Get the current value of a signal`),
|
||||
|
||||
el("dt", t`signal.set(newValue)`),
|
||||
el("dd", t`Update a signal’s value and trigger reactive updates`)
|
||||
el("dd", t`Update a signal's value and trigger reactive updates`)
|
||||
)
|
||||
),
|
||||
|
||||
@ -220,7 +200,7 @@ export function page({ pkg, info }){
|
||||
el("dd", t`Provides access to component context, signal, host element`),
|
||||
|
||||
el("dt", t`dispatchEvent(type, element)`),
|
||||
el("dd", t`Creates a function for dispatching custom events`),
|
||||
el("dd", t`Creates a function for dispatching custom events`),
|
||||
|
||||
el("dt", t`Signal Factories`),
|
||||
el("dd", t`Functions that create and configure signals with domain-specific behavior`)
|
||||
@ -232,45 +212,19 @@ export function page({ pkg, info }){
|
||||
el("h4", t`Code Organization`),
|
||||
el("ul").append(
|
||||
el("li").append(T`
|
||||
${el("strong", "Follow the 3PS pattern")}: Separate state creation, binding to elements, and state updates
|
||||
${el("strong", "Follow the 3PS pattern:")}: Separate state creation, binding to elements, and state updates
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Use component functions")}: Create reusable UI components as functions
|
||||
${el("strong", "Use component functions:")}: Create reusable UI components as functions
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Create signal factories")}: Extract reusable signal patterns into factory functions
|
||||
${el("strong", "Create signal factories:")}: Extract reusable signal patterns into factory functions
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Leverage scopes")}: Use scope for component context and clean resource management
|
||||
${el("strong", "Leverage scopes:")}: Use scope for component context and clean resource management
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Event delegation")}: Prefer component-level event handlers over many individual handlers
|
||||
`)
|
||||
)
|
||||
),
|
||||
|
||||
el("div").append(
|
||||
el("h4", t`When to Use Signals vs. Plain DOM`),
|
||||
el("ul").append(
|
||||
el("li").append(T`
|
||||
${el("strong", "Use signals for")}: Data that changes frequently, multiple elements that need to
|
||||
stay in sync, computed values dependent on other state
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Use plain DOM for")}: Static content, one-time DOM operations, simple toggling of
|
||||
elements, single-element updates
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Mixed approach")}: Start with plain DOM and events, then add signals only where
|
||||
needed for reactivity
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Consider derived signals")}: For complex transformations of data rather than manual
|
||||
updates
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Use event delegation")}: For handling multiple similar interactions without
|
||||
individual signal bindings
|
||||
${el("strong", "Event delegation:")}: Prefer component-level event handlers over many individual handlers
|
||||
`)
|
||||
)
|
||||
),
|
||||
@ -279,19 +233,19 @@ export function page({ pkg, info }){
|
||||
el("h4", t`Performance Optimization`),
|
||||
el("ul").append(
|
||||
el("li").append(T`
|
||||
${el("strong", "Memoize list items")}: Use ${el("code", "memo")} for items in frequently-updated lists
|
||||
${el("strong", "Memoize list items:")}: Use ${el("code", "memo")} for items in frequently-updated lists
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Avoid unnecessary signal updates")}: Only update signals when values actually change
|
||||
${el("strong", "Avoid unnecessary signal updates:")}: Only update signals when values actually change
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Use AbortSignals")}: Clean up resources when components are removed
|
||||
${el("strong", "Use AbortSignals:")}: Clean up resources when components are removed
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Prefer derived signals")}: Use computed values instead of manual updates
|
||||
${el("strong", "Prefer derived signals:")}: Use computed values instead of manual updates
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Avoid memoizing fragments")}: Never memoize DocumentFragments, only individual elements
|
||||
${el("strong", "Avoid memoizing fragments:")}: Never memoize DocumentFragments, only individual elements
|
||||
`)
|
||||
),
|
||||
el("p").append(T`
|
||||
@ -309,7 +263,7 @@ export function page({ pkg, info }){
|
||||
el("dd", t`Use scope.signal or AbortSignals to handle resource cleanup when elements are removed`),
|
||||
|
||||
el("dt", t`Circular Signal Dependencies`),
|
||||
el("dd", t`Avoid signals that depend on each other in a circular way, which can cause infinite update loops`),
|
||||
el("dd", t`Avoid signals that depend on each other in a circular way, which can cause infinite update loops`),
|
||||
|
||||
el("dt", t`Memoizing with Unstable Keys`),
|
||||
el("dd", t`Always use stable, unique identifiers as memo keys, not array indices or objects`),
|
||||
@ -325,46 +279,46 @@ export function page({ pkg, info }){
|
||||
el("tr").append(
|
||||
el("th", "Feature"),
|
||||
el("th", "dd<el>"),
|
||||
el("th", "VanJS"),
|
||||
el("th", "Solid"),
|
||||
el("th", "Alpine")
|
||||
el("th", "React"),
|
||||
el("th", "Vue"),
|
||||
el("th", "Svelte")
|
||||
)
|
||||
),
|
||||
el("tbody").append(
|
||||
el("tr").append(
|
||||
el("td", "No Build Step Required"),
|
||||
el("td", "✅"),
|
||||
el("td", "✅"),
|
||||
el("td", "⚠️ JSX needs transpilation"),
|
||||
el("td", "✅")
|
||||
el("td", "⚠️ SFC needs compilation"),
|
||||
el("td", "❌ Requires compilation")
|
||||
),
|
||||
el("tr").append(
|
||||
el("td", "Bundle Size (minified)"),
|
||||
el("td", "~14kb"),
|
||||
el("td", "~3kb"),
|
||||
el("td", "~20kb"),
|
||||
el("td", "~43kb")
|
||||
el("td", "Bundle Size (minimal)"),
|
||||
el("td", "~10-15kb"),
|
||||
el("td", "~40kb+"),
|
||||
el("td", "~33kb+"),
|
||||
el("td", "Minimal runtime")
|
||||
),
|
||||
el("tr").append(
|
||||
el("td", "Reactivity Model"),
|
||||
el("td", "Signal-based"),
|
||||
el("td", "Signal-based (basics only)"),
|
||||
el("td", "Signal-based"),
|
||||
el("td", "MVVM + Proxy")
|
||||
el("td", "Virtual DOM diffing"),
|
||||
el("td", "Proxy-based"),
|
||||
el("td", "Compile-time reactivity")
|
||||
),
|
||||
el("tr").append(
|
||||
el("td", "DOM Interface"),
|
||||
el("td", "Direct DOM API"),
|
||||
el("td", "Direct DOM API"),
|
||||
el("td", "Compiled DOM updates"),
|
||||
el("td", "Directive-based")
|
||||
el("td", "Virtual DOM"),
|
||||
el("td", "Virtual DOM"),
|
||||
el("td", "Compiled DOM updates")
|
||||
),
|
||||
el("tr").append(
|
||||
el("td", "Server-Side Rendering"),
|
||||
el("td", "✅ Basic Support"),
|
||||
el("td", "✅ Basic Support"),
|
||||
el("td", "✅ Advanced"),
|
||||
el("td", "❌")
|
||||
el("td", "✅ Advanced"),
|
||||
el("td", "✅ Advanced")
|
||||
)
|
||||
)
|
||||
),
|
||||
@ -393,7 +347,7 @@ export function page({ pkg, info }){
|
||||
|
||||
el(h3, t`Contribution and Community`),
|
||||
el("p").append(T`
|
||||
dd<el> is an open-source project that welcomes contributions from the community:
|
||||
dd<el> is an open-source project that welcomes contributions from the community:
|
||||
`),
|
||||
el("ul").append(
|
||||
el("li").append(T`
|
||||
@ -413,33 +367,16 @@ export function page({ pkg, info }){
|
||||
el("div", { className: "callout" }).append(
|
||||
el("h4", t`Final Thoughts`),
|
||||
el("p").append(T`
|
||||
dd<el> provides a lightweight yet powerful approach to building modern web interfaces
|
||||
dd<el> provides a lightweight yet powerful approach to building modern web interfaces
|
||||
with minimal overhead and maximal flexibility. By embracing standard web technologies
|
||||
rather than abstracting them away, it offers a development experience that scales
|
||||
rather than abstracting them away, it offers a development experience that scales
|
||||
from simple interactive elements to complex applications while remaining close
|
||||
to what makes the web platform powerful.
|
||||
`),
|
||||
el("p").append(T`
|
||||
Whether you’re building a small interactive component or a full-featured application,
|
||||
dd<el>’s combination of declarative syntax, targeted reactivity, and pragmatic design
|
||||
provides the tools you need without the complexity you don’t.
|
||||
`)
|
||||
),
|
||||
|
||||
el(h3, t`Tools and Resources`),
|
||||
el("p").append(T`
|
||||
To help you get started with dd<el>, we provide several tools and resources:
|
||||
`),
|
||||
el("ul").append(
|
||||
el("li").append(T`
|
||||
${el("a", references.converter).append("HTML to dd<el> Converter")}: Easily convert existing HTML markup
|
||||
to dd<el> JavaScript code
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("a", references.examples).append("Examples Gallery")}: Browse real-world examples and code snippets
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("a", references.github).append("Documentation")}: Comprehensive guides and API reference
|
||||
Whether you're building a small interactive component or a full-featured application,
|
||||
dd<el>'s combination of declarative syntax, targeted reactivity, and pragmatic design
|
||||
provides the tools you need without the complexity you don't.
|
||||
`)
|
||||
),
|
||||
);
|
||||
|
@ -1,53 +0,0 @@
|
||||
import "./components/converter.html.js";
|
||||
import { T, t } from "./utils/index.js";
|
||||
export const info= {
|
||||
title: t`Convert to dd<el>`,
|
||||
fullTitle: t`HTML to dd<el> Converter`,
|
||||
description: t`Convert your HTML markup to dd<el> JavaScript code with our interactive tool`,
|
||||
};
|
||||
|
||||
import { el } from "deka-dom-el";
|
||||
import { simplePage } from "./layout/simplePage.html.js";
|
||||
import { h3 } from "./components/pageUtils.html.js";
|
||||
import { converter } from "./components/converter.html.js";
|
||||
|
||||
/** @param {import("./types.d.ts").PageAttrs} attrs */
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("p").append(T`
|
||||
Transitioning from HTML to dd<el> is simple with our interactive converter. This tool helps you quickly
|
||||
transform existing HTML markup into dd<el> JavaScript code, making it easier to adopt dd<el> in your projects.
|
||||
`),
|
||||
|
||||
el("div", { className: "warning" }).append(
|
||||
el("p").append(T`
|
||||
While the converter handles most basic HTML patterns, complex attributes or specialized elements might
|
||||
need manual adjustments. Always review the generated code before using it in production.
|
||||
`)
|
||||
),
|
||||
|
||||
// The actual converter component
|
||||
el(converter, { page_id }),
|
||||
|
||||
el(h3, t`Next Steps`),
|
||||
el("p").append(T`
|
||||
After converting your HTML to dd<el>, you might want to:
|
||||
`),
|
||||
el("ul").append(
|
||||
el("li").append(T`
|
||||
Add signal bindings for dynamic content (see ${el("a", { href: "p04-signals.html",
|
||||
textContent: "Signals section" })})
|
||||
`),
|
||||
el("li").append(T`
|
||||
Add event handlers for interactivity (see ${el("a", { href: "p03-events.html",
|
||||
textContent: "Events section" })})
|
||||
`),
|
||||
el("li").append(T`
|
||||
Organize your components with components (see ${el("a", { href:
|
||||
"p02-elements.html#h-using-components-to-build-ui-fragments", textContent: "Components section" })}
|
||||
and ${el("a", { href: "p05-scopes.html", textContent: "Scopes section" })})
|
||||
`),
|
||||
)
|
||||
);
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
import { T, t } from "./utils/index.js";
|
||||
export const info= {
|
||||
title: t`Examples Gallery`,
|
||||
fullTitle: t`DDE Examples & Code Snippets`,
|
||||
description: t`A comprehensive collection of examples and code snippets for working with Deka DOM Elements.`,
|
||||
};
|
||||
|
||||
import { el } from "deka-dom-el";
|
||||
import { simplePage } from "./layout/simplePage.html.js";
|
||||
import { h3 } from "./components/pageUtils.html.js";
|
||||
import { example } from "./components/example.html.js";
|
||||
/** @param {string} url */
|
||||
const fileURL= url=> new URL(url, import.meta.url);
|
||||
|
||||
/** @param {import("./types.d.ts").PageAttrs} attrs */
|
||||
export function page({ pkg, info }){
|
||||
const page_id= info.id;
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("p").append(T`
|
||||
Real-world application examples showcasing how to build complete, production-ready interfaces with dd<el>:
|
||||
`),
|
||||
el(h3, t`Data Dashboard`),
|
||||
el("p").append(T`
|
||||
Data visualization dashboard with charts, filters, and responsive layout. Integration with a
|
||||
third-party charting library, data fetching and state management, responsive layout design, and multiple
|
||||
interactive components working together.
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/case-studies/data-dashboard.js"), variant: "big", page_id }),
|
||||
|
||||
el(h3, t`Interactive Form`),
|
||||
el("p").append(T`
|
||||
Complete form with real-time validation, conditional rendering, and responsive design. Form handling with
|
||||
real-time validation, reactive UI updates, complex form state management, and clean separation of concerns.
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/case-studies/interactive-form.js"), variant: "big", page_id }),
|
||||
|
||||
|
||||
el(h3, t`Interactive Image Gallery`),
|
||||
el("p").append(T`
|
||||
Responsive image gallery with lightbox, keyboard navigation, and filtering. Dynamic loading of content,
|
||||
lightbox functionality, animation handling, and keyboard and gesture navigation support.
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/case-studies/image-gallery.js"), variant: "big", page_id }),
|
||||
|
||||
|
||||
el(h3, t`Task Manager`),
|
||||
el("p").append(T`
|
||||
Kanban-style task management app with drag-and-drop and localStorage persistence. Complex state management
|
||||
with signals, drag and drop functionality, local storage persistence, and responsive design for different
|
||||
devices.
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/case-studies/task-manager.js"), variant: "big", page_id }),
|
||||
|
||||
|
||||
el(h3, t`TodoMVC`),
|
||||
el("p").append(T`
|
||||
Complete TodoMVC implementation with local storage and routing. TodoMVC implementation showing routing,
|
||||
local storage persistence, filtering, and component architecture patterns. For commented code, see the
|
||||
dedicated page ${el("a", { href: "./p10-todomvc.html" }).append(T`TodoMVC`)}.
|
||||
`),
|
||||
|
||||
);
|
||||
}
|
48
index.d.ts
vendored
48
index.d.ts
vendored
@ -184,12 +184,12 @@ interface On{
|
||||
/** Listens to the DOM event. See {@link Document.addEventListener} */
|
||||
<
|
||||
Event extends keyof DocumentEventMap,
|
||||
EL extends SupportedElement
|
||||
EE extends ddeElementAddon<SupportedElement>= ddeElementAddon<HTMLElement>,
|
||||
>(
|
||||
type: Event,
|
||||
listener: (this: EL, ev: DocumentEventMap[Event] & { target: EL }) => any,
|
||||
listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any,
|
||||
options?: AddEventListenerOptions
|
||||
) : ddeElementAddon<EL>;
|
||||
) : EE;
|
||||
<
|
||||
EE extends ddeElementAddon<SupportedElement>= ddeElementAddon<HTMLElement>,
|
||||
>(
|
||||
@ -199,38 +199,28 @@ interface On{
|
||||
) : EE;
|
||||
/** Listens to the element is connected to the live DOM. In case of custom elements uses [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */// editorconfig-checker-disable-line
|
||||
connected<
|
||||
EL extends SupportedElement
|
||||
EE extends ddeElementAddon<SupportedElement>,
|
||||
El extends ( EE extends ddeElementAddon<infer El> ? El : never )
|
||||
>(
|
||||
listener: (this: EL, event: CustomEvent<NoInfer<EL>>) => any,
|
||||
listener: (this: El, event: CustomEvent<El>) => any,
|
||||
options?: AddEventListenerOptions
|
||||
) : ddeElementAddon<EL>;
|
||||
) : EE;
|
||||
/** Listens to the element is disconnected from the live DOM. In case of custom elements uses [`disconnectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */// editorconfig-checker-disable-line
|
||||
disconnected<
|
||||
EL extends SupportedElement
|
||||
EE extends ddeElementAddon<SupportedElement>,
|
||||
El extends ( EE extends ddeElementAddon<infer El> ? El : never )
|
||||
>(
|
||||
listener: (this: EL, event: CustomEvent<void>) => any,
|
||||
listener: (this: El, event: CustomEvent<void>) => any,
|
||||
options?: AddEventListenerOptions
|
||||
) : ddeElementAddon<EL>;
|
||||
/**
|
||||
* Fires after the next tick of the Javascript event loop.
|
||||
* This is handy for example to apply some property depending on the element content:
|
||||
* ```js
|
||||
* const selected= "Z";
|
||||
* //...
|
||||
* return el("form").append(
|
||||
* el("select", null, on.defer(e=> e.value=selected)).append(
|
||||
* el("option", { value: "A", textContent: "A" }),
|
||||
* //...
|
||||
* el("option", { value: "Z", textContent: "Z" }),
|
||||
* ),
|
||||
* );
|
||||
* ```
|
||||
* */
|
||||
defer<
|
||||
EL extends SupportedElement
|
||||
>(
|
||||
listener: (element: EL) => any,
|
||||
) : ddeElementAddon<EL>;
|
||||
) : EE;
|
||||
/** Listens to the element attribute changes. In case of custom elements uses [`attributeChangedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */// editorconfig-checker-disable-line
|
||||
attributeChanged<
|
||||
EE extends ddeElementAddon<SupportedElement>,
|
||||
El extends ( EE extends ddeElementAddon<infer El> ? El : never )
|
||||
>(
|
||||
listener: (this: El, event: CustomEvent<[ string, string ]>) => any,
|
||||
options?: AddEventListenerOptions
|
||||
) : EE;
|
||||
}
|
||||
export const on: On;
|
||||
|
||||
|
6
index.js
6
index.js
@ -1,3 +1,5 @@
|
||||
export * from "./src/dom-lib/index.js";
|
||||
export { memo } from "./src/memo.js";
|
||||
export * from "./src/dom.js";
|
||||
export * from "./src/customElement.js";
|
||||
export * from "./src/events.js";
|
||||
export { registerReactivity } from "./src/signals-lib/common.js";
|
||||
export { memo } from "./src/memo.js";
|
||||
|
2
jsdom.js
2
jsdom.js
@ -1,4 +1,4 @@
|
||||
import { enviroment as env } from './src/dom-lib/common.js';
|
||||
import { enviroment as env } from './src/dom-common.js';
|
||||
env.ssr= " ssr";
|
||||
|
||||
const q_store= new Set();
|
||||
|
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "deka-dom-el",
|
||||
"version": "0.9.3-alpha",
|
||||
"version": "0.9.1-alpha",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "deka-dom-el",
|
||||
"version": "0.9.3-alpha",
|
||||
"version": "0.9.1-alpha",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@size-limit/preset-small-lib": "~11.2",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "deka-dom-el",
|
||||
"version": "0.9.3-alpha",
|
||||
"version": "0.9.1-alpha",
|
||||
"description": "A low-code library that simplifies the creation of native DOM elements/components using small wrappers and tweaks.",
|
||||
"author": "Jan Andrle <andrle.jan@centrum.cz>",
|
||||
"license": "MIT",
|
||||
@ -85,12 +85,7 @@
|
||||
"modifyEsbuildConfig": {
|
||||
"platform": "browser"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no tests yet\"",
|
||||
"build": "bs/build.js",
|
||||
"lint": "bs/lint.sh",
|
||||
"docs": "bs/docs.js"
|
||||
},
|
||||
"scripts": {},
|
||||
"keywords": [
|
||||
"dom",
|
||||
"javascript",
|
||||
|
2
signals.d.ts
vendored
2
signals.d.ts
vendored
@ -2,7 +2,7 @@ export interface Signal<V, A> {
|
||||
/** The current value of the signal */
|
||||
get(): V;
|
||||
/** Set new value of the signal */
|
||||
set(value: V, force?: boolean): V;
|
||||
set(value: V): V;
|
||||
toJSON(): V;
|
||||
valueOf(): V;
|
||||
}
|
||||
|
@ -1,47 +1,6 @@
|
||||
import { keyLTE, evc, evd, eva } from "./common.js";
|
||||
import { scope } from "./scopes.js";
|
||||
import { keyLTE, evc, evd, eva } from "./dom-common.js";
|
||||
import { scope } from "./dom.js";
|
||||
import { c_ch_o } from "./events-observer.js";
|
||||
import { elementAttribute } from "./helpers.js";
|
||||
|
||||
/**
|
||||
* Simulates slot functionality for elements
|
||||
*
|
||||
* @param {HTMLElement} element - Parent element
|
||||
* @param {HTMLElement} [root=element] - Root element containing slots
|
||||
* @returns {HTMLElement} The root element
|
||||
*/
|
||||
export function simulateSlots(element, root= element){
|
||||
const mark_e= "¹⁰", mark_s= "✓"; //NOTE: Markers to identify slots processed by this function. Also “prevents” native behavior as it is unlikely to use these in names. // editorconfig-checker-disable-line
|
||||
const slots= Object.fromEntries(
|
||||
Array.from(root.querySelectorAll("slot"))
|
||||
.filter(s => !s.name.endsWith(mark_e))
|
||||
.map(s => [(s.name += mark_e), s]));
|
||||
element.append= new Proxy(element.append, {
|
||||
apply(orig, _, els){
|
||||
if(els[0]===root) return orig.apply(element, els);
|
||||
for(const el of els){
|
||||
const name= (el.slot||"")+mark_e;
|
||||
try{ elementAttribute(el, "remove", "slot"); } catch(_error){}
|
||||
const slot= slots[name];
|
||||
if(!slot) return;
|
||||
if(!slot.name.startsWith(mark_s)){
|
||||
slot.childNodes.forEach(c=> c.remove());
|
||||
slot.name= mark_s+name;
|
||||
}
|
||||
slot.append(el);
|
||||
//TODO?: el.dispatchEvent(new CustomEvent("dde:slotchange", { detail: slot }));
|
||||
}
|
||||
element.append= orig; //TODO?: better memory management, but non-native behavior!
|
||||
return element;
|
||||
}
|
||||
});
|
||||
if(element!==root){
|
||||
const els= Array.from(element.childNodes);
|
||||
//TODO?: els.forEach(el=> el.remove());
|
||||
element.append(...els);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders content into a custom element or shadow root
|
@ -21,7 +21,7 @@ export const enviroment= {
|
||||
M: globalThis.MutationObserver,
|
||||
q: p=> p || Promise.resolve(),
|
||||
};
|
||||
import { isInstance, isUndef } from '../helpers.js';
|
||||
import { isInstance, isUndef } from './helpers.js';
|
||||
|
||||
/**
|
||||
* Handles attribute setting with special undefined handling
|
@ -1,57 +0,0 @@
|
||||
import { enviroment as env } from './common.js';
|
||||
import { isInstance, isUndef } from "../helpers.js";
|
||||
/**
|
||||
* Sets or removes an attribute based on value
|
||||
*
|
||||
* @param {Element} obj - Element to modify
|
||||
* @param {string} prop - Property suffix ("Attribute")
|
||||
* @param {string} key - Attribute name
|
||||
* @param {any} val - Attribute value
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
export function setRemove(obj, prop, key, val){
|
||||
return obj[ (isUndef(val) ? "remove" : "set") + prop ](key, val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or removes a namespaced attribute based on value
|
||||
*
|
||||
* @param {Element} obj - Element to modify
|
||||
* @param {string} prop - Property suffix ("Attribute")
|
||||
* @param {string} key - Attribute name
|
||||
* @param {any} val - Attribute value
|
||||
* @param {string|null} [ns=null] - Namespace URI
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
export function setRemoveNS(obj, prop, key, val, ns= null){
|
||||
return obj[ (isUndef(val) ? "remove" : "set") + prop + "NS" ](ns, key, val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or deletes a property based on value
|
||||
*
|
||||
* @param {Object} obj - Object to modify
|
||||
* @param {string} key - Property name
|
||||
* @param {any} val - Property value
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
export function setDelete(obj, key, val){
|
||||
Reflect.set(obj, key, val); if(!isUndef(val)) return; return Reflect.deleteProperty(obj, key);
|
||||
}
|
||||
/**
|
||||
* Generic element attribute manipulation
|
||||
*
|
||||
* @param {Element} element - Element to manipulate
|
||||
* @param {string} op - Operation ("set" or "remove")
|
||||
* @param {string} key - Attribute name
|
||||
* @param {any} [value] - Attribute value
|
||||
* @returns {void}
|
||||
*/
|
||||
export function elementAttribute(element, op, key, value){
|
||||
if(isInstance(element, env.H))
|
||||
return element[op+"Attribute"](key, value);
|
||||
return element[op+"AttributeNS"](null, key, value);
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
export * from "./scopes.js";
|
||||
export * from "./el.js";
|
||||
export * from "./events.js";
|
||||
export * from "./customElement.js";
|
@ -1,82 +0,0 @@
|
||||
import { enviroment as env } from './common.js';
|
||||
import { oAssign } from "../helpers.js";
|
||||
import { on } from "./events.js";
|
||||
|
||||
/**
|
||||
* Array of scope contexts for tracking component hierarchies
|
||||
* @type {{ scope: object, prevent: boolean, host: function }[]}
|
||||
*/
|
||||
const scopes= [ {
|
||||
get scope(){ return env.D.body; },
|
||||
host: c=> c ? c(env.D.body) : env.D.body,
|
||||
prevent: true,
|
||||
} ];
|
||||
/** Store for disconnect abort controllers */
|
||||
const store_abort= new WeakMap();
|
||||
/**
|
||||
* Scope management utility for tracking component hierarchies
|
||||
*/
|
||||
export const scope= {
|
||||
/**
|
||||
* Gets the current scope
|
||||
* @returns {typeof scopes[number]} Current scope context
|
||||
*/
|
||||
get current(){ return scopes[scopes.length-1]; },
|
||||
|
||||
/**
|
||||
* Gets the host element of the current scope
|
||||
* @returns {Function} Host accessor function
|
||||
*/
|
||||
get host(){ return this.current.host; },
|
||||
|
||||
/**
|
||||
* Creates/gets an AbortController that triggers when the element disconnects
|
||||
* */
|
||||
get signal(){
|
||||
const { host }= this;
|
||||
if(store_abort.has(host)) return store_abort.get(host);
|
||||
|
||||
const a= new AbortController();
|
||||
store_abort.set(host, a);
|
||||
host(on.disconnected(()=> a.abort()));
|
||||
return a.signal;
|
||||
},
|
||||
|
||||
/**
|
||||
* Prevents default behavior in the current scope
|
||||
* @returns {Object} Current scope context
|
||||
*/
|
||||
preventDefault(){
|
||||
const { current }= this;
|
||||
current.prevent= true;
|
||||
return current;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a copy of the current scope stack
|
||||
* @returns {Array} Copy of scope stack
|
||||
*/
|
||||
get state(){ return [ ...scopes ]; },
|
||||
|
||||
/**
|
||||
* Pushes a new scope to the stack
|
||||
* @param {Object} [s={}] - Scope object to push
|
||||
* @returns {number} New length of the scope stack
|
||||
*/
|
||||
push(s= {}){ return scopes.push(oAssign({}, this.current, { prevent: false }, s)); },
|
||||
|
||||
/**
|
||||
* Pushes the root scope to the stack
|
||||
* @returns {number} New length of the scope stack
|
||||
*/
|
||||
pushRoot(){ return scopes.push(scopes[0]); },
|
||||
|
||||
/**
|
||||
* Pops the current scope from the stack
|
||||
* @returns {Object|undefined} Popped scope or undefined if only one scope remains
|
||||
*/
|
||||
pop(){
|
||||
if(scopes.length===1) return;
|
||||
return scopes.pop();
|
||||
},
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
import { signals } from "../signals-lib/common.js";
|
||||
import { enviroment as env } from './common.js';
|
||||
import { isInstance, isUndef, oAssign } from "../helpers.js";
|
||||
import { signals } from "./signals-lib/common.js";
|
||||
import { enviroment as env } from './dom-common.js';
|
||||
import { isInstance, isUndef, oAssign } from "./helpers.js";
|
||||
import { on } from "./events.js";
|
||||
|
||||
/**
|
||||
* Queues a promise, this is helpful for crossplatform components (on server side we can wait for all registered
|
||||
@ -10,6 +11,84 @@ import { isInstance, isUndef, oAssign } from "../helpers.js";
|
||||
*/
|
||||
export function queue(promise){ return env.q(promise); }
|
||||
|
||||
/**
|
||||
* Array of scope contexts for tracking component hierarchies
|
||||
* @type {{ scope: object, prevent: boolean, host: function }[]}
|
||||
*/
|
||||
const scopes= [ {
|
||||
get scope(){ return env.D.body; },
|
||||
host: c=> c ? c(env.D.body) : env.D.body,
|
||||
prevent: true,
|
||||
} ];
|
||||
/** Store for disconnect abort controllers */
|
||||
const store_abort= new WeakMap();
|
||||
/**
|
||||
* Scope management utility for tracking component hierarchies
|
||||
*/
|
||||
export const scope= {
|
||||
/**
|
||||
* Gets the current scope
|
||||
* @returns {Object} Current scope context
|
||||
*/
|
||||
get current(){ return scopes[scopes.length-1]; },
|
||||
|
||||
/**
|
||||
* Gets the host element of the current scope
|
||||
* @returns {Function} Host accessor function
|
||||
*/
|
||||
get host(){ return this.current.host; },
|
||||
|
||||
/**
|
||||
* Creates/gets an AbortController that triggers when the element disconnects
|
||||
* */
|
||||
get signal(){
|
||||
const { host }= this;
|
||||
if(store_abort.has(host)) return store_abort.get(host);
|
||||
|
||||
const a= new AbortController();
|
||||
store_abort.set(host, a);
|
||||
host(on.disconnected(()=> a.abort()));
|
||||
return a.signal;
|
||||
},
|
||||
|
||||
/**
|
||||
* Prevents default behavior in the current scope
|
||||
* @returns {Object} Current scope context
|
||||
*/
|
||||
preventDefault(){
|
||||
const { current }= this;
|
||||
current.prevent= true;
|
||||
return current;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a copy of the current scope stack
|
||||
* @returns {Array} Copy of scope stack
|
||||
*/
|
||||
get state(){ return [ ...scopes ]; },
|
||||
|
||||
/**
|
||||
* Pushes a new scope to the stack
|
||||
* @param {Object} [s={}] - Scope object to push
|
||||
* @returns {number} New length of the scope stack
|
||||
*/
|
||||
push(s= {}){ return scopes.push(oAssign({}, this.current, { prevent: false }, s)); },
|
||||
|
||||
/**
|
||||
* Pushes the root scope to the stack
|
||||
* @returns {number} New length of the scope stack
|
||||
*/
|
||||
pushRoot(){ return scopes.push(scopes[0]); },
|
||||
|
||||
/**
|
||||
* Pops the current scope from the stack
|
||||
* @returns {Object|undefined} Popped scope or undefined if only one scope remains
|
||||
*/
|
||||
pop(){
|
||||
if(scopes.length===1) return;
|
||||
return scopes.pop();
|
||||
},
|
||||
};
|
||||
/**
|
||||
* Chainable append function for elements
|
||||
* @private
|
||||
@ -28,7 +107,6 @@ export function chainableAppend(el){
|
||||
/** Current namespace for element creation */
|
||||
let namespace;
|
||||
|
||||
import { scope } from "./scopes.js";
|
||||
/**
|
||||
* Creates a DOM element with specified tag, attributes and addons
|
||||
*
|
||||
@ -38,12 +116,11 @@ import { scope } from "./scopes.js";
|
||||
* @returns {Element|DocumentFragment} Created element
|
||||
*/
|
||||
export function createElement(tag, attributes, ...addons){
|
||||
/* jshint maxcomplexity: 16 */
|
||||
/* jshint maxcomplexity: 15 */
|
||||
const s= signals(this);
|
||||
let scoped= 0;
|
||||
let el, el_host;
|
||||
const att_type= typeof attributes;
|
||||
if(att_type==="string" || att_type==="number" || s.isSignal(attributes))
|
||||
if(Object(attributes)!==attributes || s.isSignal(attributes))
|
||||
attributes= { textContent: attributes };
|
||||
switch(true){
|
||||
case typeof tag==="function": {
|
||||
@ -114,6 +191,46 @@ export function createElementNS(ns){
|
||||
/** Alias for createElementNS */
|
||||
export { createElementNS as elNS };
|
||||
|
||||
/**
|
||||
* Simulates slot functionality for elements
|
||||
*
|
||||
* @param {HTMLElement} element - Parent element
|
||||
* @param {HTMLElement} [root=element] - Root element containing slots
|
||||
* @returns {HTMLElement} The root element
|
||||
*/
|
||||
export function simulateSlots(element, root= element){
|
||||
const mark_e= "¹⁰", mark_s= "✓"; //NOTE: Markers to identify slots processed by this function. Also “prevents” native behavior as it is unlikely to use these in names. // editorconfig-checker-disable-line
|
||||
const slots= Object.fromEntries(
|
||||
Array.from(root.querySelectorAll("slot"))
|
||||
.filter(s => !s.name.endsWith(mark_e))
|
||||
.map(s => [(s.name += mark_e), s]));
|
||||
element.append= new Proxy(element.append, {
|
||||
apply(orig, _, els){
|
||||
if(els[0]===root) return orig.apply(element, els);
|
||||
for(const el of els){
|
||||
const name= (el.slot||"")+mark_e;
|
||||
try{ elementAttribute(el, "remove", "slot"); } catch(_error){}
|
||||
const slot= slots[name];
|
||||
if(!slot) return;
|
||||
if(!slot.name.startsWith(mark_s)){
|
||||
slot.childNodes.forEach(c=> c.remove());
|
||||
slot.name= mark_s+name;
|
||||
}
|
||||
slot.append(el);
|
||||
//TODO?: el.dispatchEvent(new CustomEvent("dde:slotchange", { detail: slot }));
|
||||
}
|
||||
element.append= orig; //TODO?: better memory management, but non-native behavior!
|
||||
return element;
|
||||
}
|
||||
});
|
||||
if(element!==root){
|
||||
const els= Array.from(element.childNodes);
|
||||
//TODO?: els.forEach(el=> el.remove());
|
||||
element.append(...els);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
/** Store for element assignment contexts */
|
||||
const assign_context= new WeakMap();
|
||||
const { setDeleteAttr }= env;
|
||||
@ -134,7 +251,6 @@ export function assign(element, ...attributes){
|
||||
assign_context.delete(element);
|
||||
return element;
|
||||
}
|
||||
import { setDelete } from "./helpers.js";
|
||||
/**
|
||||
* Assigns a single attribute to an element
|
||||
*
|
||||
@ -174,7 +290,6 @@ export function assignAttribute(element, key, value){
|
||||
}
|
||||
return isPropSetter(element, key) ? setDeleteAttr(element, key, value) : setRemoveAttr(key, value);
|
||||
}
|
||||
import { setRemove, setRemoveNS } from "./helpers.js";
|
||||
/**
|
||||
* Gets or creates assignment context for an element
|
||||
*
|
||||
@ -205,6 +320,21 @@ export function classListDeclarative(element, toggle){
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic element attribute manipulation
|
||||
*
|
||||
* @param {Element} element - Element to manipulate
|
||||
* @param {string} op - Operation ("set" or "remove")
|
||||
* @param {string} key - Attribute name
|
||||
* @param {any} [value] - Attribute value
|
||||
* @returns {void}
|
||||
*/
|
||||
export function elementAttribute(element, op, key, value){
|
||||
if(isInstance(element, env.H))
|
||||
return element[op+"Attribute"](key, value);
|
||||
return element[op+"AttributeNS"](null, key, value);
|
||||
}
|
||||
|
||||
//TODO: add cache? `(Map/Set)<el.tagName+key,isUndef>`
|
||||
/**
|
||||
* Checks if a property can be set on an element
|
||||
@ -255,3 +385,45 @@ function forEachEntries(s, target, element, obj, cb){
|
||||
cb(key, val);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or removes an attribute based on value
|
||||
*
|
||||
* @param {Element} obj - Element to modify
|
||||
* @param {string} prop - Property suffix ("Attribute")
|
||||
* @param {string} key - Attribute name
|
||||
* @param {any} val - Attribute value
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function setRemove(obj, prop, key, val){
|
||||
return obj[ (isUndef(val) ? "remove" : "set") + prop ](key, val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or removes a namespaced attribute based on value
|
||||
*
|
||||
* @param {Element} obj - Element to modify
|
||||
* @param {string} prop - Property suffix ("Attribute")
|
||||
* @param {string} key - Attribute name
|
||||
* @param {any} val - Attribute value
|
||||
* @param {string|null} [ns=null] - Namespace URI
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function setRemoveNS(obj, prop, key, val, ns= null){
|
||||
return obj[ (isUndef(val) ? "remove" : "set") + prop + "NS" ](ns, key, val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or deletes a property based on value
|
||||
*
|
||||
* @param {Object} obj - Object to modify
|
||||
* @param {string} key - Property name
|
||||
* @param {any} val - Property value
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function setDelete(obj, key, val){
|
||||
Reflect.set(obj, key, val); if(!isUndef(val)) return; return Reflect.deleteProperty(obj, key);
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { enviroment as env, evc, evd } from './common.js';
|
||||
import { isInstance } from "../helpers.js";
|
||||
import { enviroment as env, evc, evd } from './dom-common.js';
|
||||
import { isInstance } from "./helpers.js";
|
||||
|
||||
/**
|
||||
* Connection changes observer for tracking element connection/disconnection
|
@ -1,5 +1,5 @@
|
||||
import { keyLTE, evc, evd } from './common.js';
|
||||
import { oAssign, onAbort } from '../helpers.js';
|
||||
import { keyLTE, evc, evd } from './dom-common.js';
|
||||
import { oAssign, onAbort } from './helpers.js';
|
||||
|
||||
/**
|
||||
* Creates a function to dispatch events on elements
|
||||
@ -38,8 +38,6 @@ export function on(event, listener, options){
|
||||
};
|
||||
}
|
||||
|
||||
on.defer= fn=> setTimeout.bind(null, fn, 0);
|
||||
|
||||
import { c_ch_o } from "./events-observer.js";
|
||||
|
||||
/**
|
@ -68,3 +68,21 @@ export function observedAttributes(instance, observedAttribute){
|
||||
* @returns {string} The camelCase string
|
||||
*/
|
||||
function kebabToCamel(name){ return name.replace(/-./g, x=> x[1].toUpperCase()); }
|
||||
|
||||
/**
|
||||
* Error class for definition tracking
|
||||
* Shows the correct stack trace for debugging (signal) creation
|
||||
*/
|
||||
export class Defined extends Error{
|
||||
constructor(){
|
||||
super();
|
||||
const [ curr, ...rest ]= this.stack.split("\n");
|
||||
const curr_file= curr.slice(curr.indexOf("@"), curr.indexOf(".js:")+4);
|
||||
const curr_lib= curr_file.includes("src/helpers.js") ? "src/" : curr_file;
|
||||
this.stack= rest.find(l=> !l.includes(curr_lib)) || curr;
|
||||
}
|
||||
get compact(){
|
||||
const { stack }= this;
|
||||
return stack.slice(0, stack.indexOf("@")+1)+"…"+stack.slice(stack.lastIndexOf("/"));
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ memo.isScope= function(obj){ return obj[memoMark]; };
|
||||
* @param {AbortSignal} options.signal
|
||||
* @param {boolean} [options.onlyLast=false]
|
||||
* */
|
||||
memo.scope= function memoScopeCreate(fun, { signal, onlyLast }= {}){
|
||||
memo.scope= function memoScope(fun, { signal, onlyLast }= {}){
|
||||
let cache= oCreate();
|
||||
function memoScope(...args){
|
||||
if(signal && signal.aborted)
|
||||
|
@ -9,8 +9,7 @@ export const mark= "__dde_signal";
|
||||
* @type {Function}
|
||||
*/
|
||||
export const queueSignalWrite= (()=> {
|
||||
/** @type {Map<ddeSignal, boolean>} */
|
||||
let pendingSignals= new Map();
|
||||
let pendingSignals= new Set();
|
||||
let scheduled= false;
|
||||
|
||||
/**
|
||||
@ -20,20 +19,19 @@ export const queueSignalWrite= (()=> {
|
||||
function flushSignals() {
|
||||
scheduled = false;
|
||||
const todo= pendingSignals;
|
||||
pendingSignals= new Map();
|
||||
for(const [ signal, force ] of todo){
|
||||
pendingSignals= new Set();
|
||||
for(const signal of todo){
|
||||
const M = signal[mark];
|
||||
if(M) M.listeners.forEach(l => l(M.value, force));
|
||||
if(M) M.listeners.forEach(l => l(M.value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a signal for update
|
||||
* @param {ddeSignal} s - Signal to queue
|
||||
* @param {boolean} force - Forced update
|
||||
* @param {Object} s - Signal to queue
|
||||
*/
|
||||
return function(s, force= false){
|
||||
pendingSignals.set(s, pendingSignals.get(s) || force);
|
||||
return function(s){
|
||||
pendingSignals.add(s);
|
||||
if(scheduled) return;
|
||||
scheduled = true;
|
||||
queueMicrotask(flushSignals);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { queueSignalWrite, mark } from "./helpers.js";
|
||||
export { mark };
|
||||
import { hasOwn, oCreate, oAssign } from "../helpers.js";
|
||||
import { hasOwn, Defined, oCreate, oAssign } from "../helpers.js";
|
||||
|
||||
const Signal = oCreate(null, {
|
||||
get: { value(){ return read(this); } },
|
||||
@ -57,12 +57,12 @@ export function signal(value, actions){
|
||||
* Updates the derived signal when dependencies change
|
||||
* @private
|
||||
*/
|
||||
function contextReWatch(_, force){
|
||||
function contextReWatch(){
|
||||
const [ origin, ...deps_old ]= deps.get(contextReWatch);
|
||||
deps.set(contextReWatch, new Set([ origin ]));
|
||||
|
||||
stack_watch.push(contextReWatch);
|
||||
write(out, value(), force);
|
||||
write(out, value());
|
||||
stack_watch.pop();
|
||||
|
||||
if(!deps_old.length) return;
|
||||
@ -95,7 +95,7 @@ signal.action= function(s, name, ...a){
|
||||
throw new Error(`Action "${name}" not defined. See ${mark}.actions.`);
|
||||
actions[name].apply(M, a);
|
||||
if(M.skip) return (delete M.skip);
|
||||
queueSignalWrite(s, true);
|
||||
queueSignalWrite(s);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -159,31 +159,31 @@ signal.clear= function(...signals){
|
||||
};
|
||||
/** Property key for tracking reactive elements */
|
||||
const key_reactive= "__dde_reactive";
|
||||
import { enviroment as env, eva } from "../dom-lib/common.js";
|
||||
import { el } from "../dom-lib/index.js";
|
||||
import { scope } from "../dom-lib/scopes.js";
|
||||
import { on } from "../dom-lib/events.js";
|
||||
import { enviroment as env, eva } from "../dom-common.js";
|
||||
import { el } from "../dom.js";
|
||||
import { scope } from "../dom.js";
|
||||
import { on } from "../events.js";
|
||||
import { memo } from "../memo.js";
|
||||
|
||||
/**
|
||||
* Creates a reactive DOM element that re-renders when signal changes
|
||||
*
|
||||
* @param {Object} s - Signal object to watch
|
||||
* @param {Function} mapScoped - Function mapping signal value to DOM elements
|
||||
* @param {Function} map - Function mapping signal value to DOM elements
|
||||
* @returns {DocumentFragment} Fragment containing reactive elements
|
||||
*/
|
||||
signal.el= function(s, map){
|
||||
const mapScoped= memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
|
||||
const { current }= scope, { scope: sc }= current;
|
||||
const mark_start= el.mark({ type: "reactive", component: sc && sc.name || "" }, true);
|
||||
map= memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
|
||||
const mark_start= el.mark({ type: "reactive", source: new Defined().compact }, true);
|
||||
const mark_end= mark_start.end;
|
||||
const out= env.D.createDocumentFragment();
|
||||
out.append(mark_start, mark_end);
|
||||
const { current }= scope;
|
||||
const reRenderReactiveElement= v=> {
|
||||
if(!mark_start.parentNode || !mark_end.parentNode) // === `isConnected` or wasn’t yet rendered
|
||||
return removeSignalListener(s, reRenderReactiveElement);
|
||||
scope.push(current);
|
||||
let els= mapScoped(v);
|
||||
let els= map(v);
|
||||
scope.pop();
|
||||
if(!Array.isArray(els))
|
||||
els= [ els ];
|
||||
@ -202,7 +202,7 @@ signal.el= function(s, map){
|
||||
reRenderReactiveElement(s.get());
|
||||
current.host(on.disconnected(()=>
|
||||
/*! Clears cached elements for reactive element `S.el` */
|
||||
mapScoped.clear()
|
||||
map.clear()
|
||||
));
|
||||
return out;
|
||||
};
|
||||
@ -314,8 +314,9 @@ export const signals_config= {
|
||||
function removeSignalsFromElements(s, listener, ...notes){
|
||||
const { current }= scope;
|
||||
current.host(function(element){
|
||||
if(!element[key_reactive]) element[key_reactive]= [];
|
||||
element[key_reactive].push([ [ s, listener ], ...notes ]);
|
||||
if(element[key_reactive])
|
||||
return element[key_reactive].push([ [ s, listener ], ...notes ]);
|
||||
element[key_reactive]= [];
|
||||
if(current.prevent) return; // typically document.body, doenst need auto-remove as it should happen on page leave
|
||||
on.disconnected(()=>
|
||||
/*! Clears all Signals listeners added in the current scope/host (`S.el`, `assign`, …?).
|
||||
@ -385,6 +386,7 @@ function toSignal(s, value, actions, readonly= false){
|
||||
value: oAssign(oCreate(protoSigal), {
|
||||
value, actions, onclear, host,
|
||||
listeners: new Set(),
|
||||
defined: new Defined().stack,
|
||||
readonly
|
||||
}),
|
||||
enumerable: false,
|
||||
@ -432,7 +434,7 @@ function write(s, value, force){
|
||||
if(!M || (!force && M.value===value)) return;
|
||||
|
||||
M.value= value;
|
||||
queueSignalWrite(s, force);
|
||||
queueSignalWrite(s);
|
||||
return value;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user