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 +102 -16
- package/dist/esm/async.d.ts +10 -1
- package/dist/esm/async.d.ts.map +1 -1
- package/dist/esm/async.js +2 -1
- package/dist/esm/async.js.map +1 -1
- package/dist/esm/match.d.ts +111 -0
- package/dist/esm/match.d.ts.map +1 -0
- package/dist/esm/match.js +191 -0
- package/dist/esm/match.js.map +1 -0
- package/package.json +6 -2
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
|
|
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.
|
|
789
|
+
│ #1 🏆 │ preact@1.12.1 │ 0.000 │ 63.20 │ 1.00x (baseline) │
|
|
704
790
|
├───────┼───────────────────┼───────┼───────────────┼──────────────────┤
|
|
705
|
-
│ #2 │ balises@0.8.
|
|
791
|
+
│ #2 │ balises@0.8.3 │ 0.059 │ 90.83 │ 1.44x │
|
|
706
792
|
├───────┼───────────────────┼───────┼───────────────┼──────────────────┤
|
|
707
|
-
│ #3 │ vue@3.5.26 │ 0.103 │ 93.
|
|
793
|
+
│ #3 │ vue@3.5.26 │ 0.103 │ 93.43 │ 1.48x │
|
|
708
794
|
├───────┼───────────────────┼───────┼───────────────┼──────────────────┤
|
|
709
|
-
│ #4 │ maverick@6.0.0 │ 0.
|
|
795
|
+
│ #4 │ maverick@6.0.0 │ 0.152 │ 122.20 │ 1.93x │
|
|
710
796
|
├───────┼───────────────────┼───────┼───────────────┼──────────────────┤
|
|
711
|
-
│ #5 │ usignal@0.10.0 │ 0.
|
|
797
|
+
│ #5 │ usignal@0.10.0 │ 0.187 │ 132.51 │ 2.10x │
|
|
712
798
|
├───────┼───────────────────┼───────┼───────────────┼──────────────────┤
|
|
713
|
-
│ #6 │ angular@19.2.17 │ 0.
|
|
799
|
+
│ #6 │ angular@19.2.17 │ 0.215 │ 161.81 │ 2.56x │
|
|
714
800
|
├───────┼───────────────────┼───────┼───────────────┼──────────────────┤
|
|
715
|
-
│ #7 │ solid@1.9.10 │ 0.342 │
|
|
801
|
+
│ #7 │ solid@1.9.10 │ 0.342 │ 255.37 │ 4.04x │
|
|
716
802
|
├───────┼───────────────────┼───────┼───────────────┼──────────────────┤
|
|
717
|
-
│ #8 │ mobx@6.15.0 │ 0.
|
|
803
|
+
│ #8 │ mobx@6.15.0 │ 0.858 │ 889.72 │ 14.08x │
|
|
718
804
|
├───────┼───────────────────┼───────┼───────────────┼──────────────────┤
|
|
719
|
-
│ #9 │ hyperactiv@0.11.3 │ 1.000 │
|
|
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.
|
|
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-
|
|
850
|
+
_Last updated: 2026-01-21_
|
|
765
851
|
|
|
766
852
|
<!-- BENCHMARK_RESULTS_END -->
|
|
767
853
|
|
package/dist/esm/async.d.ts
CHANGED
|
@@ -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(
|
|
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.
|
package/dist/esm/async.d.ts.map
CHANGED
|
@@ -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
|
|
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;
|
package/dist/esm/async.js.map
CHANGED
|
@@ -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.
|
|
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.
|
|
105
|
+
"balises": "0.8.4"
|
|
102
106
|
}
|
|
103
107
|
}
|