@deijose/nix-js 0.1.2 → 0.1.4
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 +58 -955
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,1032 +1,135 @@
|
|
|
1
1
|
# ❄️ Nix.js
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A lightweight, fully reactive micro-framework for building modern web UIs — no virtual DOM, no compiler, no build-time magic. Just signals, tagged templates, and pure TypeScript.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
~28 KB source · zero dependencies · TypeScript-first · ES2022
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Table of Contents
|
|
12
|
-
|
|
13
|
-
- [Overview](#overview)
|
|
14
|
-
- [Installation & Setup](#installation--setup)
|
|
15
|
-
- [Core Concepts](#core-concepts)
|
|
16
|
-
- [Reactivity](#reactivity)
|
|
17
|
-
- [signal](#signal)
|
|
18
|
-
- [computed](#computed)
|
|
19
|
-
- [effect](#effect)
|
|
20
|
-
- [batch](#batch)
|
|
21
|
-
- [watch](#watch)
|
|
22
|
-
- [untrack](#untrack)
|
|
23
|
-
- [nextTick](#nexttick)
|
|
24
|
-
- [Templates](#templates)
|
|
25
|
-
- [html tag](#html-tag)
|
|
26
|
-
- [Text bindings](#text-bindings)
|
|
27
|
-
- [Attribute bindings](#attribute-bindings)
|
|
28
|
-
- [Event bindings & modifiers](#event-bindings--modifiers)
|
|
29
|
-
- [Conditional rendering](#conditional-rendering)
|
|
30
|
-
- [List rendering](#list-rendering)
|
|
31
|
-
- [Keyed lists: repeat()](#keyed-lists-repeat)
|
|
32
|
-
- [DOM refs: ref()](#dom-refs-ref)
|
|
33
|
-
- [Components](#components)
|
|
34
|
-
- [Function components](#function-components)
|
|
35
|
-
- [Class components: NixComponent](#class-components-nixcomponent)
|
|
36
|
-
- [Lifecycle hooks](#lifecycle-hooks)
|
|
37
|
-
- [mount()](#mount)
|
|
38
|
-
- [Dependency Injection](#dependency-injection)
|
|
39
|
-
- [provide / inject](#provide--inject)
|
|
40
|
-
- [createInjectionKey](#createinjectionkey)
|
|
41
|
-
- [Global Stores](#global-stores)
|
|
42
|
-
- [createStore](#createstore)
|
|
43
|
-
- [Router](#router)
|
|
44
|
-
- [createRouter](#createrouter)
|
|
45
|
-
- [RouterView](#routerview)
|
|
46
|
-
- [Link](#link)
|
|
47
|
-
- [useRouter](#userouter)
|
|
48
|
-
- [Nested routes](#nested-routes)
|
|
49
|
-
- [Query parameters](#query-parameters)
|
|
50
|
-
- [Async & Lazy Loading](#async--lazy-loading)
|
|
51
|
-
- [suspend()](#suspend)
|
|
52
|
-
- [lazy()](#lazy)
|
|
53
|
-
- [API Reference](#api-reference)
|
|
54
|
-
- [Known Limitations](#known-limitations)
|
|
55
|
-
|
|
56
|
-
---
|
|
57
|
-
|
|
58
|
-
## Overview
|
|
59
|
-
|
|
60
|
-
Nix.js is a signal-based reactive micro-framework. Its design goals are:
|
|
61
|
-
|
|
62
|
-
- **No virtual DOM.** Bindings update individual DOM nodes directly via `effect()`.
|
|
63
|
-
- **No compiler.** Templates are standard JavaScript tagged template literals.
|
|
64
|
-
- **Fine-grained reactivity.** Only the exact text nodes and attributes that depend on a changed signal are updated — no diffing of full component trees.
|
|
65
|
-
- **Zero runtime dependencies.** The entire framework is ~28 KB of TypeScript source with no `node_modules` at runtime.
|
|
66
|
-
- **TypeScript-first.** Every public API is fully typed, including typed injection keys and typed store signals.
|
|
67
|
-
|
|
68
|
-
### Architecture at a glance
|
|
5
|
+
The `@deijose/nix-js` package provides everything you need to build reactive UIs: signals, template engine, components, stores, router, and dependency injection in a single zero-dependency bundle.
|
|
69
6
|
|
|
70
7
|
```
|
|
71
|
-
|
|
72
|
-
│ │
|
|
73
|
-
html`` │
|
|
74
|
-
│ ┌─ text node │
|
|
75
|
-
└── binding ────┤─ attribute (reactive) ─┘
|
|
76
|
-
└─ child node
|
|
8
|
+
~14 KB minified · zero dependencies · TypeScript-first · ES2022
|
|
77
9
|
```
|
|
78
10
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## Installation & Setup
|
|
84
|
-
|
|
85
|
-
Nix.js uses [Vite](https://vitejs.dev/) as its dev server and bundler.
|
|
11
|
+
## Installation
|
|
86
12
|
|
|
87
13
|
```bash
|
|
88
|
-
# Install as a dependency
|
|
89
14
|
npm install @deijose/nix-js
|
|
90
15
|
# or
|
|
91
16
|
bun add @deijose/nix-js
|
|
92
17
|
```
|
|
93
18
|
|
|
94
|
-
|
|
95
|
-
import { signal, html, NixComponent, mount } from "@deijose/nix-js";
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
### Development (from source)
|
|
99
|
-
|
|
100
|
-
# Start development server
|
|
101
|
-
npm run dev # or: bun dev
|
|
102
|
-
|
|
103
|
-
# Type check
|
|
104
|
-
npx tsc --noEmit
|
|
105
|
-
|
|
106
|
-
# Production build
|
|
107
|
-
npm run build
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### Project structure
|
|
111
|
-
|
|
112
|
-
```
|
|
113
|
-
src/
|
|
114
|
-
nix/
|
|
115
|
-
reactivity.ts — signal, effect, computed, batch, watch, untrack, nextTick
|
|
116
|
-
template.ts — html``, repeat(), ref()
|
|
117
|
-
lifecycle.ts — NixComponent base class
|
|
118
|
-
component.ts — mount()
|
|
119
|
-
store.ts — createStore()
|
|
120
|
-
router.ts — createRouter(), RouterView, Link, useRouter()
|
|
121
|
-
async.ts — suspend(), lazy()
|
|
122
|
-
context.ts — provide(), inject(), createInjectionKey()
|
|
123
|
-
index.ts — re-exports everything
|
|
124
|
-
main.ts — application entry point
|
|
125
|
-
index.html
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
Import everything from the single entry point:
|
|
129
|
-
|
|
130
|
-
```typescript
|
|
131
|
-
import {
|
|
132
|
-
signal, computed, effect, batch, watch, untrack, nextTick,
|
|
133
|
-
html, repeat, ref,
|
|
134
|
-
NixComponent, mount,
|
|
135
|
-
createStore,
|
|
136
|
-
createRouter, RouterView, Link, useRouter,
|
|
137
|
-
suspend, lazy,
|
|
138
|
-
provide, inject, createInjectionKey,
|
|
139
|
-
} from "./nix";
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
---
|
|
143
|
-
|
|
144
|
-
## Core Concepts
|
|
145
|
-
|
|
146
|
-
Nix.js is built around three primitives:
|
|
147
|
-
|
|
148
|
-
| Primitive | Role |
|
|
149
|
-
|-----------|------|
|
|
150
|
-
| `signal(v)` | A reactive value. Reading it inside an `effect` creates a subscription. |
|
|
151
|
-
| `effect(fn)` | A function that re-runs whenever any signal it read changes. |
|
|
152
|
-
| `html\`\`` | A tagged template that turns an HTML string + bindings into a live DOM fragment. |
|
|
153
|
-
|
|
154
|
-
Everything else — `computed`, `watch`, `repeat`, `NixComponent`, `createStore`, the router, `provide`/`inject` — is built on top of these three primitives.
|
|
155
|
-
|
|
156
|
-
---
|
|
157
|
-
|
|
158
|
-
## Reactivity
|
|
159
|
-
|
|
160
|
-
### `signal`
|
|
161
|
-
|
|
162
|
-
Creates a reactive container for a single value.
|
|
163
|
-
|
|
164
|
-
```typescript
|
|
165
|
-
const count = signal(0);
|
|
166
|
-
|
|
167
|
-
count.value; // get — 0
|
|
168
|
-
count.value = 1; // set — notifies subscribers
|
|
169
|
-
count.update(n => n + 1); // set via updater function
|
|
170
|
-
count.peek(); // get WITHOUT subscribing (no tracking)
|
|
171
|
-
count.dispose(); // remove all subscribers
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
Signals use `Object.is` equality — setting the same value does nothing.
|
|
175
|
-
|
|
176
|
-
### `computed`
|
|
177
|
-
|
|
178
|
-
A derived signal whose value is recalculated automatically when its dependencies change.
|
|
179
|
-
|
|
180
|
-
```typescript
|
|
181
|
-
const price = signal(10);
|
|
182
|
-
const qty = signal(3);
|
|
183
|
-
const total = computed(() => price.value * qty.value);
|
|
184
|
-
|
|
185
|
-
console.log(total.value); // 30
|
|
186
|
-
|
|
187
|
-
price.value = 20;
|
|
188
|
-
console.log(total.value); // 60 — updated automatically
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
`computed` returns a `Signal<T>`, so it has `.value`, `.peek()`, etc.
|
|
192
|
-
|
|
193
|
-
### `effect`
|
|
194
|
-
|
|
195
|
-
Runs a function immediately and re-runs it whenever any signal read inside it changes. Returns a `dispose` function to stop the effect.
|
|
196
|
-
|
|
197
|
-
```typescript
|
|
198
|
-
const name = signal("Alice");
|
|
199
|
-
|
|
200
|
-
const dispose = effect(() => {
|
|
201
|
-
document.title = `Hello, ${name.value}`;
|
|
202
|
-
// optional — return a cleanup function:
|
|
203
|
-
return () => console.log("effect cleaned up");
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
name.value = "Bob"; // re-runs the effect → document.title = "Hello, Bob"
|
|
207
|
-
|
|
208
|
-
dispose(); // stops the effect
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
Effects are **self-cleaning**: before each re-run, the previous cleanup (if any) is called and all old subscriptions are dropped. This prevents stale subscriptions to signals that are no longer read.
|
|
212
|
-
|
|
213
|
-
### `batch`
|
|
214
|
-
|
|
215
|
-
Groups multiple signal writes into a single effect flush. Without `batch`, each write triggers its effects individually.
|
|
216
|
-
|
|
217
|
-
```typescript
|
|
218
|
-
const x = signal(0);
|
|
219
|
-
const y = signal(0);
|
|
220
|
-
|
|
221
|
-
effect(() => console.log(x.value + y.value));
|
|
222
|
-
|
|
223
|
-
// Without batch: effect runs twice
|
|
224
|
-
x.value = 1;
|
|
225
|
-
y.value = 2;
|
|
226
|
-
|
|
227
|
-
// With batch: effect runs once, at the end
|
|
228
|
-
batch(() => {
|
|
229
|
-
x.value = 10;
|
|
230
|
-
y.value = 20;
|
|
231
|
-
});
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
### `watch`
|
|
235
|
-
|
|
236
|
-
Watches a reactive source and calls a callback with `(newValue, oldValue)` when it changes. Unlike `effect`, it does **not** run on initialization by default.
|
|
237
|
-
|
|
238
|
-
```typescript
|
|
239
|
-
const count = signal(0);
|
|
240
|
-
|
|
241
|
-
const stop = watch(count, (newVal, oldVal) => {
|
|
242
|
-
console.log(`${oldVal} → ${newVal}`);
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
count.value = 1; // logs: "0 → 1"
|
|
246
|
-
|
|
247
|
-
stop(); // stop watching
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
**Options:**
|
|
251
|
-
|
|
252
|
-
| Option | Type | Default | Description |
|
|
253
|
-
|--------|------|---------|-------------|
|
|
254
|
-
| `immediate` | `boolean` | `false` | Run callback immediately with the current value |
|
|
255
|
-
| `once` | `boolean` | `false` | Auto-dispose after the first callback invocation |
|
|
256
|
-
|
|
257
|
-
```typescript
|
|
258
|
-
// Watch a computed expression
|
|
259
|
-
watch(
|
|
260
|
-
() => user.value.role,
|
|
261
|
-
(role) => console.log("Role changed:", role),
|
|
262
|
-
{ immediate: true }
|
|
263
|
-
);
|
|
264
|
-
|
|
265
|
-
// One-shot watcher
|
|
266
|
-
watch(
|
|
267
|
-
isReady,
|
|
268
|
-
() => initApp(),
|
|
269
|
-
{ once: true }
|
|
270
|
-
);
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
### `untrack`
|
|
274
|
-
|
|
275
|
-
Reads signals inside `fn` without creating subscriptions. Useful when you need a value but don't want the current `effect` to re-run when that signal changes.
|
|
276
|
-
|
|
277
|
-
```typescript
|
|
278
|
-
const a = signal(1);
|
|
279
|
-
const b = signal(2);
|
|
280
|
-
|
|
281
|
-
effect(() => {
|
|
282
|
-
const aVal = a.value; // subscribed — effect re-runs when a changes
|
|
283
|
-
const bVal = untrack(() => b.value); // NOT subscribed — b changes won't trigger this
|
|
284
|
-
console.log(aVal + bVal);
|
|
285
|
-
});
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
### `nextTick`
|
|
289
|
-
|
|
290
|
-
Returns a `Promise<void>` that resolves after the current synchronous effect queue has flushed. Use it to read the DOM after a reactive change.
|
|
291
|
-
|
|
292
|
-
```typescript
|
|
293
|
-
const text = signal("hello");
|
|
294
|
-
|
|
295
|
-
text.value = "world";
|
|
296
|
-
await nextTick();
|
|
297
|
-
console.log(document.querySelector("#el")?.textContent); // "world"
|
|
298
|
-
|
|
299
|
-
// Callback variant:
|
|
300
|
-
await nextTick(() => inputRef.el?.focus());
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
---
|
|
304
|
-
|
|
305
|
-
## Templates
|
|
306
|
-
|
|
307
|
-
### `html` tag
|
|
308
|
-
|
|
309
|
-
`html` is a tagged template literal that returns a `NixTemplate`. It parses the HTML once and creates a `DocumentFragment` with live bindings.
|
|
310
|
-
|
|
311
|
-
```typescript
|
|
312
|
-
import { html, signal, mount } from "./nix";
|
|
313
|
-
|
|
314
|
-
const name = signal("world");
|
|
315
|
-
const tpl = html`<h1>Hello, ${() => name.value}!</h1>`;
|
|
316
|
-
|
|
317
|
-
mount(tpl, "#app");
|
|
318
|
-
name.value = "Nix"; // DOM updates automatically
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
### Text bindings
|
|
322
|
-
|
|
323
|
-
| Syntax | Behavior |
|
|
324
|
-
|--------|----------|
|
|
325
|
-
| `${value}` | Static — inserted once as a text node |
|
|
326
|
-
| `${() => expr}` | Reactive — updates the text node whenever signals inside change |
|
|
327
|
-
|
|
328
|
-
```typescript
|
|
329
|
-
const count = signal(0);
|
|
330
|
-
|
|
331
|
-
html`
|
|
332
|
-
<p>Static: ${"hello"}</p>
|
|
333
|
-
<p>Reactive: ${() => count.value}</p>
|
|
334
|
-
<p>Expression: ${() => count.value > 0 ? "positive" : "zero or negative"}</p>
|
|
335
|
-
`
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
### Attribute bindings
|
|
339
|
-
|
|
340
|
-
```typescript
|
|
341
|
-
const active = signal(true);
|
|
342
|
-
const label = signal("Submit");
|
|
343
|
-
const classes = signal("btn btn-primary");
|
|
344
|
-
|
|
345
|
-
html`
|
|
346
|
-
<button
|
|
347
|
-
class=${classes}
|
|
348
|
-
disabled=${() => !active.value}
|
|
349
|
-
aria-label=${() => label.value}
|
|
350
|
-
>Submit</button>
|
|
351
|
-
`
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
- Static value → set once.
|
|
355
|
-
- `() => value` → reactive, updates via `effect`.
|
|
356
|
-
- `null`, `undefined`, or `false` → attribute is **removed**.
|
|
357
|
-
|
|
358
|
-
> **Important:** Each attribute binding must be a single interpolation that covers the entire value. Partial interpolation inside a string is not supported:
|
|
359
|
-
>
|
|
360
|
-
> ```typescript
|
|
361
|
-
> // ✅ Correct — the whole value is one interpolation
|
|
362
|
-
> html`<div class=${() => `item ${active.value ? "active" : ""}`}>`
|
|
363
|
-
>
|
|
364
|
-
> // ❌ Incorrect — mixing a literal prefix with an interpolation
|
|
365
|
-
> html`<div class="item ${() => active.value ? 'active' : ''}">`
|
|
366
|
-
> ```
|
|
367
|
-
|
|
368
|
-
### Event bindings & modifiers
|
|
369
|
-
|
|
370
|
-
Events are bound with `@eventname=`:
|
|
371
|
-
|
|
372
|
-
```typescript
|
|
373
|
-
const count = signal(0);
|
|
374
|
-
|
|
375
|
-
html`
|
|
376
|
-
<button @click=${() => count.value++}>Increment</button>
|
|
377
|
-
<input @input=${(e: Event) => console.log((e.target as HTMLInputElement).value)} />
|
|
378
|
-
`
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
**Modifiers** are chained after the event name with `.`:
|
|
382
|
-
|
|
383
|
-
| Modifier | Effect |
|
|
384
|
-
|----------|--------|
|
|
385
|
-
| `.prevent` | `e.preventDefault()` |
|
|
386
|
-
| `.stop` | `e.stopPropagation()` |
|
|
387
|
-
| `.once` | Listener removed after first call |
|
|
388
|
-
| `.capture` | `useCapture = true` |
|
|
389
|
-
| `.passive` | `passive: true` (performance hint) |
|
|
390
|
-
| `.self` | Handler runs only when `e.target === e.currentTarget` |
|
|
391
|
-
| `.enter` | Only fires when `Enter` key is pressed |
|
|
392
|
-
| `.escape` | Only fires on `Escape` |
|
|
393
|
-
| `.space` | Only fires on Space |
|
|
394
|
-
| `.tab`, `.delete`, `.backspace` | Corresponding keys |
|
|
395
|
-
| `.up`, `.down`, `.left`, `.right` | Arrow keys |
|
|
396
|
-
| `.a`–`.z`, `.0`–`.9` | Single character key filter |
|
|
397
|
-
|
|
398
|
-
```typescript
|
|
399
|
-
html`
|
|
400
|
-
<form @submit.prevent=${handleSubmit}>
|
|
401
|
-
<input @keydown.enter=${submitOnEnter} />
|
|
402
|
-
<button @click.stop.once=${doOnce}>Once</button>
|
|
403
|
-
</form>
|
|
404
|
-
`
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
### Conditional rendering
|
|
408
|
-
|
|
409
|
-
Return a `NixTemplate` or `null`/`false` from a function binding:
|
|
410
|
-
|
|
411
|
-
```typescript
|
|
412
|
-
const show = signal(true);
|
|
413
|
-
|
|
414
|
-
html`
|
|
415
|
-
<div>
|
|
416
|
-
${() => show.value
|
|
417
|
-
? html`<p>Visible content</p>`
|
|
418
|
-
: null
|
|
419
|
-
}
|
|
420
|
-
</div>
|
|
421
|
-
`
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
When the condition changes, the previous DOM is fully cleaned up (effects disposed, `onUnmount` called) and the new branch is rendered.
|
|
425
|
-
|
|
426
|
-
### List rendering
|
|
427
|
-
|
|
428
|
-
For simple, stable lists:
|
|
429
|
-
|
|
430
|
-
```typescript
|
|
431
|
-
const items = ["Apple", "Banana", "Cherry"];
|
|
432
|
-
|
|
433
|
-
html`
|
|
434
|
-
<ul>
|
|
435
|
-
${items.map(item => html`<li>${item}</li>`)}
|
|
436
|
-
</ul>
|
|
437
|
-
`
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
For reactive lists that change over time, prefer `repeat()`.
|
|
441
|
-
|
|
442
|
-
### Keyed lists: `repeat()`
|
|
443
|
-
|
|
444
|
-
`repeat()` enables efficient diffing: DOM nodes for unchanged keys are preserved and **only** added, removed, or reordered items are touched.
|
|
445
|
-
|
|
446
|
-
```typescript
|
|
447
|
-
import { repeat } from "./nix";
|
|
448
|
-
|
|
449
|
-
const todos = signal([
|
|
450
|
-
{ id: 1, text: "Buy milk" },
|
|
451
|
-
{ id: 2, text: "Write docs" },
|
|
452
|
-
]);
|
|
453
|
-
|
|
454
|
-
html`
|
|
455
|
-
<ul>
|
|
456
|
-
${() => repeat(
|
|
457
|
-
todos.value,
|
|
458
|
-
todo => todo.id, // key function — must be unique
|
|
459
|
-
todo => html`<li>${todo.text}</li>`
|
|
460
|
-
)}
|
|
461
|
-
</ul>
|
|
462
|
-
`
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
**Signature:**
|
|
466
|
-
```typescript
|
|
467
|
-
function repeat<T>(
|
|
468
|
-
items: T[],
|
|
469
|
-
keyFn: (item: T, index: number) => string | number,
|
|
470
|
-
renderFn: (item: T, index: number) => NixTemplate | NixComponent
|
|
471
|
-
): KeyedList<T>
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
### DOM refs: `ref()`
|
|
475
|
-
|
|
476
|
-
`ref()` creates a typed container that is filled with the actual DOM element after mount, and cleared on unmount.
|
|
19
|
+
## Usage
|
|
477
20
|
|
|
478
21
|
```typescript
|
|
479
|
-
import {
|
|
480
|
-
|
|
481
|
-
const inputRef = ref<HTMLInputElement>();
|
|
482
|
-
|
|
483
|
-
const tpl = html`<input ref=${inputRef} type="text" />`;
|
|
484
|
-
|
|
485
|
-
mount(tpl, "#app");
|
|
486
|
-
|
|
487
|
-
// inputRef.el is now the <input> element
|
|
488
|
-
inputRef.el?.focus();
|
|
489
|
-
inputRef.el?.value; // ""
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
The `NixRef<T>` type:
|
|
493
|
-
|
|
494
|
-
```typescript
|
|
495
|
-
interface NixRef<T extends Element = Element> {
|
|
496
|
-
el: T | null;
|
|
497
|
-
}
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
---
|
|
501
|
-
|
|
502
|
-
## Components
|
|
503
|
-
|
|
504
|
-
### Function components
|
|
505
|
-
|
|
506
|
-
The simplest form: a plain function that returns a `NixTemplate`. Signals inside close over the component's scope.
|
|
507
|
-
|
|
508
|
-
```typescript
|
|
509
|
-
import { html, signal, mount } from "./nix";
|
|
22
|
+
import { signal, html, mount } from "@deijose/nix-js";
|
|
510
23
|
|
|
511
24
|
function Counter() {
|
|
512
25
|
const count = signal(0);
|
|
26
|
+
|
|
513
27
|
return html`
|
|
514
28
|
<div>
|
|
515
|
-
<
|
|
516
|
-
<button @click=${() => count.value++}
|
|
29
|
+
<h1>${() => count.value}</h1>
|
|
30
|
+
<button @click=${() => count.value++}>Increment</button>
|
|
517
31
|
</div>
|
|
518
32
|
`;
|
|
519
33
|
}
|
|
520
34
|
|
|
521
|
-
mount(Counter
|
|
35
|
+
mount(Counter, document.getElementById("app")!);
|
|
522
36
|
```
|
|
523
37
|
|
|
524
|
-
###
|
|
525
|
-
|
|
526
|
-
For components that need lifecycle hooks, extend `NixComponent`:
|
|
38
|
+
### Signals & reactivity
|
|
527
39
|
|
|
528
40
|
```typescript
|
|
529
|
-
import {
|
|
41
|
+
import { signal, computed, effect, watch } from "@deijose/nix-js";
|
|
530
42
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
43
|
+
const price = signal(10);
|
|
44
|
+
const qty = signal(3);
|
|
45
|
+
const total = computed(() => price.value * qty.value);
|
|
534
46
|
|
|
535
|
-
|
|
536
|
-
this._id = setInterval(() => this.count.update(n => n + 1), 1000);
|
|
537
|
-
return () => clearInterval(this._id); // cleanup
|
|
538
|
-
}
|
|
47
|
+
effect(() => console.log("Total:", total.value)); // logs on every change
|
|
539
48
|
|
|
540
|
-
|
|
541
|
-
return html`<span>${() => this.count.value}s</span>`;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
49
|
+
watch(total, (next, prev) => console.log(prev, "→", next));
|
|
544
50
|
|
|
545
|
-
|
|
51
|
+
price.value = 20; // effect & watch fire automatically
|
|
546
52
|
```
|
|
547
53
|
|
|
548
|
-
|
|
54
|
+
### Components
|
|
549
55
|
|
|
550
56
|
```typescript
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
### Lifecycle hooks
|
|
57
|
+
import { NixComponent, mount, html } from "@deijose/nix-js";
|
|
58
|
+
import { signal } from "@deijose/nix-js";
|
|
555
59
|
|
|
556
|
-
All hooks are optional:
|
|
557
|
-
|
|
558
|
-
```typescript
|
|
559
60
|
class MyComponent extends NixComponent {
|
|
560
|
-
|
|
561
|
-
// Use it to initialize derived state or call provide().
|
|
562
|
-
onInit() {
|
|
563
|
-
this.derived = computed(() => this.base.value * 2);
|
|
564
|
-
provide(MY_KEY, this.value);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// ② Must be implemented. Returns the template. Called once.
|
|
568
|
-
render(): NixTemplate {
|
|
569
|
-
return html`...`;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// ③ Called AFTER the component is inserted into the DOM.
|
|
573
|
-
// Return a function for automatic cleanup on unmount.
|
|
574
|
-
onMount() {
|
|
575
|
-
const id = addEventListener("resize", this._onResize);
|
|
576
|
-
return () => removeEventListener("resize", this._onResize);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// ④ Called BEFORE the component is removed from the DOM.
|
|
580
|
-
onUnmount() {
|
|
581
|
-
console.log("bye!");
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// ⑤ Catches errors thrown inside onInit() and onMount().
|
|
585
|
-
// If not implemented, errors are re-thrown.
|
|
586
|
-
onError(err: unknown) {
|
|
587
|
-
console.error("Component error:", err);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
**Execution order:**
|
|
593
|
-
|
|
594
|
-
```
|
|
595
|
-
new MyComponent()
|
|
596
|
-
↓
|
|
597
|
-
onInit() ← no DOM, synchronous
|
|
598
|
-
↓
|
|
599
|
-
render() ← returns NixTemplate
|
|
600
|
-
↓
|
|
601
|
-
[DOM inserted]
|
|
602
|
-
↓
|
|
603
|
-
onMount() ← DOM available; return value = cleanup fn
|
|
604
|
-
↓
|
|
605
|
-
...reactive updates...
|
|
606
|
-
↓
|
|
607
|
-
onUnmount() ← DOM still present
|
|
608
|
-
cleanup from onMount()
|
|
609
|
-
↓
|
|
610
|
-
[DOM removed]
|
|
611
|
-
```
|
|
612
|
-
|
|
613
|
-
### `mount()`
|
|
614
|
-
|
|
615
|
-
Mounts a `NixTemplate` or `NixComponent` into the DOM. Returns a handle with an `unmount()` method.
|
|
616
|
-
|
|
617
|
-
```typescript
|
|
618
|
-
// Function component
|
|
619
|
-
const handle = mount(Counter(), "#app");
|
|
620
|
-
|
|
621
|
-
// Class component
|
|
622
|
-
const handle = mount(new Timer(), document.getElementById("app")!);
|
|
623
|
-
|
|
624
|
-
// Unmount later
|
|
625
|
-
handle.unmount(); // runs onUnmount, disposes all effects, removes DOM
|
|
626
|
-
```
|
|
627
|
-
|
|
628
|
-
---
|
|
629
|
-
|
|
630
|
-
## Dependency Injection
|
|
631
|
-
|
|
632
|
-
Nix.js provides a Vue-style `provide`/`inject` system for passing data down a component tree without prop drilling.
|
|
633
|
-
|
|
634
|
-
### `provide` / `inject`
|
|
635
|
-
|
|
636
|
-
- `provide(key, value)` — call inside `onInit()` to make a value available to all descendant components.
|
|
637
|
-
- `inject(key)` — retrieve the closest provided value for `key`, or `undefined` if none was provided.
|
|
638
|
-
|
|
639
|
-
```typescript
|
|
640
|
-
import { provide, inject, createInjectionKey } from "./nix";
|
|
641
|
-
|
|
642
|
-
const THEME_KEY = createInjectionKey<Signal<string>>("theme");
|
|
643
|
-
|
|
644
|
-
class ThemeProvider extends NixComponent {
|
|
645
|
-
theme = signal("dark");
|
|
61
|
+
private msg = signal("Hello");
|
|
646
62
|
|
|
647
63
|
onInit() {
|
|
648
|
-
|
|
64
|
+
setTimeout(() => (this.msg.value = "World"), 1000);
|
|
649
65
|
}
|
|
650
66
|
|
|
651
67
|
render() {
|
|
652
|
-
return html`<
|
|
68
|
+
return html`<p>${() => this.msg.value}</p>`;
|
|
653
69
|
}
|
|
654
70
|
}
|
|
655
71
|
|
|
656
|
-
|
|
657
|
-
theme = inject(THEME_KEY); // Signal<string> | undefined
|
|
658
|
-
|
|
659
|
-
render() {
|
|
660
|
-
const style = () =>
|
|
661
|
-
`background:${this.theme?.value === "dark" ? "#1e293b" : "#f0f9ff"}`;
|
|
662
|
-
return html`<button style=${style}>Click me</button>`;
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
```
|
|
666
|
-
|
|
667
|
-
**Rules:**
|
|
668
|
-
- `provide()` must be called inside `onInit()` (or a constructor), never at the module level.
|
|
669
|
-
- `inject()` searches from the current component up through its ancestors. The **nearest** ancestor wins.
|
|
670
|
-
- Calling `provide()` outside a component context throws an error.
|
|
671
|
-
- Calling `inject()` outside a component context returns `undefined` silently.
|
|
672
|
-
|
|
673
|
-
### `createInjectionKey`
|
|
674
|
-
|
|
675
|
-
Creates a globally unique, typed symbol to use as a key. Typed keys prevent mismatches between provider and consumer.
|
|
676
|
-
|
|
677
|
-
```typescript
|
|
678
|
-
import type { InjectionKey } from "./nix";
|
|
679
|
-
|
|
680
|
-
// Typed key — Signal<string> is the shape of the provided value
|
|
681
|
-
const LOCALE_KEY: InjectionKey<Signal<string>> = createInjectionKey("locale");
|
|
682
|
-
const USER_KEY: InjectionKey<User> = createInjectionKey("user");
|
|
683
|
-
```
|
|
684
|
-
|
|
685
|
-
---
|
|
686
|
-
|
|
687
|
-
## Global Stores
|
|
688
|
-
|
|
689
|
-
### `createStore`
|
|
690
|
-
|
|
691
|
-
Creates a reactive global store. Every property of the initial state becomes a `Signal`. An optional factory function adds typed actions.
|
|
692
|
-
|
|
693
|
-
```typescript
|
|
694
|
-
import { createStore } from "./nix";
|
|
695
|
-
|
|
696
|
-
// Basic store — no actions
|
|
697
|
-
const theme = createStore({ dark: true, fontSize: 16 });
|
|
698
|
-
|
|
699
|
-
theme.dark.value = false; // write
|
|
700
|
-
theme.fontSize.value; // read
|
|
701
|
-
theme.$reset(); // restore all signals to initial values
|
|
72
|
+
mount(MyComponent, document.body);
|
|
702
73
|
```
|
|
703
74
|
|
|
704
|
-
|
|
75
|
+
### Stores
|
|
705
76
|
|
|
706
77
|
```typescript
|
|
707
|
-
|
|
708
|
-
{
|
|
709
|
-
items: [] as string[],
|
|
710
|
-
total: 0,
|
|
711
|
-
},
|
|
712
|
-
(s) => ({
|
|
713
|
-
add: (item: string) => s.items.update(arr => [...arr, item]),
|
|
714
|
-
remove: (item: string) => s.items.update(arr => arr.filter(i => i !== item)),
|
|
715
|
-
clear: () => cart.$reset(),
|
|
716
|
-
})
|
|
717
|
-
);
|
|
78
|
+
import { createStore } from "@deijose/nix-js";
|
|
718
79
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
```
|
|
724
|
-
|
|
725
|
-
**Types:**
|
|
80
|
+
const counter = createStore({ count: 0 }, (state) => ({
|
|
81
|
+
increment() { state.count.value++; },
|
|
82
|
+
reset() { state.count.value = 0; },
|
|
83
|
+
}));
|
|
726
84
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
type StoreSignals<T> = { readonly [K in keyof T]: Signal<T[K]> };
|
|
730
|
-
|
|
731
|
-
// Store<T, A> — signals + actions + $reset
|
|
732
|
-
type Store<T, A> = StoreSignals<T> & A & { $reset(): void };
|
|
85
|
+
counter.count.value; // reactive signal
|
|
86
|
+
counter.increment();
|
|
733
87
|
```
|
|
734
88
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
## Router
|
|
738
|
-
|
|
739
|
-
A client-side History API router with dynamic parameters, query strings, nested routes, and reactive active-link styling.
|
|
740
|
-
|
|
741
|
-
### `createRouter`
|
|
742
|
-
|
|
743
|
-
Call once at app startup. Sets up the router singleton consumed by `RouterView`, `Link`, and `useRouter`.
|
|
89
|
+
### Router
|
|
744
90
|
|
|
745
91
|
```typescript
|
|
746
|
-
import { createRouter, RouterView, Link } from "
|
|
92
|
+
import { createRouter, RouterView, Link, html } from "@deijose/nix-js";
|
|
747
93
|
|
|
748
94
|
const router = createRouter([
|
|
749
|
-
{ path: "/",
|
|
750
|
-
{ path: "/about",
|
|
751
|
-
{ path: "/users/:id", component: () => new UserDetail() },
|
|
752
|
-
{ path: "*", component: () => new NotFound() },
|
|
95
|
+
{ path: "/", component: Home },
|
|
96
|
+
{ path: "/about", component: About },
|
|
753
97
|
]);
|
|
754
|
-
```
|
|
755
|
-
|
|
756
|
-
The `Router` interface exposes:
|
|
757
|
-
|
|
758
|
-
| Property | Type | Description |
|
|
759
|
-
|----------|------|-------------|
|
|
760
|
-
| `current` | `Signal<string>` | Active pathname (`/users/42`) |
|
|
761
|
-
| `params` | `Signal<Record<string, string>>` | Dynamic route params (`{ id: "42" }`) |
|
|
762
|
-
| `query` | `Signal<Record<string, string>>` | Query string params (`{ page: "2" }`) |
|
|
763
|
-
| `navigate(path, query?)` | `void` | Navigate programmatically |
|
|
764
|
-
| `routes` | `RouteRecord[]` | Original route tree |
|
|
765
|
-
|
|
766
|
-
### `RouterView`
|
|
767
98
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
${new Link("/", "Home")}
|
|
776
|
-
${new Link("/about", "About")}
|
|
777
|
-
</nav>
|
|
778
|
-
${new RouterView()}
|
|
779
|
-
`;
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
mount(new App(), "#app");
|
|
99
|
+
const App = () => html`
|
|
100
|
+
<nav>
|
|
101
|
+
${Link({ href: "/" }, "Home")}
|
|
102
|
+
${Link({ href: "/about" }, "About")}
|
|
103
|
+
</nav>
|
|
104
|
+
${RouterView(router)}
|
|
105
|
+
`;
|
|
784
106
|
```
|
|
785
107
|
|
|
786
|
-
###
|
|
787
|
-
|
|
788
|
-
A reactive `<a>` tag that automatically applies active/inactive styles based on the current route.
|
|
108
|
+
### Dependency injection
|
|
789
109
|
|
|
790
110
|
```typescript
|
|
791
|
-
|
|
792
|
-
// <a href="/about" style="...active/inactive styles...">About Us</a>
|
|
793
|
-
```
|
|
111
|
+
import { provide, inject, createInjectionKey } from "@deijose/nix-js";
|
|
794
112
|
|
|
795
|
-
|
|
113
|
+
const ThemeKey = createInjectionKey<string>("theme");
|
|
796
114
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
```typescript
|
|
802
|
-
class UserDetail extends NixComponent {
|
|
803
|
-
render() {
|
|
804
|
-
const router = useRouter();
|
|
805
|
-
return html`
|
|
806
|
-
<h1>User: ${() => router.params.value.id}</h1>
|
|
807
|
-
<p>Page: ${() => router.query.value.page ?? "1"}</p>
|
|
808
|
-
`;
|
|
809
|
-
}
|
|
115
|
+
// Parent component
|
|
116
|
+
function App() {
|
|
117
|
+
provide(ThemeKey, "dark");
|
|
118
|
+
return html`${ThemedCard()}`;
|
|
810
119
|
}
|
|
811
|
-
```
|
|
812
|
-
|
|
813
|
-
### Nested routes
|
|
814
|
-
|
|
815
|
-
Define `children` on a route. The parent component renders `new RouterView(1)` to slot in the child:
|
|
816
120
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
component: () => new DashboardLayout(),
|
|
822
|
-
children: [
|
|
823
|
-
{ path: "/stats", component: () => new StatsPage() },
|
|
824
|
-
{ path: "/settings", component: () => new SettingsPage() },
|
|
825
|
-
],
|
|
826
|
-
},
|
|
827
|
-
]);
|
|
828
|
-
|
|
829
|
-
class DashboardLayout extends NixComponent {
|
|
830
|
-
render() {
|
|
831
|
-
return html`
|
|
832
|
-
<aside>
|
|
833
|
-
${new Link("/dashboard/stats", "Stats")}
|
|
834
|
-
${new Link("/dashboard/settings", "Settings")}
|
|
835
|
-
</aside>
|
|
836
|
-
<main>${new RouterView(1)}</main> <!-- renders the child route -->
|
|
837
|
-
`;
|
|
838
|
-
}
|
|
121
|
+
// Child component
|
|
122
|
+
function ThemedCard() {
|
|
123
|
+
const theme = inject(ThemeKey); // "dark"
|
|
124
|
+
return html`<div class="card-${theme}">...</div>`;
|
|
839
125
|
}
|
|
840
126
|
```
|
|
841
127
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
```typescript
|
|
845
|
-
const router = useRouter();
|
|
846
|
-
|
|
847
|
-
// Navigate with query params as an object
|
|
848
|
-
router.navigate("/users", { page: 2, sort: "name" });
|
|
849
|
-
// URL: /users?page=2&sort=name
|
|
850
|
-
|
|
851
|
-
// Or inline in the path string
|
|
852
|
-
router.navigate("/users?page=2&sort=name");
|
|
853
|
-
|
|
854
|
-
// Read them reactively
|
|
855
|
-
html`<p>Page: ${() => router.query.value.page}</p>`
|
|
856
|
-
|
|
857
|
-
// null/undefined removes the key
|
|
858
|
-
router.navigate("/users", { page: null });
|
|
859
|
-
// URL: /users
|
|
860
|
-
```
|
|
861
|
-
|
|
862
|
-
---
|
|
863
|
-
|
|
864
|
-
## Async & Lazy Loading
|
|
865
|
-
|
|
866
|
-
### `suspend()`
|
|
867
|
-
|
|
868
|
-
Runs an async function and renders different UIs depending on its state: `pending`, `resolved`, or `error`. The equivalent of `<Suspense>` in other frameworks.
|
|
869
|
-
|
|
870
|
-
```typescript
|
|
871
|
-
import { suspend } from "./nix";
|
|
872
|
-
|
|
873
|
-
const userView = suspend(
|
|
874
|
-
() => fetch("/api/user").then(r => r.json()),
|
|
875
|
-
(user) => html`<div>${user.name}</div>`
|
|
876
|
-
);
|
|
877
|
-
|
|
878
|
-
mount(userView, "#app");
|
|
879
|
-
```
|
|
880
|
-
|
|
881
|
-
**Options:**
|
|
882
|
-
|
|
883
|
-
```typescript
|
|
884
|
-
suspend(
|
|
885
|
-
asyncFn,
|
|
886
|
-
renderFn,
|
|
887
|
-
{
|
|
888
|
-
// Template shown while pending (default: animated spinner)
|
|
889
|
-
fallback: html`<p>Loading…</p>`,
|
|
890
|
-
|
|
891
|
-
// Called with the error if the promise rejects
|
|
892
|
-
errorFallback: (err) => html`<p style="color:red">Error: ${String(err)}</p>`,
|
|
893
|
-
|
|
894
|
-
// If true, shows the fallback on every re-fetch.
|
|
895
|
-
// If false (default), keeps the previous content visible during refresh.
|
|
896
|
-
resetOnRefresh: false,
|
|
897
|
-
}
|
|
898
|
-
)
|
|
899
|
-
```
|
|
900
|
-
|
|
901
|
-
### `lazy()`
|
|
902
|
-
|
|
903
|
-
Wraps a dynamic `import()` for code-splitting. The module chunk is loaded once and cached; subsequent renders use the cached constructor directly.
|
|
904
|
-
|
|
905
|
-
```typescript
|
|
906
|
-
import { createRouter, lazy } from "./nix";
|
|
907
|
-
|
|
908
|
-
createRouter([
|
|
909
|
-
{ path: "/", component: lazy(() => import("./pages/Home")) },
|
|
910
|
-
{ path: "/about", component: lazy(() => import("./pages/About")) },
|
|
911
|
-
{
|
|
912
|
-
path: "/admin",
|
|
913
|
-
component: lazy(
|
|
914
|
-
() => import("./pages/Admin"),
|
|
915
|
-
html`<p>Loading admin panel…</p>` // optional custom fallback
|
|
916
|
-
),
|
|
917
|
-
},
|
|
918
|
-
]);
|
|
919
|
-
```
|
|
920
|
-
|
|
921
|
-
Each page module must export its component as `export default`:
|
|
922
|
-
|
|
923
|
-
```typescript
|
|
924
|
-
// pages/Home.ts
|
|
925
|
-
import { NixComponent, html } from "../nix";
|
|
926
|
-
|
|
927
|
-
export default class HomePage extends NixComponent {
|
|
928
|
-
render() {
|
|
929
|
-
return html`<h1>Home</h1>`;
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
```
|
|
933
|
-
|
|
934
|
-
---
|
|
935
|
-
|
|
936
|
-
## API Reference
|
|
937
|
-
|
|
938
|
-
### Reactivity
|
|
939
|
-
|
|
940
|
-
| Function | Signature | Description |
|
|
941
|
-
|----------|-----------|-------------|
|
|
942
|
-
| `signal` | `<T>(initial: T) → Signal<T>` | Create a reactive value |
|
|
943
|
-
| `computed` | `<T>(fn: () => T) → Signal<T>` | Derived reactive value |
|
|
944
|
-
| `effect` | `(fn: () => void\|cleanup) → dispose` | Run and re-run on signal changes |
|
|
945
|
-
| `batch` | `(fn: () => void) → void` | Flush multiple writes as one update |
|
|
946
|
-
| `watch` | `(source, cb, opts?) → dispose` | Observe a source, receive old+new values |
|
|
947
|
-
| `untrack` | `<T>(fn: () => T) → T` | Read signals without subscribing |
|
|
948
|
-
| `nextTick` | `(fn?: () => void) → Promise<void>` | Await next microtask (post-DOM-update) |
|
|
949
|
-
|
|
950
|
-
### Signal methods
|
|
951
|
-
|
|
952
|
-
| Method | Description |
|
|
953
|
-
|--------|-------------|
|
|
954
|
-
| `.value` (get) | Read value and subscribe if inside an effect |
|
|
955
|
-
| `.value` (set) | Write and notify if changed |
|
|
956
|
-
| `.update(fn)` | Write via `fn(current) → next` |
|
|
957
|
-
| `.peek()` | Read without subscribing |
|
|
958
|
-
| `.dispose()` | Clear all subscribers |
|
|
959
|
-
|
|
960
|
-
### Templates
|
|
961
|
-
|
|
962
|
-
| Export | Description |
|
|
963
|
-
|--------|-------------|
|
|
964
|
-
| `html\`\`` | Tagged template → `NixTemplate` |
|
|
965
|
-
| `repeat(items, keyFn, renderFn)` | Keyed list with efficient diffing |
|
|
966
|
-
| `ref<T>()` | Create a `NixRef<T>` for direct DOM access |
|
|
967
|
-
|
|
968
|
-
### Components
|
|
969
|
-
|
|
970
|
-
| Export | Description |
|
|
971
|
-
|--------|-------------|
|
|
972
|
-
| `NixComponent` | Abstract base class with lifecycle hooks |
|
|
973
|
-
| `mount(component, container)` | Mount template or component → `{ unmount() }` |
|
|
974
|
-
|
|
975
|
-
### Dependency Injection
|
|
976
|
-
|
|
977
|
-
| Export | Description |
|
|
978
|
-
|--------|-------------|
|
|
979
|
-
| `createInjectionKey<T>(desc?)` | Create a typed, unique injection key |
|
|
980
|
-
| `provide(key, value)` | Register a value (call in `onInit`) |
|
|
981
|
-
| `inject(key)` | Retrieve the nearest provided value |
|
|
982
|
-
| `InjectionKey<T>` | Type for typed injection keys |
|
|
983
|
-
|
|
984
|
-
### Stores
|
|
985
|
-
|
|
986
|
-
| Export | Description |
|
|
987
|
-
|--------|-------------|
|
|
988
|
-
| `createStore(state, actions?)` | Create a reactive global store |
|
|
989
|
-
| `Store<T, A>` | Type of the returned store |
|
|
990
|
-
| `StoreSignals<T>` | Signal-mapped type of a state shape |
|
|
991
|
-
|
|
992
|
-
### Router
|
|
993
|
-
|
|
994
|
-
| Export | Description |
|
|
995
|
-
|--------|-------------|
|
|
996
|
-
| `createRouter(routes)` | Initialize the router singleton |
|
|
997
|
-
| `useRouter()` | Access the active router from anywhere |
|
|
998
|
-
| `RouterView` | Component that renders the matched route |
|
|
999
|
-
| `Link` | Reactive anchor component |
|
|
1000
|
-
| `Router` | Router instance interface |
|
|
1001
|
-
| `RouteRecord` | Route definition type |
|
|
1002
|
-
|
|
1003
|
-
### Async
|
|
1004
|
-
|
|
1005
|
-
| Export | Description |
|
|
1006
|
-
|--------|-------------|
|
|
1007
|
-
| `suspend(asyncFn, renderFn, opts?)` | Async data fetching with Suspense |
|
|
1008
|
-
| `lazy(importFn, fallback?)` | Dynamic import with caching |
|
|
1009
|
-
| `SuspenseOptions` | Options type for `suspend()` |
|
|
1010
|
-
|
|
1011
|
-
---
|
|
1012
|
-
|
|
1013
|
-
## Known Limitations
|
|
1014
|
-
|
|
1015
|
-
**Partial attribute interpolation is not supported.**
|
|
1016
|
-
|
|
1017
|
-
Each dynamic attribute must be a single interpolation covering the entire attribute value. Mixing static text and expressions inside one attribute value does not work:
|
|
1018
|
-
|
|
1019
|
-
```typescript
|
|
1020
|
-
// ✅ Works — the whole value is one expression
|
|
1021
|
-
html`<div class=${() => `item ${isActive.value ? "active" : ""}`}>`
|
|
1022
|
-
|
|
1023
|
-
// ❌ Does NOT work — static prefix + dynamic suffix in same attribute
|
|
1024
|
-
html`<div class="item ${() => isActive.value ? 'active' : ''}">`
|
|
1025
|
-
```
|
|
128
|
+
## Documentation
|
|
1026
129
|
|
|
1027
|
-
|
|
130
|
+
For the complete API reference, guides, and all features (async/lazy, lifecycle hooks, event modifiers, keyed lists, query params, nested routes, and more):
|
|
1028
131
|
|
|
1029
|
-
|
|
132
|
+
**→ [Full documentation on GitHub](https://github.com/deijose/nix-js)**
|
|
1030
133
|
|
|
1031
134
|
## License
|
|
1032
135
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deijose/nix-js",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "A lightweight, fully reactive micro-framework — no virtual DOM, no compiler, just signals and tagged templates.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -42,7 +42,9 @@
|
|
|
42
42
|
"build": "tsc && vite build",
|
|
43
43
|
"preview": "vite preview",
|
|
44
44
|
"build:lib": "vite build --config vite.lib.config.ts && tsc --project tsconfig.lib.json && terser dist/lib/nix-js.js -c -m -o dist/lib/nix-js.js && terser dist/lib/nix-js.cjs -c -m -o dist/lib/nix-js.cjs",
|
|
45
|
-
"typecheck": "tsc --noEmit"
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"prepublishOnly": "cp README.md .readme-full.md && cp .readme-npm.md README.md",
|
|
47
|
+
"postpublish": "mv .readme-full.md README.md"
|
|
46
48
|
},
|
|
47
49
|
"devDependencies": {
|
|
48
50
|
"terser": "^5.46.0",
|