@sky.ui/reactivity 0.0.1

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 ADDED
@@ -0,0 +1,981 @@
1
+ # @sky.ui/reactivity
2
+
3
+ Vue-style reactivity and an in-memory template compiler for lightweight Sky apps. Expressions are parsed with Acorn and evaluated through a safe AST interpreter — no `eval` or `new Function`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @sky.ui/reactivity
9
+ ```
10
+
11
+ Optional peer for Node SSR:
12
+
13
+ ```bash
14
+ npm install linkedom
15
+ ```
16
+
17
+ Optional: install the [**Sky UI Reactivity**](https://marketplace.visualstudio.com/items?itemName=desaisoftwaree.sky-ui-reactivity) VS Code extension for completions, diagnostics, and navigation for `sky-if`, `sky-for`, `sky-model`, and related directives.
18
+
19
+ ```js
20
+ import {
21
+ createReactivity,
22
+ reactive,
23
+ ref,
24
+ computed,
25
+ watch,
26
+ effect,
27
+ mount,
28
+ renderToString,
29
+ nextTick,
30
+ } from '@sky.ui/reactivity';
31
+ ```
32
+
33
+ ---
34
+
35
+ ---
36
+
37
+ ## Table of contents
38
+ - [API reference](#api-reference)
39
+ - [createReactivity](#createreactivity)
40
+ - [mount](#mount)
41
+ - [reactive](#reactive)
42
+ - [ref](#ref)
43
+ - [shallowRef](#shallowref)
44
+ - [triggerRef](#triggerref)
45
+ - [computed](#computed)
46
+ - [effect](#effect)
47
+ - [watch](#watch)
48
+ - [nextTick](#nexttick)
49
+ - [toRaw, markRaw, readonly, isReactive, isRef, unref](#toraw-markraw-readonly-isreactive-isref-unref)
50
+ - [Template directives](#template-directives)
51
+ - [Conditionals: sky-if / sky-else-if / sky-else](#conditionals-sky-if--sky-else-if--sky-else)
52
+ - [Lists: sky-for](#lists-sky-for)
53
+ - [Visibility: sky-show](#visibility-sky-show)
54
+ - [Two-way binding: sky-model](#two-way-binding-sky-model)
55
+ - [Element reference: sky-ref](#element-reference-sky-ref)
56
+ - [Class and style](#class-and-style)
57
+ - [Element properties: sky-bind:prop](#element-properties-sky-bindprop)
58
+ - [Event handlers](#event-handlers)
59
+ - [Raw HTML: sky-html](#raw-html-sky-html)
60
+ - [One-shot bindings: sky-once](#one-shot-bindings-sky-once)
61
+ - [Server-side rendering (SSR)](#server-side-rendering-ssr)
62
+ - [CRUD: Create, Read, Update, Delete](#crud-create-read-update-delete)
63
+ - [Arrays](#arrays)
64
+ - [Objects](#objects)
65
+ - [Nested structures](#nested-structures)
66
+ - [Expression context and ref unwrapping](#expression-context-and-ref-unwrapping)
67
+ - [What is supported in expressions](#what-is-supported-in-expressions)
68
+ - [What is blocked or unsupported](#what-is-blocked-or-unsupported)
69
+ - [Common patterns](#common-patterns)
70
+ - [Troubleshooting](#troubleshooting)
71
+ - [Vue 3 comparison: accuracy and improvements](#vue-3-comparison-accuracy-and-improvements)
72
+
73
+ ---
74
+ - [Quick start](#quick-start)
75
+
76
+ ## Quick start
77
+
78
+ **1. HTML with template directives:**
79
+
80
+ ```html
81
+ <div id="app">
82
+ <p>Count: {{ count }}</p>
83
+ <button onclick="count++">Increment</button>
84
+ <ul sky-for="item in items" sky-key="item.id">
85
+ <li>{{ item.name }}</li>
86
+ </ul>
87
+ </div>
88
+ ```
89
+
90
+ **2. Mount with a setup function (recommended):**
91
+
92
+ ```js
93
+ const { mount } = createReactivity(() => ({
94
+ count: 0,
95
+ items: [
96
+ { id: 1, name: "Alpha" },
97
+ { id: 2, name: "Beta" },
98
+ ],
99
+ }));
100
+
101
+ const { ctx, unmount } = mount("#app");
102
+ // ctx.count, ctx.items are reactive; DOM updates when they change.
103
+ ```
104
+
105
+ Same behavior in one expression:
106
+
107
+ ```js
108
+ const { ctx, unmount } = createReactivity(() => ({
109
+ count: 0,
110
+ items: [{ id: 1, name: "Alpha" }],
111
+ })).mount("#app");
112
+ ```
113
+
114
+ **3. Or mount with a plain object + optional initial state:**
115
+
116
+ ```js
117
+ const { mount } = createReactivity({
118
+ count: 0,
119
+ items: [],
120
+ });
121
+
122
+ const { ctx, unmount } = mount("#app", { count: 10 });
123
+ // ctx is merged: initial + setup. Unmount when done: unmount().
124
+ ```
125
+
126
+ ---
127
+
128
+ ## API reference
129
+
130
+ ### createReactivity
131
+
132
+ Creates a reactivity app that can mount to a DOM target.
133
+
134
+ ```ts
135
+ createReactivity(setup: (() => Record<string, unknown>) | Record<string, unknown>): {
136
+ mount(target: Element | string, initial?: Record<string, unknown>): {
137
+ ctx: Record<string, unknown>;
138
+ unmount(): void;
139
+ };
140
+ }
141
+ ```
142
+
143
+ - **`setup`**: Either a function returning an object, or a plain object. All properties are exposed on the template context and are made reactive if they are objects/arrays.
144
+ - **Function setup with `ref` / `reactive`**: Prefer **`createReactivity(() => { const items = reactive([]); const flag = ref(false); … return { items, flag, … }; })`** when you need Vue-style primitives (`ref`) or in-place mutations on objects/arrays (`reactive`). Returned refs are auto-unwrapped in template expressions; plain literals in the returned object are still wrapped by the engine where applicable.
145
+ - **`mount(target, initial?)`**: Compiles the DOM tree under `target` (selector string or `Element`), creates context from `setup` (and `initial` merged in), and returns `{ ctx, unmount }`.
146
+ - **`ctx`**: The reactive context used in templates. Add or change properties here to drive the UI.
147
+ - **`unmount()`**: Cleans up effects and event listeners. Call when removing the app from the page.
148
+
149
+ ---
150
+
151
+ ### mount
152
+
153
+ Low-level mount: you provide the root element and the context object. Use this when you build `ctx` yourself (e.g. with `reactive`/`ref`).
154
+
155
+ ```ts
156
+ mount(root: Element | DocumentFragment | string, ctx: Record<string, unknown>): Record<string, unknown>
157
+ ```
158
+
159
+ - **`root`**: DOM element, document fragment, or a CSS selector string.
160
+ - **`ctx`**: Any object; typically a reactive object or one whose properties are refs/reactive. Template expressions read from and write to `ctx`.
161
+ - **Returns**: The same `ctx` you passed in.
162
+
163
+ Example:
164
+
165
+ ```js
166
+ import { mount, reactive } from "@sky.ui/reactivity";
167
+
168
+ const ctx = reactive({ name: "World" });
169
+ mount("#app", ctx);
170
+ ```
171
+
172
+ ---
173
+
174
+ ### reactive
175
+
176
+ Makes a plain object or array reactive. Reading and writing properties is tracked; nested objects and arrays are automatically wrapped when accessed.
177
+
178
+ ```ts
179
+ reactive<T extends object>(obj: T): T
180
+ ```
181
+
182
+ - Same object identity is preserved (cached proxy).
183
+ - Use for **objects** and **arrays** that you mutate in place (e.g. `state.items.push(item)`).
184
+
185
+ **Example:**
186
+
187
+ ```js
188
+ const state = reactive({ name: "", list: [] });
189
+ state.name = "Alice"; // triggers updates
190
+ state.list.push("x"); // triggers updates
191
+ ```
192
+
193
+ ---
194
+
195
+ ### ref
196
+
197
+ Wraps a single value in an object `{ value: T }` that is reactive. In template expressions, refs are auto-unwrapped (you use `count`, not `count.value`). In JS, always use `.value` to read/write.
198
+
199
+ ```ts
200
+ ref<T>(v: T): { value: T }
201
+ ```
202
+
203
+ **Example:**
204
+
205
+ ```js
206
+ const count = ref(0);
207
+ count.value++; // in JS
208
+
209
+ // In template: {{ count }} and onclick="count++" work; the engine sets count.value.
210
+ ```
211
+
212
+ ---
213
+
214
+ ### shallowRef
215
+
216
+ Like `ref()` but the inner value is **not** deeply reactive. Only `.value` access is tracked. Use for large structures or external state. After mutating `.value` in place, call **triggerRef(ref)** to notify dependents.
217
+
218
+ ```ts
219
+ shallowRef<T>(v: T): { value: T }
220
+ triggerRef(ref: { value: unknown }): void
221
+ ```
222
+
223
+ **Example:**
224
+
225
+ ```js
226
+ const state = shallowRef({ count: 1 });
227
+ state.value.count = 2; // does NOT trigger
228
+ state.value = { count: 2 }; // triggers
229
+ triggerRef(state); // or after in-place mutation, force trigger
230
+ ```
231
+
232
+ ---
233
+
234
+ ### triggerRef
235
+
236
+ Forces effects that depend on a **shallow ref** to run. Call after mutating `ref.value` in place (e.g. `shallowRef.value.foo = 1`). See [shallowRef](#shallowref).
237
+
238
+ ---
239
+
240
+ ### computed
241
+
242
+ Creates a reactive computed value. Accepts either a getter (read-only) or `{ get, set }` (writable). The getter runs in an effect; when its dependencies change, the computed updates.
243
+
244
+ ```ts
245
+ computed<T>(getter: () => T): { value: T }
246
+ computed<T>(options: { get: () => T; set: (v: T) => void }): { value: T }
247
+ ```
248
+
249
+ **Example:**
250
+
251
+ ```js
252
+ const first = ref("Jane");
253
+ const last = ref("Doe");
254
+ const fullName = computed(() => `${first.value} ${last.value}`);
255
+ // fullName.value is 'Jane Doe'; when first or last change, fullName.value updates.
256
+
257
+ // Writable computed
258
+ const count = ref(1);
259
+ const plusOne = computed({
260
+ get: () => count.value + 1,
261
+ set: (val) => { count.value = val - 1; },
262
+ });
263
+ plusOne.value = 1; // count.value is now 0
264
+ ```
265
+
266
+ ---
267
+
268
+ ### effect
269
+
270
+ Runs a function immediately and re-runs it whenever any reactive dependency (reactive object/ref/computed) read inside it changes. Used internally by the template compiler.
271
+
272
+ ```ts
273
+ effect<T>(fn: () => T, opts?: { lazy?: boolean; scheduler?: (run: () => void) => void }): () => T
274
+ ```
275
+
276
+ - **`lazy`**: If `true`, the effect does not run until the returned runner is called.
277
+ - **`scheduler`**: If provided, when dependencies change the scheduler is called instead of running the effect immediately (default batches via microtask).
278
+ - **Returns**: A runner function. Call it to run the effect again; call `runner.stop()` to disable.
279
+
280
+ **Example:**
281
+
282
+ ```js
283
+ const state = reactive({ count: 0 });
284
+ effect(() => {
285
+ console.log(state.count);
286
+ });
287
+ state.count = 1; // logs 1
288
+ ```
289
+
290
+ ---
291
+
292
+ ### watch
293
+
294
+ Watches one or more reactive sources and runs a callback when they change. Returns a function that stops the watcher.
295
+
296
+ ```ts
297
+ watch(
298
+ source: unknown | (() => unknown) | (unknown | (() => unknown))[],
299
+ cb: (newVal, oldVal, onCleanup: (fn: () => void) => void) => void,
300
+ opts?: { immediate?: boolean; deep?: boolean; flush?: 'pre' | 'post' | 'sync' }
301
+ ): () => void
302
+ ```
303
+
304
+ - **`source`**: A ref, reactive object, getter function, or **array** of these for multiple sources.
305
+ - **`cb(newVal, oldVal, onCleanup)`**: Called when the value(s) change. For multiple sources, `newVal` and `oldVal` are arrays. **`onCleanup(fn)`** registers a cleanup that runs before the next run (e.g. cancel a prior request).
306
+ - **`opts.immediate`**: If `true`, run the callback once immediately (with `oldVal` undefined).
307
+ - **`opts.deep`**: If `true`, traverse the source so nested changes trigger the callback.
308
+ - **`opts.flush`**: `'post'` (default, microtask), `'pre'`, or `'sync'` (run synchronously when deps change).
309
+ - **Returns**: A function; call it to stop the watcher.
310
+
311
+ **Example:**
312
+
313
+ ```js
314
+ const count = ref(0);
315
+ const stop = watch(count, (newVal, oldVal) => {
316
+ console.log(oldVal, "->", newVal);
317
+ });
318
+ stop(); // when done
319
+
320
+ watch([a, b], ([aVal, bVal], [aOld, bOld], onCleanup) => {
321
+ const id = fetchSomething();
322
+ onCleanup(() => cancel(id));
323
+ }, { immediate: true, flush: 'sync' });
324
+ ```
325
+
326
+ ---
327
+
328
+ ### nextTick
329
+
330
+ Schedules a callback after the current microtask queue (e.g. after the current batch of reactive updates).
331
+
332
+ ```ts
333
+ nextTick(fn?: () => void): Promise<void>
334
+ ```
335
+
336
+ **Example:**
337
+
338
+ ```js
339
+ ctx.items.push(newItem);
340
+ await nextTick();
341
+ // DOM updated; safe to measure or focus.
342
+ ```
343
+
344
+ ---
345
+
346
+ ### toRaw, markRaw, readonly, isReactive, isRef, unref
347
+
348
+ | API | Description |
349
+ |-----|-------------|
350
+ | **toRaw(proxy)** | Returns the original object from a reactive or readonly proxy. Use to read/write without triggering. |
351
+ | **markRaw(obj)** | Marks `obj` so `reactive(obj)` returns `obj` unchanged (opt-out of reactivity). |
352
+ | **readonly(obj)** | Returns a deep read-only proxy. Mutations are no-ops; refs in properties are unwrapped. |
353
+ | **isReactive(obj)** | `true` if `obj` is a reactive proxy. |
354
+ | **isRef(v)** | `true` if `v` is a ref (including shallowRef/computed). |
355
+ | **unref(v)** | Returns `v.value` if `v` is a ref, otherwise `v`. |
356
+
357
+ **Example:**
358
+
359
+ ```js
360
+ const raw = toRaw(reactiveObj);
361
+ const obj = markRaw({ noReactivity: true });
362
+ const ro = readonly(state);
363
+ if (isReactive(ro)) { ... }
364
+ const x = unref(maybeRef);
365
+ ```
366
+
367
+ ---
368
+
369
+ ## Template directives
370
+
371
+ Directives are attributes on HTML elements. The compiler reads them once at mount and then manages the DOM reactively.
372
+
373
+ ### Conditionals: sky-if / sky-else-if / sky-else
374
+
375
+ - **`sky-if="expr"`** — Renders the element only when `expr` is truthy. Same idea as `v-if`.
376
+ - **`sky-else-if="expr"`** — Else-if branch of the previous sky-if/sky-else-if.
377
+ - **`sky-else`** — Final else branch (no expression).
378
+
379
+ Only one branch is in the DOM at a time. Use when you want to truly add/remove nodes.
380
+
381
+ ```html
382
+ <div sky-if="mode === 'edit'">Editing...</div>
383
+ <div sky-else-if="mode === 'view'">View only</div>
384
+ <div sky-else>Unknown mode</div>
385
+ ```
386
+
387
+ You can also use **`v-if`** / **`v-else-if`** / **`v-else`** or **`w-if`** / **`w-else-if`** / **`w-else`** as aliases.
388
+
389
+ ---
390
+
391
+ ### Lists: sky-for
392
+
393
+ Repeats an element for each item in a source. Syntax:
394
+
395
+ - **`sky-for="item in items"`** — `item` is the loop variable, `items` is the expression (array, object, string, or number).
396
+ - **`sky-for="(item, index) in items"`** — Same, plus `index` as the second variable.
397
+ - **`sky-key="expr"`** (optional) — Stable key per item for reuse of DOM nodes. If omitted, position is used.
398
+
399
+ **Source types:**
400
+
401
+ | Source | Iteration value | Key (when no sky-key) |
402
+ | -------- | --------------- | --------------------- |
403
+ | Array | Element | Index |
404
+ | Object | Property value | Property key |
405
+ | String | Character | Index |
406
+ | Number n | 0..n-1 | Index |
407
+
408
+ **In the loop you have:**
409
+
410
+ - `item` (or your alias) — current value
411
+ - `$item` — same as item
412
+ - `$index` — current index (0-based)
413
+ - If source is object: **`$key`** — current property key
414
+
415
+ **Examples:**
416
+
417
+ ```html
418
+ <!-- Array -->
419
+ <ul>
420
+ <li sky-for="item in items" sky-key="item.id">{{ item.name }}</li>
421
+ </ul>
422
+
423
+ <!-- With index -->
424
+ <ul>
425
+ <li sky-for="(item, i) in items">{{ i }}: {{ item }}</li>
426
+ </ul>
427
+
428
+ <!-- Object: value and $key -->
429
+ <div sky-for="(value, idx) in record" sky-key="$key">
430
+ {{ $key }}: {{ value }}
431
+ </div>
432
+
433
+ <!-- Number: repeat N times -->
434
+ <span sky-for="n in 3">{{ n }}</span>
435
+
436
+ <!-- String: per character -->
437
+ <span sky-for="char in name">{{ char }}</span>
438
+ ```
439
+
440
+ **CRUD in lists:** Prefer mutating the same reactive array (push, splice, sort, etc.) so the engine can reuse nodes by key. See [CRUD: Arrays](#arrays).
441
+
442
+ ---
443
+
444
+ ### Visibility: sky-show
445
+
446
+ Shows or hides the element with `display` (no DOM remove). Use when the element is often toggled.
447
+
448
+ ```html
449
+ <div sky-show="isVisible">Only visible when isVisible is true</div>
450
+ ```
451
+
452
+ ---
453
+
454
+ ### Raw HTML: sky-html
455
+
456
+ Sets `innerHTML` from an expression. **Only use with trusted content** — there is no built-in sanitization (XSS risk).
457
+
458
+ ```html
459
+ <div sky-html="htmlContent"></div>
460
+ ```
461
+
462
+ ---
463
+
464
+ ### One-shot bindings: sky-once
465
+
466
+ Runs a reactive binding once, then stops tracking (attribute `sky-once` or `bindReactive(..., { once: true })` internally).
467
+
468
+ ```html
469
+ <p sky-once>{{ expensive() }}</p>
470
+ ```
471
+
472
+ ---
473
+
474
+ ### Two-way binding: sky-model
475
+
476
+ Binds a form control to a context property (`input`, `textarea`, `select`, checkbox, radio).
477
+
478
+ ```html
479
+ <input type="text" sky-model="username" />
480
+ <input type="checkbox" sky-model="agreed" />
481
+ <select sky-model="choice"><option value="a">A</option></select>
482
+ ```
483
+
484
+ **Modifiers** (dot suffix on the attribute name, combinable):
485
+
486
+ | Modifier | Effect |
487
+ |----------|--------|
488
+ | `.lazy` | Use `change` instead of `input` (text fields / textarea) |
489
+ | `.number` | Coerce written value with `Number()` |
490
+ | `.trim` | `trim()` string values on write |
491
+
492
+ ```html
493
+ <input sky-model.trim="search" />
494
+ <input sky-model.number="age" />
495
+ <input sky-model.lazy="notes" />
496
+ <input sky-model.number.lazy="score" />
497
+ ```
498
+
499
+ **Custom elements** (tag with a hyphen): Vue-style prop + `update:*` event.
500
+
501
+ | Binding | Prop | Event |
502
+ |---------|------|--------|
503
+ | `sky-model="x"` | `modelValue` | `update:modelValue` |
504
+ | `sky-model:title="x"` | `title` | `update:title` |
505
+ | `sky-model:visible-items="x"` | `visibleItems` (camelCase) | `update:visibleItems` |
506
+
507
+ ```html
508
+ <sky-combobox sky-model="selectedId"></sky-combobox>
509
+ <sky-editor sky-model:content="body"></sky-editor>
510
+ ```
511
+
512
+ The element should emit `CustomEvent('update:…', { detail: newValue })`. Modifiers (`.number`, `.trim`) apply when writing into context. On native `<input>` / `<select>`, use plain `sky-model` without `:arg`.
513
+
514
+ - **Read**: Template expressions like `{{ username }}` use the current value.
515
+ - **Write**: User input updates the context property. No need to write `oninput` yourself for simple binding.
516
+ - Value must be a **single identifier** (e.g. `sky-model="username"`, not an expression).
517
+
518
+ ---
519
+
520
+ ### Element reference: sky-ref
521
+
522
+ Assigns the DOM element to a context property. Typically used with a ref so you get `someRef.value` = the element after mount.
523
+
524
+ ```html
525
+ <input sky-ref="inputEl" />
526
+ ```
527
+
528
+ In setup:
529
+
530
+ ```js
531
+ const inputEl = ref(null);
532
+ return { inputEl };
533
+ ```
534
+
535
+ After mount, `inputEl.value` is the input element. Useful for focus, measure, or native APIs. The ref is cleared on unmount.
536
+
537
+ ---
538
+
539
+ ### Class and style
540
+
541
+ - **`:class` or `sky-bind:class`** — Expression can be:
542
+ - String
543
+ - Array of strings
544
+ - Object: keys are class names, values are booleans (truthy = add class)
545
+
546
+ The result is merged with the element’s existing class (base class is preserved).
547
+
548
+ - **`:style` or `sky-bind:style`** — Expression can be a string (cssText) or an object of style properties.
549
+
550
+ ```html
551
+ <div :class="{ active: isActive, 'text-bold': bold }">...</div>
552
+ <div :class="[base, dynamicClass]">...</div>
553
+ <div :style="{ color: color, fontSize: size + 'px' }">...</div>
554
+ ```
555
+
556
+ ---
557
+
558
+ ### Element properties: sky-bind:prop
559
+
560
+ The compiler treats **`:class`** and **`:style`** (and their **`sky-bind:class`** / **`sky-bind:style`** aliases) as special bindings. **Other Vue-style `:someProp` attributes are not compiled** — use **`sky-bind:prop`** instead.
561
+
562
+ - **`sky-bind:propName="expression"`** — Evaluates the expression in context and assigns **`node[propName]`** on every change (reactive **effects**).
563
+ - The segment after **`sky-bind:`** may be **camelCase** or **kebab-case** (e.g. **`sky-bind:visible-items`** → **`visibleItems`** on the element).
564
+ - On **custom elements**, when the value is an **array** or **object**, the engine **tracks** nested reads and passes a **shallow clone** into the property (friendly for Lit-style reactive props).
565
+
566
+ ```html
567
+ <div id="app">
568
+ <sky-breadcrumb
569
+ sky-bind:items="items"
570
+ sky-bind:separator="separator"
571
+ @route-change="handleRouteChange($event)"
572
+ ></sky-breadcrumb>
573
+ </div>
574
+ ```
575
+
576
+ ```js
577
+ import { createReactivity, reactive, ref } from "@sky.ui/reactivity";
578
+
579
+ createReactivity(() => {
580
+ const items = reactive([
581
+ { title: "Home", route: "/" },
582
+ { title: "Docs", route: "/docs", active: true },
583
+ ]);
584
+ const separator = ref("/");
585
+
586
+ function handleRouteChange(e) {
587
+ const item = e.detail;
588
+ if (item?.route) {
589
+ window.history.pushState({}, "", item.route);
590
+ }
591
+ }
592
+
593
+ return {
594
+ items,
595
+ separator,
596
+ handleRouteChange,
597
+ };
598
+ }).mount("#app");
599
+ ```
600
+
601
+ Use **`reactive`** for objects and arrays you mutate in place (e.g. `items.push(…)`), and **`ref`** for primitives or values you replace wholesale (`separator.value = ">"`). Templates unwrap refs automatically (`sky-bind:separator="separator"` reads `separator.value`).
602
+
603
+ ---
604
+
605
+ ### Event handlers
606
+
607
+ - **`onclick="..."`**, **`oninput="..."`**, etc. — Value is a **statement** (can assign, call functions, etc.).
608
+ - **`@click="..."`**, **`@input="..."`** — Same, with Vue-style **`@`** prefix; the event name is everything after **`@`** (including hyphens, e.g. **`@route-change`**).
609
+
610
+ **Modifiers** (on `@` attributes only, dot-suffix): `.prevent`, `.stop`, `.self`, `.once`, `.capture`, `.passive`, `.exact`, key filters (`@keyup.enter`, `@keydown.esc`), and system keys (`.ctrl`, `.alt`, `.shift`, `.meta`).
611
+
612
+ Handlers without `await` run **synchronously** (so `if` / `switch` / loops update context before the event returns). Handlers that contain **`await`** run on the async interpreter path.
613
+
614
+ Inside the handler you can use:
615
+
616
+ - **`$event`** — The native event object.
617
+ - **`$el`** — The element the handler is attached to.
618
+ - Any context property (refs auto-unwrapped for read/assign, e.g. `count++`).
619
+
620
+ ```html
621
+ <button onclick="count++">Increment</button>
622
+ <button @click.prevent="submit()">Submit</button>
623
+ <input @keydown.enter="onEnter()" />
624
+ <button @click="await load()">Load</button>
625
+ ```
626
+
627
+ ---
628
+
629
+ ## Server-side rendering (SSR)
630
+
631
+ **`renderToString(markup, ctx, options?)`** compiles directives and `{{ }}` once (no reactive effects). Works in the browser (`DOMParser`) or in Node with optional peer **`linkedom`**.
632
+
633
+ ```js
634
+ import { renderToString } from "@sky.ui/reactivity";
635
+
636
+ const html = await renderToString(
637
+ "<!DOCTYPE html><html><body><p>{{ msg }}</p></body></html>",
638
+ { msg: "Hello" },
639
+ { stripAnchors: true } // default: remove <!--sky-for--> / <!--if-chain-->
640
+ );
641
+ ```
642
+
643
+ | Option | Default | Description |
644
+ |--------|---------|-------------|
645
+ | `root` | `body` | CSS selector for compile root |
646
+ | `initial` | — | Merged into context before compile |
647
+ | `fullDocument` | `false` | Return `<html>…</html>` instead of root outerHTML |
648
+ | `stripAnchors` | `true` | Remove compiler comment anchors from output |
649
+
650
+ There is **no hydration** yet — use SSR for static HTML, previews, or tests. For tooling/tests you can also use **`runHandlerCode(code, ctx, $event?, $el?)`**.
651
+
652
+ ---
653
+
654
+ ## CRUD: Create, Read, Update, Delete
655
+
656
+ All of these assume you use **reactive** objects/arrays (or refs holding them) so that the template and any `effect`/`watch` stay in sync.
657
+
658
+ ### Arrays
659
+
660
+ **Create (add items):**
661
+
662
+ ```js
663
+ // Add at end
664
+ ctx.items.push({ id: 1, name: "New" });
665
+
666
+ // Add at start
667
+ ctx.items.unshift({ id: 0, name: "First" });
668
+
669
+ // Add at index
670
+ ctx.items.splice(2, 0, { id: 2, name: "Middle" });
671
+ ```
672
+
673
+ **Read:**
674
+
675
+ - In template: `sky-for="item in items"` and `{{ item.name }}`, or `{{ items[0] }}`, etc.
676
+ - In JS: `ctx.items[i]`, `ctx.items.length`, or iterate as usual.
677
+
678
+ **Update (in place):**
679
+
680
+ ```js
681
+ // By index
682
+ ctx.items[0] = { id: 1, name: "Updated" };
683
+
684
+ // Find and update (same reference or replace)
685
+ const idx = ctx.items.findIndex((x) => x.id === id);
686
+ if (idx !== -1) ctx.items[idx] = { ...ctx.items[idx], name: "New Name" };
687
+ ```
688
+
689
+ **Delete:**
690
+
691
+ ```js
692
+ // By index (one item)
693
+ ctx.items.splice(index, 1);
694
+
695
+ // Remove by id (example)
696
+ const idx = ctx.items.findIndex((x) => x.id === id);
697
+ if (idx !== -1) ctx.items.splice(idx, 1);
698
+
699
+ // Clear all
700
+ ctx.items.length = 0;
701
+ // or
702
+ ctx.items.splice(0, ctx.items.length);
703
+ ```
704
+
705
+ Use **sky-key** (e.g. `sky-key="item.id"`) when doing heavy add/remove/reorder so the engine can reuse DOM nodes correctly.
706
+
707
+ ---
708
+
709
+ ### Objects
710
+
711
+ **Create (add property):**
712
+
713
+ ```js
714
+ ctx.user = { name: "Alice", age: 30 };
715
+ // or add to existing
716
+ ctx.user.role = "admin";
717
+ ```
718
+
719
+ **Read:**
720
+
721
+ - In template: `{{ user.name }}`, `{{ user.age }}`.
722
+ - In JS: `ctx.user.name`, etc.
723
+
724
+ **Update:**
725
+
726
+ ```js
727
+ ctx.user.name = "Bob";
728
+ ctx.user.age = 31;
729
+ // or replace whole object
730
+ ctx.user = { ...ctx.user, name: "Bob" };
731
+ ```
732
+
733
+ **Delete:**
734
+
735
+ ```js
736
+ delete ctx.user.role; // triggers reactivity (reactive() implements deleteProperty)
737
+ ```
738
+
739
+ Nested objects and arrays are made reactive when accessed, so mutations deep in the tree still trigger updates.
740
+
741
+ ---
742
+
743
+ ### Nested structures
744
+
745
+ Keep the same reactive root; mutate nested data in place when possible so the engine tracks changes:
746
+
747
+ ```js
748
+ // Good: same reactive object, property update
749
+ ctx.state.filters.name = "x";
750
+
751
+ // Good: same reactive array, mutation
752
+ ctx.state.list.push(item);
753
+
754
+ // Also fine: replace a nested object entirely
755
+ ctx.state.filters = { name: "x", type: "a" };
756
+ ```
757
+
758
+ For arrays of objects, prefer updating one element by index or key rather than replacing the whole array if you want minimal DOM updates and stable keys in `sky-for`.
759
+
760
+ ---
761
+
762
+ ## Expression context and ref unwrapping
763
+
764
+ Inside template expressions and event handler statements:
765
+
766
+ - **Identifiers** (e.g. `count`, `items`) resolve to context properties. If the value is a **ref**, it is **auto-unwrapped**: you read and write the inner value (e.g. `count` means `count.value`).
767
+ - **Member access** (e.g. `user.name`) also unwraps the receiver if it’s a ref, so `user.name` works when `user` is a ref.
768
+ - **Assignment** to a ref in context (e.g. `count = 5`) is compiled to `count.value = 5`.
769
+ - **$event** and **$el** are available in event handlers as the native event and the element.
770
+ - **sky-for** adds `item`, `$item`, `$index`, and for objects `$key`, in that block’s scope.
771
+
772
+ So in templates you can write `{{ count }}`, `count++`, `user.name`, `items.push(x)` without using `.value`.
773
+
774
+ ---
775
+
776
+ ## What is supported in expressions
777
+
778
+ - Literals, identifiers, property access (including computed like `obj[key]`)
779
+ - **Optional chaining** (`?.`) and **nullish coalescing** (`??`)
780
+ - Unary: `!`, `+`, `-`, `typeof`, etc.
781
+ - Binary: `+`, `-`, `*`, `/`, `%`, `**`, `==`, `===`, `!=`, `!==`, `<`, `<=`, `>`, `>=`, `&&`, `||`, `in`, `instanceof`
782
+ - Ternary: `a ? b : c`
783
+ - Assignment: `=`, `+=`, `-=`, etc.
784
+ - Increment/decrement: `++`, `--`
785
+ - Function calls: `fn()`, `obj.method()`, and **spread** `fn(...args)`
786
+ - Arrays and objects: `[a, b]`, `{ key: value }`
787
+ - Template literals: `` `Hello ${name}` ``
788
+ - In **statements** (event handlers): `if` / `else`, `for` / `while` / `do…while`, `for…in` / `for…of`, `switch` / `break` / `continue`, `try` / `catch` / `finally`, `throw`, `return`, `var`/`let`/`const`, blocks `{ }`, and top-level **`await`** (async path)
789
+
790
+ ---
791
+
792
+ ## What is blocked or unsupported
793
+
794
+ - **Blocked identifiers** (cannot be used as variable names or in scope): `window`, `self`, `document`, `globalThis`, `Function`, `eval`, `constructor`, `__proto__`, `prototype`, `location`, `localStorage`, `sessionStorage`, `indexedDB`, `navigator`, `fetch`, `XMLHttpRequest`, `WebSocket`.
795
+ - **Blocked properties**: `__proto__`, `prototype`, `constructor`.
796
+ - **`delete` in expressions**: No side effect (returns `true` only).
797
+ - **SSR hydration**, Teleport, slots, and full Vue SFC parity.
798
+
799
+ If you need to call `fetch` or use `window`, expose a function from setup and call it from the template, e.g. `@click="loadData()"`.
800
+
801
+ ---
802
+
803
+ ## Common patterns
804
+
805
+ **Form with validation:**
806
+
807
+ ```js
808
+ createReactivity(() => {
809
+ const form = reactive({ email: "", name: "" });
810
+ const error = ref("");
811
+ const submit = () => {
812
+ if (!form.email) {
813
+ error.value = "Email required";
814
+ return;
815
+ }
816
+ error.value = "";
817
+ // submit...
818
+ };
819
+ return { ...form, error, submit };
820
+ });
821
+ ```
822
+
823
+ ```html
824
+ <input sky-model="email" />
825
+ <span sky-show="error">{{ error }}</span>
826
+ <button onclick="submit()">Submit</button>
827
+ ```
828
+
829
+ **List with add/remove and selection:**
830
+
831
+ ```js
832
+ createReactivity(() => {
833
+ const items = reactive([
834
+ { id: 1, name: "A" },
835
+ { id: 2, name: "B" },
836
+ ]);
837
+ const selectedId = ref(null);
838
+ const add = () => items.push({ id: Date.now(), name: "New" });
839
+ const remove = (id) => {
840
+ const i = items.findIndex((x) => x.id === id);
841
+ if (i !== -1) items.splice(i, 1);
842
+ };
843
+ return { items, selectedId, add, remove };
844
+ });
845
+ ```
846
+
847
+ ```html
848
+ <ul>
849
+ <li sky-for="item in items" sky-key="item.id">
850
+ {{ item.name }}
851
+ <button onclick="selectedId = item.id">Select</button>
852
+ <button onclick="remove(item.id)">Remove</button>
853
+ </li>
854
+ </ul>
855
+ <button onclick="add()">Add item</button>
856
+ ```
857
+
858
+ **Filtering a list (computed):**
859
+
860
+ ```js
861
+ const query = ref('');
862
+ const allItems = reactive([...]);
863
+ const filtered = computed(() =>
864
+ allItems.filter((x) => x.name.toLowerCase().includes(query.value.toLowerCase()))
865
+ );
866
+ return { query, filtered };
867
+ ```
868
+
869
+ ```html
870
+ <input sky-model="query" />
871
+ <ul>
872
+ <li sky-for="item in filtered" sky-key="item.id">{{ item.name }}</li>
873
+ </ul>
874
+ ```
875
+
876
+ **Async load and show state:**
877
+
878
+ ```js
879
+ const data = ref(null);
880
+ const loading = ref(false);
881
+ const load = async () => {
882
+ loading.value = true;
883
+ try {
884
+ const res = await fetch("/api/data");
885
+ data.value = await res.json();
886
+ } finally {
887
+ loading.value = false;
888
+ }
889
+ };
890
+ return { data, loading, load };
891
+ ```
892
+
893
+ ```html
894
+ <button onclick="load()">Load</button>
895
+ <div sky-if="loading">Loading...</div>
896
+ <div sky-else-if="data">{{ data }}</div>
897
+ ```
898
+
899
+ ---
900
+
901
+ ## Development warnings
902
+
903
+ In development (`__SKY_DEV__` is true by default except when `NODE_ENV === 'production'`), Sky prints **Vue-style** warnings:
904
+
905
+ ```text
906
+ [Sky warn]: sky-for without sky-key may recreate DOM nodes on list updates. Add sky-key="item.id" (or a stable key).
907
+ at <li#item sky-for="item in items">
908
+ (sky-for)
909
+ ```
910
+
911
+ - **`warnOnce`** — same code + element only once (e.g. missing `sky-key`, `sky-html` XSS note).
912
+ - **`warn`** — every time (e.g. invalid `sky-model` value).
913
+ - Stable warning codes (e.g. missing `sky-key`, invalid `sky-model`) — see package exports for `WarnCodes` when building custom tooling.
914
+
915
+ ```js
916
+ import { setSkyDev, setWarnHandler, WarnCodes } from "@sky.ui/reactivity";
917
+
918
+ setSkyDev(true); // force warnings on
919
+ setWarnHandler((code, message, ctx) => {
920
+ myLogger.warn(code, message, ctx);
921
+ });
922
+ ```
923
+
924
+ ---
925
+
926
+ ## Troubleshooting
927
+
928
+ | Problem | What to check |
929
+ | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
930
+ | Template doesn’t update when I change data | Ensure the object/array is **reactive** (created with `reactive()` or returned from setup so the engine wrapped it). Replacing the whole reference (e.g. `ctx.items = []`) is fine; replacing a ref’s inner value without going through the ref can skip tracking. |
931
+ | Ref in template: “undefined” or wrong value | In templates you don’t use `.value`; the engine unwraps refs. If you see the ref object, you might be reading it in JS without `.value`, or the ref isn’t on the context. |
932
+ | sky-for not updating / wrong order | Use **sky-key** with a unique stable id. Prefer mutating the same array (push/splice) instead of replacing the whole array if you need stable DOM reuse. |
933
+ | “Blocked identifier” or “Blocked property” | Don’t use reserved globals or properties in expressions. Call a function from context that uses them in normal JS instead. |
934
+ | Event handler not firing | Use `onclick`/`oninput`/etc. or `@click`/`@input`; attribute names are case-sensitive. Ensure the attribute value is a valid statement. |
935
+ | sky-model not updating variable | The attribute value must be a single variable name (e.g. `sky-model="username"`). That name must exist on the context. |
936
+ | Need to run code after DOM update | Use `await nextTick()` after mutating state, then measure or call focus. |
937
+ | Memory leak / duplicate updates after navigation | Call **unmount()** when removing the app from the page so effects and listeners are cleared. |
938
+
939
+ ---
940
+
941
+ ## Vue 3 comparison
942
+
943
+ Sky implements a **subset** of Vue 3’s reactivity and template ergonomics, not the full runtime (no components, VNodes, or hydration).
944
+
945
+ ### Implemented (close to Vue 3)
946
+
947
+ | API / feature | Notes |
948
+ |---------------|--------|
949
+ | `ref`, `reactive`, `computed`, `watch`, `effect` | Core tracking; refs unwrapped in templates |
950
+ | `shallowRef`, `triggerRef`, `shallowReactive`, `shallowReadonly` | Shallow variants |
951
+ | `readonly`, `toRaw`, `markRaw`, `isReactive`, `isReadonly`, `isRef`, `unref` | Proxies and guards |
952
+ | `toRef`, `toRefs`, `customRef` | Ref helpers |
953
+ | `watchEffect`, `effectScope`, `getCurrentScope`, `onScopeDispose` | Effects and grouping |
954
+ | `nextTick`, `flushSync` | Scheduling (flush runs after reactive jobs) |
955
+ | `effect` cleanup / `pause` / `resume` | On returned runner |
956
+ | Directives | `sky-if` chain, keyed `sky-for` (DOM move), `sky-show`, `sky-model`, `sky-ref`, `sky-html`, `sky-once`, `:class` / `:style` / `sky-bind:*` |
957
+ | Events | `@event`, modifiers, sync handlers + `await` in handlers |
958
+ | SSR | `renderToString` + optional `linkedom` |
959
+
960
+ ### Not implemented (Vue features)
961
+
962
+ - Component model, `provide`/`inject`, Teleport, KeepAlive, transitions
963
+ - SSR **hydration** and streaming
964
+ - Full template compiler (only in-DOM directives + `{{ }}`)
965
+ - Vue SFC / multi-model on one component beyond `sky-model:arg` syntax
966
+
967
+ ---
968
+
969
+ ## Summary
970
+
971
+ - Use **createReactivity(setup).mount(target, initial)** for apps; **mount(root, ctx)** for low-level control; **renderToString** for one-shot HTML.
972
+ - Prefer **sky-key** on **sky-for**; list updates **move** existing nodes when keys match.
973
+ - Handlers support **if / loops / switch / try / await**; use setup functions for heavy logic and browser APIs.
974
+ - **sky-html** is trusted-only; call **unmount()** when tearing down the app.
975
+
976
+ ---
977
+
978
+ ## License
979
+
980
+ [Sky UI Free EULA](./LICENSE.md) — proprietary; not open source.
981
+