@weftui/dom 0.26.0 → 0.26.2
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 +66 -0
- package/dist/client/index.js +1 -1
- package/docs/explanation/boundaries-and-suspense.md +91 -0
- package/docs/explanation/combinator-api.md +164 -0
- package/docs/explanation/reactive-primitives.md +162 -0
- package/docs/explanation/rendering-model.md +65 -0
- package/docs/explanation/services-and-context.md +91 -0
- package/docs/how-to/add-routing.md +296 -0
- package/docs/how-to/author-components.md +264 -0
- package/docs/how-to/handle-forms.md +76 -0
- package/docs/how-to/load-async-data.md +70 -0
- package/docs/how-to/load-data-with-rpc.md +172 -0
- package/docs/how-to/provide-services.md +124 -0
- package/docs/how-to/render-keyed-lists.md +51 -0
- package/docs/how-to/render-on-the-server.md +86 -0
- package/docs/how-to/show-navigation-progress.md +65 -0
- package/docs/how-to/split-routes-lazily.md +62 -0
- package/docs/how-to/style-reactively.md +63 -0
- package/docs/how-to/use-element-refs.md +63 -0
- package/docs/index.md +59 -0
- package/docs/reference/core.md +496 -0
- package/docs/reference/dom.md +142 -0
- package/docs/reference/router.md +348 -0
- package/docs/tutorial/01-your-first-app.md +48 -0
- package/docs/tutorial/02-reactivity.md +57 -0
- package/docs/tutorial/03-services-and-async.md +77 -0
- package/docs/tutorial/04-errors-and-server.md +61 -0
- package/package.json +19 -5
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "@weftui/core"
|
|
3
|
+
order: 1
|
|
4
|
+
section: reference
|
|
5
|
+
description: Full API surface for @weftui/core — element builders, components, sources, streams, and boundaries.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# @weftui/core API Reference
|
|
9
|
+
|
|
10
|
+
## Element builders
|
|
11
|
+
|
|
12
|
+
### `h`
|
|
13
|
+
|
|
14
|
+
Proxy-based namespace for building HTML and SVG elements. Every property is an element builder for that tag name:
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { h } from "@weftui/core";
|
|
18
|
+
|
|
19
|
+
h.div(props, children)
|
|
20
|
+
h.span(props, child: string | number)
|
|
21
|
+
h.input(props)
|
|
22
|
+
h.ul(children)
|
|
23
|
+
// ...any HTML or SVG tag
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Each builder has these overloads:
|
|
27
|
+
|
|
28
|
+
| Signature | Description |
|
|
29
|
+
| --------------------------------------- | --------------------------- |
|
|
30
|
+
| `h.tag(props, children: Renderable[])` | Props + array of children |
|
|
31
|
+
| `h.tag(props, child: string \| number)` | Props + single static child |
|
|
32
|
+
| `h.tag(props)` | Props only, no children |
|
|
33
|
+
| `h.tag(children: Renderable[])` | Children only, no props |
|
|
34
|
+
| `h.tag(child: string \| number)` | Single static child only |
|
|
35
|
+
| `h.tag()` | No arguments |
|
|
36
|
+
|
|
37
|
+
**Return type**: `Node<PropsE<P> | ChildrenE<C>, PropsR<P> | ChildrenR<C>>`
|
|
38
|
+
|
|
39
|
+
Reactive prop values (Stream, Effect, Subscribable) contribute their `E`/`R` to the node. Static values contribute `never`. Children's channels are unioned with props' channels.
|
|
40
|
+
|
|
41
|
+
### `h.fragment`
|
|
42
|
+
|
|
43
|
+
Groups children into a fragment that renders without a wrapper element:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { h } from "@weftui/core";
|
|
47
|
+
|
|
48
|
+
h.fragment(children: Node[]): Node<ChildrenE, ChildrenR>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Use when a component needs to return multiple sibling elements.
|
|
52
|
+
|
|
53
|
+
### `Component`
|
|
54
|
+
|
|
55
|
+
Namespace exposing two factories for reusable components with caller-propagating reactive prop types: `Component.gen` (generator body) and `Component.make` (plain-function body). Both return a callable that is generic over the caller's specific `props`/`children`, so reactive prop values and reactive children contribute their `E`/`R` at the call site.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { Component } from "@weftui/core";
|
|
59
|
+
|
|
60
|
+
Component.gen<BaseProps, C>(
|
|
61
|
+
body: (props: BaseProps, children: C) => Generator<YieldedEffect, ElementDescriptor, never>
|
|
62
|
+
): <GenP extends BaseProps, GenC extends C>(
|
|
63
|
+
props: GenP,
|
|
64
|
+
children?: GenC,
|
|
65
|
+
) => Node<PropsE<GenP> | ChildrenE<…> | BodyE, PropsR<GenP> | ChildrenR<…> | BodyR>;
|
|
66
|
+
|
|
67
|
+
Component.make<BaseProps, C>(
|
|
68
|
+
body: (props: BaseProps, children: C) => Effect<ElementDescriptor, E, R>
|
|
69
|
+
): /* same call signature as above */;
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**`Children`** — the optional `children` argument may be either form:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
type Component.Children<Input = never> =
|
|
76
|
+
| readonly Renderable[]
|
|
77
|
+
| ((input: Input) => readonly Renderable[]);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
For function-children, `ChildrenE`/`ChildrenR` are extracted from the function's `ReturnType`, not from the function itself. The component's body invokes the function with whatever input it chooses.
|
|
81
|
+
|
|
82
|
+
**Example — `Component.gen`**:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
interface TextFieldProps {
|
|
86
|
+
value?: Source.Source<string>;
|
|
87
|
+
onChange?: (v: string) => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const TextField = Component.gen(function* (props: TextFieldProps) {
|
|
91
|
+
const value = yield* Source.toSubscribable(props.value);
|
|
92
|
+
return yield* h.input({
|
|
93
|
+
value,
|
|
94
|
+
oninput: (e) => props.onChange?.(e.currentTarget.value),
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Example — `Component.make` with function-children**:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
const Labeled = Component.make(
|
|
103
|
+
(props: { label: string }, children: (label: string) => readonly Renderable[]) =>
|
|
104
|
+
h.div({ class: "field" }, children(props.label)),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
Labeled({ label: "Name" }, (label) => [h.label(label), h.input()]);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
> For rendering a reactive collection, reach for the built-in [`List.each`](#listeach)
|
|
111
|
+
> rather than mapping items by hand — it reconciles by key across emissions instead of
|
|
112
|
+
> rebuilding the region.
|
|
113
|
+
|
|
114
|
+
### `Boundary` namespace
|
|
115
|
+
|
|
116
|
+
Variants for intercepting rendering-path errors in a subtree, plus `Boundary.suspend` for async fallbacks and `Boundary.rpc` for rpc-backed server data. Each returns a descriptor that the renderer processes via the same `{ type, props }` branch. The catch variants share the same call shape — props first, children array second.
|
|
117
|
+
|
|
118
|
+
**What is caught:**
|
|
119
|
+
|
|
120
|
+
- Construction-time failures — the Effect phase of building child nodes
|
|
121
|
+
- Post-mount stream failures — streams driving children or prop values that fail after mount
|
|
122
|
+
|
|
123
|
+
**What is NOT caught:** event handler errors (they run in detached fibers outside the render path).
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { Boundary } from "@weftui/core";
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### `Boundary.suspend`
|
|
130
|
+
|
|
131
|
+
Shows a fallback while async children are pending:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
Boundary.suspend(
|
|
135
|
+
props: { fallback?: Renderable },
|
|
136
|
+
children: Node[]
|
|
137
|
+
): Node<ChildrenE, ChildrenR>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- Shows `fallback` while any registered child has not yet emitted its first value
|
|
141
|
+
- Performs a single atomic DOM swap once all children have settled
|
|
142
|
+
- Works on both the server (streaming patch model) and the client
|
|
143
|
+
- `hydrate()` sees through `Boundary.suspend` boundaries and adopts resolved DOM in place
|
|
144
|
+
|
|
145
|
+
#### `Boundary.rpc`
|
|
146
|
+
|
|
147
|
+
A universal server/client render boundary backed by one `Rpc` from the app's merged `RpcGroup` ([`@effect/rpc`](https://github.com/Effect-TS/effect/tree/main/packages/rpc)). The rpc **`_tag`** is the boundary's stable identity and its **payload schema** the typed input; the handler lives in the server-only rpc Layer (`group.toLayer(...)`), which the client never imports — tree-shaking does the client/server split structurally. Unlike the catch variants it takes a `render` function — not a children array — and that `render` receives a reactive [`Resource`](#resourcea), not a bare value.
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
Boundary.rpc<R extends Rpc.Any, C extends Node<any, any>>(
|
|
151
|
+
rpc: R, // an Rpc from the merged RpcGroup; its _tag + schemas drive the boundary
|
|
152
|
+
payload: () => Rpc.Payload<R>, // thunk: a fresh typed payload per call (SSR / refetch / mount)
|
|
153
|
+
render: (resource: Resource<Rpc.Success<R>>) => C, // builds the subtree from a reactive Resource (not a bare value)
|
|
154
|
+
options?: { fallback?: Renderable }, // shown only during a client-first SPA mount
|
|
155
|
+
): Node<Node.Error<C> | Rpc.Error<R>, Node.Context<C>>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The boundary resolves the rpc through the ambient [`AppRpcClientTag`](#apprpcclienttag) seam, provided by `@weftui/router` (`RouterServer` on the server, `RouterLive` on the client). It has four lifecycles:
|
|
159
|
+
|
|
160
|
+
- **SSR:** the server resolves the rpc in-process (over the handler Layer), `successSchema`-encodes the result inline as `<script type="application/json">` at the region cursor, then renders `render(seededResource)` to HTML in place.
|
|
161
|
+
- **Hydrate:** `hydrate` reads the inline payload positionally, `successSchema`-decodes it, seeds the `Resource`, and adopts the DOM — it **never re-calls the rpc** (replay, not refetch).
|
|
162
|
+
- **Refetch:** `resource.refetch` calls the rpc again over the network (`POST /_eui/rpc`) and patches the subtree in place (**stale-on-error** — a failed refetch leaves the previous value intact).
|
|
163
|
+
- **Client-first mount:** SPA-navigating into a boundary with **no** SSR payload renders `options.fallback`, forks the rpc call, and swaps in `render(resource)` once it resolves.
|
|
164
|
+
|
|
165
|
+
**Channel algebra:** the output `E` is `render`'s error union plus the rpc's typed `Rpc.Error<R>` (`never` for an rpc with no `error` schema). The output `R` is **exactly** `render`'s `R`, untouched — there is no `provide`/`RServer` to discharge (the handler lives in the rpc Layer) and **no `Exclude`** is applied. A server-only tag accidentally referenced in `render` therefore stays in `R`, where `hydrate`'s `AssertNoServerOnly` rejects it. Brand such services with [`ServerTag`](#servertag).
|
|
166
|
+
|
|
167
|
+
**Typed-failure replay:** a resolved rpc **error** on the SSR pass is `errorSchema`-encoded and relocated to the nearest enclosing failure `Boundary`, then replayed on the client (decoded and re-raised, reproducing the same fallback DOM — never retried). A transport **defect**, or an rpc with no `error` schema, is not replayed; it propagates.
|
|
168
|
+
|
|
169
|
+
> **Not yet covered:** streamed success (`Rpc.make(..., { stream: true })`) and mutations are on the roadmap, not this pass.
|
|
170
|
+
|
|
171
|
+
##### `Resource<A>`
|
|
172
|
+
|
|
173
|
+
The reactive handle `render` receives (`A = Rpc.Success<R>`). After hydrate the region is live: `value` is seeded with the SSR payload and the client can `refetch` the same data on demand, patching the rendered subtree in place.
|
|
174
|
+
|
|
175
|
+
| Field | Type | Meaning |
|
|
176
|
+
| --------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
177
|
+
| `value` | `Subscribable.Subscribable<A>` | Current data. Seeded with the SSR `data` (await-first, emits immediately, so SSR HTML and adopted DOM are byte-identical — no fallback flash). A successful refetch pushes the new value. |
|
|
178
|
+
| `refetch` | `Effect.Effect<void>` | Re-resolves the rpc over the network with a fresh `payload()` and sets `value`. Client only — a no-op on the server. |
|
|
179
|
+
| `pending` | `Subscribable.Subscribable<boolean>` | `true` while a refetch is in flight (`false` on the server / before any refetch). |
|
|
180
|
+
| `error` | `Subscribable.Subscribable<Option<unknown>>` | `Some` with the last refetch error, else `None`. A failed refetch is stale-on-error — it does **not** unmount or raise into a failure `Boundary`. |
|
|
181
|
+
|
|
182
|
+
##### `RpcOptions`
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
interface RpcOptions {
|
|
186
|
+
readonly fallback?: Renderable; // shown only during a client-first mount; omit/null renders nothing while pending
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
##### `SERVER_BOUNDARY`
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
export const SERVER_BOUNDARY: unique symbol;
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The descriptor `type` every `Boundary.rpc` carries (`{ type: SERVER_BOUNDARY, props }`). Exported for renderers, which detect and handle the boundary synchronously via the `{ type, props }` branch without running the node.
|
|
197
|
+
|
|
198
|
+
##### `AppRpcClientTag`
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
interface AppRpcClient {
|
|
202
|
+
readonly call: (tag: string, payload: unknown) => Effect.Effect<unknown, unknown>;
|
|
203
|
+
}
|
|
204
|
+
class AppRpcClientTag extends Context.Tag("@weftui/core/AppRpcClient")<
|
|
205
|
+
AppRpcClientTag,
|
|
206
|
+
AppRpcClient
|
|
207
|
+
>() {}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The ambient, package-neutral seam the renderer resolves a `Boundary.rpc` through — a **flat, untyped** caller `(tag, payload) => Effect<success>`. It lets `@weftui/dom` resolve a boundary without importing `@effect/rpc` or `@weftui/router`. `@weftui/router` provides it: a **network** `RpcClient` (POST `/_eui/rpc`) in the browser, an **in-process** client over the handler Layer on the server. `call` returns the already-decoded success; the renderer owns `successSchema`/`errorSchema` decoding of the inline SSR payload only. Both `AppRpcClientTag` and the `AppRpcClient` type are re-exported from `@weftui/core`. Absent in a router-less mount, where a `Boundary.rpc` resolves to a descriptive "needs router/rpc" error (not a defect).
|
|
211
|
+
|
|
212
|
+
See the [rpc data boundaries guide](https://weftui.dev/docs/how-to/load-data-with-rpc) and [examples/router-ssr](https://github.com/stefvw93/weft/tree/main/examples/router-ssr).
|
|
213
|
+
|
|
214
|
+
#### `Boundary.catchAll`
|
|
215
|
+
|
|
216
|
+
Catches all typed failures (`Cause.fail`). Defects (`Cause.die`) are not caught and re-raise.
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
Boundary.catchAll<C, FE, FR>(
|
|
220
|
+
props: { fallback: (e: ChildrenE<C>) => Node<FE, FR> },
|
|
221
|
+
children: C,
|
|
222
|
+
): Node<FE, ChildrenR<C> | FR>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The children's `E` is fully consumed. The output `E` is only the fallback's own error channel.
|
|
226
|
+
|
|
227
|
+
#### `Boundary.catchAllCause`
|
|
228
|
+
|
|
229
|
+
Catches every `Cause` including defects and interruptions.
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
Boundary.catchAllCause<C, FE, FR>(
|
|
233
|
+
props: { fallback: (cause: Cause.Cause<ChildrenE<C>>) => Node<FE, FR> },
|
|
234
|
+
children: C,
|
|
235
|
+
): Node<FE, ChildrenR<C> | FR>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The fallback receives the full `Cause`, not just the failure value.
|
|
239
|
+
|
|
240
|
+
#### `Boundary.catchTag`
|
|
241
|
+
|
|
242
|
+
Catches errors whose `_tag` equals `props.tag`. Unmatched errors re-raise to the nearest parent boundary.
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
Boundary.catchTag<C, Tag, FE, FR>(
|
|
246
|
+
props: {
|
|
247
|
+
tag: Tag; // must be a key of ChildrenE<C>["_tag"]
|
|
248
|
+
fallback: (e: Extract<ChildrenE<C>, { _tag: Tag }>) => Node<FE, FR>;
|
|
249
|
+
},
|
|
250
|
+
children: C,
|
|
251
|
+
): Node<Exclude<ChildrenE<C>, { _tag: Tag }> | FE, ChildrenR<C> | FR>
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The matched tag is removed from the output `E` union.
|
|
255
|
+
|
|
256
|
+
#### `Boundary.catchTags`
|
|
257
|
+
|
|
258
|
+
Catches multiple tags in one call. The handlers record IS the first argument (no wrapping object). Unregistered tags re-raise.
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
Boundary.catchTags<C, Handlers>(
|
|
262
|
+
handlers: {
|
|
263
|
+
[Tag in ChildrenE<C>["_tag"]]?: (e: Extract<ChildrenE<C>, { _tag: Tag }>) => Node<any, any>
|
|
264
|
+
},
|
|
265
|
+
children: C,
|
|
266
|
+
): Node<UnhandledE | HandlersE, ChildrenR<C> | HandlersR>
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
#### `Boundary.catchSome`
|
|
270
|
+
|
|
271
|
+
The fallback returns `Option<Node>`. `Option.none()` re-raises the error; `Option.some(node)` catches it.
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
Boundary.catchSome<C, FE, FR>(
|
|
275
|
+
props: { fallback: (e: ChildrenE<C>) => Option.Option<Node<FE, FR>> },
|
|
276
|
+
children: C,
|
|
277
|
+
): Node<ChildrenE<C> | FE, ChildrenR<C> | FR>
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
The children's `E` is preserved in the output because the boundary may or may not handle any given error.
|
|
281
|
+
|
|
282
|
+
#### `Boundary.catchIf`
|
|
283
|
+
|
|
284
|
+
A predicate gates the fallback. `false` re-raises.
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
Boundary.catchIf<C, FE, FR>(
|
|
288
|
+
props: {
|
|
289
|
+
predicate: (e: ChildrenE<C>) => boolean;
|
|
290
|
+
fallback: (e: ChildrenE<C>) => Node<FE, FR>;
|
|
291
|
+
},
|
|
292
|
+
children: C,
|
|
293
|
+
): Node<ChildrenE<C> | FE, ChildrenR<C> | FR>
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
#### Re-raise and nesting
|
|
297
|
+
|
|
298
|
+
When a boundary's `match` returns `null` (unmatched error), the error propagates to the nearest **parent** `Boundary` via `BoundaryContext`. If there is no parent boundary, the error fails the enclosing mount.
|
|
299
|
+
|
|
300
|
+
Inner boundaries shadow outer ones for their subtree — the innermost boundary is always tried first.
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// Inner catches FooError; BarError propagates to outer
|
|
304
|
+
Boundary.catchAll({ fallback: (e) => h.div(`Outer: ${e.message}`) }, [
|
|
305
|
+
Boundary.catchTag({ tag: "Foo", fallback: (e) => h.span(`Foo: ${e.msg}`) }, [
|
|
306
|
+
ChildWithFooOrBarError(),
|
|
307
|
+
]),
|
|
308
|
+
]);
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## ServerTag
|
|
314
|
+
|
|
315
|
+
A `Context.Tag` whose identifier is branded server-only. Use it exactly like `Context.Tag` for services that must only ever be provided on the server — e.g. a database handle read inside an rpc handler Layer. The brand also guards [`Boundary.rpc`](#boundaryrpc): a server-only tag accidentally referenced in `render` stays in the requirement channel, where `hydrate`'s `AssertNoServerOnly` rejects it at compile time.
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
import { ServerTag } from "@weftui/core";
|
|
319
|
+
import { Effect } from "effect";
|
|
320
|
+
|
|
321
|
+
class Database extends ServerTag("Database")<
|
|
322
|
+
Database,
|
|
323
|
+
{ readonly getProduct: () => Effect.Effect<Product> }
|
|
324
|
+
>() {}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
- The server-only brand rides along in the requirement channel `R` of any effect that uses the tag.
|
|
328
|
+
- An rpc handler Layer (`group.toLayer(...)`) discharges it on the server, where it is provided; it never enters a [`Boundary.rpc`](#boundaryrpc)'s output `R`, since `render` only reads the decoded result.
|
|
329
|
+
- If a branded tag ever reaches client code — referenced in `render` and surviving into `hydrate`'s requirement channel — `AssertNoServerOnly` resolves `R` to a compile-error sentinel (`ServerOnlyLeak`) at the `hydrate` call site, rather than failing silently at runtime.
|
|
330
|
+
|
|
331
|
+
`ServerOnly`, `ServerOnlyLeak`, and `AssertNoServerOnly<R>` are exported alongside `ServerTag` for advanced typing; most code only needs `ServerTag` itself.
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## Keyed lists
|
|
336
|
+
|
|
337
|
+
### `List` namespace
|
|
338
|
+
|
|
339
|
+
The keyed-list combinator. It is the opt-in alternative to wholesale child rebuilds: items are rendered **once per key** and reconciled across emissions, so reordering, inserting, or removing items reuses and moves existing DOM rather than rebuilding the region.
|
|
340
|
+
|
|
341
|
+
> **Note:** This exported `List` namespace is the built-in, key-reconciling way to
|
|
342
|
+
> render collections — prefer it over hand-rolling a component that maps items into
|
|
343
|
+
> elements.
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
import { h, List } from "@weftui/core";
|
|
347
|
+
|
|
348
|
+
h.ul([List.each({ of: rows.changes, by: (row) => row.id }, (row) => h.li(row.name))]);
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
#### `List.each`
|
|
352
|
+
|
|
353
|
+
Declares a keyed reactive list region.
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
List.each<S extends Source.Source<Iterable<any>, any, any>, CE, CR, K>(
|
|
357
|
+
options: List.Options<S, K>,
|
|
358
|
+
render: (item: ItemOf<S>, index: number) => Node<CE, CR>,
|
|
359
|
+
): Node<Source.Error<S> | CE, Source.Context<S> | CR>
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
`render` runs **once per key**; a persisted key keeps its DOM nodes and its running subscription fibers across re-emits (it is never re-invoked). The returned node's `E`/`R` are the union of the source channels and the channels of the node `render` returns.
|
|
363
|
+
|
|
364
|
+
> **⚠️ Render-once / index-key footgun:** because `render` runs exactly once per key, reconciliation never refreshes a kept row's content — refresh a row by threading a `Stream` **inside** it, not by re-running `render`. Keying by index (`by: (_, i) => i`) reuses rows positionally and will show stale content after a reorder; prefer a stable identity key (`by: (item) => item.id`).
|
|
365
|
+
|
|
366
|
+
**`List.Options<S, K>`**
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
interface List.Options<S, K> {
|
|
370
|
+
readonly of: S; // static Iterable<T>, or an Effect/Stream/Subscribable of one
|
|
371
|
+
readonly by?: (item: ItemOf<S>, index: number) => K; // key projection; compared via Effect Equal / Hash
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
- **`of`** — the list source. Each emission is materialized to an array to fix order, then reconciled by key.
|
|
376
|
+
- **`by`** — projects each item to its reconciliation key. Omitted ⇒ the item itself is the key (structural for `Data`, by reference otherwise).
|
|
377
|
+
|
|
378
|
+
#### `List.Error<N>` and `List.Context<N>`
|
|
379
|
+
|
|
380
|
+
Type-level accessors that extract the `E` and `R` channels from a list `Node`. Re-exported from the canonical `Node.Error` / `Node.Context` accessors.
|
|
381
|
+
|
|
382
|
+
See the `examples/keyed-list` example for a full reconciliation walkthrough (focus, uncontrolled inputs, and per-row counters surviving reorders).
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Types
|
|
387
|
+
|
|
388
|
+
### `Node<E, R>`
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
type Node<E = never, R = never> = Effect.Effect<ElementDescriptor, E, R>;
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
The core tree type. Every element builder and component returns a `Node`. Because `Node` is an alias for `Effect.Effect`, all Effect operators work on nodes directly.
|
|
395
|
+
|
|
396
|
+
### `Source` namespace
|
|
397
|
+
|
|
398
|
+
The `Source` namespace contains the reactive prop vocabulary type and its normalization utility:
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
import { Source } from "@weftui/core";
|
|
402
|
+
|
|
403
|
+
// The type union
|
|
404
|
+
type Source.Source<A, E, R> = A | Effect.Effect<A, E, R> | Stream.Stream<A, E, R> | Subscribable<A, E, R>
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Any prop or child that supports reactivity accepts a `Source.Source`. Static values, Effects, Streams, and Subscribables are all valid.
|
|
408
|
+
|
|
409
|
+
#### `Source.toSubscribable(source, key?)`
|
|
410
|
+
|
|
411
|
+
Normalizes any `Source.Source` into a hot `Subscribable<A, E | NoPropValue, R>` scoped to the enclosing `Scope`:
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
Source.toSubscribable<A, E, R>(
|
|
415
|
+
source: Source.Source<A, E, R>,
|
|
416
|
+
key?: string
|
|
417
|
+
): Effect.Effect<Subscribable<A, E | NoPropValue, R>, never, Scope>
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Normalization rules:
|
|
421
|
+
|
|
422
|
+
- **`Subscribable`** → returned by reference, no new ref or fiber
|
|
423
|
+
- **Static value** → `get` succeeds immediately; `changes` emits once
|
|
424
|
+
- **`Effect`** → memoized via `Effect.cached`; `changes` emits the resolved value once
|
|
425
|
+
- **`Stream`** → forks a scoped pump fiber that drains into a `SubscriptionRef`; `get` awaits the first emission
|
|
426
|
+
|
|
427
|
+
The pump fiber is tied to the enclosing scope via `Effect.forkScoped` — it terminates when the scope closes.
|
|
428
|
+
|
|
429
|
+
### `NoPropValue`
|
|
430
|
+
|
|
431
|
+
Tagged error raised when a `Stream` prop ends before emitting a value:
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
class NoPropValue extends Data.TaggedError("NoPropValue")<{
|
|
435
|
+
readonly key?: string;
|
|
436
|
+
}> {}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
The `key` field identifies which prop triggered the error when provided.
|
|
440
|
+
|
|
441
|
+
### `PropsE<P>` and `PropsR<P>`
|
|
442
|
+
|
|
443
|
+
Type-level utilities that extract the `E` and `R` channels from a props object:
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
type PropsE<P> = { [K in keyof P]: P[K] extends Stream.Stream<any, infer E, any> ? E : ... }[keyof P]
|
|
447
|
+
type PropsR<P> = { [K in keyof P]: P[K] extends Stream.Stream<any, any, infer R> ? R : ... }[keyof P]
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
These are used internally by `h` and `Component` to accumulate channels from props. You generally don't need to reference them directly unless building utilities over the combinator API.
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## Constants
|
|
455
|
+
|
|
456
|
+
### `FRAGMENT`
|
|
457
|
+
|
|
458
|
+
Internal brand used to mark fragment nodes. Not intended for direct use — use `h.fragment` instead.
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## Utility functions
|
|
463
|
+
|
|
464
|
+
### `isStream(value)`
|
|
465
|
+
|
|
466
|
+
Returns `true` if `value` is a `Stream.Stream`:
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
isStream(value: unknown): value is Stream.Stream<unknown, unknown, unknown>
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### `toStream(value)`
|
|
473
|
+
|
|
474
|
+
Normalizes a static value, `Effect`, or `Stream` into a `Stream`:
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
toStream<A>(value: A | Effect.Effect<A> | Stream.Stream<A>): Stream.Stream<A>
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
- Static value → `Stream.make(value)` (single emission)
|
|
481
|
+
- `Effect` → `Stream.fromEffect(effect)` (one-shot)
|
|
482
|
+
- `Stream` → returned as-is
|
|
483
|
+
|
|
484
|
+
### `isSubscribable(value)`
|
|
485
|
+
|
|
486
|
+
Returns `true` if `value` implements the `Subscribable` interface (keyed off `Subscribable.TypeId`):
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
isSubscribable(value: unknown): value is Subscribable<unknown, unknown, unknown>
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
## See also
|
|
493
|
+
|
|
494
|
+
- [The Combinator API](https://weftui.dev/docs/explanation/combinator-api) · [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives) · [Boundaries and Suspense](https://weftui.dev/docs/explanation/boundaries-and-suspense) — the concepts behind this surface
|
|
495
|
+
- [Author Components](https://weftui.dev/docs/how-to/author-components) · [Render Keyed Lists](https://weftui.dev/docs/how-to/render-keyed-lists) — task guides that use it
|
|
496
|
+
- [`@weftui/dom` reference](https://weftui.dev/docs/reference/dom) · [`@weftui/router` reference](https://weftui.dev/docs/reference/router)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "@weftui/dom"
|
|
3
|
+
order: 2
|
|
4
|
+
section: reference
|
|
5
|
+
description: Full API surface for @weftui/dom — the client renderer (mount, hydrate) and the server renderer (renderToString and streaming variants).
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# @weftui/dom API Reference
|
|
9
|
+
|
|
10
|
+
The DOM renderer for Weft. It has two entry points — `@weftui/dom/client` for the
|
|
11
|
+
browser and `@weftui/dom/server` for Node — plus a package root that re-exports the
|
|
12
|
+
renderer error types. See the [Server-Side Rendering guide](https://weftui.dev/docs/how-to/render-on-the-server)
|
|
13
|
+
for a narrative walkthrough.
|
|
14
|
+
|
|
15
|
+
## `@weftui/dom/client`
|
|
16
|
+
|
|
17
|
+
### `mount`
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
mount(node: Renderable, target: Element): Effect<MountHandle, RenderError, R>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Renders a Weft `node` into `target` for a fresh (non-SSR) page, building real DOM
|
|
24
|
+
and starting every reactive stream. Returns a `MountHandle` whose scope owns the
|
|
25
|
+
mounted tree; closing it tears the tree down. Use `mount` for purely client-rendered
|
|
26
|
+
apps; use `hydrate` when the markup already exists from SSR.
|
|
27
|
+
|
|
28
|
+
### `hydrate`
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
hydrate(node: Renderable, target: Element): Effect<MountHandle, HydrationMismatchError | RenderError, R>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Adopts server-rendered DOM **in place** inside `target` and resumes reactivity
|
|
35
|
+
without re-creating elements. The `node` must produce a tree structurally identical
|
|
36
|
+
to what the server rendered; a divergence fails with `HydrationMismatchError`. This
|
|
37
|
+
is the flash-free path: no second render, the existing nodes simply become live.
|
|
38
|
+
|
|
39
|
+
### `mountScoped`
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
mountScoped(app: Renderable, root: HTMLElement): Effect<MountHandle, UnsupportedNodeTypeError | StreamSubscriptionError | RenderError, Scope.Scope>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Scope-aware `mount`: identical behavior, but requires an ambient `Scope.Scope` in
|
|
46
|
+
`R` and registers `unmount` as a finalizer on it, so the mount lives until that
|
|
47
|
+
scope closes rather than only until the mount effect resolves. Provide any scoped
|
|
48
|
+
layer **outside** a long-lived scoped region so it outlives initial render — see
|
|
49
|
+
[Provide Services](https://weftui.dev/docs/how-to/provide-services) for the composition and
|
|
50
|
+
[Layer lifetime at the mount](https://weftui.dev/docs/explanation/services-and-context#layer-lifetime-at-the-mount)
|
|
51
|
+
for why.
|
|
52
|
+
|
|
53
|
+
### `hydrateScoped`
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
hydrateScoped(app: Renderable, root: HTMLElement): Effect<MountHandle, UnsupportedNodeTypeError | StreamSubscriptionError | RenderError | HydrationMismatchError, Scope.Scope>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Scope-aware `hydrate` — same relationship as `mountScoped` to `mount`, with
|
|
60
|
+
`hydrate`'s error union (`HydrationMismatchError` added) and the same client-only
|
|
61
|
+
compile-time guard: a server-only requirement left in `app`'s `R` degrades the
|
|
62
|
+
return type to `ServerOnlyLeak` via `AssertNoServerOnly`.
|
|
63
|
+
|
|
64
|
+
### `MountHandle`
|
|
65
|
+
|
|
66
|
+
The handle returned by `mount`, `hydrate`, `mountScoped`, and `hydrateScoped`. Its
|
|
67
|
+
`unmount()` interrupts every subscription and event handler and disposes the
|
|
68
|
+
mount's `ManagedRuntime`; it does **not** remove the mounted DOM nodes from `root`.
|
|
69
|
+
`unmount` is idempotent — safe to call more than once, including once
|
|
70
|
+
automatically and once explicitly.
|
|
71
|
+
|
|
72
|
+
The runtime backing the handle lives until `unmount` runs, not until the
|
|
73
|
+
`mount`/`hydrate` effect resolves — that effect completes right after initial
|
|
74
|
+
render, while streams and handlers keep running in the background. If `mount` or
|
|
75
|
+
`hydrate` runs inside a region that supplies an ambient `Scope.Scope` (e.g. under
|
|
76
|
+
`Effect.scoped`), `unmount` is auto-registered on that scope as a finalizer, so the
|
|
77
|
+
mount tears down when the scope closes; with no ambient scope, behavior is
|
|
78
|
+
unchanged and `unmount` must be called explicitly. `mountScoped`/`hydrateScoped`
|
|
79
|
+
register the same finalizer explicitly, so the typed variant does not silently
|
|
80
|
+
depend on this auto-registration.
|
|
81
|
+
|
|
82
|
+
## `@weftui/dom/server`
|
|
83
|
+
|
|
84
|
+
### `renderToString`
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
renderToString(node: Renderable): Effect<string, Error, AppRpcClientTag>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Renders `node` to a complete HTML string. Use for static, non-hydrated output.
|
|
91
|
+
|
|
92
|
+
### `renderToStringHydratable`
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
renderToStringHydratable(node: Renderable): Effect<string, Error, AppRpcClientTag>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Like `renderToString`, but embeds the hydration markers and inline boundary data
|
|
99
|
+
that `hydrate` needs on the client. Pair this with `hydrate`.
|
|
100
|
+
|
|
101
|
+
### `renderToStream` / `renderToStreamHydratable`
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
renderToStream(node: Renderable): Stream<string, Error, AppRpcClientTag>
|
|
105
|
+
renderToStreamHydratable(node: Renderable): Stream<string, Error, AppRpcClientTag>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Streaming variants that emit HTML chunks as the tree resolves, so the browser can
|
|
109
|
+
start painting before the whole page is ready. The `Hydratable` variant includes the
|
|
110
|
+
hydration markers. These back streaming SSR and suspense.
|
|
111
|
+
|
|
112
|
+
### `renderToHydratableShell`
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
renderToHydratableShell(node: Renderable): Effect<HydratableShell, Error, R>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Produces a `HydratableShell` — the document scaffold around the app — for servers
|
|
119
|
+
that assemble the response shell separately from the streamed body.
|
|
120
|
+
|
|
121
|
+
### Suspense failure handling
|
|
122
|
+
|
|
123
|
+
`SuspenseFailureHandlerTag` is the service tag for a `SuspenseFailureHandler`, which
|
|
124
|
+
maps a failed suspense boundary to a `SuspenseFailureSubstitute` (fallback markup)
|
|
125
|
+
during streaming SSR.
|
|
126
|
+
|
|
127
|
+
## Package root (`@weftui/dom`)
|
|
128
|
+
|
|
129
|
+
Re-exports the renderer error types:
|
|
130
|
+
|
|
131
|
+
- `HydrationMismatchError` — the client tree did not match the server markup.
|
|
132
|
+
- `UnsupportedNodeTypeError` — a node type the renderer cannot handle was encountered.
|
|
133
|
+
- `RenderError` — a general rendering failure.
|
|
134
|
+
- `StreamSubscriptionError` — a reactive stream backing the tree failed to subscribe.
|
|
135
|
+
|
|
136
|
+
## See also
|
|
137
|
+
|
|
138
|
+
- [Render on the Server](https://weftui.dev/docs/how-to/render-on-the-server) — a narrative walkthrough of the server/client split
|
|
139
|
+
- [Provide Services](https://weftui.dev/docs/how-to/provide-services) — recipes for value layers, `mountScoped`, and `ManagedRuntime`
|
|
140
|
+
- [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model) — hydrate-in-place and why there is no virtual DOM
|
|
141
|
+
- [Services and Context](https://weftui.dev/docs/explanation/services-and-context#layer-lifetime-at-the-mount) — why scoped layers need the mount to outlive initial render
|
|
142
|
+
- [`@weftui/core` reference](https://weftui.dev/docs/reference/core) · [`@weftui/router` reference](https://weftui.dev/docs/reference/router)
|