@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
package/docs/reference/dom.md
CHANGED
|
@@ -2,28 +2,30 @@
|
|
|
2
2
|
title: "@weftui/dom"
|
|
3
3
|
order: 2
|
|
4
4
|
section: reference
|
|
5
|
-
description: Full API surface for @weftui/dom
|
|
5
|
+
description: Full API surface for @weftui/dom, covering the WeftApp client runtime (make, mount, hydrate, errors, dispose), the server renderer (renderToString and streaming variants), and the Props.merge/Props.cx prop-bag composition utilities.
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# @weftui/dom API Reference
|
|
9
9
|
|
|
10
|
-
The DOM renderer for Weft. It has two entry points
|
|
11
|
-
browser and `@weftui/dom/server` for Node
|
|
12
|
-
|
|
10
|
+
The DOM renderer for Weft. It has two entry points: `@weftui/dom/client` for the
|
|
11
|
+
browser and `@weftui/dom/server` for Node. A package root re-exports the renderer
|
|
12
|
+
error types. See the [Server-Side Rendering guide](https://weftui.dev/docs/how-to/render-on-the-server)
|
|
13
13
|
for a narrative walkthrough.
|
|
14
14
|
|
|
15
15
|
## `@weftui/dom/client`
|
|
16
16
|
|
|
17
17
|
`WeftApp` is the client entry point's app namespace (`export * as WeftApp from
|
|
18
18
|
"./weft-app"`). One `WeftApp` value is one lazily-built `ManagedRuntime` (the app
|
|
19
|
-
layer) + one root `Scope` + one unhandled-error hub.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
layer) + one root `Scope` + one unhandled-error hub.
|
|
20
|
+
|
|
21
|
+
Each `WeftApp.mount` / `WeftApp.hydrate` call creates a child **root scope** under
|
|
22
|
+
the app scope. Layer-built services are shared **by reference** across every root
|
|
23
|
+
mounted from the same app (layer memoization). This is what makes cross-island
|
|
24
|
+
reactive state work (see [examples/shared-state-islands](https://github.com/stefvw93/weft/tree/main/examples/shared-state-islands)).
|
|
25
|
+
|
|
26
|
+
The barrel also re-exports `MountError`, `HydrateError`, `RootHandle`,
|
|
27
|
+
`UnhandledError`, and the `WeftApp` interface's type as `WeftAppType` (renamed on
|
|
28
|
+
export to avoid colliding with the `WeftApp` namespace import).
|
|
27
29
|
|
|
28
30
|
### `WeftApp.make`
|
|
29
31
|
|
|
@@ -37,12 +39,14 @@ const make: {
|
|
|
37
39
|
};
|
|
38
40
|
```
|
|
39
41
|
|
|
40
|
-
Creates a `WeftApp` from an app layer.
|
|
41
|
-
respect to the layer: the layer builds **lazily** on the first `mount` /
|
|
42
|
-
(or the first direct `app.runtime` run)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
Creates a `WeftApp` from an app layer. `make` is synchronous and side-effect-free
|
|
43
|
+
with respect to the layer: the layer builds **lazily** on the first `mount` /
|
|
44
|
+
`hydrate` (or the first direct `app.runtime` run), per `ManagedRuntime.make`
|
|
45
|
+
semantics.
|
|
46
|
+
|
|
47
|
+
A layer whose construction has an observable side effect shows that effect only
|
|
48
|
+
after the first mount, never at `make` time. `options.memoMap` shares layer
|
|
49
|
+
memoization across multiple `WeftApp` instances.
|
|
46
50
|
|
|
47
51
|
There is deliberately no `makeScoped`. To bind an app's lifetime to a scope,
|
|
48
52
|
compose it yourself:
|
|
@@ -64,16 +68,19 @@ const mount: <R, E>(
|
|
|
64
68
|
) => Effect.Effect<RootHandle, E | MountError>;
|
|
65
69
|
```
|
|
66
70
|
|
|
67
|
-
Mounts `node` into `root` as a new root of `app`.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this call does not reach components.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
Mounts `node` into `root` as a new root of `app`. The returned effect is
|
|
72
|
+
self-contained: its requirement channel is `never`, so it runs with a bare
|
|
73
|
+
`Effect.runPromise`. Services come exclusively from the app layer; an
|
|
74
|
+
`Effect.provide` wrapped around this call does not reach components.
|
|
75
|
+
|
|
76
|
+
Clears `root`'s existing children, renders, appends the result. Completes after
|
|
77
|
+
initial render; streams keep running in the background, owned by the root's scope
|
|
78
|
+
(a child of the app scope).
|
|
79
|
+
|
|
80
|
+
The app layer builds lazily here on first mount; its error channel `E` surfaces at
|
|
81
|
+
that point. On render failure the root scope is closed before the error propagates;
|
|
82
|
+
the app runtime and other roots are untouched. Mounting on a disposed app fails
|
|
83
|
+
rather than hanging.
|
|
77
84
|
|
|
78
85
|
### `WeftApp.hydrate`
|
|
79
86
|
|
|
@@ -92,11 +99,13 @@ Continues, on the client, the DOM produced on the server by
|
|
|
92
99
|
Unlike `mount`, does **not** clear `root`: it walks the node tree in lockstep with
|
|
93
100
|
the existing server DOM, adopting nodes in place. Error channel is `E |
|
|
94
101
|
HydrateError` (adds `HydrationMismatchError` on top of everything `mount` can fail
|
|
95
|
-
with).
|
|
102
|
+
with).
|
|
103
|
+
|
|
104
|
+
Preserves the compile-time `AssertNoServerOnly` → `ServerOnlyLeak` guard. A
|
|
96
105
|
server-only requirement left in `node`'s context degrades the return type to the
|
|
97
|
-
`ServerOnlyLeak` sentinel (compile error at the call site)
|
|
98
|
-
|
|
99
|
-
|
|
106
|
+
`ServerOnlyLeak` sentinel (compile error at the call site), not a runtime failure.
|
|
107
|
+
Hydration mechanics (the readiness barrier, stream-id seeding) are otherwise
|
|
108
|
+
unchanged from `mount`.
|
|
100
109
|
|
|
101
110
|
### `WeftApp.errors`
|
|
102
111
|
|
|
@@ -107,9 +116,11 @@ const errors: <R, E>(app: WeftApp<R, E>) => Stream.Stream<UnhandledError>;
|
|
|
107
116
|
The app's unhandled-error stream. While at least one subscriber exists, the default
|
|
108
117
|
`Effect.logError` fallback is suppressed and every `UnhandledError` is delivered to
|
|
109
118
|
all subscribers. With zero subscribers, each unhandled error runs the default log
|
|
110
|
-
(annotated with `weft.region`) instead.
|
|
111
|
-
|
|
112
|
-
|
|
119
|
+
(annotated with `weft.region`) instead.
|
|
120
|
+
|
|
121
|
+
There is no replay: a subscriber sees only errors published after it subscribed.
|
|
122
|
+
Multiple concurrent subscribers each receive every subsequent error. When the last
|
|
123
|
+
subscriber unsubscribes, the default log resumes.
|
|
113
124
|
|
|
114
125
|
### `WeftApp.dispose`
|
|
115
126
|
|
|
@@ -117,9 +128,13 @@ subsequent error. When the last subscriber unsubscribes, the default log resumes
|
|
|
117
128
|
const dispose: <R, E>(app: WeftApp<R, E>) => Effect.Effect<void>;
|
|
118
129
|
```
|
|
119
130
|
|
|
120
|
-
Disposes the app
|
|
121
|
-
|
|
122
|
-
|
|
131
|
+
Disposes the app, in order:
|
|
132
|
+
|
|
133
|
+
- closes every root scope (in mount order),
|
|
134
|
+
- releases the runtime's layers (`runtime.disposeEffect`),
|
|
135
|
+
- shuts the error hub down.
|
|
136
|
+
|
|
137
|
+
Idempotent: teardown effects run once. Subsequent `mount` / `hydrate` calls fail.
|
|
123
138
|
|
|
124
139
|
### `WeftApp<R, E>` (`WeftAppType`)
|
|
125
140
|
|
|
@@ -131,9 +146,9 @@ interface WeftApp<in R = never, out E = never> {
|
|
|
131
146
|
```
|
|
132
147
|
|
|
133
148
|
Re-exported from the barrel as `WeftAppType`. `runtime` is the app's
|
|
134
|
-
`ManagedRuntime`, for running app-level effects against the shared layer outside
|
|
135
|
-
root
|
|
136
|
-
`website/src/entry-client.ts`)
|
|
149
|
+
`ManagedRuntime`, for running app-level effects against the shared layer outside
|
|
150
|
+
any root. Examples: `app.runtime.runFork(trackPageviews)` (see
|
|
151
|
+
`website/src/entry-client.ts`) and `app.runtime.runPromise(Router.push("/about"))`.
|
|
137
152
|
|
|
138
153
|
### `RootHandle`
|
|
139
154
|
|
|
@@ -146,9 +161,10 @@ interface RootHandle {
|
|
|
146
161
|
|
|
147
162
|
Returned by `mount` / `hydrate`. `element` is the DOM element the root was mounted
|
|
148
163
|
into. `unmount()` closes **this root's scope only**: it interrupts its stream
|
|
149
|
-
subscriptions and any scoped work forked from its event handlers.
|
|
150
|
-
|
|
151
|
-
|
|
164
|
+
subscriptions and any scoped work forked from its event handlers.
|
|
165
|
+
|
|
166
|
+
It does **not** dispose the app runtime, touch other roots, or remove the rendered
|
|
167
|
+
DOM nodes from `element`. Idempotent: teardown side effects fire once.
|
|
152
168
|
|
|
153
169
|
### `UnhandledError`
|
|
154
170
|
|
|
@@ -168,8 +184,8 @@ in the render tree the error escaped. Sources (one entry per failing occurrence)
|
|
|
168
184
|
(region e.g. `"attribute:class"`, `"child:stream-3"`),
|
|
169
185
|
- an error escaping the **outermost** `Boundary` recovery (region
|
|
170
186
|
`"boundary:outermost"`),
|
|
171
|
-
- an event-handler effect **failing or dying** (region `"event:onClick"`)
|
|
172
|
-
in development and production alike
|
|
187
|
+
- an event-handler effect **failing or dying** (region `"event:onClick"`), reported
|
|
188
|
+
in development and production alike (there is no `NODE_ENV`-gated swallow).
|
|
173
189
|
|
|
174
190
|
Interrupt-only causes are never published. Errors handled by a nested `Boundary`
|
|
175
191
|
never reach the hub.
|
|
@@ -236,28 +252,228 @@ hydration markers. These back streaming SSR and suspense.
|
|
|
236
252
|
renderToHydratableShell(node: Renderable): Effect<HydratableShell, Error, R>
|
|
237
253
|
```
|
|
238
254
|
|
|
239
|
-
Produces a `HydratableShell`
|
|
255
|
+
Produces a `HydratableShell` (the document scaffold around the app) for servers
|
|
240
256
|
that assemble the response shell separately from the streamed body.
|
|
241
257
|
|
|
242
258
|
### Suspense failure handling
|
|
243
259
|
|
|
244
|
-
`SuspenseFailureHandlerTag` is the service tag for a `SuspenseFailureHandler
|
|
245
|
-
maps a failed suspense boundary to a `SuspenseFailureSubstitute` (fallback
|
|
246
|
-
during streaming SSR.
|
|
260
|
+
`SuspenseFailureHandlerTag` is the service tag for a `SuspenseFailureHandler`. The
|
|
261
|
+
handler maps a failed suspense boundary to a `SuspenseFailureSubstitute` (fallback
|
|
262
|
+
markup) during streaming SSR.
|
|
247
263
|
|
|
248
264
|
## Package root (`@weftui/dom`)
|
|
249
265
|
|
|
250
266
|
Re-exports the renderer error types:
|
|
251
267
|
|
|
252
|
-
- `HydrationMismatchError
|
|
253
|
-
- `UnsupportedNodeTypeError
|
|
254
|
-
- `RenderError
|
|
255
|
-
- `StreamSubscriptionError
|
|
268
|
+
- `HydrationMismatchError`: the client tree did not match the server markup.
|
|
269
|
+
- `UnsupportedNodeTypeError`: a node type the renderer cannot handle was encountered.
|
|
270
|
+
- `RenderError`: a general rendering failure.
|
|
271
|
+
- `StreamSubscriptionError`: a reactive stream backing the tree failed to subscribe.
|
|
272
|
+
|
|
273
|
+
Also re-exports the `Props` namespace, below.
|
|
274
|
+
|
|
275
|
+
## `Props`
|
|
276
|
+
|
|
277
|
+
`export * as Props from "@weftui/dom"`. Two functions for reconciling DOM prop
|
|
278
|
+
bags: `merge` combines multiple bags into one, `cx` builds a class string.
|
|
279
|
+
Both are pure and synchronous; neither subscribes anything. A reactive result
|
|
280
|
+
is a `Stream` description, subscribed later by the renderer in the element's
|
|
281
|
+
scope.
|
|
282
|
+
|
|
283
|
+
### `Props.merge`
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
function merge<const Bags extends ReadonlyArray<DomProps>>(...bags: Bags): Merged<Bags>;
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`DomProps` is `object`. `merge` is variadic and left-to-right: `merge()` is
|
|
290
|
+
`{}`, `merge(a)` is observationally `a`, and `merge(a, b, c)` folds pairwise
|
|
291
|
+
(`merge(merge(a, b), c)`). `{}` is the identity on either side.
|
|
292
|
+
|
|
293
|
+
The fold is associative per key, with one exception: `style` is not
|
|
294
|
+
associative when a non-object form (a string, or a whole-object stream) takes
|
|
295
|
+
part. See the `style` rule below.
|
|
296
|
+
|
|
297
|
+
Keys present on only one side pass through unchanged, by reference. For a key
|
|
298
|
+
present on both sides, the merged value depends on the key:
|
|
299
|
+
|
|
300
|
+
| Key | Rule | Result |
|
|
301
|
+
| --------------------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------ |
|
|
302
|
+
| `on*` (event handler, per the renderer's `on` + lowercase-third-char check) | chained: both handler bodies run, left then right | new handler function |
|
|
303
|
+
| `class` | concatenated | `string` if both sides are static, else a derived `Stream<string>` |
|
|
304
|
+
| `style` | object sides merge per property; any other form is last-wins | plain object, or the right side as-is |
|
|
305
|
+
| `ref` | fanned out | readonly array of `SubscriptionRef`s |
|
|
306
|
+
| anything else | last-wins | the right side's value, as-is |
|
|
307
|
+
|
|
308
|
+
**Handlers.** Both handler bodies run synchronously when the merged handler
|
|
309
|
+
is invoked, left then right, before either side's returned `Effect` is
|
|
310
|
+
awaited. This is what makes `event.preventDefault()` written in either body
|
|
311
|
+
observable to the other: two separate DOM listeners would both run during
|
|
312
|
+
dispatch, so neither can wait on the other's `Effect` to decide whether the
|
|
313
|
+
default action should still happen.
|
|
314
|
+
|
|
315
|
+
Only the returned Effects are sequenced: left first, then right. Both always
|
|
316
|
+
run regardless of whether the other fails. A plain void-returning handler is
|
|
317
|
+
lifted to `Effect.void` (or a died `Effect` if it throws). The merged
|
|
318
|
+
handler's error channel is the union `E_left | E_right`; if both sides fail,
|
|
319
|
+
both causes are aggregated into one, not deduplicated (two equal-looking
|
|
320
|
+
failures are still two failures).
|
|
321
|
+
|
|
322
|
+
`null` or `undefined` on either side means "not provided": the other side
|
|
323
|
+
passes through unchanged, whatever shape it has, including a reactive
|
|
324
|
+
`Stream`/`Effect`-of-handler value (not chained, since only two plain
|
|
325
|
+
functions are chained; see [Accepted limitations](#accepted-limitations)
|
|
326
|
+
below). `false` on the **right** means "explicitly disabled" and wins, since
|
|
327
|
+
the renderer reads `false` as "no handler." That is how a caller switches a
|
|
328
|
+
behavior's handler off. A `false` on the left simply loses to the right side,
|
|
329
|
+
like any other last-wins value.
|
|
330
|
+
This is the only place `merge` treats a nullish value specially. The generic
|
|
331
|
+
rule below explains why every other key does not.
|
|
332
|
+
|
|
333
|
+
**`class`.** Two static strings concatenate with a single space, no dedupe:
|
|
334
|
+
`merge({ class: "a" }, { class: "b" }).class === "a b"`. If either side is
|
|
335
|
+
reactive (`Stream`, `Effect`, or `Subscribable`), the result is a derived
|
|
336
|
+
`Stream<string>` combining the latest value from each side, space-joined. A
|
|
337
|
+
static side contributes immediately; the first emission waits only on the
|
|
338
|
+
reactive side(s) (await-first). A reactive side that ends without ever
|
|
339
|
+
emitting fails the derived stream with `NoPropValue`, which joins the merged
|
|
340
|
+
`E` channel. When both sides contribute nothing (absent, `undefined`, or
|
|
341
|
+
empty), the result is `""`, matching `cx` and clsx: it is not normalized to
|
|
342
|
+
`undefined`.
|
|
343
|
+
|
|
344
|
+
The `class` rule for two present sides is exactly `cx(left, right)`; `cx` is
|
|
345
|
+
that same engine exposed directly (see below).
|
|
346
|
+
|
|
347
|
+
**`style`.** Two per-property objects (`style: { color: "red" }`) merge by
|
|
348
|
+
key union, right side winning per key; each surviving value, static or
|
|
349
|
+
`Source`, passes through by reference. Any other shape on either side (a
|
|
350
|
+
`string`, or a whole-object stream) is last-wins: the right side replaces the
|
|
351
|
+
left entirely. This is the one case where merge is not associative, since
|
|
352
|
+
last-wins discards a side instead of combining it, so grouping the fold
|
|
353
|
+
differently changes the result. Upgrading whole-object-stream style merging
|
|
354
|
+
to a real per-key merge is additive future work, not a breaking change.
|
|
355
|
+
|
|
356
|
+
**`ref`.** Both sides concatenate into one readonly array, flattening any
|
|
357
|
+
side that is already an array, so associativity holds:
|
|
358
|
+
`merge({ ref: [a, b] }, { ref: c }).ref` is `[a, b, c]`. Nullish sides are
|
|
359
|
+
dropped, so an optional ref forwarded as `undefined` never enters the array.
|
|
360
|
+
Each ref keeps the normal per-ref contract: set once to `Some(element)` when
|
|
361
|
+
the element mounts. The renderer's `ref` prop accepts a `SubscriptionRef` or
|
|
362
|
+
a `readonly SubscriptionRef[]` directly, so `h.div({ ref: [a, b] })` fans out
|
|
363
|
+
without `merge` too.
|
|
364
|
+
|
|
365
|
+
**Everything else (generic keys).** Plain last-wins, matching object spread:
|
|
366
|
+
the right side's value wins as-is, including an explicit `undefined`. There
|
|
367
|
+
is no nullish guard on this arm (unlike the handler rule): a guard was tried
|
|
368
|
+
and reverted, because it made the runtime return the left value while the
|
|
369
|
+
type still said the right value was present, silently dropping the left
|
|
370
|
+
side's `E`/`R` channels from the merged type.
|
|
371
|
+
|
|
372
|
+
#### Type-layer contract
|
|
373
|
+
|
|
374
|
+
`Merged<Bags>`'s **value** types stay coarse (Source-shaped, not narrowed to
|
|
375
|
+
exactly what the runtime returns); its `E`/`R` **channels** stay precise,
|
|
376
|
+
because `PropsE`/`PropsR` (the machinery that feeds a merged bag's channels
|
|
377
|
+
into `h.*`'s resulting `Node<E, R>`) match `P[K]` against an exact `Stream`
|
|
378
|
+
or function shape. A looser value type would fail that match and silently
|
|
379
|
+
drop the channel.
|
|
380
|
+
|
|
381
|
+
Consequences worth knowing:
|
|
382
|
+
|
|
383
|
+
- A shared key's merged value type is **required**, even when the key is
|
|
384
|
+
optional on both input bags and absent from one side at runtime. Typing it
|
|
385
|
+
optional would fail the `PropsE`/`PropsR` match and drop the channel for
|
|
386
|
+
the common case: a behavior primitive's bag with optional props.
|
|
387
|
+
- A handler cell types as callable whenever _either_ side can carry a
|
|
388
|
+
handler, even if that side is `null` at runtime. Narrowing this would
|
|
389
|
+
require unioning the nullish outcome back in, which fails the same match.
|
|
390
|
+
- The `ref`-array cell types as `SubscriptionRef<Option<any>>`, not narrowed
|
|
391
|
+
to a specific element type. `SubscriptionRef` is invariant in its value
|
|
392
|
+
type, so a precise union would reject the headline case: fanning a
|
|
393
|
+
behavior's `SubscriptionRef<Option<HTMLElement>>` out alongside a caller's
|
|
394
|
+
`SubscriptionRef<Option<HTMLInputElement>>`. A mistyped ref inside a
|
|
395
|
+
fan-out array is therefore not caught at compile time; the set-once
|
|
396
|
+
contract keeps reads sound regardless.
|
|
397
|
+
- A bag typed with core's `HTMLAttributes`/`DOMAttributes` gets `unknown`
|
|
398
|
+
handler channels, because those types declare handlers as returning
|
|
399
|
+
`void | Effect<void, unknown, unknown>`. A behavior primitive that
|
|
400
|
+
declares precise handler signatures keeps precise channels through the
|
|
401
|
+
merge.
|
|
402
|
+
|
|
403
|
+
#### Accepted limitations
|
|
404
|
+
|
|
405
|
+
- Reactive handler _values_ (a `Stream`/`Effect` of a handler function, the
|
|
406
|
+
form core's `EventHandler` union allows) are not chained: any non-function
|
|
407
|
+
handler side falls back to last-wins, consistent with the whole-object
|
|
408
|
+
style rule. Their `E`/`R` channels are still collected in the merged type.
|
|
409
|
+
- An inline handler written directly inside a `merge` call gets no
|
|
410
|
+
contextual type for its event parameter, because `DomProps` is `object`
|
|
411
|
+
and `merge` cannot know which element it will end up on. Write
|
|
412
|
+
`onclick: (ev: MouseEvent) => …` with an explicit annotation, or give the
|
|
413
|
+
bag its own type.
|
|
414
|
+
|
|
415
|
+
### `Props.cx`
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
function cx<const Inputs extends ReadonlyArray<CxInput>>(...inputs: Inputs): CxResult<Inputs>;
|
|
419
|
+
|
|
420
|
+
type CxInput =
|
|
421
|
+
| string
|
|
422
|
+
| false
|
|
423
|
+
| null
|
|
424
|
+
| undefined
|
|
425
|
+
| Stream.Stream<string, any, any>
|
|
426
|
+
| Effect.Effect<string, any, any>
|
|
427
|
+
| Subscribable.Subscribable<string, any, any>
|
|
428
|
+
| CxRecord
|
|
429
|
+
| ReadonlyArray<CxInput>;
|
|
430
|
+
|
|
431
|
+
interface CxRecord {
|
|
432
|
+
readonly [className: string]:
|
|
433
|
+
| boolean
|
|
434
|
+
| Stream.Stream<boolean, any, any>
|
|
435
|
+
| Effect.Effect<boolean, any, any>
|
|
436
|
+
| Subscribable.Subscribable<boolean, any, any>;
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
A reactive class-name builder, clsx-compatible plus reactive conditions. Each
|
|
441
|
+
input is one of:
|
|
442
|
+
|
|
443
|
+
- a **string**: kept as a literal class name segment;
|
|
444
|
+
- a **falsy value** (`false`, `null`, `undefined`, `""`): skipped;
|
|
445
|
+
- a **nested array** of `CxInput`: flattened recursively;
|
|
446
|
+
- a **record** (`{ className: condition }`): each key is included when its
|
|
447
|
+
condition is truthy;
|
|
448
|
+
- a **reactive value** in place of a string (a `Source<string>`), or as a
|
|
449
|
+
record condition (a `Source<boolean>`).
|
|
450
|
+
|
|
451
|
+
`cx()` is `""`. All-static inputs join into a plain `string`, space-separated,
|
|
452
|
+
no dedupe, no empty segments. Any reactive input (a reactive value, or a
|
|
453
|
+
reactive condition in a record) derives a `Stream<string>` that recomputes
|
|
454
|
+
the full class string on any emission, combining the latest value from every
|
|
455
|
+
reactive input.
|
|
456
|
+
|
|
457
|
+
A reactive value that resolves to `""` contributes nothing to that emission.
|
|
458
|
+
A reactive input that ends without ever emitting fails the stream with
|
|
459
|
+
`NoPropValue`, which joins the result's `E` channel along with every
|
|
460
|
+
reactive input's own `E`/`R`.
|
|
461
|
+
|
|
462
|
+
Only a plain record (an object literal, not a class instance, `Date`, or
|
|
463
|
+
boxed value like `SubscriptionRef`) is read as a condition map; anything else
|
|
464
|
+
is ignored rather than risking a foreign field name leaking in as a class
|
|
465
|
+
name.
|
|
466
|
+
|
|
467
|
+
`merge`'s `class` rule for two present sides is observationally
|
|
468
|
+
`cx(left, right)`: one engine behind both names.
|
|
256
469
|
|
|
257
470
|
## See also
|
|
258
471
|
|
|
259
|
-
- [
|
|
260
|
-
- [
|
|
261
|
-
- [
|
|
262
|
-
- [
|
|
472
|
+
- [Compose Behavior and Markup](https://weftui.dev/docs/how-to/compose-behavior-and-markup): using `Props.merge`/`Props.cx` to combine a behavior's props with the caller's
|
|
473
|
+
- [Style Reactively](https://weftui.dev/docs/how-to/style-reactively): the `style` prop's reactive forms
|
|
474
|
+
- [Use Element Refs](https://weftui.dev/docs/how-to/use-element-refs): the `ref` prop and its fan-out form
|
|
475
|
+
- [Render on the Server](https://weftui.dev/docs/how-to/render-on-the-server): a narrative walkthrough of the server/client split
|
|
476
|
+
- [Provide Services](https://weftui.dev/docs/how-to/provide-services): recipes for app layers, scoped layers, and binding an app's lifetime to an external scope
|
|
477
|
+
- [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model): hydrate-in-place and why there is no virtual DOM
|
|
478
|
+
- [Services and Context](https://weftui.dev/docs/explanation/services-and-context): how services flow from the app layer to every root
|
|
263
479
|
- [`@weftui/core` reference](https://weftui.dev/docs/reference/core) · [`@weftui/router` reference](https://weftui.dev/docs/reference/router)
|