Skip to content

Commit c8ea32d

Browse files
committed
uhtml v5
1 parent fbae918 commit c8ea32d

175 files changed

Lines changed: 5090 additions & 31104 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

README.md

Lines changed: 207 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,230 @@
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:
1220

13-
**[Documentation](https://webreflection.github.io/uhtml/)**
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';
1429

15-
**[Release Notes](https://github.com/WebReflection/uhtml/pull/86)**
30+
function Counter() {
31+
const count = signal(0);
32+
33+
return html`
34+
<button onClick=${() => count.value++}>
35+
Clicked ${count.value} times
36+
</button>
37+
`;
38+
}
39+
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+
### About Comments
82+
83+
Useful for developers but never really relevant for end users, *comments* are ignored by default in *uhtml* except for those flagged as "*very important*".
84+
85+
The syntax to preserve a comment in the layout is `<!--! important !-->`. Every other comment will not be part of the rendered tree.
86+
87+
```js
88+
html`
89+
<!--! this is here to stay !-->
90+
<!--// this will go -->
91+
<!-- also this -->
92+
`
93+
```
94+
95+
The result will be a clear `<!-- this is here to stay -->` comment in the layout without starting and closing `!`.
96+
97+
#### Other Comments
98+
99+
There are two kind of "*logical comments*" in *uhtml*, intended to help its own functionality:
100+
101+
* `<!--◦-->` *holes*, used to *pin* in the DOM tree where changes need to happen.
102+
* `<!--<>-->` and `<!--</>-->` persistent *fragments* delimeters
20103

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.
104+
The *hole* type might disappear once replaced with different content while persistent fragments delimeters are needed to confine and/or retrieve back fragments' content.
30105

31-
### uhtml/init example
106+
Neither type will affect performance or change layout behavior.
107+
108+
- - -
109+
110+
## Exports
32111

33112
```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);
113+
import {
114+
// DOM manipulation
115+
render, html, svg, unsafe,
116+
// Preact like signals, based on alien-signals library
117+
signal, computed, effect, untracked, batch,
118+
// extras
119+
Hole, fragment,
120+
} from 'https://esm.run/uhtml';
46121
```
47122

48-
### uhtml/preactive example
123+
**In details**
124+
125+
* `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.
126+
* `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.
127+
* `unsafe(content:string)` to inject any content, even *HTML* or *SVG*, anywhere within a node: `<div>${unsafe('<em>value</em>')}</div>`
128+
* `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)
129+
* `Hole` class used internally to resolve `html` and `svg` tags' template and interpolations. This is exported mainly to simplify *TypeScript* relaed signatures.
130+
* `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.
131+
132+
- - -
133+
134+
## Loading from a CDN
135+
136+
The easiest way to start using *uhtml* is via *CDN* and here a few exported variants:
49137

50138
```js
51-
import { render, html, signal, detach } from 'uhtml/preactive';
139+
// implicit production version
140+
import { render, html } from 'https://esm.run/uhtml';
141+
// https://cdn.jsdelivr.net/npm/uhtml/dist/prod/dom.js
142+
143+
// explicit production version
144+
import { render, html } from 'https://esm.run/uhtml/prod';
145+
// https://cdn.jsdelivr.net/npm/uhtml/dist/prod/dom.js
146+
147+
// explicit developer/debugging version
148+
import { render, html } from 'https://esm.run/uhtml/dev';
149+
import { render, html } from 'https://esm.run/uhtml/debug';
150+
// https://cdn.jsdelivr.net/npm/uhtml/dist/dev/dom.js
151+
152+
// automatic prod/dev version on ?dev or ?debug
153+
import { render, html } from 'https://esm.run/uhtml/cdn';
154+
// https://cdn.jsdelivr.net/npm/uhtml/dist/prod/cdn.js
155+
```
156+
157+
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.
158+
159+
This makes it easy to switch to *dev* mode by changing the location from `https://example.com` to `https://example.com?debug`.
52160

53-
const count = signal(0);
161+
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*.
54162

55-
render(document.body, () => html`
56-
<button onclick=${() => { count.value++ }}>
57-
Clicks: ${count.value}
58-
</button>
59-
`);
163+
### Import Map
60164

61-
// stop reacting to signals in the future
62-
setTimeout(() => {
63-
detach(document.body);
64-
}, 10000);
165+
Another way to grant *CDN* and components portability is to use an import map and exclude *uhtml* from your bundler.
166+
167+
```html
168+
<!-- defined on each page -->
169+
<script type="importmap">
170+
{
171+
"imports": {
172+
"uhtml": "https://cdn.jsdelivr.net/npm/uhtml/dist/prod/cdn.js"
173+
}
174+
}
175+
</script>
176+
<!-- your library code -->
177+
<script type="module">
178+
import { html } from 'uhtml';
179+
180+
document.body.append(
181+
html`Import Maps are Awesome!`
182+
);
183+
</script>
65184
```
185+
186+
- - -
187+
188+
## Extra Tools
189+
190+
Minification is still recommended for production use cases and not only for *JS*, also for the templates and their content.
191+
192+
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*.
193+
194+
This is a *rollup* configuration example:
195+
196+
```js
197+
import terser from "@rollup/plugin-terser";
198+
import templateMinifier from "rollup-plugin-minify-template-literals";
199+
import { nodeResolve } from "@rollup/plugin-node-resolve";
200+
201+
export default {
202+
input: "src/your-component.js",
203+
plugins: [
204+
templateMinifier({
205+
options: {
206+
minifyOptions: {
207+
// allow only explicit <!--! comments !-->
208+
ignoreCustomComments: [/^!/],
209+
keepClosingSlash: true,
210+
caseSensitive: true,
211+
},
212+
},
213+
}),
214+
nodeResolve(),
215+
terser(),
216+
],
217+
output: {
218+
esModule: true,
219+
file: "dist/your-component.js",
220+
},
221+
};
222+
```
223+
224+
- - -
225+
226+
## About SSR and hydration
227+
228+
The current *pareser* is already environment agnostic, it runs on the client like it does in the server without needing dependencies at all.
229+
230+
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+
];

build/prod.js

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

cjs/package.json

Lines changed: 0 additions & 1 deletion
This file was deleted.

dist/dev/cdn.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const resolve = ({ protocol, host, pathname }) => {
2+
const dev = /[?&](?:dev|debug)(?:=|$)/.test(location.search);
3+
let path = pathname.replace(/\+\S*?$/, '');
4+
path = path.replace(/\/cdn(?:\/|\.js\S*)$/, '/');
5+
path = path.replace(/\/(?:dist\/)?(?:dev|prod)\//, '/');
6+
return `${protocol}//${host}${path}dist/${dev ? 'dev' : 'prod'}/dom.js`;
7+
};
8+
9+
const uhtml = Symbol.for('µhtml');
10+
11+
const {
12+
render, html, svg,
13+
computed, signal, batch, effect, untracked,
14+
} = globalThis[uhtml] || (globalThis[uhtml] = await import(/* webpackIgnore: true */resolve(new URL(import.meta.url))));
15+
16+
export { batch, computed, effect, html, render, signal, svg, untracked };

0 commit comments

Comments
 (0)