@weftui/core 0.28.0 → 0.29.0
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 +8 -8
- package/dist/{index-Bsh2WLtx.d.ts → index-4cTlhojA.d.ts} +45 -31
- package/dist/index.d.ts +68 -119
- package/dist/types/index.d.ts +1 -1
- package/docs/explanation/boundaries-and-suspense.md +37 -18
- package/docs/explanation/combinator-api.md +14 -12
- package/docs/explanation/reactive-primitives.md +14 -14
- package/docs/explanation/rendering-model.md +20 -18
- package/docs/explanation/services-and-context.md +35 -21
- package/docs/how-to/add-routing.md +72 -48
- package/docs/how-to/author-components.md +40 -28
- package/docs/how-to/compose-behavior-and-markup.md +144 -0
- package/docs/how-to/handle-forms.md +6 -6
- package/docs/how-to/load-async-data.md +15 -13
- package/docs/how-to/load-data-with-rpc.md +30 -28
- package/docs/how-to/provide-services.md +20 -18
- package/docs/how-to/render-keyed-lists.md +10 -8
- package/docs/how-to/render-on-the-server.md +16 -12
- package/docs/how-to/show-navigation-progress.md +10 -8
- package/docs/how-to/split-routes-lazily.md +16 -14
- package/docs/how-to/style-reactively.md +13 -13
- package/docs/how-to/use-element-refs.md +10 -8
- package/docs/index.md +20 -18
- package/docs/reference/core.md +44 -40
- package/docs/reference/dom.md +274 -58
- package/docs/reference/router.md +69 -47
- package/docs/tutorial/01-your-first-app.md +7 -9
- package/docs/tutorial/02-reactivity.md +8 -6
- package/docs/tutorial/03-services-and-async.md +10 -6
- package/docs/tutorial/04-errors-and-server.md +14 -5
- package/package.json +2 -2
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
title: Provide Services
|
|
3
3
|
order: 12
|
|
4
4
|
section: how-to
|
|
5
|
-
description: Provide plain and scoped Layers to a WeftApp
|
|
5
|
+
description: "Provide plain and scoped Layers to a WeftApp: app layers for the common case, scoped layers that just work, memoMap sharing, and binding an app's lifetime to a scope."
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Provide Services
|
|
9
9
|
|
|
10
10
|
**Goal:** provide a `Layer` to a `WeftApp` so its components can read services with `yield* Service`.
|
|
11
11
|
|
|
12
|
-
## Recipe 1
|
|
12
|
+
## Recipe 1: app layers
|
|
13
13
|
|
|
14
|
-
Pass the layer to `WeftApp.make`. This is the common case and needs nothing else
|
|
14
|
+
Pass the layer to `WeftApp.make`. This is the common case and needs nothing else. The layer builds lazily on first mount, and every component, event handler, and stream subscription in every root mounted from `app` can read it.
|
|
15
15
|
|
|
16
16
|
```typescript
|
|
17
17
|
import { WeftApp } from "@weftui/dom/client";
|
|
@@ -25,11 +25,13 @@ const app = WeftApp.make(ThemeServiceLive);
|
|
|
25
25
|
void Effect.runPromise(WeftApp.mount(app, App(), root));
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
## Recipe 2
|
|
28
|
+
## Recipe 2: scoped layers just work
|
|
29
29
|
|
|
30
|
-
A **scoped** layer
|
|
30
|
+
A **scoped** layer (`Layer.effect` backed by `acquireRelease`, or anything else that owns a subscription, listener, or registry) needs nothing different from Recipe 1. The app owns one lazy `ManagedRuntime`. The layer builds on first mount and releases only at `WeftApp.dispose(app)`, not when any individual mount's render effect resolves.
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
There is no `mountScoped`, no `Effect.never`, no manual scope threading.
|
|
33
|
+
|
|
34
|
+
`AtomRegistry.layer` (from `effect/unstable/reactivity`) is a real scoped layer. Its atom subscriptions are fibers forked for the app's whole lifetime:
|
|
33
35
|
|
|
34
36
|
```typescript
|
|
35
37
|
import { WeftApp } from "@weftui/dom/client";
|
|
@@ -41,7 +43,7 @@ const app = WeftApp.make(AtomRegistry.layer);
|
|
|
41
43
|
void Effect.runPromise(WeftApp.mount(app, App(), document.getElementById("root")!));
|
|
42
44
|
```
|
|
43
45
|
|
|
44
|
-
`RouterLive` (from `@weftui/router/client`) is another
|
|
46
|
+
`RouterLive` (from `@weftui/router/client`) is another. It owns the `popstate` listener and the same-origin link-click interceptor for as long as the app runs:
|
|
45
47
|
|
|
46
48
|
```typescript
|
|
47
49
|
const app = WeftApp.make(RouterLive(App, { rpc: { group: StockRpcs } }));
|
|
@@ -50,9 +52,9 @@ void Effect.runPromise(WeftApp.hydrate(app, RouterApp(App), root));
|
|
|
50
52
|
|
|
51
53
|
Both examples are runnable in full at [examples/effect-atom](https://github.com/stefvw93/weft/tree/main/examples/effect-atom) and [examples/router-ssr](https://github.com/stefvw93/weft/tree/main/examples/router-ssr).
|
|
52
54
|
|
|
53
|
-
## Recipe 3
|
|
55
|
+
## Recipe 3: sharing layer memoization with `memoMap`
|
|
54
56
|
|
|
55
|
-
`WeftApp.make(layer, { memoMap })` accepts an explicit `Layer.MemoMap`, so multiple `WeftApp` instances can share layer construction
|
|
57
|
+
`WeftApp.make(layer, { memoMap })` accepts an explicit `Layer.MemoMap`, so multiple `WeftApp` instances can share layer construction. For example, build one app per test case while reusing an expensive shared dependency's memoized build across them:
|
|
56
58
|
|
|
57
59
|
```typescript
|
|
58
60
|
import { WeftApp } from "@weftui/dom/client";
|
|
@@ -66,9 +68,9 @@ const appB = WeftApp.make(SharedLive, { memoMap });
|
|
|
66
68
|
|
|
67
69
|
Most apps have exactly one `WeftApp` and never need this option.
|
|
68
70
|
|
|
69
|
-
## Recipe 4
|
|
71
|
+
## Recipe 4: binding an app's lifetime to a scope
|
|
70
72
|
|
|
71
|
-
There is deliberately no `makeScoped`. To tie an app's disposal to a `Scope` you already manage
|
|
73
|
+
There is deliberately no `makeScoped`. To tie an app's disposal to a `Scope` you already manage (a framework integration or a test harness that owns one), compose it yourself with `Effect.acquireRelease`:
|
|
72
74
|
|
|
73
75
|
```typescript
|
|
74
76
|
import { Effect } from "effect";
|
|
@@ -81,22 +83,22 @@ const acquireApp = Effect.acquireRelease(
|
|
|
81
83
|
);
|
|
82
84
|
```
|
|
83
85
|
|
|
84
|
-
`acquireApp` yields a `WeftApp` and registers `WeftApp.dispose` as a finalizer on whatever scope the surrounding effect runs in
|
|
86
|
+
`acquireApp` yields a `WeftApp` and registers `WeftApp.dispose` as a finalizer on whatever scope the surrounding effect runs in. Closing that scope tears the app down the same way `WeftApp.dispose` normally would (roots, then layers, then the error hub).
|
|
85
87
|
|
|
86
88
|
## Anti-pattern: `Effect.provide` around the mount call
|
|
87
89
|
|
|
88
90
|
```typescript
|
|
89
91
|
// ❌ does nothing useful: WeftApp.mount's R is always `never`, and services
|
|
90
|
-
// come exclusively from the app layer
|
|
92
|
+
// come exclusively from the app layer: a wrapped Effect.provide never
|
|
91
93
|
// reaches components, handlers, or stream subscriptions
|
|
92
94
|
Effect.runPromise(pipe(WeftApp.mount(app, App(), root), Effect.provide(SomeLayer)));
|
|
93
95
|
```
|
|
94
96
|
|
|
95
|
-
`WeftApp.mount`/`WeftApp.hydrate` return an effect whose requirement channel is always `never
|
|
97
|
+
`WeftApp.mount`/`WeftApp.hydrate` return an effect whose requirement channel is always `never`, so there is no `R` left for `Effect.provide` to discharge. Any service a component needs must be in the layer passed to `WeftApp.make`.
|
|
96
98
|
|
|
97
99
|
## See also
|
|
98
100
|
|
|
99
|
-
- [Services and Context](https://weftui.dev/docs/explanation/services-and-context)
|
|
100
|
-
- [`WeftApp` reference](https://weftui.dev/docs/reference/dom)
|
|
101
|
-
- [examples/effect-atom](https://github.com/stefvw93/weft/tree/main/examples/effect-atom)
|
|
102
|
-
- [examples/shared-state-islands](https://github.com/stefvw93/weft/tree/main/examples/shared-state-islands)
|
|
101
|
+
- [Services and Context](https://weftui.dev/docs/explanation/services-and-context): how `R` accumulates and discharges at `WeftApp.make`, and why scoped layers no longer need special handling
|
|
102
|
+
- [`WeftApp` reference](https://weftui.dev/docs/reference/dom): full signatures for `make`, `mount`, `hydrate`, `dispose`
|
|
103
|
+
- [examples/effect-atom](https://github.com/stefvw93/weft/tree/main/examples/effect-atom): a real scoped layer (`AtomRegistry.layer`)
|
|
104
|
+
- [examples/shared-state-islands](https://github.com/stefvw93/weft/tree/main/examples/shared-state-islands): one app layer shared by reference across multiple mounted roots
|
|
@@ -9,7 +9,7 @@ description: Render a reactive collection with List.each so reordering, insertin
|
|
|
9
9
|
|
|
10
10
|
**Goal:** render a list whose items reorder, insert, or remove over time, without rebuilding the whole region (which would lose focus, scroll, and input state in the surviving rows).
|
|
11
11
|
|
|
12
|
-
Use [`List.each`](https://weftui.dev/docs/reference/core#listeach), the keyed-list combinator. It renders each item **once per key** and reconciles across emissions
|
|
12
|
+
Use [`List.each`](https://weftui.dev/docs/reference/core#listeach), the keyed-list combinator. It renders each item **once per key** and reconciles across emissions. A reorder _moves_ existing DOM nodes, an insert adds one, a remove drops one, and untouched rows are left entirely alone.
|
|
13
13
|
|
|
14
14
|
```typescript
|
|
15
15
|
import { h, List } from "@weftui/core";
|
|
@@ -25,16 +25,18 @@ h.ul([
|
|
|
25
25
|
]);
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
- **`of`**
|
|
29
|
-
- **`by`**
|
|
28
|
+
- **`of`** is the list source: any `Stream`, `Effect`, or `Subscribable` of an `Iterable`. Each emission is materialized to an array to fix order, then reconciled by key.
|
|
29
|
+
- **`by`** projects each item to its reconciliation key, compared via Effect's `Equal`/`Hash`. Omit it and the item itself is the key (structural for `Data`, by reference otherwise).
|
|
30
30
|
|
|
31
31
|
## Why not `map`?
|
|
32
32
|
|
|
33
|
-
Mapping items by hand
|
|
33
|
+
Mapping items by hand (`Stream.map(rows.changes, (rs) => rs.map(r => h.li(r.name)))`) produces a **new children array on every emission**. The renderer then rebuilds the whole region: every row's DOM node is recreated even if only one item moved.
|
|
34
|
+
|
|
35
|
+
`List.each` reconciles by key instead, so DOM identity (and the focus/scroll/typed-input state attached to it) survives across updates.
|
|
34
36
|
|
|
35
37
|
## Refresh a row's content
|
|
36
38
|
|
|
37
|
-
Because `render` runs **exactly once per key**, reconciliation never re-runs it for a kept row
|
|
39
|
+
Because `render` runs **exactly once per key**, reconciliation never re-runs it for a kept row, so it never refreshes that row's content on its own. To make a row's content reactive, thread a `Stream` **inside** the row rather than expecting a re-render:
|
|
38
40
|
|
|
39
41
|
```typescript
|
|
40
42
|
List.each({ of: rows.changes, by: (row) => row.id }, (row) =>
|
|
@@ -46,6 +48,6 @@ List.each({ of: rows.changes, by: (row) => row.id }, (row) =>
|
|
|
46
48
|
|
|
47
49
|
## See also
|
|
48
50
|
|
|
49
|
-
- [`List.each` API reference](https://weftui.dev/docs/reference/core#listeach)
|
|
50
|
-
- [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives)
|
|
51
|
-
- [examples/keyed-list](https://github.com/stefvw93/weft/tree/main/examples/keyed-list)
|
|
51
|
+
- [`List.each` API reference](https://weftui.dev/docs/reference/core#listeach): full signature, `List.Options`, and the descriptor shape
|
|
52
|
+
- [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives): the stream-shaped sources `of` accepts
|
|
53
|
+
- [examples/keyed-list](https://github.com/stefvw93/weft/tree/main/examples/keyed-list): a runnable keyed list with reordering and a browser test
|
|
@@ -7,12 +7,14 @@ description: renderToString / renderToStringHydratable / streaming variants, hyd
|
|
|
7
7
|
|
|
8
8
|
# Server-Side Rendering
|
|
9
9
|
|
|
10
|
-
Weft renders on the server and **hydrates** on the client
|
|
10
|
+
Weft renders on the server and **hydrates** on the client. The server produces HTML plus inline data, and the browser adopts that existing DOM in place rather than re-creating it.
|
|
11
|
+
|
|
12
|
+
[`Boundary.rpc`](https://weftui.dev/docs/reference/core#boundaryrpc) extends this to **rpc-backed server data**: resolve an rpc on the server, serialize its result into the HTML, and replay it on the client without a second request. The region then stays live for refetch.
|
|
11
13
|
|
|
12
14
|
## The two halves
|
|
13
15
|
|
|
14
|
-
- **Server
|
|
15
|
-
- **Client
|
|
16
|
+
- **Server**: `@weftui/dom/server` renders an app node to an HTML string (or stream). The _hydratable_ variants also emit the inline data each reactive region and `Boundary.rpc` needs to resume on the client.
|
|
17
|
+
- **Client**: `@weftui/dom/client`'s `WeftApp.hydrate` walks the server DOM, adopts it, wires up reactivity and event handlers, and resumes from the inline data. It does **not** re-render from scratch.
|
|
16
18
|
|
|
17
19
|
```typescript
|
|
18
20
|
// server entry
|
|
@@ -34,7 +36,7 @@ const app = WeftApp.make();
|
|
|
34
36
|
void Effect.runPromise(WeftApp.hydrate(app, App(), root));
|
|
35
37
|
```
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
Both entries import the same side-effect-free `App`. Splice the server HTML into your template's outlet, ship it, and let the client entry hydrate it.
|
|
38
40
|
|
|
39
41
|
`@weftui/dom/server` exports four renderers:
|
|
40
42
|
|
|
@@ -47,7 +49,9 @@ Use a hydratable renderer whenever the client will call `hydrate`. The plain ren
|
|
|
47
49
|
|
|
48
50
|
## Loading server data with `Boundary.rpc`
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
`Boundary.rpc` resolves an rpc **on the server**, serializes the result into the same HTML this page produces, and replays it on the client during `hydrate`. There is no second request and no fallback flash, and the region stays live for `refetch`.
|
|
53
|
+
|
|
54
|
+
It follows the same server/client split: the rpc **contract** (pure Schema) is shared, while its **handler** lives in a server-only Layer the client never imports.
|
|
51
55
|
|
|
52
56
|
```typescript
|
|
53
57
|
import { Boundary, h } from "@weftui/core";
|
|
@@ -68,20 +72,20 @@ const StockPanel = (productId: number) =>
|
|
|
68
72
|
);
|
|
69
73
|
```
|
|
70
74
|
|
|
71
|
-
Under SSR the server resolves the rpc in-process, `successSchema`-encodes the result inline as `<script type="application/json">`, and renders in place; `hydrate` reads that payload positionally, seeds the `Resource`, and adopts the DOM **without re-calling the rpc** (replay, never retry). The full model
|
|
75
|
+
Under SSR the server resolves the rpc in-process, `successSchema`-encodes the result inline as `<script type="application/json">`, and renders in place; `hydrate` reads that payload positionally, seeds the `Resource`, and adopts the DOM **without re-calling the rpc** (replay, never retry). The full model lives in one place, the [RPC Data Boundaries guide](https://weftui.dev/docs/how-to/load-data-with-rpc): the contract/handler split, router wiring, the four lifecycles, the `Resource` handle, and typed-failure replay. This page does not repeat it.
|
|
72
76
|
|
|
73
77
|
> **Note.** `Boundary.rpc` resolves through the ambient [`AppRpcClientTag`](https://weftui.dev/docs/reference/core#apprpcclienttag) seam, which `@weftui/router` provides on both sides. In a router-less mount there is no seam, so the boundary resolves to a descriptive "needs router/rpc" error (not a defect).
|
|
74
78
|
|
|
75
79
|
## When to use
|
|
76
80
|
|
|
77
|
-
- **`Boundary.rpc
|
|
78
|
-
- **`Boundary.suspend
|
|
81
|
+
- **`Boundary.rpc`**: data that must be resolved on the server (behind a server-only service, credential, or private network) and rendered into the initial HTML, then **refreshable** on the client (refetch / client-first SPA mount) over the same rpc.
|
|
82
|
+
- **`Boundary.suspend`**: async data that loads on the client (or streams the shell then fills); see the [Boundary API](https://weftui.dev/docs/reference/core#boundarysuspend).
|
|
79
83
|
|
|
80
84
|
## See also
|
|
81
85
|
|
|
82
|
-
- [rpc data boundaries guide](https://weftui.dev/docs/how-to/load-data-with-rpc)
|
|
83
|
-
- [Routing](https://weftui.dev/docs/how-to/add-routing)
|
|
86
|
+
- [rpc data boundaries guide](https://weftui.dev/docs/how-to/load-data-with-rpc): the full `Boundary.rpc` walkthrough, covering the contract/handler split, router wiring, the four lifecycles, and typed-failure replay
|
|
87
|
+
- [Routing](https://weftui.dev/docs/how-to/add-routing): `@weftui/router` builds on this SSR + hydration model for full-page nested routing
|
|
84
88
|
- [`Boundary.rpc` API reference](https://weftui.dev/docs/reference/core#boundaryrpc)
|
|
85
89
|
- [`ServerTag` API reference](https://weftui.dev/docs/reference/core#servertag)
|
|
86
|
-
- [examples/router-ssr](https://github.com/stefvw93/weft/tree/main/examples/router-ssr)
|
|
87
|
-
- [examples/ssr-hydration](https://github.com/stefvw93/weft/tree/main/examples/ssr-hydration)
|
|
90
|
+
- [examples/router-ssr](https://github.com/stefvw93/weft/tree/main/examples/router-ssr): a runnable shop with an SSR-replayed, refetchable live-stock `Boundary.rpc`
|
|
91
|
+
- [examples/ssr-hydration](https://github.com/stefvw93/weft/tree/main/examples/ssr-hydration): SSR + hydration without server data loading
|
|
@@ -9,7 +9,9 @@ description: Render a pending indicator (e.g. a top progress bar) during a defer
|
|
|
9
9
|
|
|
10
10
|
**Goal:** show a progress indicator while a [lazy route](https://weftui.dev/docs/how-to/split-routes-lazily) resolves its chunk and data, so a slow network is visible instead of feeling frozen.
|
|
11
11
|
|
|
12
|
-
When you navigate to a route, the router is **deferred-commit
|
|
12
|
+
When you navigate to a route, the router is **deferred-commit**. It resolves the target branch's chunk (if the component is `Router.lazy`) **and the matched leaf's own component effect**, including any data the leaf awaits in its body. Only then does it swap the URL, keeping the previous page mounted for the whole window.
|
|
13
|
+
|
|
14
|
+
That resolve window is exposed as a reactive signal, [`Router.navigating`](https://weftui.dev/docs/reference/router#routernavigating), that you read to render pending UI.
|
|
13
15
|
|
|
14
16
|
```typescript
|
|
15
17
|
import { Component, h } from "@weftui/core";
|
|
@@ -32,7 +34,7 @@ const Shell = Component.gen(function* () {
|
|
|
32
34
|
});
|
|
33
35
|
```
|
|
34
36
|
|
|
35
|
-
Thread the signal into a persistent layout (the outermost `Shell` is ideal, since it never re-renders across navigations), and style the pending class however you like
|
|
37
|
+
Thread the signal into a persistent layout (the outermost `Shell` is ideal, since it never re-renders across navigations), and style the pending class however you like: a top bar, a cursor change, a dimmed outlet.
|
|
36
38
|
|
|
37
39
|
## The signal
|
|
38
40
|
|
|
@@ -44,22 +46,22 @@ type NavState = { readonly _tag: "Idle" } | { readonly _tag: "Navigating"; reado
|
|
|
44
46
|
|
|
45
47
|
Read it two ways, mirroring `Router.params` / `Router.paramsStream`:
|
|
46
48
|
|
|
47
|
-
- `Router.navigating
|
|
48
|
-
- `Router.navigatingStream
|
|
49
|
+
- `Router.navigating`: the `Subscribable<NavState>` on the `Router` service.
|
|
50
|
+
- `Router.navigatingStream`: an `Effect` resolving that `Subscribable`, for use in a `Component.gen` body (as above).
|
|
49
51
|
|
|
50
52
|
The `to` field on `Navigating` is the target URL, if you want to label _where_ the app is going.
|
|
51
53
|
|
|
52
54
|
## Behavior to expect
|
|
53
55
|
|
|
54
|
-
- **Only navigations with real async work flip it.** A branch with no `Router.lazy` node and a leaf whose effect resolves synchronously (no async work, or a memoized revisit) commits in the same tick, and `navigating` stays `Idle
|
|
56
|
+
- **Only navigations with real async work flip it.** A branch with no `Router.lazy` node and a leaf whose effect resolves synchronously (no async work, or a memoized revisit) commits in the same tick, and `navigating` stays `Idle`. An entirely eager app never sees `Navigating`, and adding the reader costs nothing.
|
|
55
57
|
- **Latest-wins.** Rapid successive navigations commit only the newest; a superseded navigation never resets the signal (the newer one owns it).
|
|
56
58
|
- **Back/forward.** `popstate` into a route with async work also resolves before committing, so the indicator shows for browser back/forward too.
|
|
57
59
|
- **Failure resets it.** A rejected chunk load or a failing leaf pre-run (a typed error such as `notFound()`, or a defect) resets `navigating` to `Idle` (it never sticks on), then surfaces through normal error/defect handling.
|
|
58
60
|
- **Server renders `Idle`.** Server render is buffered, so `navigating` is a client-only concern; the server supplies a constant `Idle` so the same `Shell` type-checks and renders on both sides.
|
|
59
|
-
- **No built-in anti-flash delay.** The signal flips as soon as an async window opens, so a borderline-fast navigation can flash the indicator briefly. If you want to only show it past a threshold, delay the reveal in CSS rather than in the signal
|
|
61
|
+
- **No built-in anti-flash delay.** The signal flips as soon as an async window opens, so a borderline-fast navigation can flash the indicator briefly. If you want to only show it past a threshold, delay the reveal in CSS rather than in the signal (e.g. `transition-delay: 200ms` on `.is-navigating`), so genuinely fast navigations never flicker.
|
|
60
62
|
|
|
61
63
|
## See also
|
|
62
64
|
|
|
63
65
|
- [`Router.navigating` API reference](https://weftui.dev/docs/reference/router#routernavigating)
|
|
64
|
-
- [Split Routes Lazily](https://weftui.dev/docs/how-to/split-routes-lazily)
|
|
65
|
-
- [examples/router-ssr](https://github.com/stefvw93/weft/tree/main/examples/router-ssr)
|
|
66
|
+
- [Split Routes Lazily](https://weftui.dev/docs/how-to/split-routes-lazily): the `Router.lazy` deferred-commit navigation this reports on
|
|
67
|
+
- [examples/router-ssr](https://github.com/stefvw93/weft/tree/main/examples/router-ssr): wires this exact progress bar in its `Shell` (`components/shell.ts`), with a `pending-navigation.browser.test.ts`
|
|
@@ -9,7 +9,7 @@ description: Code-split a route's component into its own chunk with Router.lazy,
|
|
|
9
9
|
|
|
10
10
|
**Goal:** keep a heavy page's render code (and its dependencies) out of the initial bundle, loading it only when its route is actually rendered.
|
|
11
11
|
|
|
12
|
-
Wrap the route's `component` in [`Router.lazy`](https://weftui.dev/docs/reference/router#routerlazy). The route **descriptor** (its segment and param schemas) stays eager so the matcher, `href`, and the server's dispatch API still see it statically
|
|
12
|
+
Wrap the route's `component` in [`Router.lazy`](https://weftui.dev/docs/reference/router#routerlazy). The route **descriptor** (its segment and param schemas) stays eager, so the matcher, `href`, and the server's dispatch API still see it statically. Only the component body is split into its own chunk.
|
|
13
13
|
|
|
14
14
|
```typescript
|
|
15
15
|
import { Router } from "@weftui/router";
|
|
@@ -21,42 +21,44 @@ Router.route("docs/:category/:slug", {
|
|
|
21
21
|
});
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
The chunk loads on the server during render and on the client on navigation
|
|
24
|
+
The chunk loads on the server during render and on the client on navigation. Only the **matched branch's** chunks are ever fetched.
|
|
25
|
+
|
|
26
|
+
`E`/`R` are preserved: a lazy route has the exact same channels as the same component declared eagerly. An unmet service requirement is still a compile error at `Router.router(...)`.
|
|
25
27
|
|
|
26
28
|
## Make the split real
|
|
27
29
|
|
|
28
|
-
`Router.lazy` only splits if the dynamic `import()` is the **only eager path** to the heavy module. Keep the `Router.route(…)` descriptor in an eagerly-imported file
|
|
30
|
+
`Router.lazy` only splits if the dynamic `import()` is the **only eager path** to the heavy module. Keep the `Router.route(…)` descriptor in an eagerly-imported file. Move the component implementation (and its heavy deps) into a separate module referenced _only_ through `Router.lazy(() => import("./impl"))`:
|
|
29
31
|
|
|
30
32
|
```typescript
|
|
31
|
-
// routes.ts
|
|
33
|
+
// routes.ts: eager and tiny, just the descriptor
|
|
32
34
|
export const docsRoute = Router.route("docs/:category/:slug", {
|
|
33
35
|
path: { category: Schema.String, slug: Schema.String },
|
|
34
36
|
component: Router.lazy(() => import("./doc-page-impl").then((m) => m.DocsPage)),
|
|
35
37
|
});
|
|
36
38
|
|
|
37
|
-
// doc-page-impl.ts
|
|
39
|
+
// doc-page-impl.ts: heavy, pulled into its own chunk, never in the initial graph
|
|
38
40
|
export const DocsPage = Component.gen(function* () {
|
|
39
41
|
/* renderHast, code highlighting, … */
|
|
40
42
|
});
|
|
41
43
|
```
|
|
42
44
|
|
|
43
|
-
A descriptor file that still `import`s the impl statically gains nothing
|
|
45
|
+
A descriptor file that still `import`s the impl statically gains nothing: the bundler keeps it in the initial graph.
|
|
44
46
|
|
|
45
47
|
## What you get for free
|
|
46
48
|
|
|
47
|
-
- **Flash-free hydration.** On a directly-loaded lazy route, the client re-invokes the same slot, awaits the chunk, and adopts the server DOM in place
|
|
48
|
-
- **Blank-free navigation.** Client navigation is **deferred-commit**: the router resolves the target branch's chunk **and the matched leaf's own component effect** _before_ committing the URL
|
|
49
|
+
- **Flash-free hydration.** On a directly-loaded lazy route, the client re-invokes the same slot, awaits the chunk, and adopts the server DOM in place. The first production matches, so nothing is mutated.
|
|
50
|
+
- **Blank-free navigation.** Client navigation is **deferred-commit**: the router resolves the target branch's chunk **and the matched leaf's own component effect** _before_ committing the URL. The previous page stays mounted through the fetch and any data the leaf awaits, and the swap is a single tick. See [Show Navigation Progress](https://weftui.dev/docs/how-to/show-navigation-progress) for the `Router.navigating` signal this exposes.
|
|
49
51
|
- **Synchronous revisits.** `Router.lazy` memoizes its load per slot, so a second visit to a loaded route commits immediately.
|
|
50
52
|
|
|
51
53
|
## Edge cases
|
|
52
54
|
|
|
53
|
-
- **Lazy layouts.** A `Router.layout({ component: Router.lazy(...) })` splits too
|
|
54
|
-
- **Chunk-load failure is a defect.** If the `import()` rejects (offline, or a stale client requesting a chunk a new deploy removed), it dies as a defect and surfaces through normal defect handling
|
|
55
|
-
- **Not a lazy _subtree_.** Only the component is lazy
|
|
55
|
+
- **Lazy layouts.** A `Router.layout({ component: Router.lazy(...) })` splits too. Each lazy node in the matched branch is awaited; nodes outside it never load.
|
|
56
|
+
- **Chunk-load failure is a defect.** If the `import()` rejects (offline, or a stale client requesting a chunk a new deploy removed), it dies as a defect and surfaces through normal defect handling. It never hangs or silently 404s. The rejection is memoized, so the route keeps failing until a reload (the deploy-skew case).
|
|
57
|
+
- **Not a lazy _subtree_.** Only the component is lazy. You cannot defer a whole `RouteNode` behind an `import()`; the matcher needs every leaf's segment and param schema before anything loads.
|
|
56
58
|
|
|
57
59
|
## See also
|
|
58
60
|
|
|
59
61
|
- [`Router.lazy` API reference](https://weftui.dev/docs/reference/router#routerlazy)
|
|
60
|
-
- [Show Navigation Progress](https://weftui.dev/docs/how-to/show-navigation-progress)
|
|
61
|
-
- [Add Routing](https://weftui.dev/docs/how-to/add-routing)
|
|
62
|
-
- [examples/router-ssr](https://github.com/stefvw93/weft/tree/main/examples/router-ssr)
|
|
62
|
+
- [Show Navigation Progress](https://weftui.dev/docs/how-to/show-navigation-progress): the deferred-commit `Router.navigating` signal
|
|
63
|
+
- [Add Routing](https://weftui.dev/docs/how-to/add-routing): authoring the route tree `Router.lazy` plugs into
|
|
64
|
+
- [examples/router-ssr](https://github.com/stefvw93/weft/tree/main/examples/router-ssr): includes a `Router.lazy` page (`lazy-page.ts`) with a browser test
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
title: Style Reactively
|
|
3
3
|
order: 10
|
|
4
4
|
section: how-to
|
|
5
|
-
description: Drive inline styles from streams
|
|
5
|
+
description: Drive inline styles from streams (a single property, or a whole style object) so the DOM updates in place with CSS transitions.
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Style Reactively
|
|
9
9
|
|
|
10
|
-
**Goal:** animate or react to state in an element's inline style without re-rendering
|
|
10
|
+
**Goal:** animate or react to state in an element's inline style without re-rendering. Drive a single CSS property, or a whole style object, from a stream.
|
|
11
11
|
|
|
12
|
-
The `style` prop accepts the [`Source`](https://weftui.dev/docs/explanation/reactive-primitives) vocabulary at any level
|
|
12
|
+
The `style` prop accepts the [`Source`](https://weftui.dev/docs/explanation/reactive-primitives) vocabulary at any level. A property value can be a stream, and you can spread a stream of style objects. CSS `transition` composes naturally, because the renderer mutates the existing node in place.
|
|
13
13
|
|
|
14
14
|
```typescript
|
|
15
15
|
import { h } from "@weftui/core";
|
|
@@ -36,9 +36,9 @@ const AnimatedHue = () => {
|
|
|
36
36
|
|
|
37
37
|
## Three modes
|
|
38
38
|
|
|
39
|
-
1. **A single property as a stream
|
|
40
|
-
2. **A static object
|
|
41
|
-
3. **A whole style object as a stream
|
|
39
|
+
1. **A single property as a stream.** As above: one key's value is a `Stream`, the others are static strings. Each stream property is subscribed independently.
|
|
40
|
+
2. **A static object.** An ordinary `style: { backgroundColor: "#667eea" }` with no streams; nothing updates.
|
|
41
|
+
3. **A whole style object as a stream.** Spread a stream that emits complete style objects, merged with static props:
|
|
42
42
|
|
|
43
43
|
```typescript
|
|
44
44
|
const pulse = Stream.make(1, 0.5).pipe(
|
|
@@ -49,15 +49,15 @@ const pulse = Stream.make(1, 0.5).pipe(
|
|
|
49
49
|
h.div({ style: { ...pulse, transition: "opacity 0.4s ease-in-out" } }, "Pulse");
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
Each emitted object is merged with the static properties on the element.
|
|
53
|
-
|
|
54
52
|
## Notes
|
|
55
53
|
|
|
56
|
-
- **Property names are camelCase** (`backgroundColor`, `boxShadow`)
|
|
57
|
-
- **CSS transitions just work.**
|
|
58
|
-
- **Pace with `Schedule`.** `Stream.iterate`/`Stream.make` paced by `Stream.schedule(Schedule.spaced(…))` and looped with `Stream.forever
|
|
54
|
+
- **Property names are camelCase** (`backgroundColor`, `boxShadow`), the same keys as the DOM `style` object.
|
|
55
|
+
- **CSS transitions just work.** A stream emission patches the DOM node directly (no re-render), so the browser applies the `transition` as it would for any style mutation.
|
|
56
|
+
- **Pace with `Schedule`.** The idiom for time-based style animation: `Stream.iterate`/`Stream.make` paced by `Stream.schedule(Schedule.spaced(…))` and looped with `Stream.forever`. Combine with any Effect timing you like.
|
|
57
|
+
- **Classes have a reactive builder too.** `Props.cx` builds a class string from strings, falsy values, nested arrays, and `{ className: condition }` records, where a condition may be a stream. Merging two bags that both carry `class` concatenates them. See [Compose Behavior and Markup](https://weftui.dev/docs/how-to/compose-behavior-and-markup).
|
|
59
58
|
|
|
60
59
|
## See also
|
|
61
60
|
|
|
62
|
-
- [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives)
|
|
63
|
-
- [
|
|
61
|
+
- [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives): reactive style props and the `Source` vocabulary
|
|
62
|
+
- [Compose Behavior and Markup](https://weftui.dev/docs/how-to/compose-behavior-and-markup): `Props.cx` and merging `class` across two prop bags
|
|
63
|
+
- [examples/reactive-styles](https://github.com/stefvw93/weft/tree/main/examples/reactive-styles): per-property and whole-object stream styles with CSS transitions
|
|
@@ -7,9 +7,9 @@ description: Capture a DOM element with the ref prop into a SubscriptionRef<Opti
|
|
|
7
7
|
|
|
8
8
|
# Use Element Refs
|
|
9
9
|
|
|
10
|
-
**Goal:** get a handle to a real DOM element
|
|
10
|
+
**Goal:** get a handle to a real DOM element, to focus it, measure it, or call an imperative browser API on it.
|
|
11
11
|
|
|
12
|
-
Declare a `SubscriptionRef<Option<HTMLElement
|
|
12
|
+
Declare a `SubscriptionRef<Option<HTMLElement>>` and attach it with the `ref` prop. Then either **react** to the element appearing (a scoped observer on `SubscriptionRef.changes(ref)`) or **read** it later inside a handler.
|
|
13
13
|
|
|
14
14
|
```typescript
|
|
15
15
|
import { h } from "@weftui/core";
|
|
@@ -34,9 +34,9 @@ const AutoFocusInput = () =>
|
|
|
34
34
|
|
|
35
35
|
## How it works
|
|
36
36
|
|
|
37
|
-
- **The `ref` prop** takes a `SubscriptionRef<Option<T>>`. The renderer sets it to `Option.some(element)` **once**, when the element is created
|
|
38
|
-
- **React to mount** by observing `SubscriptionRef.changes(ref)
|
|
39
|
-
- **Use `Effect.forkScoped`, not `Effect.forkChild`.** `forkScoped` ties the observer fiber to the component's **instance scope** (the ambient `Scope` the renderer provides)
|
|
37
|
+
- **The `ref` prop** takes a `SubscriptionRef<Option<T>>`. The renderer sets it to `Option.some(element)` **once**, when the element is created. The ref is therefore an `Option`: `None` until mount, `Some(el)` after.
|
|
38
|
+
- **React to mount** by observing `SubscriptionRef.changes(ref)`. `Stream.filter(Option.isSome)` waits for the element, `Stream.take(1)` takes just the first appearance, and `Stream.runForEach` does the imperative work. This is the equivalent of a mount effect.
|
|
39
|
+
- **Use `Effect.forkScoped`, not `Effect.forkChild`.** `forkScoped` ties the observer fiber to the component's **instance scope** (the ambient `Scope` the renderer provides). It lives as long as the component is mounted. A bare `Effect.forkChild` binds to the transient component-body fiber and is interrupted the instant the generator returns, so the observer would never fire.
|
|
40
40
|
|
|
41
41
|
## Read a ref imperatively
|
|
42
42
|
|
|
@@ -54,10 +54,12 @@ const scroll = () =>
|
|
|
54
54
|
|
|
55
55
|
- A plain `Ref` suffices if you **only** read the element imperatively; use `SubscriptionRef` when you need to **react** to it becoming available.
|
|
56
56
|
- Refs are set once at element creation and are not cleared on unmount.
|
|
57
|
+
- **Several refs can share one element.** `ref` also accepts an array, and every entry receives the element: `h.div({ ref: [measure, focus] })`. `Props.merge` produces such an array when both bags carry a `ref`, so a shared behavior's ref and your own can coexist. See [Compose Behavior and Markup](https://weftui.dev/docs/how-to/compose-behavior-and-markup).
|
|
57
58
|
- Coming from React: `SubscriptionRef.make<Option<T>>(Option.none())` ↔ `useRef<T>(null)`; the `Stream.filter(Option.isSome)` observer ↔ a `useEffect` mount guard.
|
|
58
59
|
|
|
59
60
|
## See also
|
|
60
61
|
|
|
61
|
-
- [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives)
|
|
62
|
-
- [Author Components](https://weftui.dev/docs/how-to/author-components)
|
|
63
|
-
- [
|
|
62
|
+
- [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives): `SubscriptionRef` and `SubscriptionRef.changes`
|
|
63
|
+
- [Author Components](https://weftui.dev/docs/how-to/author-components): instance scope and `Effect.forkScoped`
|
|
64
|
+
- [Compose Behavior and Markup](https://weftui.dev/docs/how-to/compose-behavior-and-markup): merging a shared behavior's `ref` with your own
|
|
65
|
+
- [examples/element-ref](https://github.com/stefvw93/weft/tree/main/examples/element-ref): auto-focus, element measurement, and imperative scroll via refs
|
package/docs/index.md
CHANGED
|
@@ -2,40 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
**Reactive UI, woven from Effect.**
|
|
4
4
|
|
|
5
|
-
Weft is an Effect-native reactive DOM library
|
|
5
|
+
Weft is an Effect-native reactive DOM library, in the browser and on the server. `Node<E, R>` is `Effect.Effect<ElementDescriptor, E, R>`: every element is an Effect. Error and requirement channels accumulate through the tree, all Effect combinators apply to nodes directly, and services flow from mount through the whole app.
|
|
6
|
+
|
|
7
|
+
Streams drive every update; there is no virtual DOM. The same tree renders to HTML on the server and `hydrate()`s in place on the client, flash-free. No JSX.
|
|
6
8
|
|
|
7
9
|
The docs follow the [Diátaxis](https://diataxis.fr) model. Pick your entry point by what you are trying to do:
|
|
8
10
|
|
|
9
11
|
## Start here
|
|
10
12
|
|
|
11
|
-
**[→ Tutorial](https://weftui.dev/docs/tutorial/01-your-first-app)
|
|
13
|
+
**[→ Tutorial](https://weftui.dev/docs/tutorial/01-your-first-app)**: a four-step guided path from a static component to a server-rendered, error-handled app. Start here if you are new to Weft:
|
|
12
14
|
|
|
13
|
-
1. [Your First App](https://weftui.dev/docs/tutorial/01-your-first-app)
|
|
14
|
-
2. [Reactivity](https://weftui.dev/docs/tutorial/02-reactivity)
|
|
15
|
-
3. [Services and Async](https://weftui.dev/docs/tutorial/03-services-and-async)
|
|
16
|
-
4. [Errors and Server Rendering](https://weftui.dev/docs/tutorial/04-errors-and-server)
|
|
15
|
+
1. [Your First App](https://weftui.dev/docs/tutorial/01-your-first-app): `h` and `WeftApp`
|
|
16
|
+
2. [Reactivity](https://weftui.dev/docs/tutorial/02-reactivity): `SubscriptionRef` and streams
|
|
17
|
+
3. [Services and Async](https://weftui.dev/docs/tutorial/03-services-and-async): handlers, services, async loading
|
|
18
|
+
4. [Errors and Server Rendering](https://weftui.dev/docs/tutorial/04-errors-and-server): boundaries and SSR
|
|
17
19
|
|
|
18
20
|
## The four quadrants
|
|
19
21
|
|
|
20
|
-
| |
|
|
21
|
-
| ------------------------------------------------- |
|
|
22
|
-
| **[Tutorial](https://weftui.dev/docs/tutorial/01-your-first-app)** | Learning-oriented. One guided path, start to finish.
|
|
23
|
-
| **[How-to guides](https://weftui.dev/docs/how-to/author-components)** | Task-oriented. Author components, render on the server, load data with rpc, add routing
|
|
24
|
-
| **[Explanation](https://weftui.dev/docs/explanation/rendering-model)** | Understanding-oriented. The rendering model, the combinator API, reactive primitives, boundaries, and services & context.
|
|
25
|
-
| **[Reference](https://weftui.dev/docs/reference/core)** | Information-oriented. Full API: [`@weftui/core`](https://weftui.dev/docs/reference/core), [`@weftui/dom`](https://weftui.dev/docs/reference/dom), [`@weftui/router`](https://weftui.dev/docs/reference/router).
|
|
22
|
+
| | |
|
|
23
|
+
| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
24
|
+
| **[Tutorial](https://weftui.dev/docs/tutorial/01-your-first-app)** | Learning-oriented. One guided path, start to finish. |
|
|
25
|
+
| **[How-to guides](https://weftui.dev/docs/how-to/author-components)** | Task-oriented. Author components, render on the server, load data with rpc, add routing. Plus recipes for forms, async data, keyed lists, reactive styles, refs, and lazy routing. |
|
|
26
|
+
| **[Explanation](https://weftui.dev/docs/explanation/rendering-model)** | Understanding-oriented. The rendering model, the combinator API, reactive primitives, boundaries, and services & context. |
|
|
27
|
+
| **[Reference](https://weftui.dev/docs/reference/core)** | Information-oriented. Full API: [`@weftui/core`](https://weftui.dev/docs/reference/core), [`@weftui/dom`](https://weftui.dev/docs/reference/dom), [`@weftui/router`](https://weftui.dev/docs/reference/router). |
|
|
26
28
|
|
|
27
|
-
New to the model itself? Read [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model)
|
|
29
|
+
New to the model itself? Read [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model): why there is no virtual DOM, and what "streams are the weft" means.
|
|
28
30
|
|
|
29
31
|
## Packages
|
|
30
32
|
|
|
31
33
|
Three published packages make up Weft's public API, plus one build-time plugin:
|
|
32
34
|
|
|
33
|
-
- **`@weftui/core
|
|
34
|
-
- **`@weftui/dom
|
|
35
|
-
- **`@weftui/router
|
|
36
|
-
- **`@weftui/vite
|
|
35
|
+
- **`@weftui/core`**: element builders (`h`), components, sources/streams, and boundaries. Start here.
|
|
36
|
+
- **`@weftui/dom`**: the renderer, with `./client` (`WeftApp.mount`/`WeftApp.hydrate`) and `./server` (`renderToString*`) entry points.
|
|
37
|
+
- **`@weftui/router`**: universal nested routing, `Router.lazy`, and the rpc seam.
|
|
38
|
+
- **`@weftui/vite`**: a build-time Vite plugin (tooling, not a runtime API).
|
|
37
39
|
|
|
38
|
-
`@weftui/base` is an internal, currently-empty stub
|
|
40
|
+
`@weftui/base` is an internal, currently-empty stub with no public primitives. Ignore it.
|
|
39
41
|
|
|
40
42
|
## Examples
|
|
41
43
|
|