|
1 | | -# <em>µ</em>html |
| 1 | +# uhtml |
2 | 2 |
|
3 | 3 | [](https://www.npmjs.com/package/uhtml) [](https://github.com/WebReflection/uhtml/actions) [](https://coveralls.io/github/WebReflection/uhtml?branch=main) [](https://webreflection.github.io/csp/#-csp-strict) |
4 | 4 |
|
5 | | - |
6 | 5 |
|
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. |
8 | 7 |
|
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'; |
10 | 12 |
|
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); |
12 | 32 |
|
13 | | -**[Documentation](https://webreflection.github.io/uhtml/)** |
| 33 | + return html` |
| 34 | + <button onClick=${() => count.value++}> |
| 35 | + Clicked ${count.value} times |
| 36 | + </button> |
| 37 | + `; |
| 38 | +} |
14 | 39 |
|
15 | | -**[Release Notes](https://github.com/WebReflection/uhtml/pull/86)** |
| 40 | +document.body.append( |
| 41 | + html`<${Counter} />` |
| 42 | +); |
| 43 | +``` |
16 | 44 |
|
17 | 45 | - - - |
18 | 46 |
|
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 |
20 | 106 |
|
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: |
30 | 108 |
|
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 |
32 | 119 |
|
33 | 120 | ```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'; |
46 | 129 | ``` |
47 | 130 |
|
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: |
49 | 145 |
|
50 | 146 | ```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 |
52 | 172 |
|
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. |
54 | 174 |
|
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'; |
60 | 187 |
|
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> |
65 | 192 | ``` |
| 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. |
0 commit comments