@vanijs/vani 0.3.0 → 0.5.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/DOCS.md +811 -101
- package/README.md +29 -310
- package/dist/lib/index.d.mts +4 -3
- package/dist/lib/index.mjs +2 -1
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/jsx-dev-runtime.d.mts +1 -1
- package/dist/lib/jsx-dev-runtime.mjs +1 -1
- package/dist/lib/jsx-runtime-BK1MMitM.d.mts +287 -0
- package/dist/lib/jsx-runtime-BUSOLzm1.mjs +2 -0
- package/dist/lib/jsx-runtime-BUSOLzm1.mjs.map +1 -0
- package/dist/lib/jsx-runtime.d.mts +1 -26
- package/dist/lib/jsx-runtime.mjs +1 -1
- package/package.json +40 -26
- package/dist/lib/runtime-BIk4OH1t.mjs +0 -1
- package/dist/lib/runtime-Dp-nlil7.d.mts +0 -166
package/DOCS.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
# Vani Documentation
|
|
1
|
+
# Vani Framework Documentation
|
|
2
2
|
|
|
3
3
|
Vani is a small, dependency‑free UI runtime built around a simple idea:
|
|
4
4
|
|
|
5
5
|
> Rendering should be explicit, local, and predictable.
|
|
6
6
|
|
|
7
|
-
Vani is **not** a Virtual DOM, not reactive‑by‑default
|
|
8
|
-
DOM subtree delimited by anchors and only update when you
|
|
7
|
+
Vani is **not** a Virtual DOM, not reactive‑by‑default (signals are opt‑in), and not
|
|
8
|
+
compiler‑driven. Components own a DOM subtree delimited by anchors and only update when you
|
|
9
|
+
explicitly ask them to.
|
|
9
10
|
|
|
10
11
|
---
|
|
11
12
|
|
|
@@ -13,8 +14,29 @@ DOM subtree delimited by anchors and only update when you explicitly ask them to
|
|
|
13
14
|
|
|
14
15
|
```bash
|
|
15
16
|
pnpm add @vanijs/vani
|
|
17
|
+
# or
|
|
18
|
+
npm install @vanijs/vani
|
|
19
|
+
# or
|
|
20
|
+
yarn add @vanijs/vani
|
|
21
|
+
# or
|
|
22
|
+
bun add @vanijs/vani
|
|
23
|
+
# or
|
|
24
|
+
deno add @vanijs/vani
|
|
16
25
|
```
|
|
17
26
|
|
|
27
|
+
## Install skills (AI commands)
|
|
28
|
+
|
|
29
|
+
Vani ships AI skills (commands) you can add to your agent tooling:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx add-skill itsjavi/vani
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Use documentation as a context
|
|
36
|
+
|
|
37
|
+
You can use the documentation as a context by feeding your agents the doc file directly, or by using
|
|
38
|
+
[Context7](https://context7.com/itsjavi/vani) to feed your agents the doc file.
|
|
39
|
+
|
|
18
40
|
---
|
|
19
41
|
|
|
20
42
|
## Quick Start (SPA)
|
|
@@ -42,7 +64,7 @@ const Counter = component((_, handle: Handle) => {
|
|
|
42
64
|
const appRoot = document.getElementById('app')
|
|
43
65
|
if (!appRoot) throw new Error('#app not found')
|
|
44
66
|
|
|
45
|
-
renderToDOM(
|
|
67
|
+
renderToDOM(Counter(), appRoot)
|
|
46
68
|
```
|
|
47
69
|
|
|
48
70
|
---
|
|
@@ -52,6 +74,17 @@ renderToDOM([Counter()], appRoot)
|
|
|
52
74
|
Vani is JS-first and transpiler-free, with an optional JSX adapter. JSX mode requires
|
|
53
75
|
`jsxImportSource` to be set to `@vanijs/vani` and a `.tsx` file.
|
|
54
76
|
|
|
77
|
+
TypeScript config example:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"compilerOptions": {
|
|
82
|
+
"jsx": "react-jsx",
|
|
83
|
+
"jsxImportSource": "@vanijs/vani"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
55
88
|
### 1) JSX counter button
|
|
56
89
|
|
|
57
90
|
```tsx
|
|
@@ -72,7 +105,7 @@ const Counter = component((_, handle: Handle) => {
|
|
|
72
105
|
)
|
|
73
106
|
})
|
|
74
107
|
|
|
75
|
-
renderToDOM(
|
|
108
|
+
renderToDOM(Counter(), document.getElementById('app')!)
|
|
76
109
|
```
|
|
77
110
|
|
|
78
111
|
### 2) JSX component inside JS-first components
|
|
@@ -143,7 +176,7 @@ export function MyReactComponent() {
|
|
|
143
176
|
if (!containerRef.current) return
|
|
144
177
|
|
|
145
178
|
// Mount Vani into a React-managed DOM element (the mounting point)
|
|
146
|
-
vaniHandlesRef.current = renderToDOM(
|
|
179
|
+
vaniHandlesRef.current = renderToDOM(VaniCounter(), containerRef.current)
|
|
147
180
|
|
|
148
181
|
// Cleanup when React unmounts
|
|
149
182
|
return () => {
|
|
@@ -160,6 +193,148 @@ export function MyReactComponent() {
|
|
|
160
193
|
|
|
161
194
|
---
|
|
162
195
|
|
|
196
|
+
## Philosophy and positioning
|
|
197
|
+
|
|
198
|
+
### Core design principles
|
|
199
|
+
|
|
200
|
+
| Principle | Meaning |
|
|
201
|
+
| ----------------- | ----------------------------------------- |
|
|
202
|
+
| Explicit updates | Only `handle.update()` triggers rendering |
|
|
203
|
+
| Locality | Updates affect only the owning subtree |
|
|
204
|
+
| Determinism | Same inputs → same DOM mutations |
|
|
205
|
+
| No hidden work | No background tracking or diffing |
|
|
206
|
+
| Runtime clarity | Debuggable without tooling |
|
|
207
|
+
| Opt-in complexity | Advanced features are explicit |
|
|
208
|
+
|
|
209
|
+
### Goals
|
|
210
|
+
|
|
211
|
+
- Predictable performance at scale
|
|
212
|
+
- Leaf-only updates by default
|
|
213
|
+
- Zero runtime dependencies
|
|
214
|
+
- SSR without hydration heuristics
|
|
215
|
+
- Clear mental model for developers
|
|
216
|
+
- Long-term maintainability over convenience
|
|
217
|
+
- Web-standards-first
|
|
218
|
+
- ESM-first and designed to run in any modern environment
|
|
219
|
+
- Good and intuitive developer experience
|
|
220
|
+
- Reduce magic and complexity, give freedom back to the developers
|
|
221
|
+
|
|
222
|
+
### Ergonomic features
|
|
223
|
+
|
|
224
|
+
- Real HTML attribute names, with a small set of library-specific exceptions (`ref`, `key`,
|
|
225
|
+
`fallback`, `clientOnly`)
|
|
226
|
+
- Optional fine-grained state with `signal()`, `derive()`, `effect()`, plus `text()`/`attr()`
|
|
227
|
+
helpers
|
|
228
|
+
- Async components with fallbacks
|
|
229
|
+
- `onMount(getNodes, parent)` gives access to the rendered DOM subtree without ref plumbing,
|
|
230
|
+
aligning with explicit updates and simplifying vanilla JS integrations
|
|
231
|
+
- `className` accepts string, array, and object forms for ergonomic composition
|
|
232
|
+
- ESM-first and designed to run in any modern environment
|
|
233
|
+
|
|
234
|
+
### What Vani is NOT
|
|
235
|
+
|
|
236
|
+
- ❌ Not a Virtual DOM
|
|
237
|
+
- ❌ Not reactive-by-default (signals are opt-in)
|
|
238
|
+
- ❌ Not JSX-mandatory (optional adapter)
|
|
239
|
+
- ❌ Not compiler-driven
|
|
240
|
+
- ❌ Not a template language
|
|
241
|
+
- ❌ Not a framework that guesses intent
|
|
242
|
+
|
|
243
|
+
### Why not Web Components (yet)
|
|
244
|
+
|
|
245
|
+
Vani does not use Web Components today because the developer ergonomics are still rough and SSR
|
|
246
|
+
support is a key goal. We may revisit this if Web Components bring clear benefits without harming
|
|
247
|
+
productivity and cross-browser compatibility.
|
|
248
|
+
|
|
249
|
+
### Comparison with popular frameworks
|
|
250
|
+
|
|
251
|
+
| Feature / Framework | Vani | React | Vue | Svelte | Solid |
|
|
252
|
+
| ---------------------- | ---- | ----- | --- | ------ | ----- |
|
|
253
|
+
| Virtual DOM | ❌ | ✅ | ✅ | ❌ | ❌ |
|
|
254
|
+
| Implicit reactivity | ❌ | ⚠️ | ✅ | ✅ | ✅ |
|
|
255
|
+
| Compiler required | ❌ | ❌ | ❌ | ✅ | ❌ |
|
|
256
|
+
| JSX required | ❌ | ✅ | ❌ | ❌ | ❌ |
|
|
257
|
+
| Explicit updates | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
258
|
+
| Leaf-only updates | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
259
|
+
| Runtime-only | ✅ | ⚠️ | ⚠️ | ❌ | ⚠️ |
|
|
260
|
+
| SSR without heuristics | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
261
|
+
| Dependency-free core | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
262
|
+
|
|
263
|
+
⚠️ = partially / indirectly supported / average
|
|
264
|
+
|
|
265
|
+
The strength of Vani is its predictability and simplicity, while other frameworks focus on developer
|
|
266
|
+
productivity and ease of use, handling a lot of complexity behind the scenes automatically.
|
|
267
|
+
|
|
268
|
+
### Vani's sweet spot
|
|
269
|
+
|
|
270
|
+
✅ Perfect for:
|
|
271
|
+
|
|
272
|
+
- Dashboard widgets
|
|
273
|
+
- Micro-frontends
|
|
274
|
+
- Live-coding in the browser
|
|
275
|
+
- Embeddable components in other frameworks
|
|
276
|
+
- Performance-critical UIs where you need exact control
|
|
277
|
+
- Server-rendered sites
|
|
278
|
+
- Learning UI fundamentals (no magic, direct DOM)
|
|
279
|
+
- Lightweight SPAs or small Multi-Page Applications
|
|
280
|
+
|
|
281
|
+
❌ Not ideal for:
|
|
282
|
+
|
|
283
|
+
- Large, complex web applications with many interrelated states
|
|
284
|
+
- Teams that want framework conventions to handle complexity
|
|
285
|
+
- Projects needing a mature ecosystem
|
|
286
|
+
|
|
287
|
+
(at least not yet)
|
|
288
|
+
|
|
289
|
+
### Mental model
|
|
290
|
+
|
|
291
|
+
Think of Vani as:
|
|
292
|
+
|
|
293
|
+
> **“Manually invalidated, DOM-owned UI subtrees.”**
|
|
294
|
+
|
|
295
|
+
You decide:
|
|
296
|
+
|
|
297
|
+
- when something updates
|
|
298
|
+
- how much updates
|
|
299
|
+
- and why it updates
|
|
300
|
+
|
|
301
|
+
Nothing else happens behind your back.
|
|
302
|
+
|
|
303
|
+
### Who Vani is for
|
|
304
|
+
|
|
305
|
+
Vani is a good fit if you value:
|
|
306
|
+
|
|
307
|
+
- full control over rendering
|
|
308
|
+
- predictable performance
|
|
309
|
+
- small runtimes
|
|
310
|
+
- explicit data flow
|
|
311
|
+
- SSR without complexity
|
|
312
|
+
- understanding your tools deeply
|
|
313
|
+
|
|
314
|
+
It is **not** optimized for:
|
|
315
|
+
|
|
316
|
+
- rapid prototyping
|
|
317
|
+
- beginners
|
|
318
|
+
- implicit magic
|
|
319
|
+
- large teams that rely on conventions
|
|
320
|
+
|
|
321
|
+
### Status
|
|
322
|
+
|
|
323
|
+
Vani is experimental and evolving. The core architecture is intentionally small and stable.
|
|
324
|
+
|
|
325
|
+
Expect:
|
|
326
|
+
|
|
327
|
+
- iteration
|
|
328
|
+
- refinement
|
|
329
|
+
- careful additions
|
|
330
|
+
|
|
331
|
+
Not:
|
|
332
|
+
|
|
333
|
+
- rapid feature creep
|
|
334
|
+
- breaking conceptual changes
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
163
338
|
## Core Concepts
|
|
164
339
|
|
|
165
340
|
### 1) Components are functions
|
|
@@ -213,23 +388,267 @@ Each component owns a DOM range delimited by anchors:
|
|
|
213
388
|
|
|
214
389
|
Updates replace only the DOM between anchors.
|
|
215
390
|
|
|
391
|
+
### 3.1) Nested component hierarchies (isolated subtrees)
|
|
392
|
+
|
|
393
|
+
To build a nested tree, have a parent return child component instances as part of its render output.
|
|
394
|
+
Each component instance creates its own anchor range, so parent and child updates stay isolated.
|
|
395
|
+
|
|
396
|
+
#### How DOM isolation works
|
|
397
|
+
|
|
398
|
+
When you nest components, each component gets its own pair of `<!--vani:start-->` and
|
|
399
|
+
`<!--vani:end-->` comment anchors. The DOM structure for a parent containing a child looks like:
|
|
400
|
+
|
|
401
|
+
```html
|
|
402
|
+
<!--vani:start-->
|
|
403
|
+
<!-- Parent start -->
|
|
404
|
+
<div>
|
|
405
|
+
<div>Parent title</div>
|
|
406
|
+
<button>Rename parent</button>
|
|
407
|
+
<!--vani:start-->
|
|
408
|
+
<!-- Child start -->
|
|
409
|
+
<div>
|
|
410
|
+
Child clicks: 0
|
|
411
|
+
<button>Click child</button>
|
|
412
|
+
</div>
|
|
413
|
+
<!--vani:end-->
|
|
414
|
+
<!-- Child end -->
|
|
415
|
+
</div>
|
|
416
|
+
<!--vani:end-->
|
|
417
|
+
<!-- Parent end -->
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
When the parent updates, Vani replaces the DOM between the parent's anchors **but preserves the
|
|
421
|
+
child's anchors and their contents**. When the child updates, only the DOM between the child's
|
|
422
|
+
anchors is replaced. This anchor-based isolation is automatic—no special API is needed.
|
|
423
|
+
|
|
424
|
+
#### Update isolation rules
|
|
425
|
+
|
|
426
|
+
- **Parent update**: replaces parent’s subtree but leaves nested child anchor ranges intact.
|
|
427
|
+
- **Child update**: replaces only the child’s subtree; parent is unaffected.
|
|
428
|
+
- **Re-render with new props**: if the parent re-renders and returns the same child component with
|
|
429
|
+
new props, the child’s component instance is preserved (not recreated) and the child can read the
|
|
430
|
+
new props on its next `update()`.
|
|
431
|
+
|
|
432
|
+
#### Basic parent-child example
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
import { component, div, button, type Handle, type ComponentRef } from '@vanijs/vani'
|
|
436
|
+
|
|
437
|
+
const Child = component<{ label: string }>((props, handle: Handle) => {
|
|
438
|
+
let clicks = 0
|
|
439
|
+
return () =>
|
|
440
|
+
div(
|
|
441
|
+
`${props.label} clicks: ${clicks}`,
|
|
442
|
+
button(
|
|
443
|
+
{
|
|
444
|
+
onclick: () => {
|
|
445
|
+
clicks += 1
|
|
446
|
+
handle.update()
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
'Click child',
|
|
450
|
+
),
|
|
451
|
+
)
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
const Parent = component((_, handle: Handle) => {
|
|
455
|
+
let title = 'Parent'
|
|
456
|
+
const childRef: ComponentRef = { current: null }
|
|
457
|
+
|
|
458
|
+
const rename = () => {
|
|
459
|
+
title = title === 'Parent' ? 'Parent (renamed)' : 'Parent'
|
|
460
|
+
handle.update()
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return () =>
|
|
464
|
+
div(
|
|
465
|
+
div(`Title: ${title}`),
|
|
466
|
+
button({ onclick: rename }, 'Rename parent'),
|
|
467
|
+
// Nested component subtree (isolated updates)
|
|
468
|
+
Child({ ref: childRef, label: 'Child' }),
|
|
469
|
+
)
|
|
470
|
+
})
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
When you click "Rename parent", only the parent subtree updates. When you click "Click child", only
|
|
474
|
+
the child subtree updates.
|
|
475
|
+
|
|
476
|
+
#### Deeper nesting (grandparent → parent → child)
|
|
477
|
+
|
|
478
|
+
You can nest components to any depth. Each level maintains its own isolated subtree:
|
|
479
|
+
|
|
480
|
+
```ts
|
|
481
|
+
import { component, div, button, type Handle } from '@vanijs/vani'
|
|
482
|
+
|
|
483
|
+
const GrandChild = component<{ name: string }>((props, handle: Handle) => {
|
|
484
|
+
let value = 0
|
|
485
|
+
return () =>
|
|
486
|
+
div(
|
|
487
|
+
`GrandChild (${props.name}): ${value}`,
|
|
488
|
+
button(
|
|
489
|
+
{
|
|
490
|
+
onclick: () => {
|
|
491
|
+
value += 1
|
|
492
|
+
handle.update()
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
'+',
|
|
496
|
+
),
|
|
497
|
+
)
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
const Child = component<{ id: number }>((props, handle: Handle) => {
|
|
501
|
+
let label = `Child #${props.id}`
|
|
502
|
+
return () =>
|
|
503
|
+
div(
|
|
504
|
+
div(label),
|
|
505
|
+
button(
|
|
506
|
+
{
|
|
507
|
+
onclick: () => {
|
|
508
|
+
label += '!'
|
|
509
|
+
handle.update()
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
'Edit label',
|
|
513
|
+
),
|
|
514
|
+
GrandChild({ name: `gc-${props.id}` }),
|
|
515
|
+
)
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
const Parent = component((_, handle: Handle) => {
|
|
519
|
+
let count = 2
|
|
520
|
+
return () =>
|
|
521
|
+
div(
|
|
522
|
+
div(`Parent has ${count} children`),
|
|
523
|
+
button(
|
|
524
|
+
{
|
|
525
|
+
onclick: () => {
|
|
526
|
+
count += 1
|
|
527
|
+
handle.update()
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
'Add child',
|
|
531
|
+
),
|
|
532
|
+
...Array.from({ length: count }, (_, i) => Child({ id: i })),
|
|
533
|
+
)
|
|
534
|
+
})
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
Each `GrandChild` can update independently of its `Child`, and each `Child` can update independently
|
|
538
|
+
of the `Parent`. The anchor isolation means you can have deeply nested trees without cascading
|
|
539
|
+
re-renders.
|
|
540
|
+
|
|
541
|
+
#### Passing props and callbacks (prop drilling)
|
|
542
|
+
|
|
543
|
+
Props flow down explicitly. If a child needs data from an ancestor, pass it through props:
|
|
544
|
+
|
|
545
|
+
```ts
|
|
546
|
+
import { component, div, button, type Handle } from '@vanijs/vani'
|
|
547
|
+
|
|
548
|
+
const Display = component<{ value: number }>((props) => {
|
|
549
|
+
return () => div(`Current value: ${props.value}`)
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
const Controls = component<{ onIncrement: () => void }>((props) => {
|
|
553
|
+
return () => button({ onclick: props.onIncrement }, 'Increment')
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
const App = component((_, handle: Handle) => {
|
|
557
|
+
let count = 0
|
|
558
|
+
const increment = () => {
|
|
559
|
+
count += 1
|
|
560
|
+
handle.update()
|
|
561
|
+
}
|
|
562
|
+
return () => div(Display({ value: count }), Controls({ onIncrement: increment }))
|
|
563
|
+
})
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
When `increment` is called, the `App` re-renders. Because `Display` and `Controls` are nested
|
|
567
|
+
components with their own anchor ranges, their internal state (if any) is preserved. The new `value`
|
|
568
|
+
prop is available to `Display` on the next render.
|
|
569
|
+
|
|
570
|
+
#### Updating children from the parent via refs
|
|
571
|
+
|
|
572
|
+
If you need to trigger a child update without re-rendering the parent, store a `ComponentRef` and
|
|
573
|
+
call `update()` on it directly:
|
|
574
|
+
|
|
575
|
+
```ts
|
|
576
|
+
import { component, div, button, type Handle, type ComponentRef } from '@vanijs/vani'
|
|
577
|
+
|
|
578
|
+
const Counter = component<{ start: number }>((props, handle: Handle) => {
|
|
579
|
+
let count = props.start
|
|
580
|
+
return () =>
|
|
581
|
+
div(
|
|
582
|
+
`Count: ${count}`,
|
|
583
|
+
button(
|
|
584
|
+
{
|
|
585
|
+
onclick: () => {
|
|
586
|
+
count += 1
|
|
587
|
+
handle.update()
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
'+',
|
|
591
|
+
),
|
|
592
|
+
)
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
const Dashboard = component(() => {
|
|
596
|
+
const counterRef: ComponentRef = { current: null }
|
|
597
|
+
|
|
598
|
+
const resetCounter = () => {
|
|
599
|
+
// Update only the Counter subtree, not the Dashboard
|
|
600
|
+
counterRef.current?.update()
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return () =>
|
|
604
|
+
div(
|
|
605
|
+
Counter({ ref: counterRef, start: 0 }),
|
|
606
|
+
button({ onclick: resetCounter }, 'Refresh counter'),
|
|
607
|
+
)
|
|
608
|
+
})
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
Clicking "Refresh counter" calls `counterRef.current?.update()`, which re-renders only the `Counter`
|
|
612
|
+
subtree. The `Dashboard` itself does not re-render.
|
|
613
|
+
|
|
614
|
+
#### Summary
|
|
615
|
+
|
|
616
|
+
- Nesting is standard component composition—no special API.
|
|
617
|
+
- Each component instance owns an anchor-delimited DOM range.
|
|
618
|
+
- Updates are isolated: a component’s `handle.update()` only affects its own subtree.
|
|
619
|
+
- Props and callbacks flow down explicitly (prop drilling).
|
|
620
|
+
- Use `ComponentRef` to update children without re-rendering parents.
|
|
621
|
+
|
|
216
622
|
### 4) Lists and item-level updates
|
|
217
623
|
|
|
218
|
-
Lists
|
|
219
|
-
|
|
220
|
-
|
|
624
|
+
Lists scale well in Vani when each item is its own component. Every item owns a tiny subtree and can
|
|
625
|
+
update itself (or be updated via a ref) without touching siblings. Use `key` to preserve identity
|
|
626
|
+
across reorders.
|
|
221
627
|
|
|
222
628
|
Key ideas:
|
|
223
629
|
|
|
224
630
|
- Represent list data by id (Map or array + id).
|
|
225
631
|
- Render each row as a keyed component.
|
|
226
632
|
- Store a `ComponentRef` per id so you can call `ref.current?.update()` for that item only.
|
|
227
|
-
- Call
|
|
633
|
+
- Call `renderKeyedChildren()` for structural list changes (add/remove/reorder).
|
|
634
|
+
- Keys are only respected by `renderKeyedChildren()`. Passing keyed children into `div()` or another
|
|
635
|
+
component does not trigger keyed diffing.
|
|
228
636
|
|
|
229
637
|
Example:
|
|
230
638
|
|
|
231
639
|
```ts
|
|
232
|
-
import {
|
|
640
|
+
import {
|
|
641
|
+
component,
|
|
642
|
+
div,
|
|
643
|
+
ul,
|
|
644
|
+
li,
|
|
645
|
+
input,
|
|
646
|
+
button,
|
|
647
|
+
renderKeyedChildren,
|
|
648
|
+
type ComponentRef,
|
|
649
|
+
type DomRef,
|
|
650
|
+
type Handle,
|
|
651
|
+
} from '@vanijs/vani'
|
|
233
652
|
|
|
234
653
|
type Todo = { id: string; text: string; done: boolean }
|
|
235
654
|
|
|
@@ -267,6 +686,7 @@ const List = component((_, handle: Handle) => {
|
|
|
267
686
|
])
|
|
268
687
|
|
|
269
688
|
const refs = new Map<string, ComponentRef>()
|
|
689
|
+
const listRef: DomRef<HTMLUListElement> = { current: null }
|
|
270
690
|
const getRef = (id: string) => {
|
|
271
691
|
let ref = refs.get(id)
|
|
272
692
|
if (!ref) {
|
|
@@ -293,32 +713,43 @@ const List = component((_, handle: Handle) => {
|
|
|
293
713
|
|
|
294
714
|
const rename = (id: string, text: string) => updateItem(id, { text })
|
|
295
715
|
|
|
716
|
+
const renderRows = () => {
|
|
717
|
+
if (!listRef.current) return
|
|
718
|
+
renderKeyedChildren(
|
|
719
|
+
listRef.current,
|
|
720
|
+
order.map((id) =>
|
|
721
|
+
Row({
|
|
722
|
+
key: id,
|
|
723
|
+
ref: getRef(id),
|
|
724
|
+
id,
|
|
725
|
+
getItem,
|
|
726
|
+
onToggle: toggle,
|
|
727
|
+
onRename: rename,
|
|
728
|
+
}),
|
|
729
|
+
),
|
|
730
|
+
)
|
|
731
|
+
}
|
|
732
|
+
handle.onBeforeMount(() => {
|
|
733
|
+
queueMicrotask(renderRows)
|
|
734
|
+
})
|
|
735
|
+
|
|
296
736
|
const add = (text: string) => {
|
|
297
737
|
const id = String(order.length + 1)
|
|
298
738
|
items.set(id, { id, text, done: false })
|
|
299
739
|
order = [...order, id]
|
|
300
|
-
|
|
740
|
+
renderRows()
|
|
301
741
|
}
|
|
302
742
|
|
|
303
743
|
const remove = (id: string) => {
|
|
304
744
|
items.delete(id)
|
|
305
745
|
refs.delete(id)
|
|
306
746
|
order = order.filter((value) => value !== id)
|
|
307
|
-
|
|
747
|
+
renderRows()
|
|
308
748
|
}
|
|
309
749
|
|
|
310
750
|
return () =>
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
Row({
|
|
314
|
-
key: id,
|
|
315
|
-
ref: getRef(id),
|
|
316
|
-
id,
|
|
317
|
-
getItem,
|
|
318
|
-
onToggle: toggle,
|
|
319
|
-
onRename: rename,
|
|
320
|
-
}),
|
|
321
|
-
),
|
|
751
|
+
div(
|
|
752
|
+
ul({ ref: listRef }),
|
|
322
753
|
button({ onclick: () => add('New item') }, 'Add'),
|
|
323
754
|
button(
|
|
324
755
|
{
|
|
@@ -334,7 +765,7 @@ const List = component((_, handle: Handle) => {
|
|
|
334
765
|
```
|
|
335
766
|
|
|
336
767
|
This pattern keeps updates local: changing an item triggers only that row’s subtree update, while
|
|
337
|
-
structural
|
|
768
|
+
structural changes are handled by `renderKeyedChildren()` on the list container.
|
|
338
769
|
|
|
339
770
|
---
|
|
340
771
|
|
|
@@ -471,7 +902,31 @@ component’s subtree; the conditional element is added or removed accordingly.
|
|
|
471
902
|
|
|
472
903
|
---
|
|
473
904
|
|
|
474
|
-
### 7)
|
|
905
|
+
### 7) Signals (optional)
|
|
906
|
+
|
|
907
|
+
Signals are opt-in fine-grained state. They do **not** auto-render components; you either bind them
|
|
908
|
+
directly to DOM helpers like `text()` / `attr()` or explicitly call `handle.update()`.
|
|
909
|
+
|
|
910
|
+
Example:
|
|
911
|
+
|
|
912
|
+
```ts
|
|
913
|
+
import { component, button, div, signal, text } from '@vanijs/vani'
|
|
914
|
+
|
|
915
|
+
const Counter = component(() => {
|
|
916
|
+
const [count, setCount] = signal(0)
|
|
917
|
+
return () =>
|
|
918
|
+
div(
|
|
919
|
+
text(() => `Count: ${count()}`),
|
|
920
|
+
button({ onclick: () => setCount((value) => value + 1) }, 'Inc'),
|
|
921
|
+
)
|
|
922
|
+
})
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
Use `derive()` for computed getters and `effect()` for side effects tied to signals.
|
|
926
|
+
|
|
927
|
+
---
|
|
928
|
+
|
|
929
|
+
### 8) Scheduling across independent regions
|
|
475
930
|
|
|
476
931
|
In large apps, keep each UI region as its own component root, and schedule updates explicitly. Use
|
|
477
932
|
microtasks for immediate batching and `startTransition()` for non‑urgent work. This lets you control
|
|
@@ -522,6 +977,82 @@ This design keeps scheduling predictable: each region updates at most once per f
|
|
|
522
977
|
decide which updates are urgent vs. deferred. If a region needs data from another, call its public
|
|
523
978
|
API first, then schedule both regions explicitly in the same flush.
|
|
524
979
|
|
|
980
|
+
### Explicit multi-region scheduling strategy
|
|
981
|
+
|
|
982
|
+
When multiple independent UI regions update in the same tick, prefer a central scheduler that:
|
|
983
|
+
|
|
984
|
+
- deduplicates updates per region (one update per flush)
|
|
985
|
+
- batches synchronous state changes into a microtask
|
|
986
|
+
- separates urgent vs. non‑urgent work (`queueMicrotask` vs. `startTransition`)
|
|
987
|
+
- avoids cascades by flushing a single pass and re‑queueing if new work appears
|
|
988
|
+
|
|
989
|
+
Example:
|
|
990
|
+
|
|
991
|
+
```ts
|
|
992
|
+
import { startTransition, type Handle } from '@vanijs/vani'
|
|
993
|
+
|
|
994
|
+
type RegionId = 'header' | 'content' | 'sidebar' | 'status'
|
|
995
|
+
type Priority = 'urgent' | 'transition'
|
|
996
|
+
|
|
997
|
+
const handles = new Map<RegionId, Handle>()
|
|
998
|
+
const pendingUrgent = new Set<RegionId>()
|
|
999
|
+
const pendingTransition = new Set<RegionId>()
|
|
1000
|
+
let microtaskScheduled = false
|
|
1001
|
+
let transitionScheduled = false
|
|
1002
|
+
|
|
1003
|
+
export const registerRegion = (id: RegionId, handle: Handle) => {
|
|
1004
|
+
handles.set(id, handle)
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
export const scheduleRegion = (id: RegionId, priority: Priority = 'urgent') => {
|
|
1008
|
+
if (priority === 'transition') {
|
|
1009
|
+
pendingTransition.add(id)
|
|
1010
|
+
if (!transitionScheduled) {
|
|
1011
|
+
transitionScheduled = true
|
|
1012
|
+
startTransition(flushTransition)
|
|
1013
|
+
}
|
|
1014
|
+
return
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
pendingUrgent.add(id)
|
|
1018
|
+
if (!microtaskScheduled) {
|
|
1019
|
+
microtaskScheduled = true
|
|
1020
|
+
queueMicrotask(flushUrgent)
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const flushUrgent = () => {
|
|
1025
|
+
microtaskScheduled = false
|
|
1026
|
+
for (const id of pendingUrgent) {
|
|
1027
|
+
handles.get(id)?.update()
|
|
1028
|
+
}
|
|
1029
|
+
pendingUrgent.clear()
|
|
1030
|
+
|
|
1031
|
+
// If urgent updates caused more urgent work, queue another microtask.
|
|
1032
|
+
if (pendingUrgent.size > 0 && !microtaskScheduled) {
|
|
1033
|
+
microtaskScheduled = true
|
|
1034
|
+
queueMicrotask(flushUrgent)
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const flushTransition = () => {
|
|
1039
|
+
transitionScheduled = false
|
|
1040
|
+
for (const id of pendingTransition) {
|
|
1041
|
+
handles.get(id)?.update()
|
|
1042
|
+
}
|
|
1043
|
+
pendingTransition.clear()
|
|
1044
|
+
}
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
Guidelines:
|
|
1048
|
+
|
|
1049
|
+
- **Priority conflicts**: if a region is queued in both sets, let urgent win and clear it from
|
|
1050
|
+
transition during flush.
|
|
1051
|
+
- **Cascading updates**: if an update triggers more updates, re‑queue explicitly rather than looping
|
|
1052
|
+
synchronously.
|
|
1053
|
+
- **Predictability**: keep region IDs stable and avoid cross‑region reads during render; use
|
|
1054
|
+
coordinators to update shared data first, then schedule regions in a known order.
|
|
1055
|
+
|
|
525
1056
|
---
|
|
526
1057
|
|
|
527
1058
|
## Advanced patterns
|
|
@@ -557,7 +1088,7 @@ import { component, div, button, type Handle } from '@vanijs/vani'
|
|
|
557
1088
|
import { getState, setState, subscribe } from './store'
|
|
558
1089
|
|
|
559
1090
|
const Counter = component((_, handle: Handle) => {
|
|
560
|
-
handle.
|
|
1091
|
+
handle.onBeforeMount(() => subscribe(() => handle.update()))
|
|
561
1092
|
|
|
562
1093
|
return () => {
|
|
563
1094
|
const { count } = getState()
|
|
@@ -627,42 +1158,78 @@ export const emit = (event: string, payload?: unknown) => {
|
|
|
627
1158
|
|
|
628
1159
|
## Large-scale app architecture
|
|
629
1160
|
|
|
630
|
-
Vani scales best when
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
1161
|
+
Vani scales best when update paths stay explicit and module boundaries stay tight. Treat updates as
|
|
1162
|
+
messages: state lives in a feature module, views read snapshots, and invalidation is triggered by
|
|
1163
|
+
that module's commands. This prevents hidden dependencies and makes cross-feature coordination an
|
|
1164
|
+
explicit, testable surface.
|
|
1165
|
+
|
|
1166
|
+
### Architecting large-scale, coordinated modules
|
|
1167
|
+
|
|
1168
|
+
Use explicit feature modules with clear APIs, bind views to module subscriptions, and coordinate
|
|
1169
|
+
cross-module workflows through a small coordinator or typed event channel. Keep invalidation scoped
|
|
1170
|
+
to feature roots or keyed row components, and batch updates per handle. Avoid implicit dependencies
|
|
1171
|
+
by never mutating another module's state directly; instead call exported commands or emit events. At
|
|
1172
|
+
scale, manual invalidation challenges include fan-out, over- or under-invalidating, update
|
|
1173
|
+
ordering/races, stale reads during transitions, lifecycle leaks, and lack of observability. Mitigate
|
|
1174
|
+
with explicit contracts, centralized command surfaces, predictable ordering, cleanup via
|
|
1175
|
+
`handle.onBeforeMount()`, and lightweight logging around invalidation helpers.
|
|
1176
|
+
|
|
1177
|
+
Architecture sketch:
|
|
1178
|
+
|
|
1179
|
+
```
|
|
1180
|
+
[UI/View A] --subscribe--> [Feature A]
|
|
1181
|
+
| |
|
|
1182
|
+
| update() | commands
|
|
1183
|
+
v v
|
|
1184
|
+
[Handle A] [Coordinator] ----> [Feature B]
|
|
1185
|
+
^ | |
|
|
1186
|
+
| update() | events | notify
|
|
1187
|
+
[UI/View B] --subscribe--> [Feature B] <---------+
|
|
1188
|
+
```
|
|
635
1189
|
|
|
636
1190
|
### Suggested architecture
|
|
637
1191
|
|
|
638
1192
|
1. Feature modules (state + commands)
|
|
639
1193
|
|
|
640
|
-
Each module exposes:
|
|
1194
|
+
Each module owns its state and exposes a small API:
|
|
641
1195
|
|
|
642
|
-
-
|
|
643
|
-
-
|
|
644
|
-
-
|
|
645
|
-
-
|
|
1196
|
+
- snapshot getters (`getState`, `getUsers`, `getFilters`)
|
|
1197
|
+
- commands that mutate state (`setUsers`, `applyFilter`)
|
|
1198
|
+
- `subscribe(listener)` to notify views of invalidation
|
|
1199
|
+
- optional selectors for derived data
|
|
646
1200
|
|
|
647
1201
|
2. View adapters (bind handles)
|
|
648
1202
|
|
|
649
|
-
Views subscribe once via `handle.
|
|
650
|
-
This keeps invalidation scoped to the subtree that owns the handle.
|
|
1203
|
+
Views subscribe once via `handle.onBeforeMount()` and call `handle.update()` when their module
|
|
1204
|
+
notifies. This keeps invalidation scoped to the subtree that owns the handle.
|
|
651
1205
|
|
|
652
|
-
3. Coordinator
|
|
1206
|
+
3. Coordinator or message hub
|
|
653
1207
|
|
|
654
|
-
For
|
|
1208
|
+
For workflows that span multiple modules, add a thin coordinator that:
|
|
655
1209
|
|
|
656
|
-
- orchestrates sequences (
|
|
1210
|
+
- orchestrates sequences (save → refresh → notify)
|
|
657
1211
|
- calls public APIs of each module
|
|
658
|
-
- never
|
|
1212
|
+
- never reaches into private state
|
|
1213
|
+
- batches invalidations when multiple modules change together
|
|
659
1214
|
|
|
660
1215
|
4. Stable, explicit contracts
|
|
661
1216
|
|
|
662
|
-
Use interfaces,
|
|
1217
|
+
Use interfaces, small message payloads, or callbacks to avoid implicit coupling. If one feature
|
|
663
1218
|
needs another to update, it calls that module's exported command (or `invalidate()` helper) rather
|
|
664
1219
|
than mutating shared data.
|
|
665
1220
|
|
|
1221
|
+
### Cross-module coordination patterns
|
|
1222
|
+
|
|
1223
|
+
Pick one, keep it explicit, and avoid hidden dependencies:
|
|
1224
|
+
|
|
1225
|
+
- **Coordinator**: a domain-level workflow function that calls module commands in order.
|
|
1226
|
+
- **Event channel**: a small bus where modules emit typed events and subscribers decide how to
|
|
1227
|
+
update; views still call `handle.update()` explicitly.
|
|
1228
|
+
- **Shared readonly data**: for truly global data, use a shared store with strict write APIs and
|
|
1229
|
+
localized subscriptions.
|
|
1230
|
+
|
|
1231
|
+
The goal is to keep "who invalidates whom" visible at the call site.
|
|
1232
|
+
|
|
666
1233
|
### Example: Feature module with explicit invalidation
|
|
667
1234
|
|
|
668
1235
|
```ts
|
|
@@ -703,7 +1270,7 @@ export const createUsersFeature = (): { api: UsersFeatureApi; View: Component }
|
|
|
703
1270
|
}
|
|
704
1271
|
|
|
705
1272
|
const View = component((_, handle: Handle) => {
|
|
706
|
-
handle.
|
|
1273
|
+
handle.onBeforeMount(() => api.subscribe(() => handle.update()))
|
|
707
1274
|
return () => div(api.getUsers().map((user) => div(user.name)))
|
|
708
1275
|
})
|
|
709
1276
|
|
|
@@ -729,6 +1296,33 @@ export const createCoordinator = (users: UsersFeatureApi): Coordinator => {
|
|
|
729
1296
|
}
|
|
730
1297
|
```
|
|
731
1298
|
|
|
1299
|
+
### Example: Event channel for cross-feature updates
|
|
1300
|
+
|
|
1301
|
+
```ts
|
|
1302
|
+
type EventMap = {
|
|
1303
|
+
userSaved: { id: string }
|
|
1304
|
+
searchChanged: { query: string }
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
type Listener<K extends keyof EventMap> = (payload: EventMap[K]) => void
|
|
1308
|
+
const listeners = new Map<keyof EventMap, Set<Listener<keyof EventMap>>>()
|
|
1309
|
+
|
|
1310
|
+
export const on = <K extends keyof EventMap>(event: K, listener: Listener<K>) => {
|
|
1311
|
+
const set = listeners.get(event) ?? new Set()
|
|
1312
|
+
set.add(listener as Listener<keyof EventMap>)
|
|
1313
|
+
listeners.set(event, set)
|
|
1314
|
+
return () => set.delete(listener as Listener<keyof EventMap>)
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
export const emit = <K extends keyof EventMap>(event: K, payload: EventMap[K]) => {
|
|
1318
|
+
const set = listeners.get(event)
|
|
1319
|
+
if (!set) return
|
|
1320
|
+
for (const listener of set) {
|
|
1321
|
+
listener(payload)
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
```
|
|
1325
|
+
|
|
732
1326
|
### Example: Scoped invalidation by key
|
|
733
1327
|
|
|
734
1328
|
When a large list exists, invalidate only the affected rows.
|
|
@@ -748,51 +1342,62 @@ export const invalidateUserRow = (id: string) => {
|
|
|
748
1342
|
}
|
|
749
1343
|
|
|
750
1344
|
export const UserRow = component<{ id: string; name: string }>((props, handle) => {
|
|
751
|
-
handle.
|
|
1345
|
+
handle.onBeforeMount(() => bindUserRow(props.id, handle))
|
|
752
1346
|
return () => div(props.name)
|
|
753
1347
|
})
|
|
754
1348
|
```
|
|
755
1349
|
|
|
756
1350
|
### Explicit batching (optional)
|
|
757
1351
|
|
|
758
|
-
If you dispatch many invalidations in a single tick,
|
|
1352
|
+
If you dispatch many invalidations in a single tick, you can wrap them in `batch()` or queue them
|
|
1353
|
+
and update once per handle.
|
|
1354
|
+
|
|
1355
|
+
Useful cases:
|
|
1356
|
+
|
|
1357
|
+
- Multiple store mutations in one click (only one update per handle).
|
|
1358
|
+
- Toggling many rows or cards at once.
|
|
1359
|
+
- Applying filters that update multiple feature roots.
|
|
759
1360
|
|
|
760
1361
|
```ts
|
|
761
|
-
import type
|
|
1362
|
+
import { batch, type Handle } from '@vanijs/vani'
|
|
762
1363
|
|
|
763
1364
|
const pending = new Set<Handle>()
|
|
764
1365
|
let scheduled = false
|
|
765
1366
|
|
|
766
1367
|
export const queueUpdate = (handle: Handle) => {
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1368
|
+
batch(() => {
|
|
1369
|
+
pending.add(handle)
|
|
1370
|
+
if (scheduled) return
|
|
1371
|
+
scheduled = true
|
|
1372
|
+
queueMicrotask(() => {
|
|
1373
|
+
scheduled = false
|
|
1374
|
+
for (const item of pending) item.update()
|
|
1375
|
+
pending.clear()
|
|
1376
|
+
})
|
|
774
1377
|
})
|
|
775
1378
|
}
|
|
776
1379
|
```
|
|
777
1380
|
|
|
778
1381
|
### Challenges with manual invalidation at scale
|
|
779
1382
|
|
|
780
|
-
- Update fan-out:
|
|
781
|
-
|
|
782
|
-
- Over-invalidating: calling `handle.update()`
|
|
783
|
-
small, keyed targets (row components, feature roots).
|
|
784
|
-
- Under-invalidating: missing
|
|
785
|
-
|
|
786
|
-
- Ordering and race conditions: when
|
|
787
|
-
invalidate in a
|
|
788
|
-
-
|
|
789
|
-
|
|
790
|
-
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1383
|
+
- Update fan-out: a single command may need to notify many modules. Use a coordinator or explicit
|
|
1384
|
+
event channel so the fan-out is visible and testable.
|
|
1385
|
+
- Over-invalidating: calling `handle.update()` on large roots can cause avoidable work. Prefer
|
|
1386
|
+
small, keyed targets (row components, feature roots) and batch per handle.
|
|
1387
|
+
- Under-invalidating: missing a manual update leaves views stale. Make "update after mutation" part
|
|
1388
|
+
of the module contract and centralize mutations behind commands.
|
|
1389
|
+
- Ordering and race conditions: when modules depend on shared data, update data first, then
|
|
1390
|
+
invalidate in a stable order; avoid interleaving async refreshes without a coordinator.
|
|
1391
|
+
- Stale reads during transitions: if you defer with `startTransition()`, ensure that the render
|
|
1392
|
+
reads the latest snapshot or that the transition captures the intended version.
|
|
1393
|
+
- Lifecycle leaks: if a handle isn't unsubscribed, updates keep firing. Always return cleanup from
|
|
1394
|
+
`subscribe()` and bind it through `handle.onBeforeMount()`.
|
|
1395
|
+
- Observability gaps: without implicit reactivity, you need traceability. Wrap invalidation helpers
|
|
1396
|
+
to log or count updates per module and catch runaway loops early.
|
|
1397
|
+
|
|
1398
|
+
Vani trades automatic coordination for transparency. In large apps, invest in clear module
|
|
1399
|
+
boundaries, explicit cross-module APIs, and small invalidation targets to keep manual invalidation
|
|
1400
|
+
manageable.
|
|
796
1401
|
|
|
797
1402
|
---
|
|
798
1403
|
|
|
@@ -806,7 +1411,7 @@ Creates a component factory. The `fn` receives `props` and a `handle`.
|
|
|
806
1411
|
import { component, div, type Handle } from '@vanijs/vani'
|
|
807
1412
|
|
|
808
1413
|
const Card = component<{ title: string }>((props, handle: Handle) => {
|
|
809
|
-
handle.
|
|
1414
|
+
handle.onBeforeMount(() => {
|
|
810
1415
|
console.log('Mounted:', props.title)
|
|
811
1416
|
})
|
|
812
1417
|
|
|
@@ -817,11 +1422,10 @@ const Card = component<{ title: string }>((props, handle: Handle) => {
|
|
|
817
1422
|
Components can return other component instances directly:
|
|
818
1423
|
|
|
819
1424
|
```ts
|
|
820
|
-
import { component } from '@vanijs/vani'
|
|
821
|
-
import * as h from 'vani/html'
|
|
1425
|
+
import { component, h1 } from '@vanijs/vani'
|
|
822
1426
|
|
|
823
1427
|
const Hero = component(() => {
|
|
824
|
-
return () =>
|
|
1428
|
+
return () => h1('Hello')
|
|
825
1429
|
})
|
|
826
1430
|
|
|
827
1431
|
const Page = component(() => {
|
|
@@ -831,39 +1435,43 @@ const Page = component(() => {
|
|
|
831
1435
|
|
|
832
1436
|
### `renderToDOM(components, root)`
|
|
833
1437
|
|
|
834
|
-
Mounts components
|
|
1438
|
+
Mounts components and schedules the first render on the next microtask. Accepts a single component
|
|
1439
|
+
or an array of components.
|
|
835
1440
|
|
|
836
1441
|
```ts
|
|
837
1442
|
import { renderToDOM, component, div } from '@vanijs/vani'
|
|
838
1443
|
|
|
839
1444
|
const App = component(() => () => div('App'))
|
|
840
|
-
renderToDOM(
|
|
1445
|
+
renderToDOM(App(), document.getElementById('app')!)
|
|
841
1446
|
```
|
|
842
1447
|
|
|
843
1448
|
### `hydrateToDOM(components, root)`
|
|
844
1449
|
|
|
845
|
-
Binds handles to existing DOM (SSR/SSG) without rendering.
|
|
846
|
-
activate.
|
|
1450
|
+
Binds handles to existing DOM (SSR/SSG) without rendering. Accepts a single component or an array of
|
|
1451
|
+
components. You must call `handle.update()` to activate.
|
|
847
1452
|
|
|
848
1453
|
```ts
|
|
849
1454
|
import { hydrateToDOM } from '@vanijs/vani'
|
|
850
1455
|
import { App } from './app'
|
|
851
1456
|
|
|
852
1457
|
const root = document.getElementById('app')!
|
|
853
|
-
const handles = hydrateToDOM(
|
|
1458
|
+
const handles = hydrateToDOM(App(), root)
|
|
854
1459
|
handles.forEach((handle) => handle.update())
|
|
855
1460
|
```
|
|
856
1461
|
|
|
1462
|
+
If hydration fails due to missing anchors or mismatched structure, Vani raises a `HydrationError`
|
|
1463
|
+
and `hydrateToDOM()` logs it. Other errors are rethrown so they surface immediately.
|
|
1464
|
+
|
|
857
1465
|
### `renderToString(components)`
|
|
858
1466
|
|
|
859
|
-
Server‑side render to HTML with anchors.
|
|
1467
|
+
Server‑side render to HTML with anchors. Accepts a single component or an array of components.
|
|
1468
|
+
Import from `@vanijs/vani`.
|
|
860
1469
|
|
|
861
1470
|
```ts
|
|
862
|
-
import { component } from '@vanijs/vani'
|
|
863
|
-
import { renderToString } from 'vani/ssr'
|
|
1471
|
+
import { component, renderToString } from '@vanijs/vani'
|
|
864
1472
|
|
|
865
1473
|
const App = component(() => () => 'Hello SSR')
|
|
866
|
-
const html = await renderToString(
|
|
1474
|
+
const html = await renderToString(App())
|
|
867
1475
|
```
|
|
868
1476
|
|
|
869
1477
|
### `mount(component, props)`
|
|
@@ -901,21 +1509,39 @@ div(span('Label'), input({ type: 'text' }), button({ onclick: () => {} }, 'Submi
|
|
|
901
1509
|
|
|
902
1510
|
### SVG icons (Lucide)
|
|
903
1511
|
|
|
904
|
-
|
|
905
|
-
the
|
|
1512
|
+
Use the Vite SVG plugin at `src/ecosystem/vite-plugin-vani-svg.ts` and import SVGs with `?vani`.
|
|
1513
|
+
This keeps the bundle small by only including the icons you actually import. Invalid SVG strings
|
|
1514
|
+
throw an error so failures stay obvious.
|
|
1515
|
+
|
|
1516
|
+
In your `vite.config.ts`:
|
|
1517
|
+
|
|
1518
|
+
```ts
|
|
1519
|
+
import vitePluginVaniSvg from './src/ecosystem/vite-plugin-vani-svg'
|
|
1520
|
+
|
|
1521
|
+
export default defineConfig({
|
|
1522
|
+
plugins: [vitePluginVaniSvg()],
|
|
1523
|
+
})
|
|
1524
|
+
```
|
|
906
1525
|
|
|
907
1526
|
```ts
|
|
1527
|
+
import GithubIcon from 'lucide-static/icons/github.svg?vani'
|
|
908
1528
|
import { component } from '@vanijs/vani'
|
|
909
|
-
import { renderSvgString } from '@vanijs/vani/svg'
|
|
910
|
-
import { Github } from 'lucide-static'
|
|
911
1529
|
|
|
912
1530
|
const GithubLink = component(() => {
|
|
913
|
-
return () =>
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1531
|
+
return () => GithubIcon({ size: 16, className: 'h-4 w-4', 'aria-hidden': true })
|
|
1532
|
+
})
|
|
1533
|
+
```
|
|
1534
|
+
|
|
1535
|
+
### SVGs as components
|
|
1536
|
+
|
|
1537
|
+
Any SVG can be turned into a Vani component with the same `?vani` suffix.
|
|
1538
|
+
|
|
1539
|
+
```ts
|
|
1540
|
+
import LogoIcon from './logo.svg?vani'
|
|
1541
|
+
import { component } from '@vanijs/vani'
|
|
1542
|
+
|
|
1543
|
+
const HeaderLogo = component(() => {
|
|
1544
|
+
return () => LogoIcon({ className: 'h-8 w-8', 'aria-hidden': true })
|
|
919
1545
|
})
|
|
920
1546
|
```
|
|
921
1547
|
|
|
@@ -946,21 +1572,22 @@ const Parent = component(() => {
|
|
|
946
1572
|
})
|
|
947
1573
|
```
|
|
948
1574
|
|
|
949
|
-
### Cleanup and
|
|
1575
|
+
### Cleanup and lifecycle
|
|
950
1576
|
|
|
951
|
-
|
|
1577
|
+
The `onBeforeMount` hook runs once during component setup (before the first render) and can return a
|
|
1578
|
+
cleanup function.
|
|
952
1579
|
|
|
953
|
-
If you plan to use vani for a SSR/SSG application, you should use
|
|
954
|
-
such as accessing the window object, accessing the DOM, etc.
|
|
1580
|
+
If you plan to use vani for a SSR/SSG application, you should use `onBeforeMount` to run client-only
|
|
1581
|
+
code such as accessing the window object, accessing the DOM, etc.
|
|
955
1582
|
|
|
956
|
-
|
|
957
|
-
not re-run on every `handle.update()`; updates only call the render function.
|
|
1583
|
+
The `onBeforeMount` hook is very simple and runs once during component setup (the component function
|
|
1584
|
+
run). It does not re-run on every `handle.update()`; updates only call the render function.
|
|
958
1585
|
|
|
959
1586
|
```ts
|
|
960
1587
|
import { component, div } from '@vanijs/vani'
|
|
961
1588
|
|
|
962
1589
|
const Timer = component((_, handle) => {
|
|
963
|
-
handle.
|
|
1590
|
+
handle.onBeforeMount(() => {
|
|
964
1591
|
const id = setInterval(() => console.log('tick'), 1000)
|
|
965
1592
|
return () => clearInterval(id)
|
|
966
1593
|
})
|
|
@@ -968,6 +1595,89 @@ const Timer = component((_, handle) => {
|
|
|
968
1595
|
})
|
|
969
1596
|
```
|
|
970
1597
|
|
|
1598
|
+
The `onMount` hook runs after the first render, once the component's nodes are in the DOM. It
|
|
1599
|
+
receives a lazy `getNodes()` function and the parent mount point.
|
|
1600
|
+
|
|
1601
|
+
Benefits:
|
|
1602
|
+
|
|
1603
|
+
- No need to set up refs ahead of time just to access nodes after render.
|
|
1604
|
+
- Easy to initialize external, vanilla JS libraries with all rendered nodes in one place.
|
|
1605
|
+
- `getNodes()` is lazy, so you only pay for DOM traversal if you actually need it.
|
|
1606
|
+
|
|
1607
|
+
```ts
|
|
1608
|
+
import { component, div } from '@vanijs/vani'
|
|
1609
|
+
|
|
1610
|
+
const Measure = component((_, handle) => {
|
|
1611
|
+
handle.onMount((getNodes, parent) => {
|
|
1612
|
+
const nodes = getNodes()
|
|
1613
|
+
const firstElement = nodes.find((node) => node instanceof HTMLElement)
|
|
1614
|
+
if (!firstElement) return
|
|
1615
|
+
|
|
1616
|
+
const rect = (firstElement as HTMLElement).getBoundingClientRect()
|
|
1617
|
+
console.log('Mounted in', parent, 'size', rect.width, rect.height)
|
|
1618
|
+
})
|
|
1619
|
+
|
|
1620
|
+
return () => div('Measured')
|
|
1621
|
+
})
|
|
1622
|
+
```
|
|
1623
|
+
|
|
1624
|
+
You can also register cleanup functions directly with `handle.onCleanup()`:
|
|
1625
|
+
|
|
1626
|
+
```ts
|
|
1627
|
+
import { component, div } from '@vanijs/vani'
|
|
1628
|
+
|
|
1629
|
+
const Subscription = component((_, handle) => {
|
|
1630
|
+
const unsubscribe = someStore.subscribe(() => handle.update())
|
|
1631
|
+
handle.onCleanup(unsubscribe)
|
|
1632
|
+
|
|
1633
|
+
return () => div('Subscribed')
|
|
1634
|
+
})
|
|
1635
|
+
```
|
|
1636
|
+
|
|
1637
|
+
Both patterns are equivalent. Use `handle.onBeforeMount()` when the setup and cleanup are logically
|
|
1638
|
+
grouped, and `handle.onCleanup()` when you need to register cleanup separately from initialization.
|
|
1639
|
+
|
|
1640
|
+
### Signals and DOM bindings (optional)
|
|
1641
|
+
|
|
1642
|
+
Signals are opt-in fine-grained state. They update only the DOM nodes bound to them.
|
|
1643
|
+
|
|
1644
|
+
```ts
|
|
1645
|
+
import { component, button, div, signal, text } from '@vanijs/vani'
|
|
1646
|
+
|
|
1647
|
+
const Counter = component(() => {
|
|
1648
|
+
const [count, setCount] = signal(0)
|
|
1649
|
+
return () =>
|
|
1650
|
+
div(
|
|
1651
|
+
text(() => `Count: ${count()}`),
|
|
1652
|
+
button({ onclick: () => setCount((value) => value + 1) }, 'Inc'),
|
|
1653
|
+
)
|
|
1654
|
+
})
|
|
1655
|
+
```
|
|
1656
|
+
|
|
1657
|
+
- `signal(initial)` returns `[get, set]`.
|
|
1658
|
+
- `derive(fn)` returns a computed getter.
|
|
1659
|
+
- `effect(fn)` re-runs when signals used inside `fn` change.
|
|
1660
|
+
- `text(getter)` binds a text node to a signal.
|
|
1661
|
+
- `attr(el, name, value)` binds an attribute/class to a signal getter or static value.
|
|
1662
|
+
|
|
1663
|
+
### Partial attribute updates
|
|
1664
|
+
|
|
1665
|
+
When you only need to update attributes (like `className`), you can request an attribute-only
|
|
1666
|
+
refresh:
|
|
1667
|
+
|
|
1668
|
+
```ts
|
|
1669
|
+
ref.current?.update({ onlyAttributes: true })
|
|
1670
|
+
```
|
|
1671
|
+
|
|
1672
|
+
This preserves existing event listeners and children and only patches the root element’s attributes.
|
|
1673
|
+
It applies when the component returns a single root element.
|
|
1674
|
+
|
|
1675
|
+
Useful cases:
|
|
1676
|
+
|
|
1677
|
+
- Toggle a selected row class without touching children.
|
|
1678
|
+
- Flip aria/disabled flags on a button.
|
|
1679
|
+
- Update theme/state classes on a card while leaving its subtree intact.
|
|
1680
|
+
|
|
971
1681
|
### Transitions
|
|
972
1682
|
|
|
973
1683
|
`startTransition` marks a group of updates as non-urgent, so they are deferred and batched
|