balises 0.3.0 → 0.4.0

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
@@ -4,15 +4,37 @@
4
4
  <img alt="balises" src="./assets/logo.svg" width="280">
5
5
  </picture>
6
6
 
7
- ### A minimal reactive HTML templating library. ~3.0KB gzipped.
7
+ ### A minimal reactive HTML templating library for building websites and web components. ~3.0KB gzipped.
8
+
9
+ Balises gives you reactive signals and HTML templates without the framework overhead. Works great with custom elements, vanilla JavaScript projects, or anywhere you need dynamic UIs but don't want to pull in React.
10
+
11
+ **You can also use it as a standalone signals library** - the reactivity system works independently of the templating, making it useful for any JavaScript project that needs reactive state management.
8
12
 
9
13
  **[📚ïļ Documentation & Examples](https://elbywan.github.io/balises/)**
10
14
 
11
- ## Note
15
+ ## Preamble
16
+
17
+ > [!WARNING]
18
+ > 🚧 Use at your own discretion .
19
+
20
+ This library was built in a couple of days **using LLM assistance** as an experiment to see if it was possible to produce something high-quality and performant very quickly.
21
+
22
+ It all begun with me needing a lightweight reactive templating solution with zero dependencies for a non-critical work project at [Datadog](https://www.datadoghq.com/), and since I wanted to explore what modern AI-assisted development could achieve it was a good fit.
23
+
24
+ Ultimately it turns out that I am quite happy with the result! It is quite performant, ergonomic, has a very small bundle size, is thoroughly tested and suits my needs well. 🌟
12
25
 
13
- This is a personal side project with limited maintenance. Most of the code was written with LLM assistance.
26
+ **However, please be aware that this is a personal side project with limited maintenance and no guarantees of long-term support.**
14
27
 
15
- 🚧 Use at your own discretion.
28
+ ## Table of Contents
29
+
30
+ - [Installation](#installation)
31
+ - [Quick Start](#quick-start)
32
+ - [Building Web Components](#building-web-components)
33
+ - [Composable Function Components](#composable-function-components)
34
+ - [Template Syntax](#template-syntax)
35
+ - [Reactivity API](#reactivity-api)
36
+ - [Tree-Shaking / Modular Imports](#tree-shaking--modular-imports)
37
+ - [Benchmarks](#benchmarks)
16
38
 
17
39
  ## Installation
18
40
 
@@ -22,6 +44,8 @@ npm install balises
22
44
 
23
45
  ## Quick Start
24
46
 
47
+ Balises uses tagged template literals to create reactive HTML. Just interpolate signals into your markup and they'll automatically update the DOM when they change.
48
+
25
49
  ```ts
26
50
  import { html, signal } from "balises";
27
51
 
@@ -37,9 +61,72 @@ document.body.appendChild(fragment);
37
61
  // Call dispose() when done to clean up subscriptions
38
62
  ```
39
63
 
64
+ ## Building Web Components
65
+
66
+ Balises works naturally with the Web Components API. Just render templates in `connectedCallback` and clean up in `disconnectedCallback`.
67
+
68
+ ```ts
69
+ import { html, signal, effect } from "balises";
70
+
71
+ class Counter extends HTMLElement {
72
+ #count = signal(0);
73
+ #dispose?: () => void;
74
+
75
+ connectedCallback() {
76
+ // Auto-sync to localStorage
77
+ const syncEffect = effect(() => {
78
+ localStorage.setItem("counter", String(this.#count.value));
79
+ });
80
+
81
+ const { fragment, dispose } = html`
82
+ <div>
83
+ <p>Count: ${this.#count}</p>
84
+ <button @click=${() => this.#count.update((n) => n - 1)}>-</button>
85
+ <button @click=${() => this.#count.update((n) => n + 1)}>+</button>
86
+ </div>
87
+ `.render();
88
+
89
+ this.appendChild(fragment);
90
+ this.#dispose = () => {
91
+ syncEffect();
92
+ dispose();
93
+ };
94
+ }
95
+
96
+ disconnectedCallback() {
97
+ this.#dispose?.();
98
+ }
99
+ }
100
+
101
+ customElements.define("x-counter", Counter);
102
+ ```
103
+
104
+ Use it in your HTML:
105
+
106
+ ```html
107
+ <x-counter></x-counter>
108
+ ```
109
+
110
+ You can build entire apps this way, or just add interactive widgets to existing pages. No build step required if you use it from a CDN.
111
+
112
+ ## Composable Function Components
113
+
114
+ You can also use plain functions that return templates. Pass the store and access its properties in function wrappers to keep things reactive:
115
+
116
+ ```ts
117
+ function Counter({ state }) {
118
+ return html`
119
+ <button @click=${() => state.count++}>${() => state.count}</button>
120
+ `;
121
+ }
122
+
123
+ const state = store({ count: 0 });
124
+ html`<div>${Counter({ state })}</div>`.render();
125
+ ```
126
+
40
127
  ## Template Syntax
41
128
 
42
- The `html` tagged template creates reactive DOM fragments.
129
+ The `html` tagged template creates reactive DOM fragments. When you interpolate a signal, that specific part of the DOM updates automatically when the signal changes.
43
130
 
44
131
  ### Interpolation Types
45
132
 
@@ -53,6 +140,20 @@ The `html` tagged template creates reactive DOM fragments.
53
140
 
54
141
  All interpolations accept reactive values (`Signal` or `Computed`) and will auto-update when they change.
55
142
 
143
+ ### Function Interpolation
144
+
145
+ Functions are wrapped in `computed()` automatically:
146
+
147
+ ```ts
148
+ const state = store({ count: 0 });
149
+
150
+ html`
151
+ <p>Count: ${() => state.count}</p>
152
+ <p>Doubled: ${() => state.count * 2}</p>
153
+ ${() => (state.count > 10 ? html`<p>High score!</p>` : null)}
154
+ `.render();
155
+ ```
156
+
56
157
  ### Nested Templates
57
158
 
58
159
  Templates can be nested, and arrays of templates are flattened:
@@ -62,14 +163,14 @@ const items = signal(["a", "b", "c"]);
62
163
 
63
164
  html`
64
165
  <ul>
65
- ${computed(() => items.value.map((item) => html`<li>${item}</li>`))}
166
+ ${() => items.value.map((item) => html`<li>${item}</li>`)}
66
167
  </ul>
67
168
  `.render();
68
169
  ```
69
170
 
70
171
  ### Efficient List Rendering with `each()`
71
172
 
72
- For lists that change frequently, use `each()` for keyed reconciliation. Templates are cached by key and reused:
173
+ When rendering lists that change frequently, use `each()` for keyed reconciliation. It caches templates by key so items can be reordered, added, or removed without recreating the DOM nodes:
73
174
 
74
175
  ```ts
75
176
  import { html, signal, each } from "balises";
@@ -99,34 +200,20 @@ items.value[0].name.value = "Alicia";
99
200
  items.value = [...items.value].reverse();
100
201
  ```
101
202
 
102
- The `each()` helper has two forms:
103
-
104
- **With key function (recommended when objects may be recreated with same identity):**
105
-
106
- ```ts
107
- each(list, keyFn, renderFn);
108
- ```
109
-
110
- - `list` - A reactive array (Signal, Computed, or getter function)
111
- - `keyFn` - Extracts a unique key from each item: `(item, index) => key`
112
- - `renderFn` - Renders each item (called once per unique key)
113
-
114
- **Without key function (automatic keying):**
203
+ Signatures:
115
204
 
116
205
  ```ts
117
- each(list, renderFn);
206
+ each(list, keyFn, renderFn); // keyed by keyFn(item, index)
207
+ each(list, renderFn); // keyed by object reference or index
118
208
  ```
119
209
 
120
- - Objects use their reference as key (reordering works correctly)
121
- - Primitives use index as key (duplicates are handled correctly)
122
-
123
- For content updates without list reconciliation, use nested signals (like `name: signal("Alice")` above).
210
+ If you want to update item content without triggering list reconciliation, nest signals inside your items (like `name: signal("Alice")` above).
124
211
 
125
212
  ## Reactivity API
126
213
 
127
- ### `signal<T>(value)` / `new Signal<T>(value)`
214
+ ### `signal<T>(value)`
128
215
 
129
- Creates a reactive value container.
216
+ Wraps a value to make it reactive.
130
217
 
131
218
  ```ts
132
219
  const name = signal("world");
@@ -134,7 +221,7 @@ console.log(name.value); // "world"
134
221
  name.value = "everyone"; // Notifies subscribers
135
222
  ```
136
223
 
137
- **Updating based on current value:**
224
+ **Updating based on the current value:**
138
225
 
139
226
  ```ts
140
227
  const count = signal(0);
@@ -148,9 +235,9 @@ count.value = count.value + 1;
148
235
  count.value = count.value * 2;
149
236
  ```
150
237
 
151
- ### `computed<T>(fn)` / `new Computed<T>(fn)`
238
+ ### `computed<T>(fn)`
152
239
 
153
- Creates a derived value that auto-tracks dependencies.
240
+ Derives a value from other signals. Automatically tracks dependencies.
154
241
 
155
242
  ```ts
156
243
  const firstName = signal("John");
@@ -162,11 +249,11 @@ firstName.value = "Jane";
162
249
  console.log(fullName.value); // "Jane Doe"
163
250
  ```
164
251
 
165
- Computeds are lazy - they only recompute when accessed and when their dependencies have changed.
252
+ Computed values are lazy - they only recalculate when accessed and a dependency has changed.
166
253
 
167
254
  ### `effect(fn)`
168
255
 
169
- Creates a side effect that automatically re-runs when its dependencies change. Unlike `computed()`, effects run immediately and are intended for side effects like DOM updates, logging, or persistence.
256
+ Runs a side effect whenever its dependencies change. Under the hood, `effect()` is a computed with an automatic subscription, which makes it run eagerly on every dependency change rather than waiting to be accessed.
170
257
 
171
258
  ```ts
172
259
  import { signal, effect } from "balises";
@@ -183,14 +270,14 @@ count.value = 1; // Logs "Count is now: 1" and updates title
183
270
  dispose(); // Stop the effect
184
271
  ```
185
272
 
186
- **Use cases:**
273
+ Good for things like:
187
274
 
188
275
  - Syncing state to localStorage
189
- - Updating document.title or other DOM properties
276
+ - Updating document.title or other globals
190
277
  - Logging and analytics
191
- - Network requests triggered by state changes
278
+ - Network requests based on state
192
279
 
193
- Effects are automatically disposed when the component that created them is disposed (via the template's `dispose()` function).
280
+ When you call `dispose()` on a template, any effects created during rendering are cleaned up automatically.
194
281
 
195
282
  **Example: Auto-sync to localStorage**
196
283
 
@@ -207,7 +294,7 @@ favorites.value = [...favorites.value, "new item"];
207
294
 
208
295
  ### `store<T>(obj)`
209
296
 
210
- Proxy-based reactive wrapper. Nested plain objects are wrapped recursively.
297
+ A proxy-based alternative to signals. Nested plain objects become reactive automatically.
211
298
 
212
299
  ```ts
213
300
  const state = store({ count: 0, user: { name: "Alice" } });
@@ -215,7 +302,7 @@ state.count++; // Reactive
215
302
  state.user.name = "Bob"; // Also reactive (nested)
216
303
  ```
217
304
 
218
- **Note:** Array mutations like `push()`, `pop()`, `splice()` do **not** trigger reactivity. To update arrays reactively, reassign them:
305
+ **Note:** Array mutations like `push()`, `pop()`, `splice()` do **not** trigger reactivity. You need to reassign the array:
219
306
 
220
307
  ```ts
221
308
  const state = store({ items: [1, 2, 3] });
@@ -226,7 +313,7 @@ state.items.push(4);
226
313
  // ✅ Triggers reactivity
227
314
  state.items = [...state.items, 4];
228
315
 
229
- // Alternative: Use signal for arrays if you want .update() method
316
+ // Alternative: Use signal for arrays to get the .update() helper
230
317
  const items = signal([1, 2, 3]);
231
318
  items.update((arr) => [...arr, 4]);
232
319
  items.update((arr) => arr.filter((n) => n !== 2));
@@ -234,7 +321,7 @@ items.update((arr) => arr.filter((n) => n !== 2));
234
321
 
235
322
  ### `batch<T>(fn)`
236
323
 
237
- Batch multiple signal updates to defer subscriber notifications until the batch completes.
324
+ Batches multiple signal updates so subscribers only get notified once at the end.
238
325
 
239
326
  ```ts
240
327
  import { batch, signal } from "balises";
@@ -250,7 +337,7 @@ batch(() => {
250
337
 
251
338
  ### `scope(fn)`
252
339
 
253
- Create a disposal scope that automatically collects all computeds and effects created within, allowing cleanup with a single `dispose()` call.
340
+ Groups reactive primitives together so you can dispose them all at once.
254
341
 
255
342
  ```ts
256
343
  import { scope, signal, computed, effect } from "balises";
@@ -268,7 +355,7 @@ const [state, dispose] = scope(() => {
268
355
  dispose();
269
356
  ```
270
357
 
271
- Useful for components, temporary reactive contexts, or any scenario where you want automatic cleanup of multiple reactive primitives.
358
+ Handy for components or temporary reactive contexts where you need bulk cleanup.
272
359
 
273
360
  ### `isSignal(value)`
274
361
 
@@ -296,7 +383,7 @@ unsubscribe(); // Stop listening
296
383
 
297
384
  ### `.dispose()`
298
385
 
299
- Dispose a computed, removing all dependency links.
386
+ Stops a computed from tracking dependencies and frees memory.
300
387
 
301
388
  ```ts
302
389
  const doubled = computed(() => count.value * 2);
@@ -305,13 +392,13 @@ doubled.dispose(); // Stops tracking, frees memory
305
392
 
306
393
  ## Tree-Shaking / Modular Imports
307
394
 
308
- The library supports granular imports for optimal bundle size:
395
+ You can import just what you need to keep bundle size down:
309
396
 
310
397
  ```ts
311
398
  // Full library (~3.0KB gzipped)
312
399
  import { html, signal, computed, effect } from "balises";
313
400
 
314
- // Signals only
401
+ // Signals only (no HTML templating - use in any JS project)
315
402
  import { signal, computed, effect, store, batch, scope } from "balises/signals";
316
403
 
317
404
  // Individual modules
@@ -322,6 +409,25 @@ import { store } from "balises/signals/store";
322
409
  import { batch, scope } from "balises/signals/context";
323
410
  ```
324
411
 
412
+ ### Using as a Standalone Signals Library
413
+
414
+ The reactivity system is completely independent of the HTML templating. You can use just the signals in Node.js, Electron, or any JavaScript environment:
415
+
416
+ ```ts
417
+ import { signal, computed, effect } from "balises/signals";
418
+
419
+ // Reactive state management without DOM
420
+ const users = signal([]);
421
+ const userCount = computed(() => users.value.length);
422
+
423
+ effect(() => {
424
+ console.log(`Total users: ${userCount.value}`);
425
+ });
426
+
427
+ users.value = [{ name: "Alice" }, { name: "Bob" }];
428
+ // Logs: "Total users: 2"
429
+ ```
430
+
325
431
  ## Full Example
326
432
 
327
433
  ```ts
@@ -336,7 +442,7 @@ class Counter extends HTMLElement {
336
442
 
337
443
  const { fragment, dispose } = html`
338
444
  <div>
339
- <p>Count: ${computed(() => state.count)} (double: ${double})</p>
445
+ <p>Count: ${() => state.count} (double: ${double})</p>
340
446
  <button @click=${() => state.count++}>+</button>
341
447
  <button @click=${() => state.count--}>-</button>
342
448
  </div>
@@ -416,19 +522,19 @@ Performance comparison of Balises against other popular reactive libraries. Benc
416
522
  ┌───────┮───────────────────┮──────────┮───────────────┮──────────────────┐
417
523
  │ Rank │ Library │ Avg Rank │ Avg Time (Ξs) │ vs Fastest │
418
524
  ├───────┾───────────────────┾──────────┾───────────────┾──────────────────â”Ī
419
- │ #1 🏆 │ balises@0.2.1 │ 1.5 │ 69.76 │ 1.00x (baseline) │
525
+ │ #1 🏆 │ preact@1.12.1 │ 1.5 │ 47.33 │ 1.00x (baseline) │
420
526
  ├───────┾───────────────────┾──────────┾───────────────┾──────────────────â”Ī
421
- │ #2 │ preact@1.12.1 │ 1.7 │ 51.59 │ 0.74x │
527
+ │ #2 │ balises@0.4.0 │ 1.5 │ 71.15 │ 1.50x │
422
528
  ├───────┾───────────────────┾──────────┾───────────────┾──────────────────â”Ī
423
- │ #3 │ vue@3.5.26 │ 3.0 │ 72.79 │ 1.04x │
529
+ │ #3 │ vue@3.5.26 │ 3.2 │ 80.96 │ 1.71x │
424
530
  ├───────┾───────────────────┾──────────┾───────────────┾──────────────────â”Ī
425
- │ #4 │ maverick@6.0.0 │ 3.8 │ 91.40 │ 1.31x │
531
+ │ #4 │ maverick@6.0.0 │ 3.8 │ 89.56 │ 1.89x │
426
532
  ├───────┾───────────────────┾──────────┾───────────────┾──────────────────â”Ī
427
- │ #5 │ solid@1.9.10 │ 5.3 │ 225.26 │ 3.23x │
533
+ │ #5 │ solid@1.9.10 │ 5.2 │ 214.92 │ 4.54x │
428
534
  ├───────┾───────────────────┾──────────┾───────────────┾──────────────────â”Ī
429
- │ #6 │ mobx@6.15.0 │ 5.8 │ 719.17 │ 10.31x │
535
+ │ #6 │ mobx@6.15.0 │ 6.0 │ 623.36 │ 13.17x │
430
536
  ├───────┾───────────────────┾──────────┾───────────────┾──────────────────â”Ī
431
- │ #7 │ hyperactiv@0.11.3 │ 6.8 │ 833.03 │ 11.94x │
537
+ │ #7 │ hyperactiv@0.11.3 │ 6.8 │ 695.71 │ 14.70x │
432
538
  └───────â”ī───────────────────â”ī──────────â”ī───────────────â”ī──────────────────┘
433
539
  ```
434
540
 
@@ -438,17 +544,17 @@ Performance comparison of Balises against other popular reactive libraries. Benc
438
544
  ┌───────────────────┮───────────────┮─────────────┮────────────────┮────────────────────┮─────────────┮──────────────┮──────────┐
439
545
  │ Library │ S1: 1: Layers │ S2: 2: Wide │ S3: 3: Diamond │ S4: 4: Conditional │ S5: 5: List │ S6: 6: Batch │ Avg Rank │
440
546
  ├───────────────────┾───────────────┾─────────────┾────────────────┾────────────────────┾─────────────┾──────────────┾──────────â”Ī
441
- │ balises@0.2.1 │ #3 │ #1 🏆 │ #1 🏆 │ #2 │ #1 🏆 │ #1 🏆 │ 1.5 │
547
+ │ preact@1.12.1 │ #1 🏆 │ #1 🏆 │ #2 │ #1 🏆 │ #2 │ #2 │ 1.5 │
442
548
  ├───────────────────┾───────────────┾─────────────┾────────────────┾────────────────────┾─────────────┾──────────────┾──────────â”Ī
443
- │ preact@1.12.1 │ #1 🏆 │ #2 │ #2 │ #1 🏆 │ #2 │ #2 │ 1.7 │
549
+ │ balises@0.4.0 │ #2 │ #2 │ #1 🏆 │ #2 │ #1 🏆 │ #1 🏆 │ 1.5 │
444
550
  ├───────────────────┾───────────────┾─────────────┾────────────────┾────────────────────┾─────────────┾──────────────┾──────────â”Ī
445
- │ vue@3.5.26 │ #2 │ #3 │ #3 │ #3 │ #3 │ #4 │ 3.0 │
551
+ │ vue@3.5.26 │ #3 │ #3 │ #3 │ #3 │ #3 │ #4 │ 3.2 │
446
552
  ├───────────────────┾───────────────┾─────────────┾────────────────┾────────────────────┾─────────────┾──────────────┾──────────â”Ī
447
553
  │ maverick@6.0.0 │ #4 │ #4 │ #4 │ #4 │ #4 │ #3 │ 3.8 │
448
554
  ├───────────────────┾───────────────┾─────────────┾────────────────┾────────────────────┾─────────────┾──────────────┾──────────â”Ī
449
- │ solid@1.9.10 │ #5 │ #6 │ #5 │ #5 │ #6 │ #5 │ 5.3 │
555
+ │ solid@1.9.10 │ #5 │ #6 │ #5 │ #5 │ #5 │ #5 │ 5.2 │
450
556
  ├───────────────────┾───────────────┾─────────────┾────────────────┾────────────────────┾─────────────┾──────────────┾──────────â”Ī
451
- │ mobx@6.15.0 │ #7 │ #5 │ #6 │ #6 │ #5 │ #6 │ 5.8 │
557
+ │ mobx@6.15.0 │ #7 │ #5 │ #6 │ #6 │ #6 │ #6 │ 6.0 │
452
558
  ├───────────────────┾───────────────┾─────────────┾────────────────┾────────────────────┾─────────────┾──────────────┾──────────â”Ī
453
559
  │ hyperactiv@0.11.3 │ #6 │ #7 │ #7 │ #7 │ #7 │ #7 │ 6.8 │
454
560
  └───────────────────â”ī───────────────â”ī─────────────â”ī────────────────â”ī────────────────────â”ī─────────────â”ī──────────────â”ī──────────┘
@@ -465,11 +571,11 @@ Performance comparison of Balises against other popular reactive libraries. Benc
465
571
 
466
572
  **Interpretation:**
467
573
 
468
- - Balises excels at diamond dependencies, list operations, and batching while maintaining competitive performance across all scenarios
469
- - Results show pure reactivity performance - real-world apps should consider framework ecosystem, DX, and specific use cases
574
+ - Balises performs well across all scenarios, particularly excelling at diamond dependencies, list operations, and batching
575
+ - These are synthetic benchmarks measuring pure reactivity - real apps should consider the whole picture (ecosystem, docs, community, etc.)
470
576
  - Lower rank = better performance
471
577
 
472
- _Last updated: 2025-12-30_
578
+ _Last updated: 2025-12-31_
473
579
 
474
580
  <!-- BENCHMARK_RESULTS_END -->
475
581