@deijose/nix-js 1.0.6 → 1.0.8

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 CHANGED
@@ -1,322 +1,105 @@
1
- # ❄️ Nix.js
1
+ # Nix.js
2
2
 
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.
3
+ [![npm version](https://img.shields.io/npm/v/@deijose/nix-js.svg)](https://www.npmjs.com/package/@deijose/nix-js)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+ [![Tests](https://img.shields.io/badge/tests-152%20passing-brightgreen.svg)]()
6
+ [![Bundle size](https://img.shields.io/badge/min%2Bgzip-~8%20KB-orange.svg)]()
7
+ [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-success.svg)]()
4
8
 
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.
9
+ 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.
6
10
 
7
11
  ```
8
- ~14 KB minified · zero dependencies · TypeScript-first · ES2022
12
+ ~24 KB minified · ~8 KB gzipped · zero dependencies · TypeScript-first · ES2022
9
13
  ```
10
14
 
11
15
  ## Installation
12
16
 
13
17
  ```bash
14
18
  npm install @deijose/nix-js
15
- # or
16
- bun add @deijose/nix-js
17
19
  ```
18
20
 
19
- ## Usage
21
+ ## Quick Start
20
22
 
21
23
  ```typescript
22
- import { signal, html, mount } from "@deijose/nix-js";
24
+ import { signal, html, NixTemplate, NixComponent, mount, createRouter, RouterView, Link, useRouter } from "@deijose/nix-js";
23
25
 
24
- function Counter() {
25
- const count = signal(0);
26
+ // --- Pages as function components (NixTemplate) ---
27
+ // Plain functions returning html`` are recommended for pages and
28
+ // display-only components — no class needed, signals just work.
26
29
 
30
+ function HomePage(): NixTemplate {
31
+ const count = signal(0);
27
32
  return html`
28
- <div>
29
- <h1>${() => count.value}</h1>
30
- <button @click=${() => count.value++}>Increment</button>
31
- </div>
33
+ <h1>Home</h1>
34
+ <p>Count: ${() => count.value}</p>
35
+ <button @click=${() => count.value++}>+1</button>
32
36
  `;
33
37
  }
34
38
 
35
- mount(Counter, document.getElementById("app")!);
36
- ```
37
-
38
- ### Signals & reactivity
39
-
40
- ```typescript
41
- import { signal, computed, effect, watch } from "@deijose/nix-js";
42
-
43
- const price = signal(10);
44
- const qty = signal(3);
45
- const total = computed(() => price.value * qty.value);
46
-
47
- effect(() => console.log("Total:", total.value)); // logs on every change
48
-
49
- watch(total, (next, prev) => console.log(prev, "→", next));
50
-
51
- price.value = 20; // effect & watch fire automatically
52
- ```
53
-
54
- ### Components
39
+ function UserPage(): NixTemplate {
40
+ const router = useRouter();
41
+ return html`<h1>User: ${() => router.params.value.id}</h1>`;
42
+ }
55
43
 
56
- ```typescript
57
- import { NixComponent, mount, html } from "@deijose/nix-js";
58
- import { signal } from "@deijose/nix-js";
44
+ // --- Stateful component as class component (NixComponent) ---
45
+ // Use a class when you need lifecycle hooks: onInit / onMount / onUnmount.
59
46
 
60
- class MyComponent extends NixComponent {
61
- private msg = signal("Hello");
47
+ class Clock extends NixComponent {
48
+ private time = signal(new Date().toLocaleTimeString());
49
+ private _id = 0;
62
50
 
63
- onInit() {
64
- setTimeout(() => (this.msg.value = "World"), 1000);
51
+ onMount() {
52
+ this._id = setInterval(() => {
53
+ this.time.value = new Date().toLocaleTimeString();
54
+ }, 1000);
55
+ return () => clearInterval(this._id); // auto-cleanup on unmount
65
56
  }
66
57
 
67
58
  render() {
68
- return html`<p>${() => this.msg.value}</p>`;
59
+ return html`<p>Clock: ${() => this.time.value}</p>`;
69
60
  }
70
61
  }
71
62
 
72
- mount(MyComponent, document.body);
73
- ```
74
-
75
- ### Stores
76
-
77
- ```typescript
78
- import { createStore } from "@deijose/nix-js";
79
-
80
- const counter = createStore({ count: 0 }, (state) => ({
81
- increment() { state.count.value++; },
82
- reset() { state.count.value = 0; },
83
- }));
84
-
85
- counter.count.value; // reactive signal
86
- counter.increment();
87
- ```
63
+ // --- Router ---
88
64
 
89
- ### Router
90
-
91
- ```typescript
92
- import { createRouter, RouterView, Link, html } from "@deijose/nix-js";
93
-
94
- const router = createRouter([
95
- { path: "/", component: Home },
96
- { path: "/about", component: About },
65
+ createRouter([
66
+ { path: "/", component: () => HomePage() },
67
+ { path: "/user/:id", component: () => UserPage() },
97
68
  ]);
98
69
 
99
- const App = () => html`
100
- <nav>
101
- ${Link({ href: "/" }, "Home")}
102
- ${Link({ href: "/about" }, "About")}
103
- </nav>
104
- ${RouterView(router)}
105
- `;
106
- ```
107
-
108
- ### Forms
109
-
110
- ```typescript
111
- import { createForm, required, email, minLength, min } from "@deijose/nix-js";
112
-
113
- const form = createForm(
114
- { name: "", email: "", age: 0 },
115
- {
116
- validators: {
117
- name: [required(), minLength(2)],
118
- email: [required(), email()],
119
- age: [required(), min(18)],
120
- },
121
- // Optional: plug in Zod, Valibot, Yup, or any schema library
122
- validate(values) {
123
- const r = zodSchema.safeParse(values);
124
- if (r.success) return null;
125
- return Object.fromEntries(
126
- Object.entries(r.error.flatten().fieldErrors).map(([k, v]) => [k, v?.[0]])
127
- );
128
- },
129
- }
130
- );
131
-
132
- html`
133
- <form @submit=${form.handleSubmit(onSubmit)}>
134
- <input value=${() => form.fields.name.value.value}
135
- @input=${form.fields.name.onInput}
136
- @blur=${form.fields.name.onBlur} />
137
- ${() => form.fields.name.error.value
138
- ? html`<p class="err">${form.fields.name.error.value}</p>`
139
- : null}
140
- <button type="submit">Submit</button>
141
- </form>
142
- `
143
-
144
- // Inject server errors after a failed API call:
145
- form.setErrors({ email: "Email already in use" });
146
- ```
147
-
148
- ### Children & slots
149
-
150
- ```typescript
151
- import type { NixChildren } from "@deijose/nix-js";
70
+ // --- App shell (function component) ---
152
71
 
153
- class Card extends NixComponent {
154
- render() {
155
- return html`
156
- <div class="card">
157
- <header>${this.slot("header")}</header>
158
- <main>${this.children}</main>
159
- <footer>${this.slot("footer") ?? html`<small>Footer</small>`}</footer>
160
- </div>
161
- `;
162
- }
72
+ function App(): NixTemplate {
73
+ return html`
74
+ <nav>${new Link("/", "Home")} ${new Link("/user/42", "User 42")}</nav>
75
+ ${new Clock()}
76
+ ${new RouterView()}
77
+ `;
163
78
  }
164
79
 
165
- const app = new Card()
166
- .setSlot("header", html`<h1>Título</h1>`)
167
- .setChildren(html`<p>Cuerpo del card</p>`)
168
- .setSlot("footer", html`<small>© 2026</small>`);
169
-
170
- // Function component style:
171
- function Box({ children }: { children?: NixChildren }) {
172
- return html`<div class="box">${children}</div>`;
173
- }
80
+ mount(App(), "#app");
174
81
  ```
175
82
 
176
- ### show / hide directive
83
+ ## What's Included
177
84
 
178
- Toggle visibility **without unmounting** the element stays in the DOM,
179
- preserving its state, listeners, and child components.
85
+ Everything ships in a single zero-dependency import:
180
86
 
181
- ```typescript
182
- import { signal } from "@deijose/nix-js";
183
-
184
- const isOpen = signal(false);
185
- const loading = signal(false);
186
-
187
- html`
188
- <!-- visible when truthy -->
189
- <div show=${() => isOpen.value}>Panel content (kept in DOM)</div>
190
-
191
- <!-- hidden when truthy (inverse of show) -->
192
- <form hide=${() => loading.value}>...</form>
193
- <div show=${() => loading.value}>⏳ Submitting…</div>
194
- `
195
- ```
196
-
197
- Imperative use outside a template:
198
-
199
- ```typescript
200
- import { showWhen, effect } from "@deijose/nix-js";
201
-
202
- const panel = document.getElementById("panel") as HTMLElement;
203
- effect(() => showWhen(panel, isOpen.value));
204
- ```
205
-
206
- ### Portal
207
-
208
- Render a template into `document.body` (or any target) — escaping
209
- `overflow: hidden` and stacking contexts. Ideal for modals, tooltips, toasts.
210
- The portal is cleaned up automatically when its parent unmounts.
211
-
212
- ```typescript
213
- import { signal, portal, html } from "@deijose/nix-js";
214
-
215
- const isOpen = signal(false);
216
-
217
- html`
218
- <button @click=${() => { isOpen.value = true; }}>Open modal</button>
219
-
220
- ${() => isOpen.value
221
- ? portal(html`
222
- <div class="overlay" @click=${() => { isOpen.value = false; }}>
223
- <div class="modal" @click.stop=${() => {}}>
224
- <h2>Hello!</h2>
225
- <button @click=${() => { isOpen.value = false; }}>Close</button>
226
- </div>
227
- </div>
228
- `) // renders into document.body by default
229
- : null
230
- }
231
- `
232
-
233
- // Custom target:
234
- portal(html`<div class="toast">Saved!</div>`, "#toast-root")
235
- portal(html`<Tooltip />`, document.getElementById("tooltip-layer")!)
236
- ```
237
-
238
- ### Error Boundaries
239
-
240
- ```typescript
241
- import { createErrorBoundary, html, mount, signal } from "@deijose/nix-js";
242
-
243
- // Catch render and reactive errors in a subtree
244
- mount(
245
- createErrorBoundary(
246
- html`<div>${() => riskyData()}</div>`,
247
- (err) => html`<div class="error">Failed: ${String(err)}</div>`
248
- ),
249
- "#app"
250
- );
251
-
252
- // Catches onInit / render / onMount / reactive effects
253
- createErrorBoundary(new DataWidget(), html`<p>Service unavailable</p>`)
254
-
255
- // Nested: inner catches first, outer is app-level
256
- createErrorBoundary(
257
- html`
258
- ${createErrorBoundary(new RiskyWidget(), html`<p>Widget error</p>`)}
259
- `,
260
- html`<p>App crashed</p>`
261
- )
262
- ```
263
-
264
- ### Transitions
265
-
266
- ```typescript
267
- import { signal, html, transition, mount } from "@deijose/nix-js";
268
-
269
- const show = signal(true);
270
-
271
- /* CSS in your stylesheet:
272
- .fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; }
273
- .fade-enter-from, .fade-leave-to { opacity: 0; }
274
- */
275
-
276
- mount(
277
- transition(
278
- () => show.value ? html`<p>Hello!</p>` : null,
279
- { name: "fade" }
280
- ),
281
- "#app"
282
- );
283
-
284
- // Animate on first render too
285
- transition(html`<header>App</header>`, { name: "slide-down", appear: true });
286
-
287
- // JS hooks
288
- transition(content, {
289
- name: "fade",
290
- onBeforeEnter: (el) => el.setAttribute("aria-hidden", "false"),
291
- onAfterLeave: (el) => el.setAttribute("aria-hidden", "true"),
292
- });
293
- ```
294
-
295
- ### Dependency injection
296
-
297
- ```typescript
298
- import { provide, inject, createInjectionKey } from "@deijose/nix-js";
299
-
300
- const ThemeKey = createInjectionKey<string>("theme");
301
-
302
- // Parent component
303
- function App() {
304
- provide(ThemeKey, "dark");
305
- return html`${ThemedCard()}`;
306
- }
307
-
308
- // Child component
309
- function ThemedCard() {
310
- const theme = inject(ThemeKey); // "dark"
311
- return html`<div class="card-${theme}">...</div>`;
312
- }
313
- ```
87
+ | Category | APIs |
88
+ |---|---|
89
+ | **Reactivity** | `signal`, `computed`, `effect`, `batch`, `watch`, `untrack`, `nextTick` |
90
+ | **Templates** | `` html` ` ``, `repeat`, `ref`, `portal`, `transition`, `showWhen` |
91
+ | **Components** | `NixTemplate` (function components), `NixComponent` (lifecycle class), `mount`, children & named slots |
92
+ | **Router** | `createRouter`, `RouterView`, `Link`, `useRouter`, guards, nested routes |
93
+ | **Forms** | `useField`, `createForm`, built-in validators, Zod/Valibot interop |
94
+ | **State** | `createStore`, `provide`, `inject`, `createInjectionKey` |
95
+ | **Async** | `suspend`, `lazy` |
96
+ | **Error handling** | `createErrorBoundary` |
314
97
 
315
98
  ## Documentation
316
99
 
317
- For the complete API reference, guides, and all features (async/lazy, lifecycle hooks, event modifiers, keyed lists, query params, nested routes, and more):
100
+ Full API reference, guides, and examples:
318
101
 
319
- **→ [Full documentation on GitHub](https://github.com/DeijoseDevelop/nix-js)**
102
+ **→ [github.com/DeijoseDevelop/nix-js](https://github.com/DeijoseDevelop/nix-js)**
320
103
 
321
104
  ## License
322
105
 
@@ -1,68 +1,22 @@
1
1
  import { NixComponent } from "./lifecycle";
2
2
  import type { NixTemplate } from "./template";
3
3
  export interface SuspenseOptions {
4
- /**
5
- * Template a mostrar mientras la promesa está pendiente.
6
- * Por defecto: spinner de puntos animados.
7
- */
4
+ /** Template shown while the promise is pending. */
8
5
  fallback?: NixTemplate;
9
- /**
10
- * Factory que recibe el error y devuelve el template de error.
11
- * Por defecto: mensaje de error en rojo.
12
- */
6
+ /** Factory receiving the error, returns the error template. */
13
7
  errorFallback?: (err: unknown) => NixTemplate;
14
- /**
15
- * Si `true`, mantiene el fallback visible mientras `asyncFn` vuelve a
16
- * ejecutarse tras un cambio reactivo. Si `false` (por defecto), durante
17
- * las recargas se sigue mostrando el contenido anterior hasta que la nueva
18
- * promesa se resuelva.
19
- */
8
+ /** If `true`, shows fallback during re-fetches instead of stale content. */
20
9
  resetOnRefresh?: boolean;
21
10
  }
22
11
  /**
23
- * Ejecuta una función async y renderiza según su estado (pending / resolved /
24
- * error). Equivale al patrón <Suspense> de otros frameworks.
25
- *
26
- * @param asyncFn Función que devuelve una promesa con los datos.
27
- * @param renderFn Recibe los datos resueltos y devuelve el template/componente.
28
- * @param options `fallback`, `errorFallback`, `resetOnRefresh`.
29
- *
30
- * @example
31
- * // Uso simple con fallback por defecto
32
- * const userView = suspend(
33
- * () => fetchUser(userId.value),
34
- * user => html`<div>${user.name}</div>`
35
- * );
36
- *
37
- * @example
38
- * // Con fallback personalizado y manejo de error
39
- * suspend(
40
- * () => api.getItems(),
41
- * items => html`<ul>${items.map(i => html`<li>${i}</li>`)}</ul>`,
42
- * {
43
- * fallback: html`<span>Cargando items…</span>`,
44
- * errorFallback: err => html`<p style="color:red">Error: ${String(err)}</p>`,
45
- * }
46
- * )
12
+ * Runs an async function and renders based on its state (pending/resolved/error).
13
+ * Equivalent to the Suspense pattern in other frameworks.
47
14
  */
48
15
  export declare function suspend<T>(asyncFn: () => Promise<T>, renderFn: (data: T) => NixTemplate | NixComponent, options?: SuspenseOptions): NixComponent;
49
16
  /**
50
- * Envuelve un import dinámico para lazy loading de componentes de ruta.
51
- * El módulo se carga una sola vez (cacheado) y mientras tanto muestra el
52
- * fallback. Compatible directamente con el campo `component` de `RouteRecord`.
53
- *
54
- * El módulo importado debe exportar el componente como **export default**.
55
- *
56
- * @param importFn Función que hace el import dinámico.
57
- * @param fallback Template opcional mientras se descarga el chunk.
58
- *
59
- * @example
60
- * createRouter([
61
- * { path: "/", component: lazy(() => import("./pages/Home")) },
62
- * { path: "/about", component: lazy(() => import("./pages/About")) },
63
- * { path: "/admin", component: lazy(() => import("./pages/Admin"),
64
- * html`<p>Cargando panel…</p>`) },
65
- * ])
17
+ * Wraps a dynamic import for lazy-loading route components.
18
+ * The module is loaded once (cached). Compatible with `RouteRecord.component`.
19
+ * The imported module must use a default export.
66
20
  */
67
21
  export declare function lazy(importFn: () => Promise<{
68
22
  default: new () => NixComponent;
@@ -1,13 +1,13 @@
1
1
  import type { NixTemplate, NixMountHandle } from "./template";
2
2
  import { type NixComponent } from "./lifecycle";
3
3
  /**
4
- * Monta un NixTemplate o NixComponent en el DOM.
4
+ * Mounts a NixTemplate or NixComponent into the DOM.
5
5
  *
6
- * mount(App(), "#app"); // NixTemplate (función componente)
7
- * mount(new Timer(), "#app"); // NixComponent (clase con lifecycle)
6
+ * mount(Counter(), "#app"); // NixTemplate (function component)
7
+ * mount(new Timer(), "#app"); // NixComponent (class with lifecycle)
8
8
  *
9
- * @param component NixTemplate (resultado de html``) o instancia de NixComponent.
10
- * @param container Selector CSS o HTMLElement donde se insertará.
11
- * @returns { unmount() } para limpiar effects y remover el DOM.
9
+ * @param component NixTemplate (result of html``) or a NixComponent instance.
10
+ * @param container CSS selector or HTMLElement to mount into.
11
+ * @returns { unmount() } disposes effects and removes DOM.
12
12
  */
13
13
  export declare function mount(component: NixTemplate | NixComponent, container: Element | string): NixMountHandle;
@@ -1,61 +1,27 @@
1
- /**
2
- * Clave tipada para provide/inject.
3
- * El parámetro genérico T garantiza coherencia entre proveedor y consumidor.
4
- *
5
- * @example
6
- * const THEME_KEY = createInjectionKey<Signal<string>>("theme");
7
- */
1
+ /** Typed key for provide/inject. Generic `T` enforces type safety between provider and consumer. */
8
2
  export type InjectionKey<T> = symbol & {
9
3
  readonly __nixType?: T;
10
4
  };
11
- /**
12
- * Crea una InjectionKey única y tipada.
13
- * Cada llamada produce un símbolo distinto, evitando colisiones.
14
- *
15
- * @param description Nombre descriptivo (aparece en Symbol.toString())
16
- */
5
+ /** Creates a unique typed InjectionKey. */
17
6
  export declare function createInjectionKey<T>(description?: string): InjectionKey<T>;
18
- /** @internal — devuelve copia del stack (para capturar en closures de efectos). */
7
+ /** @internal — returns a copy of the stack for capturing in effect closures. */
19
8
  export declare function _captureContextSnapshot(): Map<unknown, unknown>[];
20
- /** @internal — push de un contexto vacío para un nuevo componente (render estático). */
9
+ /** @internal — pushes an empty context for a new component (static render). */
21
10
  export declare function _pushComponentContext(): void;
22
- /** @internal — pop del contexto del componente actual (render estático). */
11
+ /** @internal — pops the current component context (static render). */
23
12
  export declare function _popComponentContext(): void;
24
13
  /**
25
- * @internal — ejecuta `fn` con `parentSnapshot` como ancestros y un nuevo
26
- * contexto vacío en el tope, luego restaura el stack previo.
27
- *
28
- * Usado por efectos reactivos que pueden re-ejecutarse fuera del árbol de
29
- * rendering original (p.ej. NixComponents dentro de `() => new MyComp()`).
14
+ * @internal — executes `fn` with `parentSnapshot` as ancestors and a fresh
15
+ * empty context on top, then restores the previous stack.
30
16
  */
31
17
  export declare function _withComponentContext<T>(parentSnapshot: Map<unknown, unknown>[], fn: () => T): T;
32
18
  /**
33
- * Registra un valor para que los componentes descendientes puedan obtenerlo
34
- * con `inject()`.
35
- *
36
- * Debe llamarse en `onInit()` o en el constructor de un `NixComponent`.
37
- * Si se llama fuera del contexto de render de un componente, lanza un error.
38
- *
39
- * @example
40
- * class ThemeProvider extends NixComponent {
41
- * theme = signal("dark");
42
- * onInit() { provide(THEME_KEY, this.theme); } // ← aquí
43
- * render() { return html`${new ThemedButton()}`; }
44
- * }
19
+ * Registers a value so descendant components can retrieve it via `inject()`.
20
+ * Must be called inside `onInit()` of a NixComponent.
45
21
  */
46
22
  export declare function provide<T>(key: InjectionKey<T> | string | symbol, value: T): void;
47
23
  /**
48
- * Obtiene un valor provisto por un componente ancestro.
49
- * Busca de hijo a padre; retorna `undefined` si la clave no fue provista.
50
- *
51
- * Úsalo como propiedad de clase o dentro de `onInit()`.
52
- *
53
- * @example
54
- * class ThemedButton extends NixComponent {
55
- * theme = inject(THEME_KEY); // Signal<string> | undefined
56
- * render() {
57
- * return html`<button class=${() => this.theme?.value ?? "light"}>OK</button>`;
58
- * }
59
- * }
24
+ * Retrieves a value provided by an ancestor component.
25
+ * Searches child-to-parent; returns `undefined` if the key was not provided.
60
26
  */
61
27
  export declare function inject<T>(key: InjectionKey<T> | string | symbol): T | undefined;