balises 0.8.2 → 0.8.4

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
@@ -33,6 +33,8 @@ Ultimately it turns out that I am quite happy with the result! It is quite perfo
33
33
  - [Async Generators](#async-generators)
34
34
  - [DOM Preservation on Restart](#dom-preservation-on-restart)
35
35
  - [Template Syntax](#template-syntax)
36
+ - [Efficient List Rendering with each()](#efficient-list-rendering-with-each)
37
+ - [Conditional Rendering with when() and match()](#conditional-rendering-with-when-and-match)
36
38
  - [Reactivity API](#reactivity-api)
37
39
  - [Web Components](#web-components)
38
40
  - [Tree-Shaking / Modular Imports](#tree-shaking--modular-imports)
@@ -113,8 +115,11 @@ const html = baseHtml.with(asyncPlugin);
113
115
  const userId = signal(1);
114
116
 
115
117
  html`
116
- ${async function* () {
118
+ ${async function* (settled, ctx) {
119
+ void settled;
117
120
  const id = userId.value; // Track dependency - restarts when userId changes
121
+ const prev = ctx?.lastId;
122
+ if (ctx) ctx.lastId = id;
118
123
 
119
124
  yield html`<div class="loading">Loading...</div>`;
120
125
 
@@ -129,6 +134,8 @@ userId.value = 2;
129
134
 
130
135
  Async generators replace the entire yielded content on each yield. For surgical updates within a stable DOM structure, use reactive bindings (`${() => state.value}`) instead.
131
136
 
137
+ Generators receive a mutable context object as their second argument. This object persists across restarts and can hold user-defined state for diffing or caching. Use the `AsyncGeneratorContext<T>` type from `balises/async` for type safety.
138
+
132
139
  ### DOM Preservation on Restart
133
140
 
134
141
  When a signal changes, the generator restarts and normally replaces the DOM. To preserve existing DOM and enable surgical updates via reactive bindings, return the `settled` parameter:
@@ -142,8 +149,9 @@ const userId = signal(1);
142
149
  const state = store({ user: null, loading: false });
143
150
 
144
151
  html`
145
- ${async function* (settled?: RenderedContent) {
152
+ ${async function* (settled?: RenderedContent, ctx?: { lastId?: number }) {
146
153
  const id = userId.value;
154
+ if (ctx) ctx.lastId = id;
147
155
 
148
156
  if (settled) {
149
157
  // Restart: update state, preserve existing DOM
@@ -305,6 +313,77 @@ each(list, keyFn, renderFn); // keyFn extracts key, renderFn receives ReadonlySi
305
313
 
306
314
  **Important:** Access item properties through `itemSignal.value` and wrap in `() => ...` for reactive updates.
307
315
 
316
+ ### Conditional Rendering with `when()` and `match()`
317
+
318
+ When conditionally rendering content, a naive approach re-creates templates on every change, even when the condition result stays the same. Use `when()` and `match()` for conditional rendering where branches are rendered based on the selector's **result**, not the underlying data.
319
+
320
+ **Note:** Import from `balises/match` and use `html.with(matchPlugin)` to enable.
321
+
322
+ ```ts
323
+ import { html as baseHtml, store } from "balises";
324
+ import matchPlugin, { when } from "balises/match";
325
+
326
+ const html = baseHtml.with(matchPlugin);
327
+ const state = store({ user: null });
328
+
329
+ html`
330
+ ${when(
331
+ () => !!state.user,
332
+ [
333
+ () => html`<Profile>${() => state.user.name}</Profile>`,
334
+ () => html`<LoginPrompt />`,
335
+ ],
336
+ )}
337
+ `.render();
338
+
339
+ // When user changes from Alice to Bob, !!state.user stays true
340
+ // so the Profile branch is REUSED (not recreated)
341
+ state.user = { name: "Alice" };
342
+ state.user = { name: "Bob" }; // Same branch, just updates the name binding
343
+ ```
344
+
345
+ The second argument is an array `[ifTrue, ifFalse?]`. If `ifFalse` is omitted, renders nothing when false.
346
+
347
+ For multiple cases, use `match()`:
348
+
349
+ ```ts
350
+ import matchPlugin, { match } from "balises/match";
351
+
352
+ const status = signal<"idle" | "loading" | "error" | "success">("idle");
353
+
354
+ html`
355
+ ${match(() => status.value, {
356
+ idle: () => html`<IdleState />`,
357
+ loading: () => html`<Spinner />`,
358
+ error: () => html`<ErrorMessage />`,
359
+ success: () => html`<content />`,
360
+ _: () => html`<Fallback />`, // Optional default case
361
+ })}
362
+ `.render();
363
+ ```
364
+
365
+ By default, branches are disposed when switching away (freeing memory). For scenarios where you want instant switching back (like tabs), use `{ cache: true }`:
366
+
367
+ ```ts
368
+ html`
369
+ ${match(
370
+ () => state.activeTab,
371
+ {
372
+ home: () => html`<Home />`,
373
+ settings: () => html`<Settings />`,
374
+ },
375
+ { cache: true }, // Keep branches in memory for instant switching
376
+ )}
377
+ `.render();
378
+
379
+ // With cache: true, switching back reuses the same DOM nodes
380
+ state.activeTab = "settings"; // Creates Settings
381
+ state.activeTab = "home"; // Creates Home
382
+ state.activeTab = "settings"; // Reuses cached Settings (same DOM!)
383
+ ```
384
+
385
+ Reactive bindings inside branches continue to work normally regardless of caching.
386
+
308
387
  ## Reactivity API
309
388
 
310
389
  ### `signal<T>(value)`
@@ -564,7 +643,7 @@ import { batch, scope } from "balises/signals/context";
564
643
 
565
644
  ### Template Plugins
566
645
 
567
- The `each()` and async generator features are provided as opt-in plugins to keep the base bundle minimal. Use `html.with()` to compose plugins:
646
+ The `each()`, `when()`/`match()`, and async generator features are provided as opt-in plugins to keep the base bundle minimal. Use `html.with()` to compose plugins:
568
647
 
569
648
  ```ts
570
649
  // With each() support for keyed lists
@@ -573,18 +652,25 @@ import eachPlugin, { each } from "balises/each";
573
652
 
574
653
  const html = baseHtml.with(eachPlugin);
575
654
 
655
+ // With when()/match() for cached conditional rendering
656
+ import { html as baseHtml } from "balises";
657
+ import matchPlugin, { when, match } from "balises/match";
658
+
659
+ const html = baseHtml.with(matchPlugin);
660
+
576
661
  // With async generator support
577
662
  import { html as baseHtml } from "balises";
578
663
  import asyncPlugin from "balises/async";
579
664
 
580
665
  const html = baseHtml.with(asyncPlugin);
581
666
 
582
- // With both plugins
667
+ // With multiple plugins
583
668
  import { html as baseHtml } from "balises";
584
669
  import eachPlugin, { each } from "balises/each";
670
+ import matchPlugin, { when, match } from "balises/match";
585
671
  import asyncPlugin from "balises/async";
586
672
 
587
- const html = baseHtml.with(eachPlugin, asyncPlugin);
673
+ const html = baseHtml.with(eachPlugin, matchPlugin, asyncPlugin);
588
674
  ```
589
675
 
590
676
  ### Using as a Standalone Signals Library
@@ -700,23 +786,23 @@ Performance comparison of Balises against other popular reactive libraries. Benc
700
786
  ┌───────┬───────────────────┬───────┬───────────────┬──────────────────┐
701
787
  │ Rank │ Library │ Score │ Avg Time (μs) │ vs Fastest │
702
788
  ├───────┼───────────────────┼───────┼───────────────┼──────────────────┤
703
- │ #1 🏆 │ preact@1.12.1 │ 0.000 │ 63.12 │ 1.00x (baseline) │
789
+ │ #1 🏆 │ preact@1.12.1 │ 0.000 │ 63.20 │ 1.00x (baseline) │
704
790
  ├───────┼───────────────────┼───────┼───────────────┼──────────────────┤
705
- │ #2 │ balises@0.8.1 │ 0.06296.24 │ 1.52x
791
+ │ #2 │ balises@0.8.3 │ 0.05990.83 │ 1.44x
706
792
  ├───────┼───────────────────┼───────┼───────────────┼──────────────────┤
707
- │ #3 │ vue@3.5.26 │ 0.103 │ 93.97 │ 1.49x
793
+ │ #3 │ vue@3.5.26 │ 0.103 │ 93.43 │ 1.48x
708
794
  ├───────┼───────────────────┼───────┼───────────────┼──────────────────┤
709
- │ #4 │ maverick@6.0.0 │ 0.154125.73 │ 1.99x
795
+ │ #4 │ maverick@6.0.0 │ 0.152122.20 │ 1.93x
710
796
  ├───────┼───────────────────┼───────┼───────────────┼──────────────────┤
711
- │ #5 │ usignal@0.10.0 │ 0.186 │ 132.38 │ 2.10x │
797
+ │ #5 │ usignal@0.10.0 │ 0.187 │ 132.51 │ 2.10x │
712
798
  ├───────┼───────────────────┼───────┼───────────────┼──────────────────┤
713
- │ #6 │ angular@19.2.17 │ 0.210157.89 │ 2.50x
799
+ │ #6 │ angular@19.2.17 │ 0.215161.81 │ 2.56x
714
800
  ├───────┼───────────────────┼───────┼───────────────┼──────────────────┤
715
- │ #7 │ solid@1.9.10 │ 0.342 │ 260.88 │ 4.13x
801
+ │ #7 │ solid@1.9.10 │ 0.342 │ 255.37 │ 4.04x
716
802
  ├───────┼───────────────────┼───────┼───────────────┼──────────────────┤
717
- │ #8 │ mobx@6.15.0 │ 0.844892.34 │ 14.14x
803
+ │ #8 │ mobx@6.15.0 │ 0.858889.72 │ 14.08x
718
804
  ├───────┼───────────────────┼───────┼───────────────┼──────────────────┤
719
- │ #9 │ hyperactiv@0.11.3 │ 1.000 │ 1048.63 │ 16.61x
805
+ │ #9 │ hyperactiv@0.11.3 │ 1.000 │ 1023.25 │ 16.19x
720
806
  └───────┴───────────────────┴───────┴───────────────┴──────────────────┘
721
807
  ```
722
808
 
@@ -728,7 +814,7 @@ Performance comparison of Balises against other popular reactive libraries. Benc
728
814
  ├───────────────────┼───────────────┼─────────────┼────────────────┼────────────────────┼─────────────┼──────────────┼──────────┤
729
815
  │ preact@1.12.1 │ #1 🏆 │ #1 🏆 │ #1 🏆 │ #1 🏆 │ #1 🏆 │ #1 🏆 │ 1.0 │
730
816
  ├───────────────────┼───────────────┼─────────────┼────────────────┼────────────────────┼─────────────┼──────────────┼──────────┤
731
- │ balises@0.8.1 │ #3 │ #2 │ #2 │ #2 │ #2 │ #2 │ 2.2 │
817
+ │ balises@0.8.3 │ #3 │ #2 │ #2 │ #2 │ #2 │ #2 │ 2.2 │
732
818
  ├───────────────────┼───────────────┼─────────────┼────────────────┼────────────────────┼─────────────┼──────────────┼──────────┤
733
819
  │ vue@3.5.26 │ #2 │ #3 │ #5 │ #3 │ #3 │ #5 │ 3.5 │
734
820
  ├───────────────────┼───────────────┼─────────────┼────────────────┼────────────────────┼─────────────┼──────────────┼──────────┤
@@ -761,7 +847,7 @@ Performance comparison of Balises against other popular reactive libraries. Benc
761
847
  - These are synthetic benchmarks measuring pure reactivity - real apps should consider the whole picture (ecosystem, docs, community, etc.)
762
848
  - Lower rank = better performance
763
849
 
764
- _Last updated: 2026-01-11_
850
+ _Last updated: 2026-01-21_
765
851
 
766
852
  <!-- BENCHMARK_RESULTS_END -->
767
853
 
@@ -33,8 +33,13 @@ import { type InterpolationPlugin } from "./template.js";
33
33
  * ```ts
34
34
  * import asyncPlugin, { type RenderedContent } from "balises/async";
35
35
  *
36
- * async function* loadUser(settled?: RenderedContent) {
36
+ * async function* loadUser(
37
+ * settled?: RenderedContent,
38
+ * ctx?: AsyncGeneratorContext<{ lastId?: number }>,
39
+ * ) {
37
40
  * const id = userId.value; // Track dependency
41
+ * const previous = ctx?.lastId;
42
+ * ctx && (ctx.lastId = id);
38
43
  *
39
44
  * if (settled) {
40
45
  * // Restart: update state, keep existing DOM
@@ -55,6 +60,10 @@ export interface RenderedContent {
55
60
  /** @internal Brand to prevent construction outside the library */
56
61
  readonly __brand: "RenderedContent";
57
62
  }
63
+ /**
64
+ * Mutable context object that persists across async generator restarts.
65
+ */
66
+ export type AsyncGeneratorContext<T extends object = Record<string, unknown>> = T;
58
67
  /**
59
68
  * Plugin that handles async generator functions.
60
69
  * Auto-detects `async function*` without needing a wrapper.
@@ -1 +1 @@
1
- {"version":3,"file":"async.d.ts","sourceRoot":"","sources":["../../src/async.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAOH,OAAO,EAAY,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAMnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,WAAW,eAAe;IAC9B,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;CACrC;AAwDD;;;GAGG;AACH,QAAA,MAAM,WAAW,EAAE,mBAMlB,CAAC;AAEF,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"async.d.ts","sourceRoot":"","sources":["../../src/async.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAOH,OAAO,EAAY,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAMnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,WAAW,eAAe;IAC9B,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAC1E,CAAC,CAAC;AAyDJ;;;GAGG;AACH,QAAA,MAAM,WAAW,EAAE,mBAMlB,CAAC;AAEF,eAAe,WAAW,CAAC"}
package/dist/esm/async.js CHANGED
@@ -120,6 +120,7 @@ function bindAsyncGenerator(genFn, marker, disposers) {
120
120
  let iterationId = 0;
121
121
  let depUnsubscribers = [];
122
122
  let lastSettled = null;
123
+ const context = {};
123
124
  const clearNodes = () => {
124
125
  for (let i = 0; i < childDisposers.length; i++)
125
126
  childDisposers[i]();
@@ -153,7 +154,7 @@ function bindAsyncGenerator(genFn, marker, disposers) {
153
154
  cleanupGenerator();
154
155
  if (disposed)
155
156
  return;
156
- generator = genFn(lastSettled ?? undefined);
157
+ generator = genFn(lastSettled ?? undefined, context);
157
158
  let lastYielded = null;
158
159
  while (!disposed && thisIteration === iterationId) {
159
160
  let result;
@@ -1 +1 @@
1
- {"version":3,"file":"async.js","sourceRoot":"","sources":["../../src/async.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Async generator support for templates.\n *\n * This module provides opt-in support for async generators in templates,\n * enabling loading states, progressive content, and automatic restart\n * when signal dependencies change.\n *\n * @example\n * ```ts\n * import { html as baseHtml, signal } from \"balises\";\n * import asyncPlugin from \"balises/async\";\n *\n * const html = baseHtml.with(asyncPlugin);\n * const userId = signal(1);\n *\n * // Async generators are auto-detected - no wrapper needed!\n * html`<div>${async function* () {\n * yield html`<span>Loading user ${userId.value}...</span>`;\n * const user = await fetchUser(userId.value);\n * return html`<span>${user.name}</span>`;\n * }}</div>`.render();\n * ```\n */\n\nimport {\n onTrack,\n type Subscriber,\n type TrackableSource,\n} from \"./signals/context.js\";\nimport { Template, type InterpolationPlugin } from \"./template.js\";\nimport { isSignal, type Reactive } from \"./signals/index.js\";\n\n/** Reactive source type - TrackableSource may or may not be subscribable */\ntype SubscribableSource = Reactive<unknown>;\n\n/**\n * Opaque handle representing settled content from an async generator.\n *\n * When an async generator restarts due to signal changes, it receives the\n * previous settled content as its first argument. Return this value to\n * preserve the existing DOM instead of re-rendering.\n *\n * @example\n * ```ts\n * import asyncPlugin, { type RenderedContent } from \"balises/async\";\n *\n * async function* loadUser(settled?: RenderedContent) {\n * const id = userId.value; // Track dependency\n *\n * if (settled) {\n * // Restart: update state, keep existing DOM\n * const user = await fetchUser(id);\n * state.user = user; // Triggers surgical updates via reactive bindings\n * return settled; // Preserve DOM\n * }\n *\n * // First load\n * yield html`<div class=\"skeleton\">...</div>`;\n * const user = await fetchUser(id);\n * state.user = user;\n * return UserCard({ state });\n * }\n * ```\n */\nexport interface RenderedContent {\n /** @internal Brand to prevent construction outside the library */\n readonly __brand: \"RenderedContent\";\n}\n\n/** Internal structure for RenderedContent */\ninterface RenderedContentInternal extends RenderedContent {\n readonly nodes: Node[];\n readonly childDisposers: (() => void)[];\n}\n\n/** Async generator function type */\ntype AsyncGenFn = (\n settled?: RenderedContent,\n) => AsyncGenerator<unknown, unknown, unknown>;\n\n/**\n * Check if a value is an async generator function.\n */\nfunction isAsyncGeneratorFunction(\n value: unknown,\n): value is AsyncGeneratorFunction {\n if (typeof value !== \"function\") return false;\n const constructor = value.constructor;\n return (\n constructor &&\n (constructor.name === \"AsyncGeneratorFunction\" ||\n // Check prototype chain for async generator\n Object.prototype.toString.call(constructor.prototype) ===\n \"[object AsyncGeneratorFunction]\")\n );\n}\n\n/**\n * Render content and insert nodes before marker.\n * Handles Templates and primitives.\n */\nfunction insertContent(\n marker: Comment,\n value: unknown,\n nodes: Node[],\n disposers: (() => void)[],\n): void {\n const parent = marker.parentNode!;\n\n for (const item of Array.isArray(value) ? value : [value]) {\n if (item instanceof Template) {\n const { fragment, dispose } = item.render();\n disposers.push(dispose);\n nodes.push(...fragment.childNodes);\n parent.insertBefore(fragment, marker);\n } else if (item != null && typeof item !== \"boolean\") {\n const node = document.createTextNode(String(item));\n nodes.push(node);\n parent.insertBefore(node, marker);\n }\n }\n}\n\n/**\n * Plugin that handles async generator functions.\n * Auto-detects `async function*` without needing a wrapper.\n */\nconst asyncPlugin: InterpolationPlugin = (value) => {\n if (!isAsyncGeneratorFunction(value)) return null;\n\n return (marker, disposers) => {\n bindAsyncGenerator(value as AsyncGenFn, marker, disposers);\n };\n};\n\nexport default asyncPlugin;\n\n/** Result of tracking dependencies during a function call */\ninterface TrackResult<T> {\n value: T;\n subscribe: (callback: Subscriber) => void;\n unsubscribe: () => void;\n}\n\n/**\n * Track reactive dependencies accessed during a function call.\n * Sets up the onTrack hook temporarily to capture signal/computed accesses.\n */\nfunction track<T>(fn: () => T): TrackResult<T> {\n const sources = new Set<TrackableSource>();\n const prevHook = onTrack.current;\n onTrack.current = (source) => sources.add(source);\n\n let value: T;\n try {\n value = fn();\n } finally {\n onTrack.current = prevHook;\n }\n\n let unsubscribers: (() => void)[] = [];\n let subscribed = false;\n\n return {\n value,\n subscribe: (callback: Subscriber) => {\n if (subscribed) return;\n subscribed = true;\n for (const source of sources) {\n // Only subscribe to actual signals/computeds (not selector slots)\n if (isSignal(source)) {\n unsubscribers.push(\n (source as SubscribableSource).subscribe(callback),\n );\n }\n }\n },\n unsubscribe: () => {\n for (const unsub of unsubscribers) unsub();\n unsubscribers = [];\n subscribed = false;\n },\n };\n}\n\n/**\n * Bind an async generator function to a marker position.\n * Tracks signal dependencies during generator execution and restarts\n * the generator when those dependencies change.\n */\nfunction bindAsyncGenerator(\n genFn: AsyncGenFn,\n marker: Comment,\n disposers: (() => void)[],\n): void {\n let generator: AsyncGenerator<unknown> | null = null;\n let currentNodes: Node[] = [];\n let childDisposers: (() => void)[] = [];\n let disposed = false;\n let iterationId = 0;\n let depUnsubscribers: (() => void)[] = [];\n let lastSettled: RenderedContentInternal | null = null;\n\n const clearNodes = () => {\n for (let i = 0; i < childDisposers.length; i++) childDisposers[i]!();\n for (let i = 0; i < currentNodes.length; i++)\n currentNodes[i]!.parentNode?.removeChild(currentNodes[i]!);\n childDisposers = [];\n currentNodes = [];\n };\n\n const clearDeps = () => {\n for (let i = 0; i < depUnsubscribers.length; i++) depUnsubscribers[i]!();\n depUnsubscribers = [];\n };\n\n const cleanupGenerator = () => {\n clearDeps();\n if (generator) {\n generator.return(undefined);\n generator = null;\n }\n };\n\n const cleanup = () => {\n cleanupGenerator();\n clearNodes();\n };\n\n const render = (value: unknown) => {\n clearNodes();\n insertContent(marker, value, currentNodes, childDisposers);\n };\n\n const runGenerator = async () => {\n const thisIteration = ++iterationId;\n cleanupGenerator();\n\n if (disposed) return;\n\n generator = genFn(lastSettled ?? undefined);\n let lastYielded: unknown = null;\n\n while (!disposed && thisIteration === iterationId) {\n let result: IteratorResult<unknown>;\n\n try {\n const tracked = track(() => generator!.next());\n\n tracked.subscribe(() => {\n if (!disposed && thisIteration === iterationId) {\n void runGenerator();\n }\n });\n depUnsubscribers.push(tracked.unsubscribe);\n\n result = await tracked.value;\n } catch (e) {\n cleanup();\n if (!disposed) throw e;\n return;\n }\n\n if (thisIteration !== iterationId) return;\n\n const { value, done } = result;\n\n if (done) {\n if (value === lastSettled && lastSettled !== null) {\n currentNodes = lastSettled.nodes;\n childDisposers = lastSettled.childDisposers;\n } else {\n render(value !== undefined ? value : lastYielded);\n lastSettled = {\n __brand: \"RenderedContent\" as const,\n nodes: currentNodes,\n childDisposers: childDisposers,\n };\n }\n return;\n }\n\n lastYielded = value;\n render(value);\n }\n };\n\n void runGenerator();\n\n disposers.push(() => {\n disposed = true;\n cleanup();\n lastSettled = null;\n });\n}\n"]}
1
+ {"version":3,"file":"async.js","sourceRoot":"","sources":["../../src/async.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Async generator support for templates.\n *\n * This module provides opt-in support for async generators in templates,\n * enabling loading states, progressive content, and automatic restart\n * when signal dependencies change.\n *\n * @example\n * ```ts\n * import { html as baseHtml, signal } from \"balises\";\n * import asyncPlugin from \"balises/async\";\n *\n * const html = baseHtml.with(asyncPlugin);\n * const userId = signal(1);\n *\n * // Async generators are auto-detected - no wrapper needed!\n * html`<div>${async function* () {\n * yield html`<span>Loading user ${userId.value}...</span>`;\n * const user = await fetchUser(userId.value);\n * return html`<span>${user.name}</span>`;\n * }}</div>`.render();\n * ```\n */\n\nimport {\n onTrack,\n type Subscriber,\n type TrackableSource,\n} from \"./signals/context.js\";\nimport { Template, type InterpolationPlugin } from \"./template.js\";\nimport { isSignal, type Reactive } from \"./signals/index.js\";\n\n/** Reactive source type - TrackableSource may or may not be subscribable */\ntype SubscribableSource = Reactive<unknown>;\n\n/**\n * Opaque handle representing settled content from an async generator.\n *\n * When an async generator restarts due to signal changes, it receives the\n * previous settled content as its first argument. Return this value to\n * preserve the existing DOM instead of re-rendering.\n *\n * @example\n * ```ts\n * import asyncPlugin, { type RenderedContent } from \"balises/async\";\n *\n * async function* loadUser(\n * settled?: RenderedContent,\n * ctx?: AsyncGeneratorContext<{ lastId?: number }>,\n * ) {\n * const id = userId.value; // Track dependency\n * const previous = ctx?.lastId;\n * ctx && (ctx.lastId = id);\n *\n * if (settled) {\n * // Restart: update state, keep existing DOM\n * const user = await fetchUser(id);\n * state.user = user; // Triggers surgical updates via reactive bindings\n * return settled; // Preserve DOM\n * }\n *\n * // First load\n * yield html`<div class=\"skeleton\">...</div>`;\n * const user = await fetchUser(id);\n * state.user = user;\n * return UserCard({ state });\n * }\n * ```\n */\nexport interface RenderedContent {\n /** @internal Brand to prevent construction outside the library */\n readonly __brand: \"RenderedContent\";\n}\n\n/**\n * Mutable context object that persists across async generator restarts.\n */\nexport type AsyncGeneratorContext<T extends object = Record<string, unknown>> =\n T;\n\n/** Internal structure for RenderedContent */\ninterface RenderedContentInternal extends RenderedContent {\n readonly nodes: Node[];\n readonly childDisposers: (() => void)[];\n}\n\n/** Async generator function type */\ntype AsyncGenFn = (\n settled?: RenderedContent,\n ctx?: AsyncGeneratorContext,\n) => AsyncGenerator<unknown, unknown, unknown>;\n\n/**\n * Check if a value is an async generator function.\n */\nfunction isAsyncGeneratorFunction(\n value: unknown,\n): value is AsyncGeneratorFunction {\n if (typeof value !== \"function\") return false;\n const constructor = value.constructor;\n return (\n constructor &&\n (constructor.name === \"AsyncGeneratorFunction\" ||\n // Check prototype chain for async generator\n Object.prototype.toString.call(constructor.prototype) ===\n \"[object AsyncGeneratorFunction]\")\n );\n}\n\n/**\n * Render content and insert nodes before marker.\n * Handles Templates and primitives.\n */\nfunction insertContent(\n marker: Comment,\n value: unknown,\n nodes: Node[],\n disposers: (() => void)[],\n): void {\n const parent = marker.parentNode!;\n\n for (const item of Array.isArray(value) ? value : [value]) {\n if (item instanceof Template) {\n const { fragment, dispose } = item.render();\n disposers.push(dispose);\n nodes.push(...fragment.childNodes);\n parent.insertBefore(fragment, marker);\n } else if (item != null && typeof item !== \"boolean\") {\n const node = document.createTextNode(String(item));\n nodes.push(node);\n parent.insertBefore(node, marker);\n }\n }\n}\n\n/**\n * Plugin that handles async generator functions.\n * Auto-detects `async function*` without needing a wrapper.\n */\nconst asyncPlugin: InterpolationPlugin = (value) => {\n if (!isAsyncGeneratorFunction(value)) return null;\n\n return (marker, disposers) => {\n bindAsyncGenerator(value as AsyncGenFn, marker, disposers);\n };\n};\n\nexport default asyncPlugin;\n\n/** Result of tracking dependencies during a function call */\ninterface TrackResult<T> {\n value: T;\n subscribe: (callback: Subscriber) => void;\n unsubscribe: () => void;\n}\n\n/**\n * Track reactive dependencies accessed during a function call.\n * Sets up the onTrack hook temporarily to capture signal/computed accesses.\n */\nfunction track<T>(fn: () => T): TrackResult<T> {\n const sources = new Set<TrackableSource>();\n const prevHook = onTrack.current;\n onTrack.current = (source) => sources.add(source);\n\n let value: T;\n try {\n value = fn();\n } finally {\n onTrack.current = prevHook;\n }\n\n let unsubscribers: (() => void)[] = [];\n let subscribed = false;\n\n return {\n value,\n subscribe: (callback: Subscriber) => {\n if (subscribed) return;\n subscribed = true;\n for (const source of sources) {\n // Only subscribe to actual signals/computeds (not selector slots)\n if (isSignal(source)) {\n unsubscribers.push(\n (source as SubscribableSource).subscribe(callback),\n );\n }\n }\n },\n unsubscribe: () => {\n for (const unsub of unsubscribers) unsub();\n unsubscribers = [];\n subscribed = false;\n },\n };\n}\n\n/**\n * Bind an async generator function to a marker position.\n * Tracks signal dependencies during generator execution and restarts\n * the generator when those dependencies change.\n */\nfunction bindAsyncGenerator(\n genFn: AsyncGenFn,\n marker: Comment,\n disposers: (() => void)[],\n): void {\n let generator: AsyncGenerator<unknown> | null = null;\n let currentNodes: Node[] = [];\n let childDisposers: (() => void)[] = [];\n let disposed = false;\n let iterationId = 0;\n let depUnsubscribers: (() => void)[] = [];\n let lastSettled: RenderedContentInternal | null = null;\n const context: AsyncGeneratorContext = {};\n\n const clearNodes = () => {\n for (let i = 0; i < childDisposers.length; i++) childDisposers[i]!();\n for (let i = 0; i < currentNodes.length; i++)\n currentNodes[i]!.parentNode?.removeChild(currentNodes[i]!);\n childDisposers = [];\n currentNodes = [];\n };\n\n const clearDeps = () => {\n for (let i = 0; i < depUnsubscribers.length; i++) depUnsubscribers[i]!();\n depUnsubscribers = [];\n };\n\n const cleanupGenerator = () => {\n clearDeps();\n if (generator) {\n generator.return(undefined);\n generator = null;\n }\n };\n\n const cleanup = () => {\n cleanupGenerator();\n clearNodes();\n };\n\n const render = (value: unknown) => {\n clearNodes();\n insertContent(marker, value, currentNodes, childDisposers);\n };\n\n const runGenerator = async () => {\n const thisIteration = ++iterationId;\n cleanupGenerator();\n\n if (disposed) return;\n\n generator = genFn(lastSettled ?? undefined, context);\n let lastYielded: unknown = null;\n\n while (!disposed && thisIteration === iterationId) {\n let result: IteratorResult<unknown>;\n\n try {\n const tracked = track(() => generator!.next());\n\n tracked.subscribe(() => {\n if (!disposed && thisIteration === iterationId) {\n void runGenerator();\n }\n });\n depUnsubscribers.push(tracked.unsubscribe);\n\n result = await tracked.value;\n } catch (e) {\n cleanup();\n if (!disposed) throw e;\n return;\n }\n\n if (thisIteration !== iterationId) return;\n\n const { value, done } = result;\n\n if (done) {\n if (value === lastSettled && lastSettled !== null) {\n currentNodes = lastSettled.nodes;\n childDisposers = lastSettled.childDisposers;\n } else {\n render(value !== undefined ? value : lastYielded);\n lastSettled = {\n __brand: \"RenderedContent\" as const,\n nodes: currentNodes,\n childDisposers: childDisposers,\n };\n }\n return;\n }\n\n lastYielded = value;\n render(value);\n }\n };\n\n void runGenerator();\n\n disposers.push(() => {\n disposed = true;\n cleanup();\n lastSettled = null;\n });\n}\n"]}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Match plugin - Conditional rendering with optional branch caching.
3
+ *
4
+ * This module provides opt-in support for conditional rendering,
5
+ * where branches are rendered based on selector results rather than
6
+ * underlying data changes.
7
+ *
8
+ * By default, branches are disposed when switching away (cache: false).
9
+ * Set cache: true to keep branches in memory for instant re-switching.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { html as baseHtml } from "balises";
14
+ * import matchPlugin, { when, match } from "balises/match";
15
+ *
16
+ * const html = baseHtml.with(matchPlugin);
17
+ *
18
+ * // Default: branches disposed on switch
19
+ * html`${when(() => !!state.user, [
20
+ * () => html`<Profile />`,
21
+ * () => html`<Login />`
22
+ * ])}`.render();
23
+ *
24
+ * // With caching: branches preserved for instant re-switching
25
+ * html`${match(() => state.tab, {
26
+ * home: () => html`<Home />`,
27
+ * settings: () => html`<Settings />`
28
+ * }, { cache: true })}`.render();
29
+ * ```
30
+ */
31
+ import { Template, type InterpolationPlugin } from "./template.js";
32
+ declare const MATCH: unique symbol;
33
+ /** Options for when/match behavior */
34
+ export interface MatchOptions {
35
+ /**
36
+ * Whether to cache branches when switching away.
37
+ * - false (default): Dispose branches when hidden, recreate when revisited
38
+ * - true: Keep branches in memory for instant re-switching
39
+ */
40
+ cache?: boolean;
41
+ }
42
+ interface MatchDescriptor {
43
+ readonly [MATCH]: true;
44
+ /** @internal */ selector: () => unknown;
45
+ /** @internal */ cases: Record<string, () => Template>;
46
+ /** @internal */ cache: boolean;
47
+ }
48
+ /**
49
+ * Conditional rendering for boolean conditions.
50
+ * Prevents re-renders when the condition result stays the same.
51
+ *
52
+ * @param condition - Function that returns a boolean
53
+ * @param branches - Array of [ifTrue, ifFalse?] factory functions. If ifFalse is omitted, renders nothing when false.
54
+ * @param options - Optional settings. cache: false (default) disposes branches on switch.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * // Render only when true
59
+ * html`${when(() => !!state.user, [
60
+ * () => html`<Profile user=${state.user} />`
61
+ * ])}`
62
+ *
63
+ * // Render different content for true/false
64
+ * html`${when(() => !!state.user, [
65
+ * () => html`<Profile user=${state.user} />`,
66
+ * () => html`<LoginPrompt />`
67
+ * ])}`
68
+ *
69
+ * // With caching for instant switching
70
+ * html`${when(() => state.expanded, [
71
+ * () => html`<ExpandedView />`,
72
+ * () => html`<CollapsedView />`
73
+ * ], { cache: true })}`
74
+ * ```
75
+ */
76
+ export declare function when(condition: () => boolean, [ifTrue, ifFalse]: [() => Template, (() => Template)?], options?: MatchOptions): MatchDescriptor;
77
+ /**
78
+ * Conditional rendering for multiple cases.
79
+ * Prevents re-renders when the selector result stays the same.
80
+ *
81
+ * @param selector - Function that returns a key value
82
+ * @param cases - Object mapping keys to template factories. Use `_` for default case.
83
+ * @param options - Optional settings. cache: false (default) disposes branches on switch.
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * // Loading states (no caching needed)
88
+ * html`${match(() => state.status, {
89
+ * loading: () => html`<Spinner />`,
90
+ * error: () => html`<Error message=${() => state.error} />`,
91
+ * success: () => html`<Data items=${() => state.items} />`,
92
+ * _: () => html`<Fallback />`,
93
+ * })}`
94
+ *
95
+ * // Tab switching with caching
96
+ * html`${match(() => state.tab, {
97
+ * home: () => html`<Home />`,
98
+ * settings: () => html`<Settings />`
99
+ * }, { cache: true })}`
100
+ * ```
101
+ */
102
+ export declare function match<K extends string | number>(selector: () => K, cases: Partial<Record<K, () => Template>> & {
103
+ _?: () => Template;
104
+ }, options?: MatchOptions): MatchDescriptor;
105
+ /**
106
+ * Plugin that handles match/when descriptors.
107
+ * Register with: const html = baseHtml.with(matchPlugin);
108
+ */
109
+ declare const matchPlugin: InterpolationPlugin;
110
+ export default matchPlugin;
111
+ //# sourceMappingURL=match.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"match.d.ts","sourceRoot":"","sources":["../../src/match.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAGH,OAAO,EAAE,QAAQ,EAAE,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEnE,QAAA,MAAM,KAAK,eAAkB,CAAC;AAE9B,sCAAsC;AACtC,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,UAAU,eAAe;IACvB,QAAQ,CAAC,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC;IACvB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC;IACzC,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,QAAQ,CAAC,CAAC;IACvD,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC;CACjC;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,IAAI,CAClB,SAAS,EAAE,MAAM,OAAO,EACxB,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,MAAM,QAAQ,EAAE,CAAC,MAAM,QAAQ,CAAC,CAAC,CAAC,EACtD,OAAO,CAAC,EAAE,YAAY,GACrB,eAAe,CAOjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAC7C,QAAQ,EAAE,MAAM,CAAC,EACjB,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,QAAQ,CAAC,CAAC,GAAG;IAAE,CAAC,CAAC,EAAE,MAAM,QAAQ,CAAA;CAAE,EAClE,OAAO,CAAC,EAAE,YAAY,GACrB,eAAe,CAOjB;AAQD;;;GAGG;AACH,QAAA,MAAM,WAAW,EAAE,mBAsFlB,CAAC;AAEF,eAAe,WAAW,CAAC"}
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Match plugin - Conditional rendering with optional branch caching.
3
+ *
4
+ * This module provides opt-in support for conditional rendering,
5
+ * where branches are rendered based on selector results rather than
6
+ * underlying data changes.
7
+ *
8
+ * By default, branches are disposed when switching away (cache: false).
9
+ * Set cache: true to keep branches in memory for instant re-switching.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { html as baseHtml } from "balises";
14
+ * import matchPlugin, { when, match } from "balises/match";
15
+ *
16
+ * const html = baseHtml.with(matchPlugin);
17
+ *
18
+ * // Default: branches disposed on switch
19
+ * html`${when(() => !!state.user, [
20
+ * () => html`<Profile />`,
21
+ * () => html`<Login />`
22
+ * ])}`.render();
23
+ *
24
+ * // With caching: branches preserved for instant re-switching
25
+ * html`${match(() => state.tab, {
26
+ * home: () => html`<Home />`,
27
+ * settings: () => html`<Settings />`
28
+ * }, { cache: true })}`.render();
29
+ * ```
30
+ */
31
+ import { computed } from "./signals/computed.js";
32
+ import { Template } from "./template.js";
33
+ const MATCH = Symbol("match");
34
+ function isMatchDescriptor(v) {
35
+ return v != null && typeof v === "object" && MATCH in v;
36
+ }
37
+ /**
38
+ * Conditional rendering for boolean conditions.
39
+ * Prevents re-renders when the condition result stays the same.
40
+ *
41
+ * @param condition - Function that returns a boolean
42
+ * @param branches - Array of [ifTrue, ifFalse?] factory functions. If ifFalse is omitted, renders nothing when false.
43
+ * @param options - Optional settings. cache: false (default) disposes branches on switch.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * // Render only when true
48
+ * html`${when(() => !!state.user, [
49
+ * () => html`<Profile user=${state.user} />`
50
+ * ])}`
51
+ *
52
+ * // Render different content for true/false
53
+ * html`${when(() => !!state.user, [
54
+ * () => html`<Profile user=${state.user} />`,
55
+ * () => html`<LoginPrompt />`
56
+ * ])}`
57
+ *
58
+ * // With caching for instant switching
59
+ * html`${when(() => state.expanded, [
60
+ * () => html`<ExpandedView />`,
61
+ * () => html`<CollapsedView />`
62
+ * ], { cache: true })}`
63
+ * ```
64
+ */
65
+ export function when(condition, [ifTrue, ifFalse], options) {
66
+ return {
67
+ [MATCH]: true,
68
+ selector: condition,
69
+ cases: ifFalse ? { true: ifTrue, false: ifFalse } : { true: ifTrue },
70
+ cache: options?.cache ?? false,
71
+ };
72
+ }
73
+ /**
74
+ * Conditional rendering for multiple cases.
75
+ * Prevents re-renders when the selector result stays the same.
76
+ *
77
+ * @param selector - Function that returns a key value
78
+ * @param cases - Object mapping keys to template factories. Use `_` for default case.
79
+ * @param options - Optional settings. cache: false (default) disposes branches on switch.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * // Loading states (no caching needed)
84
+ * html`${match(() => state.status, {
85
+ * loading: () => html`<Spinner />`,
86
+ * error: () => html`<Error message=${() => state.error} />`,
87
+ * success: () => html`<Data items=${() => state.items} />`,
88
+ * _: () => html`<Fallback />`,
89
+ * })}`
90
+ *
91
+ * // Tab switching with caching
92
+ * html`${match(() => state.tab, {
93
+ * home: () => html`<Home />`,
94
+ * settings: () => html`<Settings />`
95
+ * }, { cache: true })}`
96
+ * ```
97
+ */
98
+ export function match(selector, cases, options) {
99
+ return {
100
+ [MATCH]: true,
101
+ selector,
102
+ cases: cases,
103
+ cache: options?.cache ?? false,
104
+ };
105
+ }
106
+ /**
107
+ * Plugin that handles match/when descriptors.
108
+ * Register with: const html = baseHtml.with(matchPlugin);
109
+ */
110
+ const matchPlugin = (value) => {
111
+ if (!isMatchDescriptor(value))
112
+ return null;
113
+ return (marker, disposers) => {
114
+ const startMarker = document.createComment("");
115
+ marker.parentNode.insertBefore(startMarker, marker);
116
+ let prevKey = null;
117
+ let prevBranch = null;
118
+ const branches = value.cache ? new Map() : null;
119
+ // Create computed that tracks the selector
120
+ const keyComputed = computed(() => String(value.selector()));
121
+ const update = () => {
122
+ const key = keyComputed.value;
123
+ const parent = marker.parentNode;
124
+ if (!parent)
125
+ return; // Marker detached, skip update
126
+ // Same key - nothing to do (internal bindings handle updates)
127
+ if (key === prevKey)
128
+ return;
129
+ // Remove current nodes from DOM
130
+ for (let n = startMarker.nextSibling; n && n !== marker;) {
131
+ const next = n.nextSibling;
132
+ if (branches && prevBranch) {
133
+ // Caching: save nodes for later
134
+ prevBranch.nodes.push(n);
135
+ }
136
+ parent.removeChild(n);
137
+ n = next;
138
+ }
139
+ // Dispose previous branch if not caching
140
+ if (!branches && prevBranch) {
141
+ prevBranch.dispose();
142
+ prevBranch = null;
143
+ }
144
+ // Get or create branch
145
+ let branch = branches?.get(key);
146
+ if (!branch) {
147
+ const factory = value.cases[key] ?? value.cases["_"];
148
+ if (!factory) {
149
+ // No matching case - render nothing
150
+ prevKey = key;
151
+ prevBranch = null;
152
+ return;
153
+ }
154
+ const { fragment, dispose } = factory().render();
155
+ parent.insertBefore(fragment, marker);
156
+ branch = { nodes: [], dispose };
157
+ branches?.set(key, branch);
158
+ }
159
+ else {
160
+ // Re-insert cached branch nodes
161
+ for (const n of branch.nodes) {
162
+ parent.insertBefore(n, marker);
163
+ }
164
+ branch.nodes.length = 0; // Clear after re-inserting (reuses array)
165
+ }
166
+ prevKey = key;
167
+ prevBranch = branch;
168
+ };
169
+ // Initial render
170
+ update();
171
+ // Subscribe to key changes
172
+ const unsub = keyComputed.subscribe(update);
173
+ disposers.push(() => {
174
+ unsub();
175
+ keyComputed.dispose();
176
+ // Dispose all branches
177
+ if (branches) {
178
+ for (const branch of branches.values()) {
179
+ branch.dispose();
180
+ }
181
+ branches.clear();
182
+ }
183
+ else if (prevBranch) {
184
+ prevBranch.dispose();
185
+ }
186
+ startMarker.remove();
187
+ });
188
+ };
189
+ };
190
+ export default matchPlugin;
191
+ //# sourceMappingURL=match.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"match.js","sourceRoot":"","sources":["../../src/match.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Match plugin - Conditional rendering with optional branch caching.\n *\n * This module provides opt-in support for conditional rendering,\n * where branches are rendered based on selector results rather than\n * underlying data changes.\n *\n * By default, branches are disposed when switching away (cache: false).\n * Set cache: true to keep branches in memory for instant re-switching.\n *\n * @example\n * ```ts\n * import { html as baseHtml } from \"balises\";\n * import matchPlugin, { when, match } from \"balises/match\";\n *\n * const html = baseHtml.with(matchPlugin);\n *\n * // Default: branches disposed on switch\n * html`${when(() => !!state.user, [\n * () => html`<Profile />`,\n * () => html`<Login />`\n * ])}`.render();\n *\n * // With caching: branches preserved for instant re-switching\n * html`${match(() => state.tab, {\n * home: () => html`<Home />`,\n * settings: () => html`<Settings />`\n * }, { cache: true })}`.render();\n * ```\n */\n\nimport { computed } from \"./signals/computed.js\";\nimport { Template, type InterpolationPlugin } from \"./template.js\";\n\nconst MATCH = Symbol(\"match\");\n\n/** Options for when/match behavior */\nexport interface MatchOptions {\n /**\n * Whether to cache branches when switching away.\n * - false (default): Dispose branches when hidden, recreate when revisited\n * - true: Keep branches in memory for instant re-switching\n */\n cache?: boolean;\n}\n\ninterface MatchDescriptor {\n readonly [MATCH]: true;\n /** @internal */ selector: () => unknown;\n /** @internal */ cases: Record<string, () => Template>;\n /** @internal */ cache: boolean;\n}\n\nfunction isMatchDescriptor(v: unknown): v is MatchDescriptor {\n return v != null && typeof v === \"object\" && MATCH in v;\n}\n\n/**\n * Conditional rendering for boolean conditions.\n * Prevents re-renders when the condition result stays the same.\n *\n * @param condition - Function that returns a boolean\n * @param branches - Array of [ifTrue, ifFalse?] factory functions. If ifFalse is omitted, renders nothing when false.\n * @param options - Optional settings. cache: false (default) disposes branches on switch.\n *\n * @example\n * ```ts\n * // Render only when true\n * html`${when(() => !!state.user, [\n * () => html`<Profile user=${state.user} />`\n * ])}`\n *\n * // Render different content for true/false\n * html`${when(() => !!state.user, [\n * () => html`<Profile user=${state.user} />`,\n * () => html`<LoginPrompt />`\n * ])}`\n *\n * // With caching for instant switching\n * html`${when(() => state.expanded, [\n * () => html`<ExpandedView />`,\n * () => html`<CollapsedView />`\n * ], { cache: true })}`\n * ```\n */\nexport function when(\n condition: () => boolean,\n [ifTrue, ifFalse]: [() => Template, (() => Template)?],\n options?: MatchOptions,\n): MatchDescriptor {\n return {\n [MATCH]: true,\n selector: condition,\n cases: ifFalse ? { true: ifTrue, false: ifFalse } : { true: ifTrue },\n cache: options?.cache ?? false,\n };\n}\n\n/**\n * Conditional rendering for multiple cases.\n * Prevents re-renders when the selector result stays the same.\n *\n * @param selector - Function that returns a key value\n * @param cases - Object mapping keys to template factories. Use `_` for default case.\n * @param options - Optional settings. cache: false (default) disposes branches on switch.\n *\n * @example\n * ```ts\n * // Loading states (no caching needed)\n * html`${match(() => state.status, {\n * loading: () => html`<Spinner />`,\n * error: () => html`<Error message=${() => state.error} />`,\n * success: () => html`<Data items=${() => state.items} />`,\n * _: () => html`<Fallback />`,\n * })}`\n *\n * // Tab switching with caching\n * html`${match(() => state.tab, {\n * home: () => html`<Home />`,\n * settings: () => html`<Settings />`\n * }, { cache: true })}`\n * ```\n */\nexport function match<K extends string | number>(\n selector: () => K,\n cases: Partial<Record<K, () => Template>> & { _?: () => Template },\n options?: MatchOptions,\n): MatchDescriptor {\n return {\n [MATCH]: true,\n selector,\n cases: cases as Record<string, () => Template>,\n cache: options?.cache ?? false,\n };\n}\n\n/** Cache entry per branch: cached nodes and dispose function */\ninterface BranchEntry {\n nodes: Node[];\n dispose: () => void;\n}\n\n/**\n * Plugin that handles match/when descriptors.\n * Register with: const html = baseHtml.with(matchPlugin);\n */\nconst matchPlugin: InterpolationPlugin = (value) => {\n if (!isMatchDescriptor(value)) return null;\n\n return (marker, disposers) => {\n const startMarker = document.createComment(\"\");\n marker.parentNode!.insertBefore(startMarker, marker);\n\n let prevKey: string | null = null;\n let prevBranch: BranchEntry | null = null;\n const branches = value.cache ? new Map<string, BranchEntry>() : null;\n\n // Create computed that tracks the selector\n const keyComputed = computed(() => String(value.selector()));\n\n const update = () => {\n const key = keyComputed.value;\n const parent = marker.parentNode;\n if (!parent) return; // Marker detached, skip update\n\n // Same key - nothing to do (internal bindings handle updates)\n if (key === prevKey) return;\n\n // Remove current nodes from DOM\n for (let n = startMarker.nextSibling; n && n !== marker; ) {\n const next = n.nextSibling;\n if (branches && prevBranch) {\n // Caching: save nodes for later\n prevBranch.nodes.push(n);\n }\n parent.removeChild(n);\n n = next;\n }\n\n // Dispose previous branch if not caching\n if (!branches && prevBranch) {\n prevBranch.dispose();\n prevBranch = null;\n }\n\n // Get or create branch\n let branch = branches?.get(key);\n if (!branch) {\n const factory = value.cases[key] ?? value.cases[\"_\"];\n if (!factory) {\n // No matching case - render nothing\n prevKey = key;\n prevBranch = null;\n return;\n }\n const { fragment, dispose } = factory().render();\n parent.insertBefore(fragment, marker);\n branch = { nodes: [], dispose };\n branches?.set(key, branch);\n } else {\n // Re-insert cached branch nodes\n for (const n of branch.nodes) {\n parent.insertBefore(n, marker);\n }\n branch.nodes.length = 0; // Clear after re-inserting (reuses array)\n }\n\n prevKey = key;\n prevBranch = branch;\n };\n\n // Initial render\n update();\n\n // Subscribe to key changes\n const unsub = keyComputed.subscribe(update);\n\n disposers.push(() => {\n unsub();\n keyComputed.dispose();\n // Dispose all branches\n if (branches) {\n for (const branch of branches.values()) {\n branch.dispose();\n }\n branches.clear();\n } else if (prevBranch) {\n prevBranch.dispose();\n }\n startMarker.remove();\n });\n };\n};\n\nexport default matchPlugin;\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "balises",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "A very simple reactive html templating library",
5
5
  "author": "Julien Elbaz <elbywan@hotmail.com>",
6
6
  "type": "module",
@@ -60,6 +60,10 @@
60
60
  "./each": {
61
61
  "import": "./dist/esm/each.js",
62
62
  "types": "./dist/esm/each.d.ts"
63
+ },
64
+ "./match": {
65
+ "import": "./dist/esm/match.js",
66
+ "types": "./dist/esm/match.d.ts"
63
67
  }
64
68
  },
65
69
  "scripts": {
@@ -98,6 +102,6 @@
98
102
  "typescript-eslint": "8.50.1"
99
103
  },
100
104
  "dependencies": {
101
- "balises": "0.8.2"
105
+ "balises": "0.8.4"
102
106
  }
103
107
  }