microtags 0.0.14 → 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 +554 -20
- 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]
|
|
@@ -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
|
|
@@ -416,6 +437,9 @@ and the submit button is disabled until valid. See `example/subscribe-form.ts`.
|
|
|
416
437
|
See [./example/ssr.ts](./example/ssr.ts) for an example of rendering an HTML
|
|
417
438
|
string and then "hydrating" the page on the client.
|
|
418
439
|
|
|
440
|
+
> [!NOTE]
|
|
441
|
+
> The server must know how to render the HTML that the components depend on.
|
|
442
|
+
|
|
419
443
|
### `.TAG`
|
|
420
444
|
|
|
421
445
|
Each tag name is exposed as a property `.TAG`. You can import the web
|
|
@@ -461,6 +485,12 @@ export function render ():string {
|
|
|
461
485
|
}
|
|
462
486
|
```
|
|
463
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
|
+
|
|
464
494
|
### `.refs`
|
|
465
495
|
|
|
466
496
|
Web components created with `microtags` have a property `.refs`. It is an
|
|
@@ -482,6 +512,24 @@ export function render () {
|
|
|
482
512
|
|
|
483
513
|
---------------------------------------------------------------------
|
|
484
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
|
+
|
|
485
533
|
|
|
486
534
|
## API
|
|
487
535
|
|
|
@@ -504,6 +552,7 @@ The object returned by [`define`](#define). Includes methods:
|
|
|
504
552
|
| `.withProps(fn)` | `ComponentBuilder` | Declare reactive attributes. See [Props](#props). |
|
|
505
553
|
| `.withRefs(fn)` | `ComponentBuilder` | Declare DOM refs resolved once the element connects. See [Refs](#refs-1). |
|
|
506
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). |
|
|
507
556
|
| `.setup(fn)` | `MicrotagElementClass` | Registers the custom element and returns its class. See [`setup`](#setup) below. |
|
|
508
557
|
|
|
509
558
|
Methods `withProps`, `withRefs`, and `withContexts` each return a new builder,
|
|
@@ -517,15 +566,18 @@ Declare reactive attributes via `withProps`. Each prop becomes:
|
|
|
517
566
|
|
|
518
567
|
* An observed HTML attribute (auto-synced via `attributeChangedCallback`)
|
|
519
568
|
* A signal at `ctx.props.propName`
|
|
520
|
-
*
|
|
521
|
-
|
|
569
|
+
* Four built-in coercers turn raw attribute strings into typed values:
|
|
570
|
+
|
|
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` |
|
|
522
577
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
| `p.number()` | `Number(val)` | `0` |
|
|
527
|
-
| `p.boolean()` | `"true"` / `""` → `true`, `"false"` → `false` | `false` |
|
|
528
|
-
| `p.oneOf(opts)` | Picklist enum, throws on invalid | throws |
|
|
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"`.
|
|
529
581
|
|
|
530
582
|
```ts
|
|
531
583
|
.withProps(p => ({
|
|
@@ -715,6 +767,105 @@ define('themed-card')
|
|
|
715
767
|
---------------------------------------------------------
|
|
716
768
|
|
|
717
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
|
+
|
|
718
869
|
### `builder.setup(ctx)`
|
|
719
870
|
|
|
720
871
|
Registers the custom element via `customElements.define` and returns the
|
|
@@ -736,6 +887,14 @@ class ComponentBuilder {
|
|
|
736
887
|
The callback function runs once per element instance, and is called with the
|
|
737
888
|
following `context` object.
|
|
738
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
|
+
|
|
739
898
|
|
|
740
899
|
#### `ctx`
|
|
741
900
|
|
|
@@ -750,11 +909,11 @@ ctx:SetupContext<Props, RefsMap<RefDefs>, ContextsMap<CtxDefs>>
|
|
|
750
909
|
| Method | Returns | Description |
|
|
751
910
|
|--------|---------|-------------|
|
|
752
911
|
| `.peek(signal)` | `T` | Read a signal's current value without subscribing, even inside `ctx.effect`. |
|
|
753
|
-
| `.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). |
|
|
754
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. |
|
|
755
|
-
| `.effect(fn)` | `void` | Run an alien-signals effect that auto-tracks the signals it reads. Auto-disposed on
|
|
756
|
-
| `.bind(signal, element, options?)` | `void` | Bind a signal to a DOM element property, auto-cleaned on
|
|
757
|
-
| `.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. |
|
|
758
917
|
| `.consume(token)` | `T \| undefined` | Read an optional context value published by an ancestor. See [`withContexts`](#withcontexts). |
|
|
759
918
|
|
|
760
919
|
See [Typed Events](#typed-events) for more about `.on` and `.emit`.
|
|
@@ -798,6 +957,36 @@ ctx.bind(() => `${count()} items`, ctx.refs.label, { prop: 'textContent' })
|
|
|
798
957
|
|
|
799
958
|
---
|
|
800
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
|
+
|
|
801
990
|
|
|
802
991
|
## Missing refs
|
|
803
992
|
|
|
@@ -1217,6 +1406,351 @@ type Attrs = Record<
|
|
|
1217
1406
|
function toAttributes (attrs:Attrs):string
|
|
1218
1407
|
```
|
|
1219
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
|
+
|
|
1220
1754
|
---
|
|
1221
1755
|
|
|
1222
1756
|
## Differences from `nanotags`
|