@vanijs/vani 0.1.0 → 0.2.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 +596 -7
- package/dist/lib/index.d.mts +691 -84
- package/dist/lib/index.mjs +1 -1
- package/llms.txt +1 -6
- package/package.json +3 -2
package/DOCS.md
CHANGED
|
@@ -12,7 +12,7 @@ DOM subtree delimited by anchors and only update when you explicitly ask them to
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
pnpm add vani
|
|
15
|
+
pnpm add @vanijs/vani
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
---
|
|
@@ -63,7 +63,9 @@ const Hello = component(() => {
|
|
|
63
63
|
|
|
64
64
|
### 2) Explicit updates
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
Re-renders are always explicit: call `handle.update()` to refresh a component’s subtree. The only
|
|
67
|
+
automatic update is the initial mount (or `clientOnly` during hydration), which schedules the first
|
|
68
|
+
render for you.
|
|
67
69
|
|
|
68
70
|
```ts
|
|
69
71
|
import { component, div, button, type Handle } from '@vanijs/vani'
|
|
@@ -98,6 +100,514 @@ Each component owns a DOM range delimited by anchors:
|
|
|
98
100
|
|
|
99
101
|
Updates replace only the DOM between anchors.
|
|
100
102
|
|
|
103
|
+
### 4) Lists and item-level updates
|
|
104
|
+
|
|
105
|
+
Lists are efficient in Vani when each item is its own component. Every item owns a tiny subtree and
|
|
106
|
+
can update itself (or be updated via a ref) without touching siblings. Use `key` to preserve
|
|
107
|
+
identity across reorders.
|
|
108
|
+
|
|
109
|
+
Key ideas:
|
|
110
|
+
|
|
111
|
+
- Represent list data by id (Map or array + id).
|
|
112
|
+
- Render each row as a keyed component.
|
|
113
|
+
- Store a `ComponentRef` per id so you can call `ref.current?.update()` for that item only.
|
|
114
|
+
- Call the list handle only when the list structure changes (add/remove/reorder).
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { component, ul, li, input, button, type Handle, type ComponentRef } from '@vanijs/vani'
|
|
120
|
+
|
|
121
|
+
type Todo = { id: string; text: string; done: boolean }
|
|
122
|
+
|
|
123
|
+
const Row = component<{
|
|
124
|
+
id: string
|
|
125
|
+
getItem: (id: string) => Todo | undefined
|
|
126
|
+
onToggle: (id: string) => void
|
|
127
|
+
onRename: (id: string, text: string) => void
|
|
128
|
+
}>((props) => {
|
|
129
|
+
return () => {
|
|
130
|
+
const item = props.getItem(props.id)
|
|
131
|
+
if (!item) return null
|
|
132
|
+
return li(
|
|
133
|
+
input({
|
|
134
|
+
type: 'checkbox',
|
|
135
|
+
checked: item.done,
|
|
136
|
+
onchange: () => props.onToggle(item.id),
|
|
137
|
+
}),
|
|
138
|
+
input({
|
|
139
|
+
value: item.text,
|
|
140
|
+
oninput: (event) => {
|
|
141
|
+
const value = (event.currentTarget as HTMLInputElement).value
|
|
142
|
+
props.onRename(item.id, value)
|
|
143
|
+
},
|
|
144
|
+
}),
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const List = component((_, handle: Handle) => {
|
|
150
|
+
let order = ['a', 'b']
|
|
151
|
+
const items = new Map<string, Todo>([
|
|
152
|
+
['a', { id: 'a', text: 'Ship Vani', done: false }],
|
|
153
|
+
['b', { id: 'b', text: 'Write docs', done: true }],
|
|
154
|
+
])
|
|
155
|
+
|
|
156
|
+
const refs = new Map<string, ComponentRef>()
|
|
157
|
+
const getRef = (id: string) => {
|
|
158
|
+
let ref = refs.get(id)
|
|
159
|
+
if (!ref) {
|
|
160
|
+
ref = { current: null }
|
|
161
|
+
refs.set(id, ref)
|
|
162
|
+
}
|
|
163
|
+
return ref
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const getItem = (id: string) => items.get(id)
|
|
167
|
+
|
|
168
|
+
const updateItem = (id: string, next: Partial<Todo>) => {
|
|
169
|
+
const current = items.get(id)
|
|
170
|
+
if (!current) return
|
|
171
|
+
items.set(id, { ...current, ...next })
|
|
172
|
+
refs.get(id)?.current?.update()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const toggle = (id: string) => {
|
|
176
|
+
const current = items.get(id)
|
|
177
|
+
if (!current) return
|
|
178
|
+
updateItem(id, { done: !current.done })
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const rename = (id: string, text: string) => updateItem(id, { text })
|
|
182
|
+
|
|
183
|
+
const add = (text: string) => {
|
|
184
|
+
const id = String(order.length + 1)
|
|
185
|
+
items.set(id, { id, text, done: false })
|
|
186
|
+
order = [...order, id]
|
|
187
|
+
handle.update()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const remove = (id: string) => {
|
|
191
|
+
items.delete(id)
|
|
192
|
+
refs.delete(id)
|
|
193
|
+
order = order.filter((value) => value !== id)
|
|
194
|
+
handle.update()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return () =>
|
|
198
|
+
ul(
|
|
199
|
+
order.map((id) =>
|
|
200
|
+
Row({
|
|
201
|
+
key: id,
|
|
202
|
+
ref: getRef(id),
|
|
203
|
+
id,
|
|
204
|
+
getItem,
|
|
205
|
+
onToggle: toggle,
|
|
206
|
+
onRename: rename,
|
|
207
|
+
}),
|
|
208
|
+
),
|
|
209
|
+
button({ onclick: () => add('New item') }, 'Add'),
|
|
210
|
+
button(
|
|
211
|
+
{
|
|
212
|
+
onclick: () => {
|
|
213
|
+
const first = order[0]
|
|
214
|
+
if (first) remove(first)
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
'Remove first',
|
|
218
|
+
),
|
|
219
|
+
)
|
|
220
|
+
})
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
This pattern keeps updates local: changing an item triggers only that row’s subtree update, while
|
|
224
|
+
structural list changes re-render the list container and reuse keyed rows.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
### 5) Forms with explicit submit
|
|
229
|
+
|
|
230
|
+
For forms, you can keep input values in local variables and update the DOM only on submit. This
|
|
231
|
+
matches Vani’s model: read input changes without re-rendering, then call `handle.update()` when the
|
|
232
|
+
user explicitly submits.
|
|
233
|
+
|
|
234
|
+
Example:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import { component, form, label, input, button, div, type Handle } from '@vanijs/vani'
|
|
238
|
+
|
|
239
|
+
const ContactForm = component((_, handle: Handle) => {
|
|
240
|
+
let name = ''
|
|
241
|
+
let email = ''
|
|
242
|
+
let submitted = false
|
|
243
|
+
|
|
244
|
+
const onSubmit = (event: SubmitEvent) => {
|
|
245
|
+
event.preventDefault()
|
|
246
|
+
submitted = true
|
|
247
|
+
handle.update()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return () =>
|
|
251
|
+
form(
|
|
252
|
+
{ onsubmit: onSubmit },
|
|
253
|
+
label('Name'),
|
|
254
|
+
input({
|
|
255
|
+
name: 'name',
|
|
256
|
+
value: name,
|
|
257
|
+
oninput: (event) => {
|
|
258
|
+
name = (event.currentTarget as HTMLInputElement).value
|
|
259
|
+
},
|
|
260
|
+
}),
|
|
261
|
+
label('Email'),
|
|
262
|
+
input({
|
|
263
|
+
name: 'email',
|
|
264
|
+
type: 'email',
|
|
265
|
+
value: email,
|
|
266
|
+
oninput: (event) => {
|
|
267
|
+
email = (event.currentTarget as HTMLInputElement).value
|
|
268
|
+
},
|
|
269
|
+
}),
|
|
270
|
+
button({ type: 'submit' }, 'Send'),
|
|
271
|
+
submitted ? div(`Submitted: ${name} <${email}>`) : null,
|
|
272
|
+
)
|
|
273
|
+
})
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
The DOM only updates on submit. Input changes mutate local variables but do not trigger a render
|
|
277
|
+
until the user confirms.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
### 5.1) Inputs and focus
|
|
282
|
+
|
|
283
|
+
Vani replaces a component’s subtree on update. If you re-render on every keystroke, the input node
|
|
284
|
+
is recreated and the browser will drop focus/selection. Prefer uncontrolled inputs and update on
|
|
285
|
+
submit/blur, or split the input into its own component so only a sibling preview re-renders.
|
|
286
|
+
|
|
287
|
+
If you need a controlled input, preserve focus explicitly:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
import { component, div, input, type DomRef, type Handle } from '@vanijs/vani'
|
|
291
|
+
|
|
292
|
+
const ControlledInput = component((_, handle: Handle) => {
|
|
293
|
+
const ref: DomRef<HTMLInputElement> = { current: null }
|
|
294
|
+
let value = ''
|
|
295
|
+
|
|
296
|
+
const updateWithFocus = () => {
|
|
297
|
+
const prev = ref.current
|
|
298
|
+
const start = prev?.selectionStart ?? null
|
|
299
|
+
const end = prev?.selectionEnd ?? null
|
|
300
|
+
|
|
301
|
+
handle.updateSync()
|
|
302
|
+
|
|
303
|
+
const next = ref.current
|
|
304
|
+
if (next) {
|
|
305
|
+
next.focus()
|
|
306
|
+
if (start != null && end != null) {
|
|
307
|
+
next.setSelectionRange(start, end)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return () =>
|
|
313
|
+
div(
|
|
314
|
+
input({
|
|
315
|
+
ref,
|
|
316
|
+
value,
|
|
317
|
+
oninput: (event) => {
|
|
318
|
+
value = (event.currentTarget as HTMLInputElement).value
|
|
319
|
+
updateWithFocus()
|
|
320
|
+
},
|
|
321
|
+
}),
|
|
322
|
+
div(`Value: ${value}`),
|
|
323
|
+
)
|
|
324
|
+
})
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
### 6) Conditional rendering
|
|
330
|
+
|
|
331
|
+
Conditional rendering is just normal control flow inside the render function. You compute a boolean
|
|
332
|
+
from your local state and return either the element or `null`. Updates are still explicit: call
|
|
333
|
+
`handle.update()` when you want the condition to be re-evaluated and the DOM to change.
|
|
334
|
+
|
|
335
|
+
Example:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
import { component, div, button, type Handle } from '@vanijs/vani'
|
|
339
|
+
|
|
340
|
+
const TogglePanel = component((_, handle: Handle) => {
|
|
341
|
+
let open = false
|
|
342
|
+
|
|
343
|
+
const toggle = () => {
|
|
344
|
+
open = !open
|
|
345
|
+
handle.update()
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return () =>
|
|
349
|
+
div(
|
|
350
|
+
button({ onclick: toggle }, open ? 'Hide details' : 'Show details'),
|
|
351
|
+
open ? div('Now you see me') : null,
|
|
352
|
+
)
|
|
353
|
+
})
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
The `open` flag is local state. When it changes, you call `handle.update()` to re-render the
|
|
357
|
+
component’s subtree; the conditional element is added or removed accordingly.
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
### 7) Scheduling across independent regions
|
|
362
|
+
|
|
363
|
+
In large apps, keep each UI region as its own component root, and schedule updates explicitly. Use
|
|
364
|
+
microtasks for immediate batching and `startTransition()` for non‑urgent work. This lets you control
|
|
365
|
+
_when_ updates happen without hidden dependencies.
|
|
366
|
+
|
|
367
|
+
Strategy:
|
|
368
|
+
|
|
369
|
+
- Give each region its own `handle`.
|
|
370
|
+
- Coalesce multiple changes in the same tick into a single update per region.
|
|
371
|
+
- Use microtasks for urgent updates (input, selection).
|
|
372
|
+
- Use `startTransition()` for expensive or non‑urgent work (filters, reorders).
|
|
373
|
+
- Avoid cascading updates by keeping regions independent and coordinating through explicit APIs.
|
|
374
|
+
|
|
375
|
+
Example scheduler:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
import { startTransition, type Handle } from '@vanijs/vani'
|
|
379
|
+
|
|
380
|
+
type RegionId = 'sidebar' | 'content' | 'status'
|
|
381
|
+
|
|
382
|
+
const pending = new Set<RegionId>()
|
|
383
|
+
const handles = new Map<RegionId, Handle>()
|
|
384
|
+
|
|
385
|
+
export const registerRegion = (id: RegionId, handle: Handle) => {
|
|
386
|
+
handles.set(id, handle)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export const scheduleRegionUpdate = (id: RegionId, opts?: { transition?: boolean }) => {
|
|
390
|
+
pending.add(id)
|
|
391
|
+
|
|
392
|
+
if (opts?.transition) {
|
|
393
|
+
startTransition(flush)
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
queueMicrotask(flush)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const flush = () => {
|
|
401
|
+
for (const id of pending) {
|
|
402
|
+
handles.get(id)?.update()
|
|
403
|
+
}
|
|
404
|
+
pending.clear()
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
This design keeps scheduling predictable: each region updates at most once per flush, and you can
|
|
409
|
+
decide which updates are urgent vs. deferred. If a region needs data from another, call its public
|
|
410
|
+
API first, then schedule both regions explicitly in the same flush.
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## Advanced patterns
|
|
415
|
+
|
|
416
|
+
These patterns stay explicit while scaling across larger apps.
|
|
417
|
+
|
|
418
|
+
### Global state with subscriptions
|
|
419
|
+
|
|
420
|
+
Use a small store with `getState`, `setState`, and `subscribe`. Components subscribe once and call
|
|
421
|
+
`handle.update()` on changes.
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
// store.ts
|
|
425
|
+
type Listener = () => void
|
|
426
|
+
type AppState = { count: number }
|
|
427
|
+
|
|
428
|
+
let state: AppState = { count: 0 }
|
|
429
|
+
const listeners = new Set<Listener>()
|
|
430
|
+
|
|
431
|
+
export const getState = () => state
|
|
432
|
+
export const setState = (next: AppState) => {
|
|
433
|
+
state = next
|
|
434
|
+
for (const listener of listeners) listener()
|
|
435
|
+
}
|
|
436
|
+
export const subscribe = (listener: Listener) => {
|
|
437
|
+
listeners.add(listener)
|
|
438
|
+
return () => listeners.delete(listener)
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
import { component, div, button, type Handle } from '@vanijs/vani'
|
|
444
|
+
import { getState, setState, subscribe } from './store'
|
|
445
|
+
|
|
446
|
+
const Counter = component((_, handle: Handle) => {
|
|
447
|
+
handle.effect(() => subscribe(() => handle.update()))
|
|
448
|
+
|
|
449
|
+
return () => {
|
|
450
|
+
const { count } = getState()
|
|
451
|
+
return div(`Count: ${count}`, button({ onclick: () => setState({ count: count + 1 }) }, 'Inc'))
|
|
452
|
+
}
|
|
453
|
+
})
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Data fetching + cache invalidation
|
|
457
|
+
|
|
458
|
+
Keep a simple cache and explicit invalidation. Updates are manual and predictable.
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
type Listener = () => void
|
|
462
|
+
const listeners = new Set<Listener>()
|
|
463
|
+
const cache = new Map<string, unknown>()
|
|
464
|
+
|
|
465
|
+
export const subscribe = (listener: Listener) => {
|
|
466
|
+
listeners.add(listener)
|
|
467
|
+
return () => listeners.delete(listener)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export const getCached = <T>(key: string) => cache.get(key) as T | undefined
|
|
471
|
+
|
|
472
|
+
export const refresh = async (key: string, fetcher: () => Promise<unknown>) => {
|
|
473
|
+
cache.set(key, await fetcher())
|
|
474
|
+
for (const listener of listeners) listener()
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Derived (selector) state
|
|
479
|
+
|
|
480
|
+
Compute derived values during render, or cache them when the base state changes. This keeps updates
|
|
481
|
+
explicit and avoids hidden dependencies.
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
const getVisibleItems = (items: string[], filter: string) =>
|
|
485
|
+
filter ? items.filter((item) => item.includes(filter)) : items
|
|
486
|
+
|
|
487
|
+
// In render:
|
|
488
|
+
const visible = getVisibleItems(items, filter)
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### Event bus for cross-feature coordination
|
|
492
|
+
|
|
493
|
+
For decoupled features, use a tiny event bus and update explicitly when events fire.
|
|
494
|
+
|
|
495
|
+
```ts
|
|
496
|
+
type Listener = (payload?: unknown) => void
|
|
497
|
+
const listeners = new Map<string, Set<Listener>>()
|
|
498
|
+
|
|
499
|
+
export const on = (event: string, listener: Listener) => {
|
|
500
|
+
const set = listeners.get(event) ?? new Set<Listener>()
|
|
501
|
+
set.add(listener)
|
|
502
|
+
listeners.set(event, set)
|
|
503
|
+
return () => set.delete(listener)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export const emit = (event: string, payload?: unknown) => {
|
|
507
|
+
const set = listeners.get(event)
|
|
508
|
+
if (!set) return
|
|
509
|
+
for (const listener of set) listener(payload)
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
## Large-scale app architecture
|
|
516
|
+
|
|
517
|
+
Vani scales best when you keep update paths explicit and module boundaries clear. The core idea is
|
|
518
|
+
to let feature modules own their local state and expose small, explicit APIs for coordination,
|
|
519
|
+
instead of reaching into each other’s state or relying on global reactive graphs.
|
|
520
|
+
|
|
521
|
+
### Suggested architecture
|
|
522
|
+
|
|
523
|
+
1. Feature modules
|
|
524
|
+
|
|
525
|
+
Each module exposes:
|
|
526
|
+
|
|
527
|
+
- a small state container
|
|
528
|
+
- read accessors (snapshot getters)
|
|
529
|
+
- explicit mutation functions that call `handle.update()` on the owning component(s)
|
|
530
|
+
|
|
531
|
+
2. Coordinator (optional)
|
|
532
|
+
|
|
533
|
+
For cross-module workflows, add a thin coordinator that:
|
|
534
|
+
|
|
535
|
+
- orchestrates sequences (e.g. save → refresh → notify)
|
|
536
|
+
- calls public APIs of each module
|
|
537
|
+
- never accesses private state directly
|
|
538
|
+
|
|
539
|
+
3. Stable, explicit contracts
|
|
540
|
+
|
|
541
|
+
Use interfaces, simple message payloads, or callbacks to avoid implicit coupling. If one feature
|
|
542
|
+
needs another to update, it calls that module’s exported `invalidate()` (or specific mutation
|
|
543
|
+
method) rather than mutating shared data.
|
|
544
|
+
|
|
545
|
+
### Example: Feature module with explicit invalidation
|
|
546
|
+
|
|
547
|
+
```ts
|
|
548
|
+
import { component, div, type Handle } from '@vanijs/vani'
|
|
549
|
+
|
|
550
|
+
type User = { id: string; name: string }
|
|
551
|
+
|
|
552
|
+
export type UserFeatureApi = {
|
|
553
|
+
getUsers: () => User[]
|
|
554
|
+
refreshUsers: () => void
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export const UsersView = component((_, handle: Handle) => {
|
|
558
|
+
let users: User[] = []
|
|
559
|
+
|
|
560
|
+
const getUsers = () => users
|
|
561
|
+
|
|
562
|
+
const setUsers = (next: User[]) => {
|
|
563
|
+
users = next
|
|
564
|
+
handle.update()
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const refreshUsers = async () => {
|
|
568
|
+
const response = await fetch('/api/users')
|
|
569
|
+
const data = (await response.json()) as User[]
|
|
570
|
+
setUsers(data)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
;(UsersView as any).api = { getUsers, refreshUsers } satisfies UserFeatureApi
|
|
574
|
+
|
|
575
|
+
return () => div(getUsers().map((user) => div(user.name)))
|
|
576
|
+
})
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### Example: Coordinator calling explicit APIs
|
|
580
|
+
|
|
581
|
+
```ts
|
|
582
|
+
import type { UserFeatureApi } from './users-feature'
|
|
583
|
+
|
|
584
|
+
type Coordinator = {
|
|
585
|
+
onUserSaved: () => void
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export const createCoordinator = (users: UserFeatureApi): Coordinator => {
|
|
589
|
+
return {
|
|
590
|
+
onUserSaved: () => {
|
|
591
|
+
users.refreshUsers()
|
|
592
|
+
},
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### Challenges with manual invalidation at scale
|
|
598
|
+
|
|
599
|
+
- Update fan‑out: one action may need to notify several modules; keep this explicit via a
|
|
600
|
+
coordinator instead of hidden subscriptions.
|
|
601
|
+
- Over‑invalidating: it’s easy to call `handle.update()` too broadly; prefer small, keyed subtrees
|
|
602
|
+
(item components, feature-level roots).
|
|
603
|
+
- Stale reads: when multiple modules depend on shared data, ensure you update the data first, then
|
|
604
|
+
invalidate dependent modules in a predictable order.
|
|
605
|
+
- Debugging update paths: without implicit reactivity, you must track who called `update()`. Keep
|
|
606
|
+
module APIs narrow and name update methods clearly (`refreshUsers`, `invalidateSearch`).
|
|
607
|
+
|
|
608
|
+
Vani trades automatic coordination for transparency. In large apps, that means you should invest in
|
|
609
|
+
clear module boundaries, explicit cross-module APIs, and small invalidation targets.
|
|
610
|
+
|
|
101
611
|
---
|
|
102
612
|
|
|
103
613
|
## API Reference (with examples)
|
|
@@ -203,6 +713,26 @@ import { div, span, button, input } from '@vanijs/vani'
|
|
|
203
713
|
div(span('Label'), input({ type: 'text' }), button({ onclick: () => {} }, 'Submit'))
|
|
204
714
|
```
|
|
205
715
|
|
|
716
|
+
### SVG icons (Lucide)
|
|
717
|
+
|
|
718
|
+
Vani can render SVG strings directly using `renderSvgString()`. With `lucide-static`, import just
|
|
719
|
+
the icon you need (tree-shakable) and render it with explicit sizing and class names.
|
|
720
|
+
|
|
721
|
+
```ts
|
|
722
|
+
import { component } from '@vanijs/vani'
|
|
723
|
+
import { renderSvgString } from '@vanijs/vani/svg'
|
|
724
|
+
import { Github } from 'lucide-static'
|
|
725
|
+
|
|
726
|
+
const GithubLink = component(() => {
|
|
727
|
+
return () =>
|
|
728
|
+
renderSvgString(Github, {
|
|
729
|
+
size: 16,
|
|
730
|
+
className: 'h-4 w-4',
|
|
731
|
+
attributes: { 'aria-hidden': 'true' },
|
|
732
|
+
})
|
|
733
|
+
})
|
|
734
|
+
```
|
|
735
|
+
|
|
206
736
|
### `classNames(...classes)`
|
|
207
737
|
|
|
208
738
|
Utility for composing class names:
|
|
@@ -211,7 +741,7 @@ Utility for composing class names:
|
|
|
211
741
|
import { classNames, div } from '@vanijs/vani'
|
|
212
742
|
|
|
213
743
|
div({
|
|
214
|
-
className: classNames('base', { active: true }, ['p-2', 'rounded']),
|
|
744
|
+
className: classNames('base', { active: true }, ['p-2', 'rounded-xl']),
|
|
215
745
|
})
|
|
216
746
|
```
|
|
217
747
|
|
|
@@ -237,7 +767,8 @@ Effects are explicit and can return a cleanup function.
|
|
|
237
767
|
If you plan to use vani for a SSR/SSG application, you should use effects to run client-only code
|
|
238
768
|
such as accessing the window object, accessing the DOM, etc.
|
|
239
769
|
|
|
240
|
-
Effects are very simple
|
|
770
|
+
Effects are very simple and run once during component setup (the component function run). They do
|
|
771
|
+
not re-run on every `handle.update()`; updates only call the render function.
|
|
241
772
|
|
|
242
773
|
```ts
|
|
243
774
|
import { component, div } from '@vanijs/vani'
|
|
@@ -303,7 +834,8 @@ const App = component(
|
|
|
303
834
|
)
|
|
304
835
|
```
|
|
305
836
|
|
|
306
|
-
|
|
837
|
+
In DOM mode, the fallback is rendered until the component is ready. In SSR mode, async components
|
|
838
|
+
are awaited, so the fallback only renders for `clientOnly` components.
|
|
307
839
|
|
|
308
840
|
---
|
|
309
841
|
|
|
@@ -342,8 +874,47 @@ Use `renderToString()` on the server, then `hydrateToDOM()` on the client.
|
|
|
342
874
|
|
|
343
875
|
Use `renderToString()` at build time to generate a static `index.html`, then hydrate on the client.
|
|
344
876
|
|
|
345
|
-
**Important:** Hydration only binds to anchors. It does not render or start
|
|
346
|
-
`handle.update()` to activate the UI.
|
|
877
|
+
**Important:** Hydration only binds to anchors for normal components. It does not render or start
|
|
878
|
+
effects until you call `handle.update()` to activate the UI. Components marked `clientOnly: true` do
|
|
879
|
+
render on the client during hydration.
|
|
880
|
+
|
|
881
|
+
---
|
|
882
|
+
|
|
883
|
+
## Selective Hydration
|
|
884
|
+
|
|
885
|
+
You can hydrate a full page but only **activate** the parts that need interactivity. Since
|
|
886
|
+
`hydrateToDOM()` returns handles, you choose which ones to `update()`.
|
|
887
|
+
|
|
888
|
+
Example: hydrate everything, activate only the header.
|
|
889
|
+
|
|
890
|
+
```ts
|
|
891
|
+
import { hydrateToDOM, type ComponentRef } from '@vanijs/vani'
|
|
892
|
+
import { Header } from './header'
|
|
893
|
+
import { Main } from './main'
|
|
894
|
+
import { Footer } from './footer'
|
|
895
|
+
|
|
896
|
+
const headerRef: ComponentRef = { current: null }
|
|
897
|
+
const root = document.getElementById('app')!
|
|
898
|
+
|
|
899
|
+
// Must match server render order.
|
|
900
|
+
hydrateToDOM([Header({ ref: headerRef }), Main(), Footer()], root)
|
|
901
|
+
|
|
902
|
+
// Activate only the header.
|
|
903
|
+
headerRef.current?.update()
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
Alternative: split the page into separate roots and hydrate only the interactive region.
|
|
907
|
+
|
|
908
|
+
```ts
|
|
909
|
+
const headerRoot = document.getElementById('header-root')!
|
|
910
|
+
const [headerHandle] = hydrateToDOM([Header()], headerRoot)
|
|
911
|
+
headerHandle.update()
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
Notes:
|
|
915
|
+
|
|
916
|
+
- Non‑updated components remain inert (no handlers/effects) until you call `update()`.
|
|
917
|
+
- The hydration list must match the server render order for that root.
|
|
347
918
|
|
|
348
919
|
---
|
|
349
920
|
|
|
@@ -368,6 +939,24 @@ This avoids slow DOM diffing and keeps behavior explicit.
|
|
|
368
939
|
|
|
369
940
|
---
|
|
370
941
|
|
|
942
|
+
## Other Resources
|
|
943
|
+
|
|
944
|
+
### Configuring Tailwind CSS Intellisense (VSCode)
|
|
945
|
+
|
|
946
|
+
In order to have proper Tailwind CSS Intellisense code completion and hover documentation with Vani,
|
|
947
|
+
you need to configure the following settings in your `.vscode/settings.json` file:
|
|
948
|
+
|
|
949
|
+
```json
|
|
950
|
+
{
|
|
951
|
+
"tailwindCSS.experimental.classRegex": [
|
|
952
|
+
["(?:tw|clsx|cn)\\(([^;]*)[\\);]", "[`'\"`]([^'\"`;]*)[`'\"`]"],
|
|
953
|
+
"(?:className)=\\s*(?:\"|'|{`)([^(?:\"|'|`})]*)",
|
|
954
|
+
"(?:className):\\s*(?:\"|'|{`)([^(?:\"|'|`})]*)"
|
|
955
|
+
],
|
|
956
|
+
"tailwindCSS.classAttributes": ["class", "classes", "className", "classNames"]
|
|
957
|
+
}
|
|
958
|
+
```
|
|
959
|
+
|
|
371
960
|
## License
|
|
372
961
|
|
|
373
962
|
MIT
|