ilha 0.11.1 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +181 -787
- package/dist/index-BETRqz-0.d.ts +377 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1663 -1731
- package/dist/internal.d.ts +69 -0
- package/dist/internal.js +49 -0
- package/dist/jsx-dev-runtime.d.ts +1 -1
- package/dist/{jsx-runtime-DMkplizR.d.ts → jsx-runtime-qnxoD2YJ.d.ts} +1 -3
- package/dist/jsx-runtime.d.ts +1 -1
- package/dist/jsx-runtime.js +11 -28
- package/package.json +7 -2
- package/dist/index-DnoM0M8S.d.ts +0 -452
package/README.md
CHANGED
|
@@ -17,21 +17,22 @@ bun add ilha
|
|
|
17
17
|
## Quick Start
|
|
18
18
|
|
|
19
19
|
```ts
|
|
20
|
-
import ilha,
|
|
21
|
-
|
|
22
|
-
const Counter = ilha
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
</
|
|
33
|
-
|
|
34
|
-
|
|
20
|
+
import { ilha, state, action, html, mount } from "ilha";
|
|
21
|
+
|
|
22
|
+
const Counter = ilha(() => {
|
|
23
|
+
const count = state(0);
|
|
24
|
+
|
|
25
|
+
const increment = action(() => {
|
|
26
|
+
count((value) => value + 1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return html`
|
|
30
|
+
<div>
|
|
31
|
+
<p>Count: ${count()}</p>
|
|
32
|
+
<button onclick=${increment}>Increment</button>
|
|
33
|
+
</div>
|
|
34
|
+
`;
|
|
35
|
+
});
|
|
35
36
|
|
|
36
37
|
// SSR
|
|
37
38
|
Counter.toString(); // → '<div><p>Count: 0</p><button>Increment</button></div>'
|
|
@@ -44,936 +45,329 @@ Counter.mount(document.getElementById("app"));
|
|
|
44
45
|
|
|
45
46
|
## Core Concepts
|
|
46
47
|
|
|
47
|
-
Islands are **
|
|
48
|
+
Islands are **function components** that know how to render themselves to an HTML string (SSR) and mount themselves into the DOM (client). Create one by passing the component to `ilha()`.
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
An island reruns when a reactive value it reads during rendering changes. Reactive primitives — `state()`, `derived()`, `action()`, `effect()`, `effect.once()`, and `onError()` — are registered by call order and persist across rerenders. Declare them at the component's top level in a stable order.
|
|
50
51
|
|
|
51
52
|
### Choose the smallest component form
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
| `const View = () => JSX` | You need reusable markup inside another island |
|
|
58
|
-
| `const View = ilha(() => JSX)` | The component needs its own reactive scope, lifecycle, mount, or hydration |
|
|
59
|
-
| `ilha.state(...).action(...).render(...)` | The island needs local state, derived values, actions, input, or lifecycle hooks |
|
|
60
|
-
|
|
61
|
-
Start with a plain component:
|
|
54
|
+
| Form | Ownership |
|
|
55
|
+
| ------------------------------ | ------------------------------------------------------------------- |
|
|
56
|
+
| `const View = () => JSX` | The containing island owns rendering, events, and cleanup |
|
|
57
|
+
| `const View = ilha(() => JSX)` | `View` owns an independent reactive scope, lifecycle, and hydration |
|
|
62
58
|
|
|
63
59
|
```tsx
|
|
64
|
-
const
|
|
60
|
+
const Label = ilha<{ label: string }>(({ label }) => <span>{label}</span>);
|
|
65
61
|
```
|
|
66
62
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
```tsx
|
|
70
|
-
const Status = ilha(() => <p>Ready</p>);
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Expand it into the builder when it needs local capabilities:
|
|
74
|
-
|
|
75
|
-
```tsx
|
|
76
|
-
const Status = ilha.state("message", "Ready").render(({ state }) => <p>{state.message()}</p>);
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
Both `ilha(() => JSX)` and the builder return a complete `Island`. A plain function remains transparent: its rendering, events, and cleanup belong to the containing island.
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## Builder API
|
|
63
|
+
A plain function component stays transparent: its rendering, events, and cleanup belong to the containing island. Wrap it with `ilha()` when it needs independent ownership.
|
|
84
64
|
|
|
85
|
-
|
|
65
|
+
## Primitives
|
|
86
66
|
|
|
87
|
-
### `
|
|
67
|
+
### `state(init?)`
|
|
88
68
|
|
|
89
|
-
|
|
69
|
+
Island-local reactive state. Returns a signal accessor — a function that reads or writes depending on how you call it:
|
|
90
70
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
.render(({ input }) => `<p>Hello, ${input.name}!</p>`);
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
**2. With a [Standard Schema](https://standardschema.dev/) validator** (Zod, Valibot, ArkType, etc.) — runs validation at render time and uses the schema's inferred output type:
|
|
100
|
-
|
|
101
|
-
```ts
|
|
102
|
-
import { z } from "zod";
|
|
103
|
-
|
|
104
|
-
const MyIsland = ilha
|
|
105
|
-
.input(z.object({ name: z.string().default("World") }))
|
|
106
|
-
.render(({ input }) => `<p>Hello, ${input.name}!</p>`);
|
|
107
|
-
|
|
108
|
-
MyIsland.toString({ name: "Ilha" }); // → '<p>Hello, Ilha!</p>'
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
Async schemas are not supported.
|
|
112
|
-
|
|
113
|
-
---
|
|
114
|
-
|
|
115
|
-
### `.state(key, init?)`
|
|
116
|
-
|
|
117
|
-
Declares a reactive state signal. The initial value can be a static value or a function receiving the resolved `input`.
|
|
71
|
+
```tsx
|
|
72
|
+
const Counter = ilha(() => {
|
|
73
|
+
const count = state(0);
|
|
74
|
+
return <p>{count()}</p>;
|
|
75
|
+
});
|
|
118
76
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
.state("name", "anonymous")
|
|
123
|
-
.state("double", ({ count }) => count * 2) // init from input
|
|
124
|
-
.render(({ state }) => `<p>${state.count()}</p>`);
|
|
77
|
+
count(); // read
|
|
78
|
+
count(5); // write
|
|
79
|
+
count((previous) => previous + 1); // update from the latest value
|
|
125
80
|
```
|
|
126
81
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
```ts
|
|
130
|
-
state.count(); // → 0 (read)
|
|
131
|
-
state.count(5); // → sets to 5 (write)
|
|
132
|
-
```
|
|
82
|
+
A function argument is a lazy initializer. To store a function value, return it from an updater wrapper: `callback(() => nextCallback)`.
|
|
133
83
|
|
|
134
|
-
|
|
84
|
+
A state initializer applies only when the instance is created. Later prop changes rerender the component but do not reset state:
|
|
135
85
|
|
|
136
|
-
```
|
|
137
|
-
|
|
86
|
+
```tsx
|
|
87
|
+
const Counter = ilha<{ start: number }>(({ start }) => {
|
|
88
|
+
const count = state(start);
|
|
89
|
+
return <p>{count()}</p>;
|
|
90
|
+
});
|
|
138
91
|
```
|
|
139
92
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
### `.derived(key, fn)`
|
|
93
|
+
### `derived(fn)`
|
|
143
94
|
|
|
144
|
-
|
|
95
|
+
Compute a value from state or props. Supports synchronous values, promises, and async generators, with a built-in `{ loading, value, error }` envelope:
|
|
145
96
|
|
|
146
|
-
```
|
|
147
|
-
ilha
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const res = await fetch(`/api/users/${state.userId()}`, { signal });
|
|
97
|
+
```tsx
|
|
98
|
+
const UserCard = ilha<{ userId: string }>(({ userId }) => {
|
|
99
|
+
const user = derived(async ({ signal }) => {
|
|
100
|
+
const res = await fetch(`/api/users/${userId}`, { signal });
|
|
151
101
|
return res.json();
|
|
152
|
-
})
|
|
153
|
-
.render(({ derived }) => {
|
|
154
|
-
if (derived.user.loading) return `<p>Loading…</p>`;
|
|
155
|
-
if (derived.user.error) return `<p>Error: ${derived.user.error.message}</p>`;
|
|
156
|
-
return `<p>${derived.user().name}</p>`;
|
|
157
102
|
});
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
Read with `derived.name()` like state. Each accessor also exposes `loading`, `value`, and `error` for async work. Write `derived.name(value)` for optimistic UI.
|
|
161
103
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
Declares a reusable synchronous or asynchronous operation. Define actions before consumers, then call them from lowercase native event handlers:
|
|
167
|
-
|
|
168
|
-
```tsx
|
|
169
|
-
const Counter = ilha
|
|
170
|
-
.state("count", 0)
|
|
171
|
-
.action("increment", (amount: number, { state }) => {
|
|
172
|
-
state.count((count) => count + amount);
|
|
173
|
-
return state.count();
|
|
174
|
-
})
|
|
175
|
-
.render(({ state, action }) => (
|
|
176
|
-
<button onclick={() => action.increment(1)}>{state.count()}</button>
|
|
177
|
-
));
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
Async actions expose reactive `pending`, `data`, and `error` properties. Action callbacks receive state, derived values, input, host, and an abort signal.
|
|
181
|
-
|
|
182
|
-
---
|
|
183
|
-
|
|
184
|
-
### `.on(selector, handler)`
|
|
185
|
-
|
|
186
|
-
Prefer lowercase native event props for handlers owned by one rendered element. Use `.on()` when you need a CSS selector, an island-host listener, the full handler context, or combined modifiers.
|
|
187
|
-
|
|
188
|
-
The selector string uses the format `"cssSelector@eventName"`. Omit the selector part to target the island host itself.
|
|
189
|
-
|
|
190
|
-
```ts
|
|
191
|
-
ilha
|
|
192
|
-
.state("count", 0)
|
|
193
|
-
.on("@click", ({ state }) => state.count((count) => count + 1)) // host click
|
|
194
|
-
.on("button.inc@click", ({ state }) => state.count((count) => count + 1)) // child click
|
|
195
|
-
.on("input@input", ({ state, event }) => {
|
|
196
|
-
state.query((event.target as HTMLInputElement).value);
|
|
197
|
-
})
|
|
198
|
-
.render(({ state }) => html`<div><button class="inc">+</button></div>`);
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
**Event modifiers** — append after a `:` separator:
|
|
202
|
-
|
|
203
|
-
| Modifier | Description |
|
|
204
|
-
| ----------- | ------------------------------------------------------------------------- |
|
|
205
|
-
| `once` | Listener fires only once |
|
|
206
|
-
| `capture` | Capture phase |
|
|
207
|
-
| `passive` | `{ passive: true }` |
|
|
208
|
-
| `abortable` | `ctx.signal` aborts when the same listener fires again on the same target |
|
|
209
|
-
|
|
210
|
-
Multiple modifiers can be combined: `@click:once:capture`.
|
|
211
|
-
|
|
212
|
-
The handler receives a `HandlerContext`:
|
|
213
|
-
|
|
214
|
-
```ts
|
|
215
|
-
{
|
|
216
|
-
state: IslandState; // reactive state signals
|
|
217
|
-
derived: IslandDerived; // derived values
|
|
218
|
-
input: TInput; // resolved input props
|
|
219
|
-
host: Element; // island root element
|
|
220
|
-
target: Element; // element that fired the event (typed per event name)
|
|
221
|
-
event: Event; // the native event (typed per event name)
|
|
222
|
-
signal: AbortSignal; // aborts on unmount, and on next fire if `:abortable`
|
|
223
|
-
}
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
**Cancelling async work with `ctx.signal`** — pass it to `fetch` or any abort-aware API to cancel stale requests when the island unmounts:
|
|
227
|
-
|
|
228
|
-
```ts
|
|
229
|
-
ilha
|
|
230
|
-
.state("results", [])
|
|
231
|
-
.on("button@click", async ({ state, signal }) => {
|
|
232
|
-
const res = await fetch("/api/data", { signal });
|
|
233
|
-
state.results(await res.json());
|
|
234
|
-
})
|
|
235
|
-
.render(
|
|
236
|
-
() =>
|
|
237
|
-
html`<button>Load</button>
|
|
238
|
-
<ul></ul>`,
|
|
239
|
-
);
|
|
104
|
+
if (user.loading) return <p>Loading…</p>;
|
|
105
|
+
if (user.error) return <p>Error: {user.error.message}</p>;
|
|
106
|
+
return <p>{user()?.name}</p>;
|
|
107
|
+
});
|
|
240
108
|
```
|
|
241
109
|
|
|
242
|
-
|
|
110
|
+
Async generators stream: each yielded value feeds the envelope. Stale runs abort via the passed `signal`.
|
|
243
111
|
|
|
244
|
-
|
|
245
|
-
ilha
|
|
246
|
-
.on("input@input:abortable", async ({ state, event, signal }) => {
|
|
247
|
-
const q = (event.target as HTMLInputElement).value;
|
|
248
|
-
const res = await fetch(`/search?q=${q}`, { signal }); // earlier requests cancelled
|
|
249
|
-
if (signal.aborted) return;
|
|
250
|
-
state.results(await res.json());
|
|
251
|
-
})
|
|
252
|
-
.render(
|
|
253
|
-
() =>
|
|
254
|
-
html`<input />
|
|
255
|
-
<ul></ul>`,
|
|
256
|
-
);
|
|
257
|
-
```
|
|
112
|
+
### `action(fn)`
|
|
258
113
|
|
|
259
|
-
|
|
114
|
+
Define a reusable operation with reactive execution state. Use it when you render `pending`, `data`, or `error`, or when you need unmount cancellation:
|
|
260
115
|
|
|
261
|
-
|
|
116
|
+
```tsx
|
|
117
|
+
const save = action(async (form: FormData, { signal }) => {
|
|
118
|
+
const response = await fetch("/save", { method: "POST", body: form, signal });
|
|
119
|
+
return response.json();
|
|
120
|
+
});
|
|
262
121
|
|
|
263
|
-
|
|
264
|
-
.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
state.c(3); // → one render, not three
|
|
268
|
-
})
|
|
122
|
+
save(payload);
|
|
123
|
+
save.pending;
|
|
124
|
+
save.data;
|
|
125
|
+
save.error;
|
|
269
126
|
```
|
|
270
127
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
---
|
|
128
|
+
Direct action references work as native event handlers (`onclick={save}`). Use plain functions for ordinary operations.
|
|
274
129
|
|
|
275
|
-
###
|
|
130
|
+
### `effect(fn)` and `effect.once(fn)`
|
|
276
131
|
|
|
277
|
-
|
|
132
|
+
`effect()` runs a reactive side effect that reruns when its dependencies change, with cleanup before rerun and on unmount:
|
|
278
133
|
|
|
279
|
-
```
|
|
280
|
-
ilha
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
document.title = state.title();
|
|
134
|
+
```tsx
|
|
135
|
+
const App = ilha(() => {
|
|
136
|
+
effect(() => {
|
|
137
|
+
document.title = count();
|
|
284
138
|
return () => {
|
|
285
|
-
|
|
286
|
-
};
|
|
287
|
-
})
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
The handler receives an `EffectContext`:
|
|
292
|
-
|
|
293
|
-
```ts
|
|
294
|
-
{
|
|
295
|
-
state: IslandState;
|
|
296
|
-
derived: IslandDerived;
|
|
297
|
-
input: TInput;
|
|
298
|
-
host: Element;
|
|
299
|
-
signal: AbortSignal; // aborts when the effect re-runs OR the island unmounts
|
|
300
|
-
}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
Reading `derived.name()` subscribes the effect. Writing `derived.name(value)` does not — use writes for optimistic UI without pinning the effect to every derived update.
|
|
304
|
-
|
|
305
|
-
**Cancelling async work with `ctx.signal`** — unlike `.on()`, race-cancellation is the **default** behaviour for effects (no opt-in modifier needed) because dependency changes invariably make the previous run stale. Pass `signal` to async work to bail out of stale invocations without needing a manual cleanup function:
|
|
306
|
-
|
|
307
|
-
```ts
|
|
308
|
-
ilha
|
|
309
|
-
.state("userId", 1)
|
|
310
|
-
.state("user", null)
|
|
311
|
-
.effect(({ state, signal }) => {
|
|
312
|
-
(async () => {
|
|
313
|
-
try {
|
|
314
|
-
const res = await fetch(`/api/users/${state.userId()}`, { signal });
|
|
315
|
-
if (signal.aborted) return;
|
|
316
|
-
state.user(await res.json());
|
|
317
|
-
} catch (err) {
|
|
318
|
-
if (err && (err as Error).name === "AbortError") return;
|
|
319
|
-
throw err;
|
|
320
|
-
}
|
|
321
|
-
})();
|
|
322
|
-
})
|
|
323
|
-
.render(({ state }) => html`<p>${state.user?.name ?? "Loading…"}</p>`);
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
Both the user-supplied cleanup function (if any) and the signal abort fire when the effect re-runs, so you can mix patterns.
|
|
327
|
-
|
|
328
|
-
**Implicit batching** — multiple synchronous state writes inside an effect run produce a single propagation pass.
|
|
329
|
-
|
|
330
|
-
---
|
|
331
|
-
|
|
332
|
-
### `.onMount(fn)`
|
|
333
|
-
|
|
334
|
-
Runs once after the island is mounted into the DOM. **Client-only** — SSR never invokes it (matching `.on()` and `.effect()`), so server-rendered markup must not depend on onMount side effects. Receives `{ state, derived, input, host, hydrated }` where `hydrated` is `true` when the island was mounted over existing SSR content. Optionally returns a cleanup function called on unmount.
|
|
335
|
-
|
|
336
|
-
```ts
|
|
337
|
-
ilha
|
|
338
|
-
.onMount(({ host, hydrated }) => {
|
|
339
|
-
console.log("mounted", hydrated ? "(hydrated)" : "(fresh)");
|
|
340
|
-
return () => console.log("unmounted");
|
|
341
|
-
})
|
|
342
|
-
.render(() => `<div>hello</div>`);
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
`.onMount()` is skipped when `snapshot.skipOnMount` is set via `.hydratable()`.
|
|
346
|
-
|
|
347
|
-
---
|
|
348
|
-
|
|
349
|
-
### `.onError(fn)`
|
|
350
|
-
|
|
351
|
-
Registers an error handler that catches errors thrown by `.on()` handlers (sync throws and async rejections) and `.effect()` runs (sync throws). Multiple `.onError()` calls compose — all run in declaration order. If no `.onError()` is registered, errors fall back to `console.error` so they are never silently swallowed.
|
|
352
|
-
|
|
353
|
-
```ts
|
|
354
|
-
ilha
|
|
355
|
-
.state("count", 0)
|
|
356
|
-
.on("@click", ({ state }) => {
|
|
357
|
-
if (state.count() > 5) throw new Error("too many clicks");
|
|
358
|
-
state.count((count) => count + 1);
|
|
359
|
-
})
|
|
360
|
-
.onError(({ error, source }) => {
|
|
361
|
-
console.error(`[${source}] ${error.message}`);
|
|
362
|
-
Sentry.captureException(error);
|
|
363
|
-
})
|
|
364
|
-
.render(({ state }) => `<button>${state.count()}</button>`);
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
The handler receives an `ErrorContext`:
|
|
368
|
-
|
|
369
|
-
```ts
|
|
370
|
-
{
|
|
371
|
-
error: Error; // always wrapped to Error if a non-Error was thrown
|
|
372
|
-
source: "on" | "effect";
|
|
373
|
-
state: IslandState;
|
|
374
|
-
derived: IslandDerived;
|
|
375
|
-
input: TInput;
|
|
376
|
-
host: Element;
|
|
377
|
-
}
|
|
139
|
+
/* cleanup */
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
return <p>{count()}</p>;
|
|
143
|
+
});
|
|
378
144
|
```
|
|
379
145
|
|
|
380
|
-
`
|
|
146
|
+
`effect.once()` runs once after mount for one-time setup. It receives `{ host, signal, hydrated }` and supports cleanup. `effect()` and `effect.once()` are client-only; SSR never invokes them.
|
|
381
147
|
|
|
382
|
-
|
|
148
|
+
### `onError(fn)`
|
|
383
149
|
|
|
384
|
-
|
|
150
|
+
Register a per-island error handler. Context: `{ error, source, host }` with sources `"effect"`, `"once"`, `"event"`, and `"action"`. Fall back to the global [`onUncaughtError()`](#onuncaughterrorfn) for app-wide sinks.
|
|
385
151
|
|
|
386
|
-
|
|
152
|
+
## Typed props and validation
|
|
387
153
|
|
|
388
|
-
|
|
154
|
+
Pass a [Standard Schema](https://standardschema.dev)-compatible validator as the first `ilha()` argument to validate, coerce, and default props at runtime:
|
|
389
155
|
|
|
390
|
-
```
|
|
391
|
-
import {
|
|
156
|
+
```tsx
|
|
157
|
+
import { z } from "zod";
|
|
392
158
|
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
`.render(
|
|
397
|
-
({ state }) => html`
|
|
398
|
-
<div>
|
|
399
|
-
<p class="title">Hello</p>
|
|
400
|
-
<button>Toggle</button>
|
|
401
|
-
</div>
|
|
402
|
-
`,
|
|
403
|
-
);
|
|
159
|
+
const Greeting = ilha(z.object({ name: z.string().default("World") }), ({ name }) => (
|
|
160
|
+
<p>Hello, {name}!</p>
|
|
161
|
+
));
|
|
404
162
|
```
|
|
405
163
|
|
|
406
|
-
|
|
164
|
+
Validation runs during SSR, hydration, mount, and prop updates.
|
|
407
165
|
|
|
408
|
-
|
|
409
|
-
const accent = "teal";
|
|
166
|
+
## Composing Islands
|
|
410
167
|
|
|
411
|
-
|
|
412
|
-
```
|
|
168
|
+
Nest islands as JSX components. Child islands render inline during SSR and mount independently on the client — a state change in a child does not re-render the parent:
|
|
413
169
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
```ts
|
|
417
|
-
import styles from "./card.css?raw";
|
|
418
|
-
|
|
419
|
-
ilha.css(styles).render(() => `<div class="card">…</div>`);
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
**SSR output** — a `<style data-ilha-css>` tag is prepended as the first child of the island's rendered HTML:
|
|
170
|
+
```tsx
|
|
171
|
+
const Badge = ilha<{ label: string }>(({ label }) => <strong>{label}</strong>);
|
|
423
172
|
|
|
424
|
-
|
|
425
|
-
<style data-ilha-css>
|
|
426
|
-
@scope (:scope) to ([data-ilha]) {
|
|
427
|
-
.title {
|
|
428
|
-
font-weight: 700;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
</style>
|
|
432
|
-
<div>…</div>
|
|
173
|
+
const Page = ilha(() => <Badge label="New" />);
|
|
433
174
|
```
|
|
434
175
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
**`.hydratable()` integration** — the style tag is included inside the `data-ilha` wrapper regardless of the `snapshot` option.
|
|
438
|
-
|
|
439
|
-
> **Note:** Calling `.css()` more than once on the same builder chain is not supported. In dev mode a warning is logged and only the last stylesheet is used. Compose all your styles into a single `.css()` call.
|
|
440
|
-
|
|
441
|
-
---
|
|
442
|
-
|
|
443
|
-
### Composing Islands
|
|
444
|
-
|
|
445
|
-
Child islands are interpolated directly inside a parent's html template. During SSR the child's HTML is rendered inline; during client mount the child is activated independently inside its own host element.
|
|
176
|
+
Each nested island is wrapped in a slot element (default `div`). Choose the wrapper tag with the child's `{ as }` constructor option:
|
|
446
177
|
|
|
447
|
-
```
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
const Card = ilha(
|
|
451
|
-
() => html`
|
|
452
|
-
<div class="card">
|
|
453
|
-
${Icon}
|
|
454
|
-
<p>Card content</p>
|
|
455
|
-
</div>
|
|
456
|
-
`,
|
|
457
|
-
);
|
|
178
|
+
```tsx
|
|
179
|
+
const Row = ilha(({ label }) => <li>{label}</li>, { as: "li" });
|
|
458
180
|
```
|
|
459
181
|
|
|
460
|
-
|
|
182
|
+
For keyed child islands in lists, create a keyed component with `Island.key()` before rendering it so identity — state, DOM, and focus — survives reorders:
|
|
461
183
|
|
|
462
184
|
```tsx
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
</
|
|
185
|
+
const Item = ilha<{ label: string }>(({ label }) => <li>{label}</li>);
|
|
186
|
+
|
|
187
|
+
const List = ilha(() => (
|
|
188
|
+
<ul>
|
|
189
|
+
{items.map((item) => {
|
|
190
|
+
const Keyed = Item.key(item.id);
|
|
191
|
+
return <Keyed label={item.label} />;
|
|
192
|
+
})}
|
|
193
|
+
</ul>
|
|
472
194
|
));
|
|
473
195
|
```
|
|
474
196
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
**Keyed children** — use `.key()` when a child may reorder or appear conditionally. Keys must be unique within a parent render:
|
|
197
|
+
Keys must be unique within a parent render and cannot contain `:`.
|
|
478
198
|
|
|
479
|
-
|
|
480
|
-
const List = ilha(
|
|
481
|
-
() =>
|
|
482
|
-
html`<ul>
|
|
483
|
-
${items.map((item) => html`<li>${Item.key(item.id)({ name: item.name })}</li>`)}
|
|
484
|
-
</ul>`,
|
|
485
|
-
);
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
---
|
|
199
|
+
## Island Interface
|
|
489
200
|
|
|
490
|
-
###
|
|
201
|
+
### `island.toString(props?)`
|
|
491
202
|
|
|
492
|
-
|
|
203
|
+
Synchronous SSR:
|
|
493
204
|
|
|
494
205
|
```ts
|
|
495
|
-
|
|
496
|
-
.transition({
|
|
497
|
-
enter: async (host) => {
|
|
498
|
-
host.animate([{ opacity: 0 }, { opacity: 1 }], 300).finished;
|
|
499
|
-
},
|
|
500
|
-
leave: async (host) => {
|
|
501
|
-
await host.animate([{ opacity: 1 }, { opacity: 0 }], 300).finished;
|
|
502
|
-
},
|
|
503
|
-
})
|
|
504
|
-
.render(() => `<div>content</div>`);
|
|
206
|
+
Counter.toString(); // → string
|
|
505
207
|
```
|
|
506
208
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
---
|
|
209
|
+
If the island declares async `derived()` values, `toString()` renders their loading state — use `await toStringAsync()` instead (a dev warning tells you when this happens).
|
|
510
210
|
|
|
511
|
-
###
|
|
211
|
+
### `await island.toStringAsync(props?)`
|
|
512
212
|
|
|
513
|
-
|
|
213
|
+
Async SSR — awaits async derived values and pulls the first value from async-generator derived values:
|
|
514
214
|
|
|
515
215
|
```ts
|
|
516
|
-
const
|
|
216
|
+
const html = await Counter.toStringAsync();
|
|
517
217
|
```
|
|
518
218
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
```tsx
|
|
522
|
-
const StaticIsland = ilha(() => <p>Hello</p>);
|
|
523
|
-
const Label = ilha<{ label: string }>(({ input }) => <p>{input.label}</p>);
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
---
|
|
527
|
-
|
|
528
|
-
## Island Interface
|
|
529
|
-
|
|
530
|
-
Every island produced by `.render()` or `ilha(fn)` exposes:
|
|
531
|
-
|
|
532
|
-
### `island.toString(props?)`
|
|
533
|
-
|
|
534
|
-
Render the island to an HTML string synchronously. If `.derived()` entries have async functions, they render in `loading: true` state.
|
|
219
|
+
### `island.mount(host, props?)`
|
|
535
220
|
|
|
536
|
-
|
|
221
|
+
Mount into a DOM element. Returns an unmount function that stops listeners, effects, and other active behavior:
|
|
537
222
|
|
|
538
223
|
```ts
|
|
539
|
-
|
|
540
|
-
MyIsland.toString({ name: "Ilha" }); // with props
|
|
541
|
-
await MyIsland.toStringAsync({ name: "Ilha" }); // async SSR — awaits async derived values
|
|
542
|
-
await MyIsland({ name: "Ilha" }); // callable async form (also used for JSX/html slot composition)
|
|
224
|
+
const unmount = Counter.mount(document.getElementById("app"));
|
|
543
225
|
```
|
|
544
226
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
### `island.mount(host, props?)`
|
|
548
|
-
|
|
549
|
-
Mounts the island into a DOM element. Reads `data-ilha-props` and `data-ilha-state` from the host element automatically — no need to pass props when hydrating SSR output.
|
|
227
|
+
### `await island.hydratable(props, options)`
|
|
550
228
|
|
|
551
|
-
|
|
229
|
+
Emit hydration markup with serialized props and an optional state snapshot. `name` must match the client registry key:
|
|
552
230
|
|
|
553
231
|
```ts
|
|
554
|
-
|
|
555
|
-
|
|
232
|
+
// Server
|
|
233
|
+
const html = await Counter.hydratable({}, { name: "Counter", snapshot: true });
|
|
556
234
|
```
|
|
557
235
|
|
|
558
|
-
In dev mode, double-mounting the same element logs a warning and returns a no-op.
|
|
559
|
-
|
|
560
|
-
---
|
|
561
|
-
|
|
562
|
-
### `island.hydratable(props, options)`
|
|
563
|
-
|
|
564
|
-
Async method that renders the island wrapped in a `data-ilha` hydration container. Used for SSR+hydration pipelines.
|
|
565
|
-
|
|
566
236
|
```ts
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
{
|
|
570
|
-
name: "MyIsland", // registry key for client-side activation
|
|
571
|
-
as: "div", // wrapper tag (default: "div")
|
|
572
|
-
snapshot: true, // embed state + derived as data-ilha-state
|
|
573
|
-
skipOnMount: false, // skip onMount on hydration (default: true when snapshot)
|
|
574
|
-
},
|
|
575
|
-
);
|
|
576
|
-
// → '<div data-ilha="MyIsland" data-ilha-props="…" data-ilha-state="…">…</div>'
|
|
237
|
+
// Client
|
|
238
|
+
mount({ Counter });
|
|
577
239
|
```
|
|
578
240
|
|
|
579
|
-
|
|
241
|
+
Snapshots are positional: state and derived values restore by primitive order. Malformed or incompatible snapshots are ignored safely.
|
|
580
242
|
|
|
581
|
-
|
|
582
|
-
| --------------------------------- | --------------------------------------------- |
|
|
583
|
-
| `false` | No snapshot — onMount always runs |
|
|
584
|
-
| `true` | Snapshots both state and derived values |
|
|
585
|
-
| `{ state: true, derived: false }` | Fine-grained control over what is snapshotted |
|
|
243
|
+
### `island.key(key)`
|
|
586
244
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
## Top-level Helpers
|
|
245
|
+
Create a keyed child invocation for stable slot identity in lists (see [Composing Islands](#composing-islands)).
|
|
590
246
|
|
|
591
|
-
### `
|
|
247
|
+
### `island.define(tagName, options?)`
|
|
592
248
|
|
|
593
|
-
|
|
249
|
+
Register the island as a custom element, usable from plain HTML or any framework:
|
|
594
250
|
|
|
595
251
|
```ts
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const { unmount } = mount(
|
|
599
|
-
{ counter: Counter, card: Card },
|
|
600
|
-
{
|
|
601
|
-
root: document.getElementById("app"), // default: document.body
|
|
602
|
-
lazy: true, // use IntersectionObserver (mount on visibility)
|
|
603
|
-
},
|
|
604
|
-
);
|
|
605
|
-
|
|
606
|
-
unmount(); // → unmounts all discovered islands
|
|
252
|
+
Counter.define("x-counter", { observe: ["start"] });
|
|
607
253
|
```
|
|
608
254
|
|
|
609
|
-
|
|
255
|
+
## Top-level Helpers
|
|
610
256
|
|
|
611
|
-
### `
|
|
257
|
+
### `mount(registry, options?)`
|
|
612
258
|
|
|
613
|
-
|
|
259
|
+
Auto-discover and mount `[data-ilha="Name"]` hosts:
|
|
614
260
|
|
|
615
261
|
```ts
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const unmount = from("#hero", HeroIsland, { title: "Welcome" });
|
|
262
|
+
mount({ Counter, Badge });
|
|
619
263
|
```
|
|
620
264
|
|
|
621
|
-
|
|
265
|
+
Pass `{ root, lazy }` to scope discovery to a root element or defer mounting until hosts scroll into view.
|
|
622
266
|
|
|
623
267
|
### `signal(initial)`
|
|
624
268
|
|
|
625
|
-
|
|
269
|
+
Create a free-standing signal for shared state. Same accessor shape as `state()` but lives outside any island:
|
|
626
270
|
|
|
627
271
|
```ts
|
|
628
|
-
import { signal } from "ilha";
|
|
629
|
-
|
|
630
272
|
const count = signal(0);
|
|
631
|
-
|
|
632
|
-
count(); //
|
|
633
|
-
count(5); // → sets to 5 (write)
|
|
273
|
+
count(); // read
|
|
274
|
+
count(5); // write
|
|
634
275
|
```
|
|
635
276
|
|
|
636
|
-
|
|
277
|
+
### `computed(fn)`
|
|
278
|
+
|
|
279
|
+
Create a lazy, cached, read-only value derived from signals:
|
|
637
280
|
|
|
638
281
|
```ts
|
|
639
|
-
|
|
282
|
+
const total = computed(() => price() * qty());
|
|
283
|
+
```
|
|
640
284
|
|
|
641
|
-
|
|
285
|
+
### `effect(fn)`
|
|
642
286
|
|
|
643
|
-
|
|
644
|
-
const Footer = ilha(() => html`<footer>Logged in as ${username()}</footer>`);
|
|
287
|
+
Run a standalone reactive effect outside any island; returns a stop function:
|
|
645
288
|
|
|
646
|
-
|
|
647
|
-
|
|
289
|
+
```ts
|
|
290
|
+
const stop = effect(() => {
|
|
291
|
+
document.title = `${count()} items`;
|
|
292
|
+
});
|
|
648
293
|
```
|
|
649
294
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
---
|
|
295
|
+
Inside an island render, `effect()` registers an island effect slot instead.
|
|
653
296
|
|
|
654
297
|
### `context(key, initial)`
|
|
655
298
|
|
|
656
|
-
|
|
299
|
+
Get or create a keyed, app-wide shared signal:
|
|
657
300
|
|
|
658
301
|
```ts
|
|
659
|
-
import { context } from "ilha";
|
|
660
|
-
|
|
661
302
|
const theme = context("app.theme", "light");
|
|
662
|
-
|
|
663
|
-
theme(); // → "light"
|
|
664
|
-
theme("dark"); // → sets to "dark"
|
|
665
303
|
```
|
|
666
304
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
> **`signal()` vs `context()`** — both return the same accessor shape and can be used with `bind:` template syntax. Use `signal()` for one-off shared state where you'd hold the reference yourself; use `context()` when you want a name-keyed registry so the same signal can be looked up from anywhere by string key.
|
|
670
|
-
>
|
|
671
|
-
> **SSR caveat** — `context()` is process-global, not request-scoped. It is safe to read in SSR **only for app-wide singletons** (theme, locale, feature flags). Per-request state (current user, session) must not live in `context()`: concurrent SSR requests would leak it across requests. Pass per-request data through island props / loaders or use `@ilha/router`'s request-scope instead.
|
|
672
|
-
|
|
673
|
-
---
|
|
674
|
-
|
|
675
|
-
### `batch(fn)`
|
|
305
|
+
### `batch(fn)` / `untrack(fn)`
|
|
676
306
|
|
|
677
|
-
|
|
307
|
+
`batch()` groups multiple writes into one propagation pass. `untrack()` reads signals without subscribing the surrounding scope:
|
|
678
308
|
|
|
679
309
|
```ts
|
|
680
|
-
import { signal, batch } from "ilha";
|
|
681
|
-
|
|
682
|
-
const a = signal(0);
|
|
683
|
-
const b = signal(0);
|
|
684
|
-
|
|
685
|
-
// Without batch: each write triggers a propagation pass.
|
|
686
|
-
a(1); // → effects re-run
|
|
687
|
-
b(2); // → effects re-run
|
|
688
|
-
|
|
689
|
-
// With batch: both writes flush together.
|
|
690
310
|
batch(() => {
|
|
691
|
-
a(
|
|
692
|
-
b(
|
|
693
|
-
});
|
|
694
|
-
```
|
|
311
|
+
a(1);
|
|
312
|
+
b(2);
|
|
313
|
+
});
|
|
695
314
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
---
|
|
699
|
-
|
|
700
|
-
### `untrack(fn)`
|
|
701
|
-
|
|
702
|
-
Runs `fn` with reactive tracking suspended. Reading signals inside `fn` returns their current value without subscribing the surrounding scope. Use this in effects or deriveds when you want to peek at state without causing a re-run on its changes.
|
|
703
|
-
|
|
704
|
-
```ts
|
|
705
|
-
import ilha, { signal, untrack } from "ilha";
|
|
706
|
-
|
|
707
|
-
const tracked = signal(0);
|
|
708
|
-
const peeked = signal("hello");
|
|
709
|
-
|
|
710
|
-
ilha
|
|
711
|
-
.effect(() => {
|
|
712
|
-
// Re-runs when `tracked` changes, but NOT when `peeked` changes.
|
|
713
|
-
console.log(
|
|
714
|
-
tracked(),
|
|
715
|
-
untrack(() => peeked()),
|
|
716
|
-
);
|
|
717
|
-
})
|
|
718
|
-
.render(() => `<p>x</p>`);
|
|
315
|
+
const value = untrack(() => secret());
|
|
719
316
|
```
|
|
720
317
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
---
|
|
724
|
-
|
|
725
|
-
### `html\`\`` tagged template
|
|
318
|
+
### `persist(accessor, key)`
|
|
726
319
|
|
|
727
|
-
|
|
320
|
+
Keep a standalone signal in sync with `localStorage`:
|
|
728
321
|
|
|
729
322
|
```ts
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
const name = "<script>alert(1)</script>";
|
|
733
|
-
html`<p>${name}</p>`; // → <p><script>…</p> (escaped)
|
|
734
|
-
html`<p>${raw("<b>hi</b>")}</p>`; // → <p><b>hi</b></p> (raw)
|
|
323
|
+
persist(cart, "cart");
|
|
735
324
|
```
|
|
736
325
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
| Value type | Behaviour |
|
|
740
|
-
| -------------------- | ------------------------------------------- |
|
|
741
|
-
| `string` / `number` | HTML-escaped |
|
|
742
|
-
| `null` / `undefined` | Omitted (empty string) |
|
|
743
|
-
| `raw(str)` | Inserted as-is (no escaping) |
|
|
744
|
-
| `html\`…\`` | Inserted as-is (already safe) |
|
|
745
|
-
| Signal accessor | Called and escaped |
|
|
746
|
-
| Island / Island call | Emitted as `data-ilha-slot` host element |
|
|
747
|
-
| Array | Each item processed recursively (no commas) |
|
|
326
|
+
### `onUncaughtError(fn)`
|
|
748
327
|
|
|
749
|
-
|
|
328
|
+
Register an app-wide error sink for islands with no local `onError()`:
|
|
750
329
|
|
|
751
330
|
```ts
|
|
752
|
-
|
|
753
|
-
({ state }) => html`
|
|
754
|
-
<input bind:value=${state.name} />
|
|
755
|
-
<p>Hello, ${state.name()}!</p>
|
|
756
|
-
`,
|
|
757
|
-
);
|
|
331
|
+
const stop = onUncaughtError((error, source) => telemetry.capture(error, { source }));
|
|
758
332
|
```
|
|
759
333
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
| Binding | Element | Bound property | Trigger event |
|
|
763
|
-
| -------------------- | ------------------------------------------------- | ------------------- | ------------- |
|
|
764
|
-
| `bind:value` | `<input>`, `<textarea>`, `<select>` | `value` | `input` |
|
|
765
|
-
| `bind:valueAsNumber` | `<input type="number">` | `valueAsNumber` | `input` |
|
|
766
|
-
| `bind:valueAsDate` | `<input type="date">` | `valueAsDate` | `input` |
|
|
767
|
-
| `bind:checked` | `<input type="checkbox">` | `checked` | `change` |
|
|
768
|
-
| `bind:group` | `<input type="radio">`, `<input type="checkbox">` | `checked` / `value` | `change` |
|
|
769
|
-
| `bind:open` | `<details>` | `open` | `toggle` |
|
|
770
|
-
| `bind:files` | `<input type="file">` | `files` | `change` |
|
|
771
|
-
| `bind:this` | Any element | element reference | — |
|
|
772
|
-
|
|
773
|
-
`bind:group` connects multiple inputs to a single signal — radio buttons hold the selected `value`, checkboxes hold an array of checked values. `bind:this` writes the DOM element into a signal on mount and `null` on unmount. External signals from `signal()` or `context()` work as binding targets too, enabling shared state across islands.
|
|
334
|
+
### `html` / `raw`
|
|
774
335
|
|
|
775
|
-
|
|
336
|
+
`html\`…\``is an XSS-safe tagged template that accepts signals, arrays, and nested templates.`raw(str)` opts into trusted markup:
|
|
776
337
|
|
|
777
338
|
```ts
|
|
778
|
-
|
|
779
|
-
html`<ul>
|
|
780
|
-
${items.map((item) => html`<li>${item}</li>`)}
|
|
781
|
-
</ul>`;
|
|
782
|
-
```
|
|
783
|
-
|
|
784
|
-
---
|
|
785
|
-
|
|
786
|
-
### `raw(value)`
|
|
787
|
-
|
|
788
|
-
Marks a string as trusted raw HTML, bypassing escaping when used inside `html\`\``.
|
|
789
|
-
|
|
790
|
-
```ts
|
|
791
|
-
import { raw } from "ilha";
|
|
339
|
+
import { html, raw } from "ilha";
|
|
792
340
|
|
|
793
|
-
|
|
341
|
+
html`<p>${count()}</p>`;
|
|
342
|
+
html`<button>${raw(icon)}</button>`;
|
|
794
343
|
```
|
|
795
344
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
### `css\`\`` tagged template
|
|
799
|
-
|
|
800
|
-
A passthrough tagged template for CSS strings. Functionally identical to a plain template literal — no runtime transformation occurs. Its purpose is purely to enable editor tooling (LSP syntax highlighting, Prettier formatting) to recognise the contents as CSS.
|
|
345
|
+
## Bindings
|
|
801
346
|
|
|
802
|
-
|
|
803
|
-
import { css } from "ilha";
|
|
347
|
+
Use `bind:*` inside JSX or `html`` for two-way form synchronization:
|
|
804
348
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
color: white;
|
|
809
|
-
}
|
|
810
|
-
.label {
|
|
811
|
-
font-weight: 700;
|
|
812
|
-
}
|
|
813
|
-
`;
|
|
814
|
-
|
|
815
|
-
ilha.css(styles).render(() => `<button class="label">Go</button>`);
|
|
816
|
-
```
|
|
817
|
-
|
|
818
|
-
Interpolations work as normal string concatenation:
|
|
819
|
-
|
|
820
|
-
```ts
|
|
821
|
-
const accent = "coral";
|
|
822
|
-
const styles = css`
|
|
823
|
-
button {
|
|
824
|
-
background: ${accent};
|
|
825
|
-
}
|
|
826
|
-
`;
|
|
349
|
+
```tsx
|
|
350
|
+
<input bind:value={name} />
|
|
351
|
+
<input type="checkbox" bind:checked={done} />
|
|
827
352
|
```
|
|
828
353
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
---
|
|
354
|
+
Supported kinds: `bind:value`, `bind:checked`, `bind:group`, `bind:open`, `bind:files`, and `bind:this`. Native event props keep modifiers `:once`, `:capture`, `:passive`, and `:abortable`.
|
|
832
355
|
|
|
833
|
-
##
|
|
356
|
+
## Security
|
|
834
357
|
|
|
835
|
-
|
|
358
|
+
JSX children and attributes are escaped by default. Use `raw()` only for trusted markup you control. `srcdoc` is always dropped, disallowed URL schemes are stripped, and unsafe inline styles are rejected.
|
|
836
359
|
|
|
837
|
-
|
|
360
|
+
## TypeScript
|
|
838
361
|
|
|
839
|
-
|
|
362
|
+
Configure JSX with the automatic runtime:
|
|
840
363
|
|
|
841
|
-
```
|
|
364
|
+
```json
|
|
842
365
|
{
|
|
843
366
|
"compilerOptions": {
|
|
844
367
|
"jsx": "react-jsx",
|
|
845
|
-
"jsxImportSource": "ilha"
|
|
846
|
-
}
|
|
368
|
+
"jsxImportSource": "ilha"
|
|
369
|
+
}
|
|
847
370
|
}
|
|
848
371
|
```
|
|
849
372
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
```tsx
|
|
853
|
-
import ilha from "ilha";
|
|
854
|
-
|
|
855
|
-
const Counter = ilha
|
|
856
|
-
.state("count", 0)
|
|
857
|
-
.action("increment", (_, { state }) => {
|
|
858
|
-
state.count((count) => count + 1);
|
|
859
|
-
})
|
|
860
|
-
.render(({ state, action }) => (
|
|
861
|
-
<div>
|
|
862
|
-
<p>Count: {state.count}</p>
|
|
863
|
-
<button onclick={action.increment}>Increment</button>
|
|
864
|
-
</div>
|
|
865
|
-
));
|
|
866
|
-
```
|
|
867
|
-
|
|
868
|
-
Interpolated children follow the same rules as the `html` tag — strings are escaped, signal accessors are auto-called, islands become hydration slots, arrays are flattened. Use `raw()` to opt out of escaping, and `<></>` (Fragment) to group siblings without a wrapper element.
|
|
869
|
-
|
|
870
|
-
### Attributes
|
|
871
|
-
|
|
872
|
-
| Feature | Behaviour |
|
|
873
|
-
| --------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
874
|
-
| `class` / `className` | Accepts a string, an array (`["a", cond && "b"]`), or an object (`{ active: isActive }`) |
|
|
875
|
-
| `htmlFor` | Alias for `for` |
|
|
876
|
-
| `style` | Accepts a string or an object (`{ backgroundColor: "teal" }` → `background-color:teal`) |
|
|
877
|
-
| Boolean attributes | `true` renders the bare attribute, `false`/`null`/`undefined` omit it |
|
|
878
|
-
| `bind:*` | Two-way bindings, same as in `html` templates — pass a signal accessor: `<input bind:value={state.name} />` |
|
|
879
|
-
| `key` | Keys a child island for reorder-safe rendering (same as `.key()`). Keys must be non-empty and must not contain `:` |
|
|
880
|
-
|
|
881
|
-
Lowercase native event props such as `onclick={handler}` attach with `addEventListener` during island mount; executable handlers never become inline SSR attributes. `srcdoc` is stripped, and URL attributes (`href`, `src`, `action`, …) with unsafe schemes such as `javascript:` are dropped.
|
|
882
|
-
|
|
883
|
-
### Islands as components
|
|
884
|
-
|
|
885
|
-
Islands are callable JSX components, so props and keys work as expected. Unlike transparent plain components, each island keeps its own reactive scope and lifecycle:
|
|
886
|
-
|
|
887
|
-
```tsx
|
|
888
|
-
const Badge = ilha
|
|
889
|
-
.input<{ label: string }>()
|
|
890
|
-
.render(({ input }) => <span class="badge">{input.label}</span>);
|
|
891
|
-
|
|
892
|
-
const Card = ilha(() => (
|
|
893
|
-
<div class="card">
|
|
894
|
-
<Badge label="New" />
|
|
895
|
-
<p>Card content</p>
|
|
896
|
-
</div>
|
|
897
|
-
));
|
|
898
|
-
|
|
899
|
-
const List = ilha(() => (
|
|
900
|
-
<ul>
|
|
901
|
-
{items.map((item) => (
|
|
902
|
-
<li>
|
|
903
|
-
<Item key={item.id} name={item.name} />
|
|
904
|
-
</li>
|
|
905
|
-
))}
|
|
906
|
-
</ul>
|
|
907
|
-
));
|
|
908
|
-
```
|
|
909
|
-
|
|
910
|
-
---
|
|
911
|
-
|
|
912
|
-
## SSR + Hydration
|
|
913
|
-
|
|
914
|
-
The recommended SSR + hydration pattern uses `.hydratable()` on the server and `ilha.mount()` on the client.
|
|
915
|
-
|
|
916
|
-
### Server
|
|
917
|
-
|
|
918
|
-
```ts
|
|
919
|
-
import { MyIsland } from "./islands";
|
|
920
|
-
|
|
921
|
-
const html = await MyIsland.hydratable({ count: 42 }, { name: "my-island", snapshot: true });
|
|
922
|
-
|
|
923
|
-
return `<!doctype html><html><body>${html}</body></html>`;
|
|
924
|
-
```
|
|
925
|
-
|
|
926
|
-
### Client
|
|
927
|
-
|
|
928
|
-
```ts
|
|
929
|
-
import { mount } from "ilha";
|
|
930
|
-
import { MyIsland } from "./islands";
|
|
931
|
-
|
|
932
|
-
mount({ MyIsland });
|
|
933
|
-
```
|
|
934
|
-
|
|
935
|
-
The client reads `data-ilha-state` to restore signal values from the snapshot, skipping a needless re-render and calling `.onMount()` only if `skipOnMount` is not set.
|
|
936
|
-
|
|
937
|
-
### State snapshot flow
|
|
938
|
-
|
|
939
|
-
```
|
|
940
|
-
server client
|
|
941
|
-
────────────────────────────────────── ──────────────────────────────────────────
|
|
942
|
-
.hydratable({ count: 42 }, { mount({ MyIsland })
|
|
943
|
-
name: "my-island", → reads data-ilha-state
|
|
944
|
-
snapshot: true → restores signals from snapshot
|
|
945
|
-
}) → skips onMount (skipOnMount: true)
|
|
946
|
-
→ data-ilha-state='{"count":42}' → attaches event listeners
|
|
947
|
-
→ starts effects + derived watchers
|
|
948
|
-
```
|
|
949
|
-
|
|
950
|
-
---
|
|
951
|
-
|
|
952
|
-
## TypeScript
|
|
953
|
-
|
|
954
|
-
Key exported types:
|
|
955
|
-
|
|
956
|
-
```ts
|
|
957
|
-
import type {
|
|
958
|
-
Island,
|
|
959
|
-
IslandState,
|
|
960
|
-
IslandDerived,
|
|
961
|
-
DerivedValue,
|
|
962
|
-
KeyedIsland,
|
|
963
|
-
HydratableOptions,
|
|
964
|
-
OnMountContext,
|
|
965
|
-
HandlerContext,
|
|
966
|
-
HandlerContextFor,
|
|
967
|
-
ErrorContext,
|
|
968
|
-
ErrorSource,
|
|
969
|
-
ExternalSignal,
|
|
970
|
-
MountOptions,
|
|
971
|
-
MountResult,
|
|
972
|
-
} from "ilha";
|
|
973
|
-
```
|
|
974
|
-
|
|
975
|
-
---
|
|
976
|
-
|
|
977
|
-
## License
|
|
978
|
-
|
|
979
|
-
MIT
|
|
373
|
+
Build tools resolve `ilha/jsx-runtime` in production and `ilha/jsx-dev-runtime` in development.
|