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 CHANGED
@@ -9,11 +9,11 @@
9
9
  [![gzip size](https://img.shields.io/bundlephobia/minzip/microtags?style=flat-square)](https://bundlephobia.com/package/microtags)
10
10
  [![license](https://img.shields.io/badge/license-Big_Time-blue?style=flat-square)](LICENSE)
11
11
 
12
- Web component helper with reactive props, automatic cleanup,
13
- and no Shadow DOM. Reactive via
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**: 3.41 kB with all dependencies, minified and brotlied
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
- - [Subpaths](#subpaths)
44
- * [microtags/context](#microtagscontext)
45
- * [microtags/render](#microtagsrender)
46
- - [Divergence from nanotags](#divergence-from-nanotags)
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"` / `""` &rarr; `true`, `"false"` &rarr; `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"] — throws if missing
360
- items: r.all(), // resolves all [data-ref="items"] — empty array if none
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: string): ComponentBuilder
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 { createContext } from 'microtags/context'
492
+ import { define } from 'microtags'
493
+ import { createContext, provide } from 'microtags/context'
425
494
 
426
495
  const ThemeToken = createContext<string>()
427
496
 
428
- define('my-child')
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
- console.log('theme is', ctx.contexts.theme)
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 — the HTMLElement itself
447
- // ctx.props — callable signals for each declared prop
448
- // ctx.refs — resolved DOM refs
449
- // ctx.contexts — consumed context values (reactive)
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
- ## Subpaths
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
- ### microtags/context
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
- Cross-component context backed by alien-signals.
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 { createContext, provide, resolveContextSignal } from 'microtags/context'
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
- // In a provider component's setup():
501
- const cleanup = provide(ctx.host, ThemeToken, 'dark')
502
- ctx.onCleanup(cleanup)
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
- // In a consumer component via withContexts:
505
- ctx.contexts.theme // 'dark' (reactive — re-runs effects when value changes)
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
- ### microtags/render
804
+ ##### `render` Example
509
805
 
510
- DOM diffing helpers for rendering lists and single items without a virtual DOM.
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, renderList } from 'microtags/render'
818
+ import { render } from 'microtags/render'
514
819
 
515
- // Render a single item into a container from a <template>.
820
+ // Render a single item into the container from the <template>.
516
821
  render(container, template, {
517
822
  data: item,
518
- update: (el, item) => { el.querySelector('span')!.textContent = item.name },
823
+ update: (el, item) => {
824
+ el.querySelector('span')!.textContent = item.name
825
+ },
519
826
  })
827
+ ```
520
828
 
521
- // Render a keyed list with minimal DOM mutations.
522
- renderList(container, template, {
523
- data: items,
524
- key: item => item.id,
525
- update: (el, item) => { /* update el from item */ },
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
- ## Divergence from nanotags
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 native alien-signals callable signals.
539
- There is no `$` prefix convention — all prop names are plain identifiers.
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
- /ed3d-plan-and-execute:execute-implementation-plan /Users/nick/code/_microtags/docs/implementation-plans/2026-06-28-zod-validation-example/ /Users/nick/code/_microtags/
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.