microtags 0.0.13 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +567 -40
- package/dist/build.cjs +412 -0
- package/dist/build.cjs.map +7 -0
- package/dist/build.d.ts +17 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +395 -0
- package/dist/build.js.map +7 -0
- package/dist/builders.d.ts +70 -4
- package/dist/builders.d.ts.map +1 -1
- package/dist/{chunk-CCYWRJW6.js → chunk-46UEUDVX.js} +4 -26
- package/dist/chunk-46UEUDVX.js.map +7 -0
- package/dist/chunk-LS4L5QRD.js +26 -0
- package/dist/chunk-LS4L5QRD.js.map +7 -0
- package/dist/chunk-RKWPTT6P.js +69 -0
- package/dist/chunk-RKWPTT6P.js.map +7 -0
- package/dist/chunk-ZZ4ZLOQY.js +33 -0
- package/dist/chunk-ZZ4ZLOQY.js.map +7 -0
- package/dist/cli.cjs +536 -0
- package/dist/cli.cjs.map +7 -0
- package/dist/context.js +2 -1
- package/dist/esbuild.cjs +451 -0
- package/dist/esbuild.cjs.map +7 -0
- package/dist/esbuild.d.ts +7 -0
- package/dist/esbuild.d.ts.map +1 -0
- package/dist/esbuild.js +434 -0
- package/dist/esbuild.js.map +7 -0
- package/dist/factory.d.ts +7 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/htm.browser.cjs +591 -0
- package/dist/htm.browser.cjs.map +7 -0
- package/dist/htm.browser.d.ts +30 -0
- package/dist/htm.browser.d.ts.map +1 -0
- package/dist/htm.browser.js +498 -0
- package/dist/htm.browser.js.map +7 -0
- package/dist/htm.browser.min.js +3 -0
- package/dist/htm.browser.min.js.map +7 -0
- package/dist/htm.cjs +284 -0
- package/dist/htm.cjs.map +7 -0
- package/dist/htm.d.ts +14 -0
- package/dist/htm.d.ts.map +1 -0
- package/dist/htm.js +196 -0
- package/dist/htm.js.map +7 -0
- package/dist/htm.min.js +3 -0
- package/dist/htm.min.js.map +7 -0
- package/dist/index.cjs +125 -11
- package/dist/index.cjs.map +2 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +133 -17
- package/dist/index.js.map +2 -2
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +3 -3
- package/dist/meta.json +411 -26
- package/dist/preact-shared.d.ts +61 -0
- package/dist/preact-shared.d.ts.map +1 -0
- package/dist/preact.browser.d.ts +8 -0
- package/dist/preact.browser.d.ts.map +1 -0
- package/dist/preact.browser.js +45 -0
- package/dist/preact.browser.js.map +7 -0
- package/dist/preact.browser.min.js +2 -0
- package/dist/preact.browser.min.js.map +7 -0
- package/dist/preact.cjs +128 -0
- package/dist/preact.cjs.map +7 -0
- package/dist/preact.d.ts +9 -0
- package/dist/preact.d.ts.map +1 -0
- package/dist/preact.js +35 -0
- package/dist/preact.js.map +7 -0
- package/dist/render-support.cjs +86 -0
- package/dist/render-support.cjs.map +7 -0
- package/dist/render-support.d.ts +23 -0
- package/dist/render-support.d.ts.map +1 -0
- package/dist/render-support.js +12 -0
- package/dist/render-support.js.map +7 -0
- package/dist/render-support.min.js +2 -0
- package/dist/render-support.min.js.map +7 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vite.cjs +448 -0
- package/dist/vite.cjs.map +7 -0
- package/dist/vite.d.ts +7 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +429 -0
- package/dist/vite.js.map +7 -0
- package/package.json +92 -13
- package/dist/chunk-CCYWRJW6.js.map +0 -7
package/README.md
CHANGED
|
@@ -11,12 +11,16 @@
|
|
|
11
11
|
|
|
12
12
|
Web component helper with reactive props, automatic cleanup, and
|
|
13
13
|
[no Shadow DOM](https://gomakethings.com/the-shadow-dom-is-an-antipattern/).
|
|
14
|
-
Reactive via [alien-signals](https://github.com/stackblitz/alien-signals)
|
|
14
|
+
Reactive via [`alien-signals`](https://github.com/stackblitz/alien-signals),
|
|
15
|
+
inspired by [`nanotags`](https://github.com/psd-coder/nanotags).
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
This package adds some provisions for serverside rendering.
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
| Entry | Size (min + brotli) |
|
|
20
|
+
|-------|---------------------|
|
|
21
|
+
| `microtags` | <!-- size -->3.99 KB<!-- /size --> |
|
|
22
|
+
| `microtags/htm` | <!-- size:htm -->1.99 KB<!-- /size:htm --> |
|
|
23
|
+
| `microtags/preact` | <!-- size:preact -->0.58 KB<!-- /size:preact --> |
|
|
20
24
|
|
|
21
25
|
<details><summary><h2>Contents</h2></summary>
|
|
22
26
|
|
|
@@ -37,6 +41,7 @@ Inspired by [nanotags](https://nanotags.psdcoder.dev/). Reactive props use
|
|
|
37
41
|
- [Serverside Example](#serverside-example)
|
|
38
42
|
* [`.TAG`](#tag)
|
|
39
43
|
* [`.refs`](#refs)
|
|
44
|
+
- [Hydration](#hydration)
|
|
40
45
|
- [API](#api)
|
|
41
46
|
* [`define`](#define)
|
|
42
47
|
+ [`ComponentBuilder`](#componentbuilder)
|
|
@@ -46,8 +51,12 @@ Inspired by [nanotags](https://nanotags.psdcoder.dev/). Reactive props use
|
|
|
46
51
|
* [`builder.withRefs(fn)`](#builderwithrefsfn)
|
|
47
52
|
* [`builder.withContexts`](#builderwithcontexts)
|
|
48
53
|
+ [`.withContexts` Example](#withcontexts-example)
|
|
54
|
+
* [`builder.render(fn, options?)`](#builderrenderfn-options)
|
|
49
55
|
* [`builder.setup(ctx)`](#buildersetupctx)
|
|
50
56
|
+ [`ctx`](#ctx)
|
|
57
|
+
- [`ctx` Methods](#ctx-methods)
|
|
58
|
+
- [`ctx.bind`](#ctxbind)
|
|
59
|
+
* [`bindProp`](#bindprop)
|
|
51
60
|
- [Missing refs](#missing-refs)
|
|
52
61
|
* [Checking refs in tests](#checking-refs-in-tests)
|
|
53
62
|
- [Typed Events](#typed-events)
|
|
@@ -56,11 +65,22 @@ Inspired by [nanotags](https://nanotags.psdcoder.dev/). Reactive props use
|
|
|
56
65
|
* [`microtags/context`](#microtagscontext)
|
|
57
66
|
* [`microtags/render`](#microtagsrender)
|
|
58
67
|
+ [`renderList`](#renderlist)
|
|
68
|
+
- [`renderList` Example](#renderlist-example)
|
|
59
69
|
+ [`render`](#render)
|
|
70
|
+
- [`render` Example](#render-example)
|
|
60
71
|
* [`microtags/util`](#microtagsutil)
|
|
61
72
|
+ [Coercion](#coercion)
|
|
62
73
|
+ [`runAll`](#runall)
|
|
63
74
|
+ [`toAttributes`](#toattributes)
|
|
75
|
+
* [`microtags/htm`](#microtagshtm)
|
|
76
|
+
+ [What escaping does not cover](#what-escaping-does-not-cover)
|
|
77
|
+
* [`microtags/render-support`](#microtagsrender-support)
|
|
78
|
+
* [`microtags/preact`](#microtagspreact)
|
|
79
|
+
* [`microtags/build`](#microtagsbuild)
|
|
80
|
+
* [`microtags/esbuild`](#microtagsesbuild)
|
|
81
|
+
* [`microtags/vite`](#microtagsvite)
|
|
82
|
+
* [`microbuild`](#microbuild)
|
|
83
|
+
* [`client:true` vs `--keep`](#clienttrue-vs---keep)
|
|
64
84
|
- [Differences from `nanotags`](#differences-from-nanotags)
|
|
65
85
|
* [Reactivity primitive](#reactivity-primitive)
|
|
66
86
|
* [Effects + Subscriptions](#effects--subscriptions)
|
|
@@ -151,7 +171,8 @@ myCounter
|
|
|
151
171
|
|
|
152
172
|
### 3. Call `.setup`
|
|
153
173
|
|
|
154
|
-
Method `.setup`
|
|
174
|
+
Method `.setup` is the terminal step in the method chain. It takes a function
|
|
175
|
+
that gets called with a `context` object. This is where "hydration" happens.
|
|
155
176
|
|
|
156
177
|
```ts
|
|
157
178
|
myCounter
|
|
@@ -190,10 +211,10 @@ myCounter
|
|
|
190
211
|
## Full Example
|
|
191
212
|
|
|
192
213
|
This example expects the full HTML to already be in the DOM.
|
|
193
|
-
Defining the components
|
|
214
|
+
Defining the components "hydrates" the page (adds behavior).
|
|
194
215
|
|
|
195
216
|
If you want to client-side render the components, that is possible too.
|
|
196
|
-
You
|
|
217
|
+
You would update the DOM inside `.setup`.
|
|
197
218
|
|
|
198
219
|
>
|
|
199
220
|
> [!NOTE]
|
|
@@ -262,11 +283,11 @@ export const myCounter = define('my-counter')
|
|
|
262
283
|
There are two separate things people call "validation":
|
|
263
284
|
|
|
264
285
|
1. **Prop validation** checks the attributes passed into a component.
|
|
265
|
-
Pass a [Standard Schema](https://standardschema.dev)
|
|
266
|
-
ArkType) to [`p.schema`](#withprops)
|
|
267
|
-
and falls back to `undefined` when the value doesn't coerce.
|
|
268
|
-
2. **Form input validation** -- client-side validation -- checks live user
|
|
269
|
-
in a form, for example a value that changes on every keystroke.
|
|
286
|
+
Pass a [Standard Schema](https://standardschema.dev)
|
|
287
|
+
(Zod, Valibot, ArkType) to [`p.schema`](#withprops), and it coerces the
|
|
288
|
+
attribute value and falls back to `undefined` when the value doesn't coerce.
|
|
289
|
+
2. **Form input validation** -- aka client-side validation -- checks live user
|
|
290
|
+
input in a form, for example a value that changes on every keystroke.
|
|
270
291
|
There is no dedicated API for this. Validate the schema directly
|
|
271
292
|
inside `.setup()` and hold the result as a signal.
|
|
272
293
|
|
|
@@ -371,7 +392,7 @@ define('subscribe-form')
|
|
|
371
392
|
const value = signal('')
|
|
372
393
|
const touched = signal<boolean>(false)
|
|
373
394
|
|
|
374
|
-
// capture the live value (
|
|
395
|
+
// capture the live value (clears the field on reset)
|
|
375
396
|
ctx.bind(value, ctx.refs.input)
|
|
376
397
|
|
|
377
398
|
// only reveal errors after the first blur
|
|
@@ -413,19 +434,16 @@ and the submit button is disabled until valid. See `example/subscribe-form.ts`.
|
|
|
413
434
|
|
|
414
435
|
## Serverside Example
|
|
415
436
|
|
|
416
|
-
See [./example/ssr.ts](./example/ssr.ts)
|
|
437
|
+
See [./example/ssr.ts](./example/ssr.ts) for an example of rendering an HTML
|
|
438
|
+
string and then "hydrating" the page on the client.
|
|
417
439
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
Each tag name that you pass to `define` is exposed as a property `.TAG`
|
|
421
|
-
on the return value. It is possible to import your web components in Node,
|
|
422
|
-
even though Node does not have browser APIs.
|
|
440
|
+
> [!NOTE]
|
|
441
|
+
> The server must know how to render the HTML that the components depend on.
|
|
423
442
|
|
|
424
|
-
|
|
425
|
-
selector to `r.one()`. If you pass explicit CSS selectors to `withRefs`,
|
|
426
|
-
e.g. `{ copy: r.one('button') }`, then the HTML does not require a `data-ref`
|
|
427
|
-
attribute.
|
|
443
|
+
### `.TAG`
|
|
428
444
|
|
|
445
|
+
Each tag name is exposed as a property `.TAG`. You can import the web
|
|
446
|
+
components in Node, even though Node does not have browser APIs.
|
|
429
447
|
|
|
430
448
|
```ts
|
|
431
449
|
import { MyCounter, CountBtn, CopyBtn } from './index.js'
|
|
@@ -467,11 +485,16 @@ export function render ():string {
|
|
|
467
485
|
}
|
|
468
486
|
```
|
|
469
487
|
|
|
488
|
+
> [!IMPORTANT]
|
|
489
|
+
> Custom elements must always have a closing tag, even when empty, like
|
|
490
|
+
> `<copy-btn></copy-btn>` above. HTML has no self-closing custom
|
|
491
|
+
> elements. The browser parses `<copy-btn />` as an open tag and treats
|
|
492
|
+
> everything after it as its children.
|
|
493
|
+
|
|
470
494
|
### `.refs`
|
|
471
495
|
|
|
472
|
-
Web components created with `microtags` have a property `.refs
|
|
473
|
-
object of
|
|
474
|
-
`data-ref` attributes.
|
|
496
|
+
Web components created with `microtags` have a property `.refs`. It is an
|
|
497
|
+
object of ref names you passed in. Use it to create `data-ref` attributes.
|
|
475
498
|
|
|
476
499
|
```ts
|
|
477
500
|
import { MyCounter } from '../example.js'
|
|
@@ -489,17 +512,35 @@ export function render () {
|
|
|
489
512
|
|
|
490
513
|
---------------------------------------------------------------------
|
|
491
514
|
|
|
515
|
+
## Hydration
|
|
516
|
+
|
|
517
|
+
In the [preceding serverside example](#serverside-example), the
|
|
518
|
+
server must know how to render the HTML that the client components depend on.
|
|
519
|
+
That means we have an unspecified dependency between HTML and JS.
|
|
520
|
+
Hydration is one way to render the correct HTML.
|
|
521
|
+
|
|
522
|
+
You can write a `.render` method, and then use a build tool
|
|
523
|
+
to strip the `.render` method from the JS sent to the client. That way
|
|
524
|
+
the clientside bundle stays small, but on the server, every component can
|
|
525
|
+
render itself, and the components shipped to the client deal with behavior only.
|
|
526
|
+
|
|
527
|
+
See [`example-hydration/`](./example-hydration/) for a working example that
|
|
528
|
+
uses Vite, and more docs.
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
---------------------------------------------------------------
|
|
532
|
+
|
|
492
533
|
|
|
493
534
|
## API
|
|
494
535
|
|
|
495
536
|
### `define`
|
|
496
537
|
|
|
538
|
+
Function `define(tagName)` returns a [`ComponentBuilder`](#componentbuilder).
|
|
539
|
+
|
|
497
540
|
```js
|
|
498
541
|
function define (tagName:string):ComponentBuilder
|
|
499
542
|
```
|
|
500
543
|
|
|
501
|
-
Function `define(tagName)` returns a [`ComponentBuilder`](#componentbuilder).
|
|
502
|
-
|
|
503
544
|
|
|
504
545
|
#### `ComponentBuilder`
|
|
505
546
|
|
|
@@ -511,6 +552,7 @@ The object returned by [`define`](#define). Includes methods:
|
|
|
511
552
|
| `.withProps(fn)` | `ComponentBuilder` | Declare reactive attributes. See [Props](#props). |
|
|
512
553
|
| `.withRefs(fn)` | `ComponentBuilder` | Declare DOM refs resolved once the element connects. See [Refs](#refs-1). |
|
|
513
554
|
| `.withContexts(fn)` | `ComponentBuilder` | Consume context values published by an ancestor. See [`withContexts`](#withcontexts). |
|
|
555
|
+
| `.render(fn, opts?)` | `ComponentBuilder` | Attach a server-render function. See [`.render`](#builderrenderfn-options) below. Not to be confused with the DOM helper [`microtags/render`](#microtagsrender). |
|
|
514
556
|
| `.setup(fn)` | `MicrotagElementClass` | Registers the custom element and returns its class. See [`setup`](#setup) below. |
|
|
515
557
|
|
|
516
558
|
Methods `withProps`, `withRefs`, and `withContexts` each return a new builder,
|
|
@@ -524,15 +566,18 @@ Declare reactive attributes via `withProps`. Each prop becomes:
|
|
|
524
566
|
|
|
525
567
|
* An observed HTML attribute (auto-synced via `attributeChangedCallback`)
|
|
526
568
|
* A signal at `ctx.props.propName`
|
|
527
|
-
*
|
|
528
|
-
* Four built-in validators coerce raw attribute strings to typed values:
|
|
569
|
+
* Four built-in coercers turn raw attribute strings into typed values:
|
|
529
570
|
|
|
530
|
-
|
|
|
531
|
-
|
|
532
|
-
| `p.string()` |
|
|
533
|
-
| `p.number()` | `Number(val)` | `
|
|
534
|
-
| `p.boolean()` | `
|
|
535
|
-
| `p.
|
|
571
|
+
| Coercer | Attribute present | Attribute absent |
|
|
572
|
+
|---------|-------------------|------------------|
|
|
573
|
+
| `p.string()` | the raw string | `""` |
|
|
574
|
+
| `p.number()` | `Number(val)`, so `NaN` if not numeric | `NaN` |
|
|
575
|
+
| `p.boolean()` | `true`, whatever the value, including `""` and `"false"` | `false` |
|
|
576
|
+
| `p.json<T>()` | `JSON.parse(val)`, or `undefined` if it does not parse | `undefined` |
|
|
577
|
+
|
|
578
|
+
`p.boolean()` follows HTML boolean-attribute semantics: presence means
|
|
579
|
+
`true`, like `disabled` or `checked`. To turn a boolean prop off, remove the
|
|
580
|
+
attribute rather than setting it to `"false"`.
|
|
536
581
|
|
|
537
582
|
```ts
|
|
538
583
|
.withProps(p => ({
|
|
@@ -722,6 +767,105 @@ define('themed-card')
|
|
|
722
767
|
---------------------------------------------------------
|
|
723
768
|
|
|
724
769
|
|
|
770
|
+
### `builder.render(fn, options?)`
|
|
771
|
+
|
|
772
|
+
Attach a render function that produces the component's inner HTML as a
|
|
773
|
+
string. The render function receives a context with `props` (plain
|
|
774
|
+
resolved values, not signals), `refs`, `TAG`, `children` (inner HTML
|
|
775
|
+
from the caller), and `attrs` (extra attributes from the caller).
|
|
776
|
+
|
|
777
|
+
```ts
|
|
778
|
+
define('my-counter')
|
|
779
|
+
.withProps(p => ({
|
|
780
|
+
start: p.number(),
|
|
781
|
+
}))
|
|
782
|
+
.withRefs(r => ({
|
|
783
|
+
display: r.one<HTMLDivElement>(),
|
|
784
|
+
inc: r.one(),
|
|
785
|
+
dec: r.one(),
|
|
786
|
+
}))
|
|
787
|
+
.render(({ props, refs }) => {
|
|
788
|
+
return `
|
|
789
|
+
<div data-ref="${refs.display}">
|
|
790
|
+
${props.start}
|
|
791
|
+
</div>
|
|
792
|
+
<button data-ref="${refs.dec}">-</button>
|
|
793
|
+
<button data-ref="${refs.inc}">+</button>
|
|
794
|
+
`
|
|
795
|
+
})
|
|
796
|
+
.setup(ctx => {
|
|
797
|
+
// hydration code only
|
|
798
|
+
})
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
The returned element class gains three statics: `render(props,
|
|
802
|
+
options?)` which returns complete host markup, `renderParts(props,
|
|
803
|
+
options?)` which returns the same render as parts, and `props`, the list
|
|
804
|
+
of declared prop names. `options` accepts `children` (a string of inner
|
|
805
|
+
HTML) and `attrs` (an object of extra attributes, all strings).
|
|
806
|
+
|
|
807
|
+
`renderParts` returns `{ tag, attrs, html }`: the host tag name, the
|
|
808
|
+
host attributes as serialized (not yet escaped) strings, and the inner
|
|
809
|
+
HTML. `attrs` lists the declared props first, then the extra `attrs`,
|
|
810
|
+
skipping prop-only props, null or false values, and extra names that are
|
|
811
|
+
not valid attribute names. A true boolean is the empty string. Use it to
|
|
812
|
+
build the host with another renderer, such as a UI framework, while
|
|
813
|
+
`render` stays the string form:
|
|
814
|
+
|
|
815
|
+
```ts
|
|
816
|
+
import 'microtags/render-support'
|
|
817
|
+
import type { RenderParts } from 'microtags'
|
|
818
|
+
|
|
819
|
+
const { tag, attrs, html }:RenderParts = MyCounter.renderParts({
|
|
820
|
+
start: 5,
|
|
821
|
+
})
|
|
822
|
+
// tag: 'my-counter', attrs: { start: '5' }, html: '<div ...'
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
`props` are the names declared in `.withProps`. They are serialized onto
|
|
826
|
+
the host tag with the prop's serializer, so `.setup` can coerce them back
|
|
827
|
+
on the client. `attrs` are any other attributes the caller put on the
|
|
828
|
+
tag, such as `class`, `id`, or `aria-*`. They are written to the host tag
|
|
829
|
+
as-is, and are available to the render function so it can forward them
|
|
830
|
+
inside. See [Hydration](#hydration) for an example.
|
|
831
|
+
|
|
832
|
+
The static `render` (and `renderParts`) needs the server render
|
|
833
|
+
support module, which builds the host tag and serializes props. The `microtags/htm`
|
|
834
|
+
templater loads it, on the server and in the browser. Code that calls
|
|
835
|
+
a component's `render` static directly, without the templater, must
|
|
836
|
+
import [`microtags/render-support`](#microtagsrender-support) once.
|
|
837
|
+
Without it, `render` and `renderParts` throw an error naming that
|
|
838
|
+
module.
|
|
839
|
+
|
|
840
|
+
```ts
|
|
841
|
+
import 'microtags/render-support'
|
|
842
|
+
import { MyCounter } from './components.js'
|
|
843
|
+
|
|
844
|
+
const markup = MyCounter.render({ start: 5 })
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
> [!IMPORTANT]
|
|
848
|
+
> This is a breaking change in 0.0.x. Before it, `render` worked with
|
|
849
|
+
> no extra import.
|
|
850
|
+
|
|
851
|
+
Pass `{ client: true }` to keep the render function in the client
|
|
852
|
+
bundle after [`microbuild`](#microbuild) strips it:
|
|
853
|
+
|
|
854
|
+
```ts
|
|
855
|
+
.render(renderFn, { client: true })
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
See [`client:true` vs `--keep`](#clienttrue-vs---keep) for when to
|
|
859
|
+
use each.
|
|
860
|
+
|
|
861
|
+
> [!NOTE]
|
|
862
|
+
> `.render` on the builder is unrelated to
|
|
863
|
+
> [`microtags/render`](#microtagsrender), which is a DOM reconciler
|
|
864
|
+
> for lists and single items.
|
|
865
|
+
|
|
866
|
+
---------------------------------------------------------
|
|
867
|
+
|
|
868
|
+
|
|
725
869
|
### `builder.setup(ctx)`
|
|
726
870
|
|
|
727
871
|
Registers the custom element via `customElements.define` and returns the
|
|
@@ -743,6 +887,14 @@ class ComponentBuilder {
|
|
|
743
887
|
The callback function runs once per element instance, and is called with the
|
|
744
888
|
following `context` object.
|
|
745
889
|
|
|
890
|
+
Teardown waits a microtask. When the element is removed, its cleanups
|
|
891
|
+
(listeners, effects, bindings, `onCleanup` callbacks) run on the next
|
|
892
|
+
microtask, and only if it is still disconnected then. So an element that is
|
|
893
|
+
removed and re-inserted in the same tick (a move) keeps its setup and
|
|
894
|
+
state, even in browsers without `moveBefore`. After a real removal, cleanup has
|
|
895
|
+
happened once `await Promise.resolve()` returns. If the element is inserted
|
|
896
|
+
again later, setup runs again.
|
|
897
|
+
|
|
746
898
|
|
|
747
899
|
#### `ctx`
|
|
748
900
|
|
|
@@ -757,11 +909,11 @@ ctx:SetupContext<Props, RefsMap<RefDefs>, ContextsMap<CtxDefs>>
|
|
|
757
909
|
| Method | Returns | Description |
|
|
758
910
|
|--------|---------|-------------|
|
|
759
911
|
| `.peek(signal)` | `T` | Read a signal's current value without subscribing, even inside `ctx.effect`. |
|
|
760
|
-
| `.on(target, type, handler, options?)` | `void` | Add a DOM event listener, auto-removed on disconnect. |
|
|
912
|
+
| `.on(target, type, handler, options?)` | `void` | Add a DOM event listener, auto-removed on teardown (a microtask after disconnect). |
|
|
761
913
|
| `.emit(event)` / `.emit(name, detail?, options?)` | `boolean` | Dispatch a pre-built `Event` as-is, or construct and dispatch a bubbling `CustomEvent` from a name. Returns `dispatchEvent`'s result: `false` if a cancelable event had `preventDefault()` called. |
|
|
762
|
-
| `.effect(fn)` | `void` | Run an alien-signals effect that auto-tracks the signals it reads. Auto-disposed on
|
|
763
|
-
| `.bind(signal, element, options?)` | `void` | Bind a signal to a DOM element property, auto-cleaned on
|
|
764
|
-
| `.onCleanup(fn)` | `void` | Register custom teardown
|
|
914
|
+
| `.effect(fn)` | `void` | Run an alien-signals effect that auto-tracks the signals it reads. Auto-disposed on teardown. |
|
|
915
|
+
| `.bind(signal, element, options?)` | `void` | Bind a signal to a DOM element property, auto-cleaned on teardown. |
|
|
916
|
+
| `.onCleanup(fn)` | `void` | Register custom teardown, run a microtask after disconnect. |
|
|
765
917
|
| `.consume(token)` | `T \| undefined` | Read an optional context value published by an ancestor. See [`withContexts`](#withcontexts). |
|
|
766
918
|
|
|
767
919
|
See [Typed Events](#typed-events) for more about `.on` and `.emit`.
|
|
@@ -805,6 +957,36 @@ ctx.bind(() => `${count()} items`, ctx.refs.label, { prop: 'textContent' })
|
|
|
805
957
|
|
|
806
958
|
---
|
|
807
959
|
|
|
960
|
+
### `bindProp`
|
|
961
|
+
|
|
962
|
+
Bind a signal (or computed) to a declared prop of another host, so the
|
|
963
|
+
prop reads through to it. This is the same as assigning the signal to
|
|
964
|
+
the host's prop accessor, but typed: the signal's value type must match
|
|
965
|
+
the prop's type, and only declared props are accepted.
|
|
966
|
+
|
|
967
|
+
```ts
|
|
968
|
+
function bindProp (
|
|
969
|
+
host:HTMLElement,
|
|
970
|
+
name:string, // a declared prop of `host`
|
|
971
|
+
source:() => T // T is the prop's type
|
|
972
|
+
):void
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
```ts
|
|
976
|
+
import { define, bindProp } from 'microtags'
|
|
977
|
+
import { computed } from 'alien-signals'
|
|
978
|
+
|
|
979
|
+
// in a parent's setup, drive a nested host's `label` prop
|
|
980
|
+
const label = computed(() => String(ctx.props.count()))
|
|
981
|
+
bindProp(ctx.refs.badge, 'label', label)
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
While bound, the prop is read-only inside the nested component, and the
|
|
985
|
+
host's HTML attribute is no longer updated. Assigning a plain value to
|
|
986
|
+
the accessor removes the binding.
|
|
987
|
+
|
|
988
|
+
---
|
|
989
|
+
|
|
808
990
|
|
|
809
991
|
## Missing refs
|
|
810
992
|
|
|
@@ -1224,6 +1406,351 @@ type Attrs = Record<
|
|
|
1224
1406
|
function toAttributes (attrs:Attrs):string
|
|
1225
1407
|
```
|
|
1226
1408
|
|
|
1409
|
+
### `microtags/htm`
|
|
1410
|
+
|
|
1411
|
+
A tagged-template string renderer that will expand any microtags
|
|
1412
|
+
instances if finds. Exports `html`, `h`, `raw`, and the `Markup` type.
|
|
1413
|
+
`html`, `h`, and `raw` return `Markup`, not a string. Convert it with
|
|
1414
|
+
`String(m)` where a string is required, for example `innerHTML`. In the
|
|
1415
|
+
browser the same import resolves to a build that also exports `mount`;
|
|
1416
|
+
see [ADR-007](./docs/adr/ADR-007-client-templater.md).
|
|
1417
|
+
|
|
1418
|
+
```ts
|
|
1419
|
+
import { html, raw } from 'microtags/htm'
|
|
1420
|
+
import { MyCounter } from './components.js'
|
|
1421
|
+
|
|
1422
|
+
const page = html`<div>
|
|
1423
|
+
<${MyCounter} start=${5}>
|
|
1424
|
+
<p>Extra children</p>
|
|
1425
|
+
</${MyCounter}>
|
|
1426
|
+
${raw('<b>trusted markup</b>')}
|
|
1427
|
+
</div>`
|
|
1428
|
+
```
|
|
1429
|
+
|
|
1430
|
+
When `html` meets a microtags element class, it splits props from
|
|
1431
|
+
passthrough attributes using the class's static `props` list, joins
|
|
1432
|
+
children into one string, and calls the static `render`. Plain string
|
|
1433
|
+
tag names emit ordinary HTML.
|
|
1434
|
+
|
|
1435
|
+
**Escaping.** Every interpolated string is escaped (`<`, `>`, `&`, `"`,
|
|
1436
|
+
`'`) before the parser sees it. Component output and `raw()` output
|
|
1437
|
+
are trusted and never re-escaped. Static template text is trusted.
|
|
1438
|
+
|
|
1439
|
+
**Arrays.** Array children (from `.map()`) are flattened automatically.
|
|
1440
|
+
|
|
1441
|
+
**Void elements.** Write void tags in the self-closing form, like
|
|
1442
|
+
`<br />`, `<img src="x" />`, and `<input value=${v} />`. The output has
|
|
1443
|
+
no closing tag and no trailing slash, eg `<br>`. The `htm` parser does
|
|
1444
|
+
not know which tags are void, so an unclosed `<br>` would be treated
|
|
1445
|
+
as an open tag and swallow the content after it. Instead of dropping
|
|
1446
|
+
that content, the templater throws, on the server and in the browser,
|
|
1447
|
+
and the error names the tag and its self-closing form.
|
|
1448
|
+
|
|
1449
|
+
**Custom elements.** A component tag always gets a closing tag in the
|
|
1450
|
+
output, eg `<my-counter start="5">...</my-counter>`, because HTML has no
|
|
1451
|
+
self-closing custom elements. In a template, close a component with
|
|
1452
|
+
`</${MyComponent}>` or with the `htm` shorthand `<//>`, which closes
|
|
1453
|
+
the nearest open tag without repeating its name.
|
|
1454
|
+
|
|
1455
|
+
#### What escaping does not cover
|
|
1456
|
+
|
|
1457
|
+
Escaping keeps an interpolated string from becoming markup. It does not
|
|
1458
|
+
make every value safe in every place it can go.
|
|
1459
|
+
|
|
1460
|
+
**URL schemes.** Attribute escaping keeps a value inside its quotes,
|
|
1461
|
+
but `href=${url}` with `url` set to `javascript:alert(1)` is still a
|
|
1462
|
+
working link. The same goes for `src`, `action`, `formaction`, and
|
|
1463
|
+
other attributes that take a URL. Check the scheme yourself (for
|
|
1464
|
+
example, allow only `http:`, `https:` and `mailto:`) before a URL from
|
|
1465
|
+
user data reaches the template.
|
|
1466
|
+
|
|
1467
|
+
**`<script>` and `<style>`.** HTML escaping is the wrong encoding inside
|
|
1468
|
+
these elements. The browser does not decode entities there, so an
|
|
1469
|
+
escaped value comes out mangled, and HTML escaping does not stop a
|
|
1470
|
+
value from breaking out of a JavaScript string or a CSS rule. Do not
|
|
1471
|
+
interpolate user data into them. To pass data to a script, serialize it
|
|
1472
|
+
as JSON in a `<script type="application/json">` and escape `<` in the
|
|
1473
|
+
JSON, the way the hydration example's state script does.
|
|
1474
|
+
|
|
1475
|
+
**`raw`.** `raw()` skips escaping by construction. Pass it only markup
|
|
1476
|
+
you wrote or trust, never user data.
|
|
1477
|
+
|
|
1478
|
+
**Attribute names.** Escaping covers values, not names. A name must
|
|
1479
|
+
start with a letter, `_` or `:`, followed by letters, digits, `-`,
|
|
1480
|
+
`_`, `:` or `.`. Any other name, for example a key from a spread of
|
|
1481
|
+
user data such as `x" onload="...`, is skipped without an error, on the
|
|
1482
|
+
server, in the browser, and on a component's host tag. Valid keys in the
|
|
1483
|
+
same spread are still written.
|
|
1484
|
+
|
|
1485
|
+
**Void elements.** A void element with children throws instead of
|
|
1486
|
+
rendering, on the server and in the browser. Write void tags in the
|
|
1487
|
+
self-closing form, `<input />`, so an unclosed tag cannot swallow the
|
|
1488
|
+
markup after it.
|
|
1489
|
+
|
|
1490
|
+
---
|
|
1491
|
+
|
|
1492
|
+
### `microtags/render-support`
|
|
1493
|
+
|
|
1494
|
+
The host-tag builder, prop serializers and attribute escaping that a
|
|
1495
|
+
component's static `render` and `renderParts` use. Import it for its
|
|
1496
|
+
side effect:
|
|
1497
|
+
|
|
1498
|
+
```ts
|
|
1499
|
+
import 'microtags/render-support'
|
|
1500
|
+
```
|
|
1501
|
+
|
|
1502
|
+
It is separate from core so that a client bundle with every
|
|
1503
|
+
`.render` link stripped leaves this code out. The `microtags/htm`
|
|
1504
|
+
templater imports it already, and the [`microtags/build`](#microtagsbuild)
|
|
1505
|
+
transform adds it to any module where it keeps a `.render` link, so
|
|
1506
|
+
only code that calls a `render` or `renderParts` static without
|
|
1507
|
+
either needs this import. See
|
|
1508
|
+
[ADR-009](./docs/adr/ADR-009-server-render-support-through-render-link.md).
|
|
1509
|
+
|
|
1510
|
+
---
|
|
1511
|
+
|
|
1512
|
+
### `microtags/preact`
|
|
1513
|
+
|
|
1514
|
+
Use a microtags component inside a Preact app, rendered on the server
|
|
1515
|
+
and hydrated in the browser, or rendered client-only. `toPreact(El,
|
|
1516
|
+
{ events })` takes an element class and returns a Preact component that
|
|
1517
|
+
renders the host tag. Preact owns the host and its attributes, and the
|
|
1518
|
+
element owns the host's children. See
|
|
1519
|
+
[ADR-010](./docs/adr/ADR-010-preact-adapter-ownership-split.md).
|
|
1520
|
+
|
|
1521
|
+
```ts
|
|
1522
|
+
import { toPreact } from 'microtags/preact'
|
|
1523
|
+
import { NumberInput } from './number-input.js'
|
|
1524
|
+
|
|
1525
|
+
// callback prop name -> DOM event name
|
|
1526
|
+
export const PreactNumberInput = toPreact(NumberInput, {
|
|
1527
|
+
events: { onChange: 'number-input:change' },
|
|
1528
|
+
})
|
|
1529
|
+
```
|
|
1530
|
+
|
|
1531
|
+
The component takes the element's declared props (typed from the
|
|
1532
|
+
class), one callback prop per entry in `events`, and `class`, `id`,
|
|
1533
|
+
`aria-*` and `data-*` props, which pass through to the host.
|
|
1534
|
+
|
|
1535
|
+
**Server render, then hydrate.** On the server, the adapter fills the
|
|
1536
|
+
host with the component's own markup, so `renderToString` gives the
|
|
1537
|
+
same HTML as the component's `render` static. Server code must import
|
|
1538
|
+
a variant of the component that keeps its `.render` link; a class with
|
|
1539
|
+
no render link throws an error naming its tag.
|
|
1540
|
+
|
|
1541
|
+
```ts
|
|
1542
|
+
// server
|
|
1543
|
+
import { h } from 'preact'
|
|
1544
|
+
import { renderToString } from 'preact-render-to-string'
|
|
1545
|
+
|
|
1546
|
+
const body = renderToString(h(App, null))
|
|
1547
|
+
```
|
|
1548
|
+
|
|
1549
|
+
```ts
|
|
1550
|
+
// browser
|
|
1551
|
+
import { h, hydrate } from 'preact'
|
|
1552
|
+
|
|
1553
|
+
hydrate(h(App, null), document.getElementById('root')!)
|
|
1554
|
+
```
|
|
1555
|
+
|
|
1556
|
+
In `App`, use the component like any other:
|
|
1557
|
+
|
|
1558
|
+
```ts
|
|
1559
|
+
h(PreactNumberInput, {
|
|
1560
|
+
min: 1,
|
|
1561
|
+
class: 'qty',
|
|
1562
|
+
onChange: (ev:CustomEvent) => setQty(ev.detail),
|
|
1563
|
+
})
|
|
1564
|
+
```
|
|
1565
|
+
|
|
1566
|
+
In the browser, the host gets a `dangerouslySetInnerHTML` that never
|
|
1567
|
+
changes once the host is in the document. While hydrating, Preact
|
|
1568
|
+
leaves that element's children alone, so the element adopts the server
|
|
1569
|
+
markup. Later renders never write `innerHTML` either. Declared props
|
|
1570
|
+
reach the element as properties, including after `hydrate`, where
|
|
1571
|
+
Preact itself sets no props: the adapter assigns them once the host is
|
|
1572
|
+
committed, so a prop-only value or an object arrives too.
|
|
1573
|
+
|
|
1574
|
+
**Client-only render.** With no server markup, call Preact's `render`.
|
|
1575
|
+
On that first render the adapter fills the host with the component's
|
|
1576
|
+
inner HTML from its `renderParts` static, before the element connects,
|
|
1577
|
+
so the element finds its markup and refs. This needs the variant of
|
|
1578
|
+
the component that keeps its render link, with
|
|
1579
|
+
[`microtags/render-support`](#microtagsrender-support) loaded (the
|
|
1580
|
+
build transform imports it for a kept render link). A component whose
|
|
1581
|
+
render link was stripped mounts as an empty host.
|
|
1582
|
+
|
|
1583
|
+
```ts
|
|
1584
|
+
import { h, render } from 'preact'
|
|
1585
|
+
|
|
1586
|
+
render(h(PreactNumberInput, { min: 1 }), document.body)
|
|
1587
|
+
```
|
|
1588
|
+
|
|
1589
|
+
**Two builds.** The import resolves by export condition, the same way
|
|
1590
|
+
as [`microtags/htm`](#microtagshtm). The `browser` condition selects a
|
|
1591
|
+
build that contains no render code. The default build, for Node and
|
|
1592
|
+
SSR, imports [`microtags/render-support`](#microtagsrender-support).
|
|
1593
|
+
|
|
1594
|
+
**Peer dependency.** `preact` is an optional peer dependency. Install
|
|
1595
|
+
it yourself. Only `microtags/preact` imports it.
|
|
1596
|
+
|
|
1597
|
+
**No children.** The adapter does not accept Preact children. Passing
|
|
1598
|
+
them is a type error, and the adapter throws.
|
|
1599
|
+
|
|
1600
|
+
**Props before definition.** If the element is not defined yet when
|
|
1601
|
+
Preact first sets a prop, the element has no property by that name, so
|
|
1602
|
+
Preact sets an attribute instead. The component reads its declared
|
|
1603
|
+
props from attributes when it connects, so a value that survives
|
|
1604
|
+
conversion to an attribute string still arrives. The adapter does not work around this.
|
|
1605
|
+
|
|
1606
|
+
**Other host renderers.** The adapter is built on the
|
|
1607
|
+
[`renderParts`](#builderrenderfn-options) static, which returns the
|
|
1608
|
+
host tag, its serialized attributes and its inner HTML separately. An
|
|
1609
|
+
adapter for another framework can build on the same parts.
|
|
1610
|
+
|
|
1611
|
+
---
|
|
1612
|
+
|
|
1613
|
+
### `microtags/build`
|
|
1614
|
+
|
|
1615
|
+
Node-only. Strips [`.render()`](#builderrenderfn-options) calls from
|
|
1616
|
+
define chains so the client bundle contains only hydration code.
|
|
1617
|
+
Requires `typescript` as a peer dependency.
|
|
1618
|
+
|
|
1619
|
+
```ts
|
|
1620
|
+
import { transform } from 'microtags/build'
|
|
1621
|
+
|
|
1622
|
+
const result = transform(source, {
|
|
1623
|
+
keep: ['my-notice'], // keep these tags' render in the bundle
|
|
1624
|
+
cascade: true, // dead-import removal, default false
|
|
1625
|
+
filename: 'app.ts', // shown in warnings
|
|
1626
|
+
})
|
|
1627
|
+
|
|
1628
|
+
console.log(result.code)
|
|
1629
|
+
for (const w of result.warnings) {
|
|
1630
|
+
console.warn(`${w.file}:${w.line} ${w.message}`)
|
|
1631
|
+
}
|
|
1632
|
+
```
|
|
1633
|
+
|
|
1634
|
+
**Strip rule:** a `.render(...)` call is removed when it is a link in a
|
|
1635
|
+
call chain rooted at a `define(...)` call imported from `microtags`,
|
|
1636
|
+
with import aliases tracked. Nothing else named `render` is touched.
|
|
1637
|
+
|
|
1638
|
+
**Keep rule (union):** the call is kept when its options argument is an
|
|
1639
|
+
object literal containing `client: true`, or when the string literal
|
|
1640
|
+
passed to `define` is listed in `keep`.
|
|
1641
|
+
|
|
1642
|
+
**Output fidelity:** removed spans are replaced with the same number of
|
|
1643
|
+
newlines. Every other byte is unchanged, apart from the support import
|
|
1644
|
+
below. TS in, TS out.
|
|
1645
|
+
|
|
1646
|
+
**Support import:** when a module keeps at least one `.render` link,
|
|
1647
|
+
the transform appends `import 'microtags/render-support'` after the
|
|
1648
|
+
last line, unless the module already imports it, so kept components
|
|
1649
|
+
can render in the browser. A module where every link was stripped gets
|
|
1650
|
+
no such import, and a bundle with no kept link and no templater import
|
|
1651
|
+
contains none of the server render code. A bundler config that aliases
|
|
1652
|
+
`microtags` to source should alias `microtags/render-support` too.
|
|
1653
|
+
|
|
1654
|
+
**Unsupported:** `const b = define('x'); b.render(fn)` -- the
|
|
1655
|
+
non-chained form is not matched and is left unchanged.
|
|
1656
|
+
|
|
1657
|
+
**`client: true` detection:** only an object literal is recognised. A
|
|
1658
|
+
variable or spread in the options position is not.
|
|
1659
|
+
|
|
1660
|
+
**Type gap:** `MicrotagElementClass` declares `render` and `props` as
|
|
1661
|
+
always present. After stripping, `render` is `undefined` at runtime.
|
|
1662
|
+
|
|
1663
|
+
**Warnings** (each with file and line):
|
|
1664
|
+
|
|
1665
|
+
| Kind | Meaning |
|
|
1666
|
+
|------|---------|
|
|
1667
|
+
| `outside-chain` | `.render(` call not in a define chain |
|
|
1668
|
+
| `stripped-static` | `X.render(` after X's render was stripped |
|
|
1669
|
+
| `non-literal-tag` | `define(variable)` with `keep` set |
|
|
1670
|
+
|
|
1671
|
+
---
|
|
1672
|
+
|
|
1673
|
+
### `microtags/esbuild`
|
|
1674
|
+
|
|
1675
|
+
Node-only esbuild plugin that calls [`transform`](#microtagsbuild) on
|
|
1676
|
+
every matching file during the build.
|
|
1677
|
+
|
|
1678
|
+
```ts
|
|
1679
|
+
import { microtags } from 'microtags/esbuild'
|
|
1680
|
+
|
|
1681
|
+
// in your esbuild config
|
|
1682
|
+
plugins: [
|
|
1683
|
+
microtags({
|
|
1684
|
+
cascade: true,
|
|
1685
|
+
keep: ['my-notice'],
|
|
1686
|
+
}),
|
|
1687
|
+
]
|
|
1688
|
+
```
|
|
1689
|
+
|
|
1690
|
+
The plugin accepts the same options as `transform` plus a file `filter`.
|
|
1691
|
+
Warnings are forwarded to esbuild's warning channel.
|
|
1692
|
+
|
|
1693
|
+
---
|
|
1694
|
+
|
|
1695
|
+
### `microtags/vite`
|
|
1696
|
+
|
|
1697
|
+
Node-only Vite plugin, same interface as the
|
|
1698
|
+
[esbuild plugin](#microtagsesbuild).
|
|
1699
|
+
|
|
1700
|
+
```ts
|
|
1701
|
+
import { microtags } from 'microtags/vite'
|
|
1702
|
+
|
|
1703
|
+
// in vite.config.ts
|
|
1704
|
+
plugins: [
|
|
1705
|
+
microtags({
|
|
1706
|
+
cascade: true,
|
|
1707
|
+
keep: ['my-notice'],
|
|
1708
|
+
}),
|
|
1709
|
+
]
|
|
1710
|
+
```
|
|
1711
|
+
|
|
1712
|
+
---
|
|
1713
|
+
|
|
1714
|
+
### `microbuild`
|
|
1715
|
+
|
|
1716
|
+
CLI for stripping `.render()` calls outside a bundler.
|
|
1717
|
+
|
|
1718
|
+
```sh
|
|
1719
|
+
# stdin/stdout -- one file
|
|
1720
|
+
cat src/counter.ts | microbuild --cascade > dist/counter.ts
|
|
1721
|
+
|
|
1722
|
+
# glob mode -- requires --outdir
|
|
1723
|
+
microbuild 'src/**/*.ts' --outdir dist --cascade --keep my-notice
|
|
1724
|
+
```
|
|
1725
|
+
|
|
1726
|
+
`--keep` is repeatable and accepts comma-separated tag names. `--root`
|
|
1727
|
+
sets the base for output paths (defaults to the current directory).
|
|
1728
|
+
Files are never rewritten in place.
|
|
1729
|
+
|
|
1730
|
+
stdin mode transforms one file only. A multi-file app should use the
|
|
1731
|
+
[esbuild](#microtagsesbuild) or [vite](#microtagsvite) plugin, or
|
|
1732
|
+
glob mode.
|
|
1733
|
+
|
|
1734
|
+
---
|
|
1735
|
+
|
|
1736
|
+
### `client:true` vs `--keep`
|
|
1737
|
+
|
|
1738
|
+
Both prevent [`microbuild`](#microbuild) from stripping a component's
|
|
1739
|
+
`.render()` call, but they work at different levels:
|
|
1740
|
+
|
|
1741
|
+
- **`{ client: true }`** is set in source, in the `.render()` options.
|
|
1742
|
+
Use it for components that must render themselves in the browser --
|
|
1743
|
+
for example, a notice inserted dynamically after page load. The
|
|
1744
|
+
decision travels with the component and applies in every build.
|
|
1745
|
+
|
|
1746
|
+
- **`--keep tag-name`** (or the `keep` option in the plugin) is set at
|
|
1747
|
+
build time. Use it to override stripping for specific tags without
|
|
1748
|
+
changing source, for example during development or in a build
|
|
1749
|
+
variant that needs client rendering.
|
|
1750
|
+
|
|
1751
|
+
Both can be combined. A component is kept when either condition is met
|
|
1752
|
+
(union semantics).
|
|
1753
|
+
|
|
1227
1754
|
---
|
|
1228
1755
|
|
|
1229
1756
|
## Differences from `nanotags`
|