microtags 0.0.3 → 0.0.8
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 +504 -50
- package/dist/{chunk-E7ZGOHL3.js → chunk-P4QHFKTJ.js} +18 -2
- package/dist/chunk-P4QHFKTJ.js.map +7 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +2 -2
- package/dist/index.js +9 -1
- 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 +20 -18
- package/dist/render.cjs.map +2 -2
- package/dist/render.d.ts +4 -6
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js.map +2 -2
- package/dist/render.min.js +1 -1
- package/dist/render.min.js.map +2 -2
- package/dist/setup-context.d.ts +2 -2
- package/dist/setup-context.d.ts.map +1 -1
- package/dist/types.d.ts +11 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/util.cjs +17 -1
- package/dist/util.cjs.map +2 -2
- package/dist/util.d.ts +10 -2
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +5 -3
- package/dist/util.min.js +1 -1
- package/dist/util.min.js.map +3 -3
- package/package.json +1 -1
- package/dist/chunk-E7ZGOHL3.js.map +0 -7
package/README.md
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
[](https://bundlephobia.com/package/microtags)
|
|
10
10
|
[](LICENSE)
|
|
11
11
|
|
|
12
|
-
Web component helper with reactive props, automatic cleanup,
|
|
13
|
-
|
|
14
|
-
[alien-signals](https://github.com/stackblitz/alien-signals).
|
|
12
|
+
Web component helper with reactive props, automatic cleanup, and no
|
|
13
|
+
[Shadow DOM](https://gomakethings.com/the-shadow-dom-is-an-antipattern/).
|
|
14
|
+
Reactive via [alien-signals](https://github.com/stackblitz/alien-signals).
|
|
15
15
|
|
|
16
|
-
**Size
|
|
16
|
+
**Size: 3.41 KB** with all dependencies, minified and brotlied
|
|
17
17
|
|
|
18
18
|
Inspired by [nanotags](https://nanotags.psdcoder.dev/). Reactive props use
|
|
19
19
|
**alien-signals** instead of nanostores atoms.
|
|
@@ -36,14 +36,28 @@ Inspired by [nanotags](https://nanotags.psdcoder.dev/). Reactive props use
|
|
|
36
36
|
* [Refs](#refs)
|
|
37
37
|
+ [`withRefs`](#withrefs)
|
|
38
38
|
+ [Element types](#element-types)
|
|
39
|
-
* [define](#define)
|
|
40
|
-
* [withContexts](#withcontexts)
|
|
41
|
-
* [setup and ctx](#setup-and-ctx)
|
|
39
|
+
* [`define`](#define)
|
|
40
|
+
* [`withContexts`](#withcontexts)
|
|
41
|
+
* [`setup` and `ctx`](#setup-and-ctx)
|
|
42
42
|
+ [peek and untracked](#peek-and-untracked)
|
|
43
|
-
- [
|
|
44
|
-
|
|
45
|
-
* [microtags/
|
|
46
|
-
|
|
43
|
+
- [Templates](#templates)
|
|
44
|
+
- [Subpath Exports](#subpath-exports)
|
|
45
|
+
* [`microtags/context`](#microtagscontext)
|
|
46
|
+
* [`microtags/render`](#microtagsrender)
|
|
47
|
+
+ [`renderList`](#renderlist)
|
|
48
|
+
+ [`render`](#render)
|
|
49
|
+
* [`microtags/util`](#microtagsutil)
|
|
50
|
+
+ [Coercion](#coercion)
|
|
51
|
+
+ [`runCleanups`](#runcleanups)
|
|
52
|
+
+ [`toAttributes`](#toattributes)
|
|
53
|
+
- [Divergence from `nanotags`](#divergence-from-nanotags)
|
|
54
|
+
* [Reactivity primitive](#reactivity-primitive)
|
|
55
|
+
* [Effects + Subscriptions](#effects--subscriptions)
|
|
56
|
+
* [Refs: `r.all` instead of `r.many`](#refs-rall-instead-of-rmany)
|
|
57
|
+
* [Context API](#context-api)
|
|
58
|
+
* [Props](#props-1)
|
|
59
|
+
* [Not ported](#not-ported)
|
|
60
|
+
* [Additions / New Features](#additions--new-features)
|
|
47
61
|
|
|
48
62
|
<!-- tocstop -->
|
|
49
63
|
|
|
@@ -317,7 +331,6 @@ Declare reactive attributes via `withProps`. Each prop becomes:
|
|
|
317
331
|
| `p.boolean()` | `"true"` / `""` → `true`, `"false"` → `false` | `false` |
|
|
318
332
|
| `p.oneOf(opts)` | Picklist enum, throws on invalid | throws |
|
|
319
333
|
|
|
320
|
-
|
|
321
334
|
#### `withProps`
|
|
322
335
|
|
|
323
336
|
```ts
|
|
@@ -331,6 +344,41 @@ Declare reactive attributes via `withProps`. Each prop becomes:
|
|
|
331
344
|
}))
|
|
332
345
|
```
|
|
333
346
|
|
|
347
|
+
##### `p.json`
|
|
348
|
+
|
|
349
|
+
`p.json<T>()` -- a prop whose attribute value is parsed with
|
|
350
|
+
`JSON.parse`. The type parameter `T` annotates the parsed result; it is a
|
|
351
|
+
type only, so the value is not validated at runtime. The resulting signal is
|
|
352
|
+
typed `T | undefined`.
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
.withProps(p => ({
|
|
356
|
+
user: p.json<{ id:number; name:string }>(),
|
|
357
|
+
}))
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
```html
|
|
361
|
+
<my-el user='{"id":1,"name":"Alice"}'></my-el>
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Like the string/number/boolean validators it is attribute-backed and
|
|
365
|
+
observed, so editing the attribute re-parses and pushes the new value to
|
|
366
|
+
`ctx.props.user`. It falls back to `undefined` in two cases: when the
|
|
367
|
+
attribute is absent, and when the value is not valid JSON. Parse errors are
|
|
368
|
+
swallowed rather than thrown.
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
// check for `undefined` case
|
|
372
|
+
ctx.effect(() => {
|
|
373
|
+
const user = ctx.props.user()
|
|
374
|
+
if (!user) return
|
|
375
|
+
ctx.refs.name.textContent = user.name
|
|
376
|
+
})
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
For input you want validated rather than only parsed, pass a Standard Schema
|
|
380
|
+
to [`p.schema`](#withprops) instead.
|
|
381
|
+
|
|
334
382
|
Inside `setup()`, every prop is a callable signal:
|
|
335
383
|
|
|
336
384
|
```ts
|
|
@@ -338,6 +386,7 @@ ctx.props.count() // read
|
|
|
338
386
|
ctx.props.count(42) // write
|
|
339
387
|
```
|
|
340
388
|
|
|
389
|
+
---
|
|
341
390
|
|
|
342
391
|
### Refs
|
|
343
392
|
|
|
@@ -356,8 +405,8 @@ it instead. Queries are resolved relative to the host element, during
|
|
|
356
405
|
|
|
357
406
|
```ts
|
|
358
407
|
.withRefs(r => ({
|
|
359
|
-
button: r.one(), // resolves [data-ref="button"]
|
|
360
|
-
items: r.all(), // resolves all [data-ref="items"]
|
|
408
|
+
button: r.one(), // resolves [data-ref="button"] (throws if missing)
|
|
409
|
+
items: r.all(), // resolves all [data-ref="items"] (empty array if none)
|
|
361
410
|
}))
|
|
362
411
|
```
|
|
363
412
|
|
|
@@ -407,46 +456,83 @@ constrained to `HTMLElement`, so cast at the use site if you need one
|
|
|
407
456
|
(e.g. `ctx.refs.icon as unknown as SVGSVGElement`).
|
|
408
457
|
|
|
409
458
|
|
|
410
|
-
### define
|
|
459
|
+
### `define`
|
|
411
460
|
|
|
412
461
|
```ts
|
|
413
462
|
import { define } from 'microtags'
|
|
414
463
|
|
|
415
|
-
define(tagName:
|
|
464
|
+
define(tagName:string):ComponentBuilder
|
|
416
465
|
```
|
|
417
466
|
|
|
418
467
|
Returns a fluent builder. Call `.setup()` to register the custom element.
|
|
419
468
|
All builder methods are chainable and fully type-inferred.
|
|
420
469
|
|
|
421
|
-
### withContexts
|
|
470
|
+
### `withContexts`
|
|
471
|
+
|
|
472
|
+
Context is for the case where a parent component defines some state, e.g. a
|
|
473
|
+
`theme`, and the top-level (application) knows nothing about the theme. But the
|
|
474
|
+
top level application does determine what children the parent renders
|
|
475
|
+
(it passes in children), and the children need to know the theme.
|
|
476
|
+
|
|
477
|
+
In that case, there is no way for the "parent" component to pass the theme to
|
|
478
|
+
the children, and the root level (application) doesn't know what the theme is.
|
|
479
|
+
|
|
480
|
+
Context gives us a "pull" based way to model this. The context-provider
|
|
481
|
+
publishes the value, and any descendants opt in (or pull values) with
|
|
482
|
+
`withContexts`, and the unknown-consumer and late-arrival problems are handled.
|
|
483
|
+
|
|
484
|
+
>
|
|
485
|
+
> [!IMPORTANT]
|
|
486
|
+
> Context is only relevant on the client-side.
|
|
487
|
+
> Context gives you nothing for first paint or the no-JS case.
|
|
488
|
+
>
|
|
489
|
+
|
|
422
490
|
|
|
423
491
|
```ts
|
|
424
|
-
import {
|
|
492
|
+
import { define } from 'microtags'
|
|
493
|
+
import { createContext, provide } from 'microtags/context'
|
|
425
494
|
|
|
426
495
|
const ThemeToken = createContext<string>()
|
|
427
496
|
|
|
428
|
-
|
|
497
|
+
// Parent: owns the theme and publishes it to its descendants. It does
|
|
498
|
+
// not reference the children below. The application composes those in.
|
|
499
|
+
define('theme-provider')
|
|
500
|
+
.setup(ctx => {
|
|
501
|
+
ctx.onCleanup(provide(ctx.host, ThemeToken, 'dark'))
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
// Child: opts in to the theme and reads whichever provider it is
|
|
505
|
+
// composed into. It never receives the value as a prop.
|
|
506
|
+
define('themed-card')
|
|
429
507
|
.withContexts(() => ({
|
|
430
508
|
theme: ThemeToken,
|
|
431
509
|
}))
|
|
432
510
|
.setup(ctx => {
|
|
433
511
|
ctx.effect(() => {
|
|
434
|
-
|
|
512
|
+
ctx.host.dataset.theme = ctx.contexts.theme
|
|
435
513
|
})
|
|
436
514
|
})
|
|
437
515
|
```
|
|
438
516
|
|
|
517
|
+
```html
|
|
518
|
+
<!-- The application writes this markup. It nests <themed-card> inside
|
|
519
|
+
<theme-provider> but never sets the theme itself. -->
|
|
520
|
+
<theme-provider>
|
|
521
|
+
<themed-card>Card content</themed-card>
|
|
522
|
+
</theme-provider>
|
|
523
|
+
```
|
|
524
|
+
|
|
439
525
|
`setup()` is deferred until all required contexts resolve. See
|
|
440
|
-
[microtags/context](#microtagscontext) for the provider API.
|
|
526
|
+
[`microtags/context`](#microtagscontext) for the provider API.
|
|
441
527
|
|
|
442
|
-
### setup and ctx
|
|
528
|
+
### `setup` and `ctx`
|
|
443
529
|
|
|
444
530
|
```ts
|
|
445
531
|
.setup(ctx => {
|
|
446
|
-
// ctx.host
|
|
447
|
-
// ctx.props
|
|
448
|
-
// ctx.refs
|
|
449
|
-
// ctx.contexts
|
|
532
|
+
// ctx.host the HTMLElement itself
|
|
533
|
+
// ctx.props callable signals for each declared prop
|
|
534
|
+
// ctx.refs resolved DOM refs
|
|
535
|
+
// ctx.contexts consumed context values (reactive)
|
|
450
536
|
|
|
451
537
|
// Run a tracked effect; auto-disposed on disconnect.
|
|
452
538
|
ctx.effect(() => {
|
|
@@ -459,6 +545,11 @@ define('my-child')
|
|
|
459
545
|
// Add an event listener; auto-removed on disconnect.
|
|
460
546
|
ctx.on(el, 'click', handler)
|
|
461
547
|
|
|
548
|
+
// Dispatch an event from the host. Pass a name (with optional
|
|
549
|
+
// detail/options) for a bubbling CustomEvent, or a pre-built
|
|
550
|
+
// Event. Returns dispatchEvent's result.
|
|
551
|
+
ctx.emit('change', { value: 42 })
|
|
552
|
+
|
|
462
553
|
// Read a signal without creating a subscription.
|
|
463
554
|
const snap = ctx.peek(ctx.props.count)
|
|
464
555
|
|
|
@@ -486,47 +577,341 @@ ctx.effect(() => {
|
|
|
486
577
|
})
|
|
487
578
|
```
|
|
488
579
|
|
|
489
|
-
##
|
|
580
|
+
## Templates
|
|
581
|
+
|
|
582
|
+
[`render` and `renderList`](#microtagsrender) both take a `<template>`
|
|
583
|
+
element as their second argument. A
|
|
584
|
+
[`<template>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/template)
|
|
585
|
+
is a native HTML element that holds markup the browser parses but does not
|
|
586
|
+
render. Its contents are inert. Nothing inside paints, scripts do not run,
|
|
587
|
+
images and other resources do not load, and the nodes stay out of the
|
|
588
|
+
document until you clone them.
|
|
589
|
+
|
|
590
|
+
See [./example/ssr.ts, line 89](./example/ssr.ts#L89).
|
|
591
|
+
|
|
592
|
+
You write the markup for an item inside the template, and the
|
|
593
|
+
reconciler stamps out a copy per item by cloning `template.content`. The
|
|
594
|
+
template's first element child is the unit that gets cloned, so give each
|
|
595
|
+
template a single root element.
|
|
596
|
+
|
|
597
|
+
```html
|
|
598
|
+
<item-list>
|
|
599
|
+
<ul></ul>
|
|
600
|
+
<template>
|
|
601
|
+
<li>
|
|
602
|
+
<span class="name"></span>
|
|
603
|
+
<span class="count"></span>
|
|
604
|
+
</li>
|
|
605
|
+
</template>
|
|
606
|
+
</item-list>
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
The component resolves the list and the template as refs, then calls
|
|
610
|
+
`renderList` from inside an effect so the list re-renders whenever the
|
|
611
|
+
data signal changes.
|
|
612
|
+
|
|
613
|
+
```ts
|
|
614
|
+
import { define } from 'microtags'
|
|
615
|
+
import { signal } from 'alien-signals'
|
|
616
|
+
import { renderList } from 'microtags/render'
|
|
617
|
+
|
|
618
|
+
type Item = { id:string; name:string; count:number }
|
|
619
|
+
|
|
620
|
+
define('item-list')
|
|
621
|
+
.withRefs(r => ({
|
|
622
|
+
list: r.one('ul'),
|
|
623
|
+
row: r.one('template'), // typed as HTMLTemplateElement
|
|
624
|
+
}))
|
|
625
|
+
.setup(ctx => {
|
|
626
|
+
const items = signal<Item[]>([
|
|
627
|
+
{ id: 'a', name: 'Alice', count: 1 },
|
|
628
|
+
{ id: 'b', name: 'Bob', count: 2 },
|
|
629
|
+
])
|
|
630
|
+
|
|
631
|
+
// Re-runs on every items() change; only changed rows are touched.
|
|
632
|
+
ctx.effect(() => {
|
|
633
|
+
renderList(ctx.refs.list, ctx.refs.row, {
|
|
634
|
+
data: items(),
|
|
635
|
+
key: item => item.id,
|
|
636
|
+
update: (el, item) => {
|
|
637
|
+
el.querySelector('.name')!.textContent = item.name
|
|
638
|
+
el.querySelector('.count')!.textContent =
|
|
639
|
+
String(item.count)
|
|
640
|
+
},
|
|
641
|
+
})
|
|
642
|
+
})
|
|
643
|
+
})
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
Because the template lives in your HTML instead of a JavaScript string, each
|
|
647
|
+
render clones real DOM nodes rather than re-parsing HTML, and the `update`
|
|
648
|
+
callback function mutates the DOM through properties like `textContent`.
|
|
649
|
+
Nothing concatenates strings into markup, so there is no string-injection
|
|
650
|
+
XSS vector.
|
|
651
|
+
|
|
652
|
+
## Subpath Exports
|
|
653
|
+
|
|
654
|
+
### `microtags/context`
|
|
490
655
|
|
|
491
|
-
|
|
656
|
+
Cross-component context backed by alien-signals. A provider shares a value
|
|
657
|
+
with everything nested inside it; a consumer reads the value from its nearest
|
|
658
|
+
provider ancestor and reacts when it changes.
|
|
492
659
|
|
|
493
|
-
|
|
660
|
+
`provide` seeds a signal the first time it runs and updates that same signal
|
|
661
|
+
on later calls, so re-providing inside an effect pushes changes to consumers.
|
|
494
662
|
|
|
495
663
|
```ts
|
|
496
|
-
import {
|
|
664
|
+
import { define } from 'microtags'
|
|
665
|
+
import { signal } from 'alien-signals'
|
|
666
|
+
import { createContext, provide } from 'microtags/context'
|
|
497
667
|
|
|
498
668
|
const ThemeToken = createContext<string>()
|
|
499
669
|
|
|
500
|
-
//
|
|
501
|
-
|
|
502
|
-
|
|
670
|
+
// Provider: owns the theme and shares it with its descendants.
|
|
671
|
+
define('theme-provider')
|
|
672
|
+
.withRefs(r => ({
|
|
673
|
+
toggle: r.one('button'),
|
|
674
|
+
}))
|
|
675
|
+
.setup(ctx => {
|
|
676
|
+
const theme = signal('light')
|
|
677
|
+
|
|
678
|
+
// Share with descendants; remove the entry on disconnect.
|
|
679
|
+
const cleanUp = provide(ctx.host, ThemeToken, theme())
|
|
680
|
+
ctx.onCleanup(cleanup)
|
|
681
|
+
|
|
682
|
+
// Re-provide on change. provide() updates the existing signal,
|
|
683
|
+
// so consumers that read this value re-run.
|
|
684
|
+
ctx.effect(() => provide(ctx.host, ThemeToken, theme()))
|
|
685
|
+
|
|
686
|
+
ctx.on(ctx.refs.toggle, 'click', () => {
|
|
687
|
+
theme(theme() === 'light' ? 'dark' : 'light')
|
|
688
|
+
})
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
// Consumer: reads the nearest provider's theme, reactively.
|
|
692
|
+
define('themed-card')
|
|
693
|
+
.withContexts(() => ({
|
|
694
|
+
theme: ThemeToken,
|
|
695
|
+
}))
|
|
696
|
+
.setup(ctx => {
|
|
697
|
+
// Reading ctx.contexts.theme inside an effect subscribes to it,
|
|
698
|
+
// so this re-runs whenever the provider toggles the value.
|
|
699
|
+
ctx.effect(() => {
|
|
700
|
+
ctx.host.dataset.theme = ctx.contexts.theme
|
|
701
|
+
})
|
|
702
|
+
})
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
```html
|
|
706
|
+
<theme-provider>
|
|
707
|
+
<button>Toggle theme</button>
|
|
708
|
+
<themed-card>Card A</themed-card>
|
|
709
|
+
<themed-card>Card B</themed-card>
|
|
710
|
+
</theme-provider>
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
Both cards resolve to the same nearest `<theme-provider>`, so one click
|
|
714
|
+
updates both. `resolveContextSignal(element, token)` is the low-level lookup
|
|
715
|
+
those reads use under the hood; reach for it only when you need the provider
|
|
716
|
+
signal imperatively.
|
|
717
|
+
|
|
718
|
+
---
|
|
719
|
+
|
|
720
|
+
### `microtags/render`
|
|
721
|
+
|
|
722
|
+
A keyed reconciler for rendering lists and single items from a
|
|
723
|
+
`<template>`, with no virtual DOM and no diffing library.
|
|
724
|
+
|
|
725
|
+
Microtags' state is reactive. When a signal changes, you want the DOM to follow.
|
|
726
|
+
A way to do that for a list is to rebuild the markup on every change,
|
|
727
|
+
e.g. `container.innerHTML = items.map(toHTML).join('')`.
|
|
503
728
|
|
|
504
|
-
|
|
505
|
-
|
|
729
|
+
But rebuilding the markup on every change
|
|
730
|
+
**throws away and recreates every node on every update**, losing any DOM
|
|
731
|
+
state, like focus, text selection, scroll position, and the internal state of
|
|
732
|
+
elements like `<video>`, `<details>`, and `<iframe>`. It also re-parses HTML on
|
|
733
|
+
every pass, which creates a vector for string-concatenation XSS.
|
|
734
|
+
|
|
735
|
+
**This library exports a `renderList` and a `render` function**.
|
|
736
|
+
They diff against the live DOM. See
|
|
737
|
+
[example/render-demo.ts](./example/render-demo.ts) for a working example.
|
|
738
|
+
|
|
739
|
+
#### `renderList`
|
|
740
|
+
|
|
741
|
+
Each item is matched to an existing element by its `key` attribute, so nodes
|
|
742
|
+
are reused across renders rather than being recreated. Nodes whose keys
|
|
743
|
+
disappear from the data are removed, surviving nodes are moved into place
|
|
744
|
+
with the fewest DOM operations, and your `update` callback runs only for
|
|
745
|
+
items whose value actually changed (compared by identity). Focus state and other
|
|
746
|
+
DOM state survives a re-render, and changing one row touches only that row.
|
|
747
|
+
|
|
748
|
+
```ts
|
|
749
|
+
function renderList<T, E extends Element = Element> (
|
|
750
|
+
container:Element,
|
|
751
|
+
template:HTMLTemplateElement,
|
|
752
|
+
options:{
|
|
753
|
+
data:readonly T[];
|
|
754
|
+
key:(item:T, index:number) => string|number;
|
|
755
|
+
update:(el:E, item:T) => void;
|
|
756
|
+
}
|
|
757
|
+
):void
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
##### `renderList` Example
|
|
761
|
+
|
|
762
|
+
The markup is the list container plus a `<template>` for one row:
|
|
763
|
+
|
|
764
|
+
```html
|
|
765
|
+
<ul></ul>
|
|
766
|
+
<template>
|
|
767
|
+
<li>
|
|
768
|
+
<span class="name"></span>
|
|
769
|
+
</li>
|
|
770
|
+
</template>
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
Call `renderList` from inside an effect so it re-runs whenever the data
|
|
774
|
+
signal changes:
|
|
775
|
+
|
|
776
|
+
```ts
|
|
777
|
+
ctx.effect(() => {
|
|
778
|
+
renderList(ctx.refs.list, ctx.refs.row, {
|
|
779
|
+
data: items(),
|
|
780
|
+
key: item => item.id,
|
|
781
|
+
update: (el, item) => {
|
|
782
|
+
el.querySelector('.name')!.textContent = item.name
|
|
783
|
+
},
|
|
784
|
+
})
|
|
785
|
+
})
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
#### `render`
|
|
789
|
+
|
|
790
|
+
`render` is the same machinery for a single item. It keys on the template, so
|
|
791
|
+
repeated calls update the same element in place instead of replacing it.
|
|
792
|
+
|
|
793
|
+
```ts
|
|
794
|
+
function render<T, E extends Element = Element> (
|
|
795
|
+
container:Element,
|
|
796
|
+
template:HTMLTemplateElement,
|
|
797
|
+
options?:{
|
|
798
|
+
data?:T;
|
|
799
|
+
update?:(el:E, item:T) => void;
|
|
800
|
+
}
|
|
801
|
+
):void
|
|
506
802
|
```
|
|
507
803
|
|
|
508
|
-
|
|
804
|
+
##### `render` Example
|
|
509
805
|
|
|
510
|
-
|
|
806
|
+
The markup is a container plus a single-item `<template>`:
|
|
807
|
+
|
|
808
|
+
```html
|
|
809
|
+
<div class="item"></div>
|
|
810
|
+
<template>
|
|
811
|
+
<div>
|
|
812
|
+
<span></span>
|
|
813
|
+
</div>
|
|
814
|
+
</template>
|
|
815
|
+
```
|
|
511
816
|
|
|
512
817
|
```ts
|
|
513
|
-
import { render
|
|
818
|
+
import { render } from 'microtags/render'
|
|
514
819
|
|
|
515
|
-
// Render a single item into
|
|
820
|
+
// Render a single item into the container from the <template>.
|
|
516
821
|
render(container, template, {
|
|
517
822
|
data: item,
|
|
518
|
-
update: (el, item) => {
|
|
823
|
+
update: (el, item) => {
|
|
824
|
+
el.querySelector('span')!.textContent = item.name
|
|
825
|
+
},
|
|
519
826
|
})
|
|
827
|
+
```
|
|
520
828
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
829
|
+
For a runnable version, see `example/render-demo.ts`. It keeps an editable
|
|
830
|
+
`<input>` in every row and a single item panel rendered with `render`, then
|
|
831
|
+
re-renders on a timer and reorders on demand to show that focus, caret, and
|
|
832
|
+
typed text survive: keyed nodes are reused and at most moved, never rebuilt.
|
|
833
|
+
|
|
834
|
+
---
|
|
835
|
+
|
|
836
|
+
### `microtags/util`
|
|
837
|
+
|
|
838
|
+
Helpers for reading raw attribute strings and running cleanup
|
|
839
|
+
functions. `withProps` uses these internally to coerce attribute values.
|
|
840
|
+
They are exported for when you need to read an attribute outside of the
|
|
841
|
+
prop layer.
|
|
842
|
+
|
|
843
|
+
```ts
|
|
844
|
+
import {
|
|
845
|
+
coerceNumber,
|
|
846
|
+
coerceString,
|
|
847
|
+
coerceBoolean,
|
|
848
|
+
coerceJson,
|
|
849
|
+
runCleanups,
|
|
850
|
+
} from 'microtags/util'
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
#### Coercion
|
|
854
|
+
|
|
855
|
+
Each coercion helper takes the raw `getAttribute` result -- a `string` or
|
|
856
|
+
`null` -- and returns a typed value. None of them throw.
|
|
857
|
+
|
|
858
|
+
```ts
|
|
859
|
+
function coerceNumber (raw:string | null):number
|
|
860
|
+
function coerceString (raw:string | null):string
|
|
861
|
+
function coerceBoolean (raw:string | null):boolean
|
|
862
|
+
function coerceJson<T = unknown> (raw:string | null):T | undefined
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
- `coerceNumber` -- `Number(raw)`, or `NaN` when the attribute is absent
|
|
866
|
+
or cannot be parsed.
|
|
867
|
+
- `coerceString` -- the value as-is; a missing attribute (`null`) becomes
|
|
868
|
+
the empty string `''`.
|
|
869
|
+
- `coerceBoolean` -- presence-based, matching the HTML boolean-attribute
|
|
870
|
+
convention: absent (`null`) is `false`, present is `true`, including the
|
|
871
|
+
empty string from `disabled=""`.
|
|
872
|
+
- `coerceJson` -- `JSON.parse(raw)`, or `undefined` when the attribute is
|
|
873
|
+
absent or not valid JSON. Pass a type parameter to annotate the result.
|
|
874
|
+
|
|
875
|
+
#### `runCleanups`
|
|
876
|
+
|
|
877
|
+
Runs every function in the array, even if some throw. After all have run
|
|
878
|
+
it re-throws the first error encountered, so one failing teardown cannot
|
|
879
|
+
skip the rest. A `Cleanup` is a no-argument teardown function, `() => void`.
|
|
880
|
+
|
|
881
|
+
```ts
|
|
882
|
+
function runCleanups (fns:Cleanup[]):void
|
|
527
883
|
```
|
|
528
884
|
|
|
529
|
-
|
|
885
|
+
#### `toAttributes`
|
|
886
|
+
|
|
887
|
+
Transform an object into an HTML attributes string. The object should be
|
|
888
|
+
like `{ attributeName: value }`. Handles boolean values by adding a string, eg
|
|
889
|
+
`{ disabled: true }` = `<button disabled>`. Arrays are converted to a list in
|
|
890
|
+
quotes, eg `{ class: ['abc', 'def'] }` = `<div class="abc def">`
|
|
891
|
+
|
|
892
|
+
```ts
|
|
893
|
+
type Attrs = Record<
|
|
894
|
+
string,
|
|
895
|
+
undefined|null|string|number|boolean|(string|number)[]
|
|
896
|
+
>
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Transform an object into an HTML attributes string. The object should be
|
|
900
|
+
* like `{ attributeName: value }`.
|
|
901
|
+
*
|
|
902
|
+
* @param {Attrs} attrs An object for the attributes.
|
|
903
|
+
* @returns {string} A string suitable for use as HTML attributes.
|
|
904
|
+
*/
|
|
905
|
+
function toAttributes (attrs:Attrs):string
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
---
|
|
909
|
+
|
|
910
|
+
## Divergence from `nanotags`
|
|
911
|
+
|
|
912
|
+
### Reactivity primitive
|
|
913
|
+
|
|
914
|
+
`nanotags` uses `nanostores` atoms; `microtags` uses `alien-signals`.
|
|
530
915
|
|
|
531
916
|
| nanotags (nanostores) | microtags (alien-signals) |
|
|
532
917
|
|---|---|
|
|
@@ -535,9 +920,78 @@ renderList(container, template, {
|
|
|
535
920
|
| `$count.set(1)` | `count(1)` |
|
|
536
921
|
| `ctx.props.$count` (store) | `ctx.props.count` (callable signal) |
|
|
537
922
|
|
|
538
|
-
Props declared with `withProps` are
|
|
539
|
-
|
|
923
|
+
Props declared with `withProps` are signals. There is no `$` prefix convention;
|
|
924
|
+
all prop names are plain identifiers.
|
|
540
925
|
|
|
926
|
+
### Effects + Subscriptions
|
|
927
|
+
|
|
928
|
+
In `nanotags`, `ctx.effect` takes the store(s) to watch
|
|
929
|
+
explicitly and hands the value to the callback:
|
|
930
|
+
|
|
931
|
+
```ts
|
|
932
|
+
// nanotags
|
|
933
|
+
ctx.effect(ctx.props.$count, count => {
|
|
934
|
+
ctx.refs.display.textContent = String(count)
|
|
935
|
+
})
|
|
541
936
|
```
|
|
542
|
-
|
|
937
|
+
|
|
938
|
+
In `microtags`, `ctx.effect` takes a zero-argument function and tracks whichever
|
|
939
|
+
signals are read inside it (the `alien-signals` model):
|
|
940
|
+
|
|
941
|
+
```ts
|
|
942
|
+
// microtags
|
|
943
|
+
ctx.effect(() => {
|
|
944
|
+
ctx.refs.display.textContent = String(ctx.props.count())
|
|
945
|
+
})
|
|
543
946
|
```
|
|
947
|
+
|
|
948
|
+
The read operation in `ctx.effect` subscribes to the signal, and runs on
|
|
949
|
+
any signal change. A read inside an effect that you do *not* want to track has
|
|
950
|
+
to go through [`peek` or `untracked`](#peek-and-untracked).
|
|
951
|
+
In nanotags `.get()` never subscribes, so there is no equivalent.
|
|
952
|
+
|
|
953
|
+
### Refs: `r.all` instead of `r.many`
|
|
954
|
+
|
|
955
|
+
The multi-element ref is `r.all()` in `microtags` and `r.many()` in `nanotags`.
|
|
956
|
+
The empty-match behavior also differs: `r.all()` returns an empty array when
|
|
957
|
+
nothing matches, whereas nanotags' `r.many()` throws. `r.one()` throws on a
|
|
958
|
+
missing element in both.
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
### Context API
|
|
962
|
+
|
|
963
|
+
The context API is shaped differently. See
|
|
964
|
+
[`microtags/context`](#microtagscontext).
|
|
965
|
+
|
|
966
|
+
| nanotags | microtags |
|
|
967
|
+
|---|---|
|
|
968
|
+
| `createContext<T>('name')` | `createContext<T>()`, no name |
|
|
969
|
+
| `token.provide(ctx, value)` | `provide(ctx.host, token, value)`, standalone, returns a cleanup |
|
|
970
|
+
| `withContexts({ theme: token })` | `withContexts(() => ({ theme: token }))`, a function |
|
|
971
|
+
| `token.consume(ctx, cb)` | `ctx.consume(token)`, returns value-or-`undefined` |
|
|
972
|
+
|
|
973
|
+
### Props
|
|
974
|
+
|
|
975
|
+
| nanotags | microtags |
|
|
976
|
+
|---|---|
|
|
977
|
+
| `p.json(schema, default)`, hydrated from a `<script type="application/json">` tag, not an attribute | `p.json<T>()`, type-only, parsed from the observed attribute |
|
|
978
|
+
| custom schema passed directly as the prop def | wrap it in `p.schema(validator)` |
|
|
979
|
+
| property-only via `{ schema, attribute: false }` | `p.prop(initial)` |
|
|
980
|
+
| nullable fallback `p.string(null)` | no null-fallback argument; a failed `p.schema` falls back to `undefined` |
|
|
981
|
+
|
|
982
|
+
### Not ported
|
|
983
|
+
|
|
984
|
+
These nanotags APIs have no microtags equivalent:
|
|
985
|
+
|
|
986
|
+
- `ctx.getElement` / `ctx.getElements` -- declare refs with `withRefs`, or
|
|
987
|
+
call `ctx.host.querySelector` directly.
|
|
988
|
+
- The `setup` mixin return value -- `setup` returns `void`; it does not
|
|
989
|
+
assign members back onto the element.
|
|
990
|
+
- The `define(name, setupFn)` two-argument shorthand -- always use the
|
|
991
|
+
builder chain ending in `.setup()`.
|
|
992
|
+
|
|
993
|
+
### Additions / New Features
|
|
994
|
+
|
|
995
|
+
* Serverside-compatible API -- see [serverside example](#serverside-example) --
|
|
996
|
+
we expose `.TAG` and `.refs` on your component, which can be imported in a
|
|
997
|
+
server environment.
|