Skip to content

Commit 27f17ad

Browse files
Merge pull request #153 from WebReflection/v5
V5 🥳
2 parents fbae918 + ae8bf8f commit 27f17ad

200 files changed

Lines changed: 5823 additions & 31144 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/node.js.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212

1313
strategy:
1414
matrix:
15-
node-version: [20]
15+
node-version: [24]
1616

1717
steps:
1818
- uses: actions/checkout@v4

.gitignore

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,6 @@
1-
.nyc_output/
1+
.DS_Store
2+
.nyc_output
3+
v8.log
24
coverage/
5+
dist/
36
node_modules/
4-
types/
5-
cjs/*
6-
!cjs/package.json
7-
dom.js
8-
esm/init*.js
9-
init*.js
10-
worker.js
11-
keyed.js
12-
!esm/keyed.js
13-
!esm/dom/keyed.js
14-
index.js
15-
!esm/index.js
16-
!esm/dom/index.js
17-
node.js
18-
!esm/node.js
19-
!esm/dom/node.js
20-
!test/dom/node.js
21-
reactive.js
22-
!esm/reactive.js
23-
!esm/render/reactive.js
24-
signal.js
25-
!esm/signal.js
26-
!esm/render/signal.js
27-
preactive.js
28-
!test/preactive.js

.npmignore

Lines changed: 0 additions & 14 deletions
This file was deleted.

README.md

Lines changed: 215 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,238 @@
1-
# <em>µ</em>html
1+
# uhtml
22

33
[![Downloads](https://img.shields.io/npm/dm/uhtml.svg)](https://www.npmjs.com/package/uhtml) [![build status](https://github.com/WebReflection/uhtml/actions/workflows/node.js.yml/badge.svg)](https://github.com/WebReflection/uhtml/actions) [![Coverage Status](https://coveralls.io/repos/github/WebReflection/uhtml/badge.svg?branch=main)](https://coveralls.io/github/WebReflection/uhtml?branch=main) [![CSP strict](https://webreflection.github.io/csp/strict.svg)](https://webreflection.github.io/csp/#-csp-strict)
44

5-
![snow flake](./docs/uhtml-head.jpg)
65

7-
<sup>**Social Media Photo by [Andrii Ganzevych](https://unsplash.com/@odya_kun) on [Unsplash](https://unsplash.com/)**</sup>
6+
A minimalistic library to create fast and reactive Web pages.
87

9-
*uhtml* (micro *µ* html) is one of the smallest, fastest, memory consumption friendly, yet zero-tools based, library to safely help creating or manipulating DOM content.
8+
```html
9+
<!doctype html>
10+
<script type="module">
11+
import { html } from 'https://esm.run/uhtml';
1012
11-
### 📣 uhtml v4 is out
13+
document.body.prepend(
14+
html`<h1>Hello DOM !</h2>`
15+
);
16+
</script>
17+
```
18+
19+
*uhtml* (micro *µ* html) offers the following features without needing specialized tools:
20+
21+
* *JSX* inspired syntax through template literal `html` and `svg` tags
22+
* *React* like components with *Preact* like *signals*
23+
* compatible with native custom elements and other Web standards out of the box
24+
* simplified accessibility via `aria` attribute and easy *dataset* handling via `data`
25+
* developers enhanced mode runtime debugging sessions
26+
27+
```js
28+
import { html, signal } from 'https://esm.run/uhtml';
29+
30+
function Counter() {
31+
const count = signal(0);
1232

13-
**[Documentation](https://webreflection.github.io/uhtml/)**
33+
return html`
34+
<button onClick=${() => count.value++}>
35+
Clicked ${count.value} times
36+
</button>
37+
`;
38+
}
1439

15-
**[Release Notes](https://github.com/WebReflection/uhtml/pull/86)**
40+
document.body.append(
41+
html`<${Counter} />`
42+
);
43+
```
1644

1745
- - -
1846

19-
### Exports
47+
## Syntax
48+
49+
If you are familiar with *JSX* you will find *uhtml* syntax very similar:
50+
51+
* self closing tags, such as `<p />`
52+
* self closing elements, such as `<custom-element>...</>`
53+
* object spread operation via `<${Component} ...=${{any: 'prop'}} />`
54+
* `key` attribute to ensure *same DOM node* within a list of nodes
55+
* `ref` attribute to retrieve the element via effects or by any other mean
56+
57+
The main difference between *uhtml* and *JSX* is that *fragments* do **not** require `<>...</>` around:
58+
59+
```js
60+
// uhtml fragment example
61+
html`
62+
<div>first element</div>
63+
<p> ... </p>
64+
<div>last element</div>
65+
`
66+
```
67+
68+
### Special Attributes
69+
70+
On top of *JSX* like features, there are other attributes with a special meaning:
71+
72+
* `aria` attribute to simplify *a11y*, such as `<button aria=${{role: 'button', labelledBy: 'id'}} />`
73+
* `data` attribute to simplify *dataset* handling, such as `<div data=${{any: 'data'}} />`
74+
* `@event` attribute for generic events handling, accepting an array when *options* are meant to be passed, such as `<button @click=${[event => {}, { once: true }]} />`
75+
* `on...` prefixed direct events, such as `<button onclick=${listener} />`
76+
* `.direct` properties access, such as `<input .value=${content} />`, `<button .textContent=${value} />` or `<div .className=${value} />`
77+
* `?toggle` boolean attributes, such as `<div ?hidden=${isHidden} />`
78+
79+
All other attributes will be handled via standard `setAttribute` or `removeAttribute` when the passed value is either `null` or `undefined`.
80+
81+
### Special Elements
82+
83+
Elements that contain *data* such as `<script>` or `<style>`, or those that contains text such as `<textarea>` require *explicit closing tag* to avoid having in between templates able to break the layout.
84+
85+
This is nothing new to learn, it's just how the Web works, so that one cannot have `</script>` within a `<script>` tag content and the same applies in here.
86+
87+
In *debugging* mode, an error telling you which template is malformed will be triggered in these cases.
88+
89+
### About Comments
90+
91+
Useful for developers but never really relevant for end users, *comments* are ignored by default in *uhtml* except for those flagged as "*very important*".
92+
93+
The syntax to preserve a comment in the layout is `<!--! important !-->`. Every other comment will not be part of the rendered tree.
94+
95+
```js
96+
html`
97+
<!--! this is here to stay !-->
98+
<!--// this will go -->
99+
<!-- also this -->
100+
`
101+
```
102+
103+
The result will be a clear `<!-- this is here to stay -->` comment in the layout without starting and closing `!`.
104+
105+
#### Other Comments
20106

21-
* **[uhtml](https://cdn.jsdelivr.net/npm/uhtml/index.js)** as default `{ Hole, render, html, svg, attr }` with smart auto-keyed nodes - read [keyed or not ?](https://webreflection.github.io/uhtml/#keyed-or-not-) paragraph to know more
22-
* **[uhtml/keyed](https://cdn.jsdelivr.net/npm/uhtml/keyed.js)** with extras `{ Hole, render, html, svg, htmlFor, svgFor, attr }`, providing keyed utilities - read [keyed or not ?](https://webreflection.github.io/uhtml/#keyed-or-not-) paragraph to know more
23-
* **[uhtml/node](https://cdn.jsdelivr.net/npm/uhtml/node.js)** with *same default* exports but it's for *one-off* nodes creation only so that no cache or updates are available and it's just an easy way to hook *uhtml* into your existing project for DOM creation (not manipulation!)
24-
* **[uhtml/init](https://cdn.jsdelivr.net/npm/uhtml/init.js)** which returns a `document => uhtml/keyed` utility that can be bootstrapped with `uhtml/dom`, [LinkeDOM](https://github.com/WebReflection/linkedom), [JSDOM](https://github.com/jsdom/jsdom) for either *SSR* or *Workers* support
25-
* **uhtml/ssr** which exports an utility that both SSR or Workers can use to parse and serve documents. This export provides same keyed utilities except the keyed feature is implicitly disabled as that's usually not desirable at all for SSR or rendering use cases, actually just an overhead. This might change in the future but for now I want to benchmark and see how competitive is `uhtml/ssr` out there. The `uhtml/dom` is also embedded in this export because the `Comment` class needs an override to produce a super clean output (at least until hydro story is up and running).
26-
* **[uhtml/dom](https://cdn.jsdelivr.net/npm/uhtml/dom.js)** which returns a specialized *uhtml* compliant DOM environment that can be passed to the `uhtml/init` export to have 100% same-thing running on both client or Web Worker / Server. This entry exports `{ Document, DOMParser }` where the former can be used to create a new *document* while the latter one can parse well formed HTML or SVG content and return the document out of the box.
27-
* **[uhtml/reactive](https://cdn.jsdelivr.net/npm/uhtml/reactive.js)** which allows usage of symbols within the optionally *keyed* render function. The only difference with other exports, beside exporting a `reactive` field instead of `render`, so that `const render = reactive(effect)` creates a reactive render per each library, is that the `render(where, () => what)`, with a function as second argument is mandatory when the rendered stuff has signals in it, otherwise these can't side-effect properly.
28-
* **[uhtml/signal](https://cdn.jsdelivr.net/npm/uhtml/signal.js)** is an already bundled `uhtml/reactive` with `@webreflection/signal` in it, so that its `render` exported function is already reactive. This is the smallest possible bundle as it's ~3.3Kb but it's not nearly as complete, in terms of features, as *preact* signals are.
29-
* **[uhtml/preactive](https://cdn.jsdelivr.net/npm/uhtml/preactive.js)** is an already bundled `uhtml/reactive` with `@preact/signals-core` in it, so that its `render` exported function, among all other *preact* related exports, is already working. This is a *drop-in* replacement with extra *Preact signals* goodness in it so you can start small with *uhtml/signal* and switch any time to this more popular solution.
107+
There are two kind of "*logical comments*" in *uhtml*, intended to help its own functionality:
30108

31-
### uhtml/init example
109+
* `<!--◦-->` *holes*, used to *pin* in the DOM tree where changes need to happen.
110+
* `<!--<>-->` and `<!--</>-->` persistent *fragments* delimeters
111+
112+
The *hole* type might disappear once replaced with different content while persistent fragments delimeters are needed to confine and/or retrieve back fragments' content.
113+
114+
Neither type will affect performance or change layout behavior.
115+
116+
- - -
117+
118+
## Exports
32119

33120
```js
34-
import init from 'uhtml/init';
35-
import { Document } from 'uhtml/dom';
36-
37-
const document = new Document;
38-
39-
const {
40-
Hole,
41-
render,
42-
html, svg,
43-
htmlFor, svgFor,
44-
attr
45-
} = init(document);
121+
import {
122+
// DOM manipulation
123+
render, html, svg, unsafe,
124+
// Preact like signals, based on alien-signals library
125+
signal, computed, effect, untracked, batch,
126+
// extras
127+
Hole, fragment,
128+
} from 'https://esm.run/uhtml';
46129
```
47130

48-
### uhtml/preactive example
131+
**In details**
132+
133+
* `render(where:Element, what:Function|Hole|Node)` to orchestrate one-off or repeated content rendering, providing a scoped *effect* when a *function* is passed along, such as `render(document.body, () => App(data))`. This is the suggested way to enrich any element content with complex reactivity in it.
134+
* `html` and `svg` [template literal tags](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates) to create either *HTML* or *SVG* content.
135+
* `unsafe(content:string)` to inject any content, even *HTML* or *SVG*, anywhere within a node: `<div>${unsafe('<em>value</em>')}</div>`
136+
* `signal`, `computed`, `effect`, `untracked` and `batch` utilities with [Preact signals](https://github.com/preactjs/signals/blob/main/packages/core/README.md) inspired API, fueled by [alien-signals](https://github.com/stackblitz/alien-signals#readme)
137+
* `Hole` class used internally to resolve `html` and `svg` tags' template and interpolations. This is exported mainly to simplify *TypeScript* relaed signatures.
138+
* `fragment(content:string, svg?:boolean)` extra utility, used internally to create either *HTML* or *SVG* elements from a string. This is merely a simplification of a manually created `<template>` element, its `template.innerHTML = content` operation and retrieval of its `template.content` reference, use it if ever needed but remember it has no special meaning or logic attached, it's literally just standard DOM fragment creation out of a string.
139+
140+
- - -
141+
142+
## Loading from a CDN
143+
144+
The easiest way to start using *uhtml* is via *CDN* and here a few exported variants:
49145

50146
```js
51-
import { render, html, signal, detach } from 'uhtml/preactive';
147+
// implicit production version
148+
import { render, html } from 'https://esm.run/uhtml';
149+
// https://cdn.jsdelivr.net/npm/uhtml/dist/prod/dom.js
150+
151+
// explicit production version
152+
import { render, html } from 'https://esm.run/uhtml/prod';
153+
// https://cdn.jsdelivr.net/npm/uhtml/dist/prod/dom.js
154+
155+
// explicit developer/debugging version
156+
import { render, html } from 'https://esm.run/uhtml/dev';
157+
import { render, html } from 'https://esm.run/uhtml/debug';
158+
// https://cdn.jsdelivr.net/npm/uhtml/dist/dev/dom.js
159+
160+
// automatic prod/dev version on ?dev or ?debug
161+
import { render, html } from 'https://esm.run/uhtml/cdn';
162+
// https://cdn.jsdelivr.net/npm/uhtml/dist/prod/cdn.js
163+
```
164+
165+
Using `https://esm.run/uhtml/cdn` or the fully qualified `https://cdn.jsdelivr.net/npm/uhtml/dist/prod/cdn.js` URL provides an automatic switch to *debug* mode if the current page location contains `?dev` or `?debug` or `?debug=1` query string parameter plus it guarantees the library will not be imported again if other scripts use a different *CDN* that points at the same file in a different location.
166+
167+
This makes it easy to switch to *dev* mode by changing the location from `https://example.com` to `https://example.com?debug`.
168+
169+
Last, but not least, it is not recommended to bundle directly *uhtml* in your project because components portability becomes compromised, as example, if each component bundles within itself *uhtml*.
170+
171+
### Import Map
52172

53-
const count = signal(0);
173+
Another way to grant *CDN* and components portability is to use an import map and exclude *uhtml* from your bundler.
54174

55-
render(document.body, () => html`
56-
<button onclick=${() => { count.value++ }}>
57-
Clicks: ${count.value}
58-
</button>
59-
`);
175+
```html
176+
<!-- defined on each page -->
177+
<script type="importmap">
178+
{
179+
"imports": {
180+
"uhtml": "https://cdn.jsdelivr.net/npm/uhtml/dist/prod/cdn.js"
181+
}
182+
}
183+
</script>
184+
<!-- your library code -->
185+
<script type="module">
186+
import { html } from 'uhtml';
60187
61-
// stop reacting to signals in the future
62-
setTimeout(() => {
63-
detach(document.body);
64-
}, 10000);
188+
document.body.append(
189+
html`Import Maps are Awesome!`
190+
);
191+
</script>
65192
```
193+
194+
- - -
195+
196+
## Extra Tools
197+
198+
Minification is still recommended for production use cases and not only for *JS*, also for the templates and their content.
199+
200+
The [rollup-plugin-minify-template-literals](https://www.npmjs.com/package/rollup-plugin-minify-template-literals) is a wonderful example of a plugin that does not complain about *uhtml* syntax and minifies to its best *uhtml* templates in both *vite* and *rollup*.
201+
202+
This is a *rollup* configuration example:
203+
204+
```js
205+
import terser from "@rollup/plugin-terser";
206+
import templateMinifier from "rollup-plugin-minify-template-literals";
207+
import { nodeResolve } from "@rollup/plugin-node-resolve";
208+
209+
export default {
210+
input: "src/your-component.js",
211+
plugins: [
212+
templateMinifier({
213+
options: {
214+
minifyOptions: {
215+
// allow only explicit <!--! comments !-->
216+
ignoreCustomComments: [/^!/],
217+
keepClosingSlash: true,
218+
caseSensitive: true,
219+
},
220+
},
221+
}),
222+
nodeResolve(),
223+
terser(),
224+
],
225+
output: {
226+
esModule: true,
227+
file: "dist/your-component.js",
228+
},
229+
};
230+
```
231+
232+
- - -
233+
234+
## About SSR and hydration
235+
236+
The current *pareser* is already environment agnostic, it runs on the client like it does in the server without needing dependencies at all.
237+
238+
However, the current *SSR* story is still a **work in progress** but it's planned to land sooner than later.

build/dev.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { nodeResolve } from '@rollup/plugin-node-resolve';
2+
import files from './files.js';
3+
4+
const target = 'dev';
5+
const plugins = [nodeResolve()];
6+
7+
export default files(target, plugins);

build/files.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
export default (target, plugins) => [
2+
{
3+
plugins,
4+
input: './src/parser/index.js',
5+
output: {
6+
esModule: true,
7+
file: `./dist/${target}/parser.js`,
8+
}
9+
},
10+
{
11+
plugins,
12+
input: './src/dom/cdn.js',
13+
output: {
14+
esModule: true,
15+
file: `./dist/${target}/cdn.js`,
16+
}
17+
},
18+
{
19+
plugins,
20+
input: './src/json/index.js',
21+
output: {
22+
esModule: true,
23+
file: `./dist/${target}/json.js`,
24+
}
25+
},
26+
{
27+
plugins,
28+
input: './src/dom/creator.js',
29+
output: {
30+
esModule: true,
31+
file: `./dist/${target}/creator.js`,
32+
}
33+
},
34+
{
35+
plugins,
36+
input: './src/dom/ish.js',
37+
output: {
38+
esModule: true,
39+
file: `./dist/${target}/ish.js`,
40+
}
41+
},
42+
{
43+
plugins,
44+
input: './src/dom/index.js',
45+
output: {
46+
esModule: true,
47+
file: `./dist/${target}/dom.js`,
48+
}
49+
},
50+
];

0 commit comments

Comments
 (0)