@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/LICENSE.md +139 -0
- package/README.md +981 -0
- package/dist/index.d.ts +198 -0
- package/dist/index.js +2671 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
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
|
+
|