@xmachines/play-dom 1.0.0-beta.32 → 1.0.0-beta.33
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 +217 -34
- package/dist/PlayRenderer.d.ts +45 -16
- package/dist/PlayRenderer.d.ts.map +1 -1
- package/dist/PlayRenderer.js +111 -22
- package/dist/PlayRenderer.js.map +1 -1
- package/dist/connect-renderer.d.ts +36 -17
- package/dist/connect-renderer.d.ts.map +1 -1
- package/dist/connect-renderer.js +54 -32
- package/dist/connect-renderer.js.map +1 -1
- package/dist/create-renderer.d.ts +80 -0
- package/dist/create-renderer.d.ts.map +1 -0
- package/dist/create-renderer.js +87 -0
- package/dist/create-renderer.js.map +1 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/json-render/index.d.ts +4 -3
- package/dist/json-render/index.d.ts.map +1 -1
- package/dist/json-render/index.js +2 -2
- package/dist/json-render/renderer.d.ts +43 -9
- package/dist/json-render/renderer.d.ts.map +1 -1
- package/dist/json-render/renderer.js +273 -18
- package/dist/json-render/renderer.js.map +1 -1
- package/dist/json-render/types.d.ts +140 -27
- package/dist/json-render/types.d.ts.map +1 -1
- package/dist/json-render/types.js +30 -5
- package/dist/json-render/types.js.map +1 -1
- package/dist/xm-types.d.ts +68 -7
- package/dist/xm-types.d.ts.map +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -2,73 +2,256 @@
|
|
|
2
2
|
|
|
3
3
|
**Vanilla DOM renderer for XMachines**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Framework-free view rendering driven by an XState v5 actor's `currentView` TC39 Signal. Implements the same catalog-typed `defineRegistry` / `ComponentFn` / `ActionFn` API surface as `@json-render/react`, `/solid`, `/vue`, and `/svelte`.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install @xmachines/play-dom
|
|
10
|
+
npm install @xmachines/play-dom @json-render/core zod
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## Key Exports
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
| Export | Description |
|
|
16
|
+
| --------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
17
|
+
| `createRenderer(catalog, componentMap)` | One-call factory: returns `mount(actor, container, options?) → disconnect` |
|
|
18
|
+
| `connectRenderer(options)` | Functional API: connect actor → DOM with full options |
|
|
19
|
+
| `defineRegistry(catalog, { components, actions })` | Build a catalog-typed `DomRegistry` with typed action handlers |
|
|
20
|
+
| `PlayRenderer` | Class-based renderer — `connect()` / `disconnect()` lifecycle |
|
|
21
|
+
| `schema` | DOM schema for `defineCatalog` (mirrors `@json-render/react/schema`) |
|
|
22
|
+
| `ComponentFn<C, K>` | Catalog-typed DOM component function type |
|
|
23
|
+
| `ComponentContext<C, K>` | Context passed to each `ComponentFn` — `props`, `on`, `emit`, `children`, `bindings`, `ctx` |
|
|
24
|
+
| `ActionFn<C, K>` | `(params, setState, state) => Promise<void>` — catalog-typed action handler |
|
|
25
|
+
| `SetState` | `(updater: prev => next) => void` — write to the local state store |
|
|
26
|
+
| `BaseComponentProps<P>` | Base type for catalog component prop definitions |
|
|
27
|
+
| `CatalogHasActions<C>` | Conditional type: `true` when catalog declares actions |
|
|
28
|
+
| `renderSpec(spec, store, registry, send, handlers)` | Pure Spec → DOM renderer (advanced use) |
|
|
22
29
|
|
|
23
|
-
##
|
|
30
|
+
## Quick Start — `createRenderer`
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
The preferred one-call pattern — mirrors all framework renderers:
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { createRenderer, schema } from "@xmachines/play-dom";
|
|
28
36
|
import { defineCatalog } from "@json-render/core";
|
|
29
|
-
import {
|
|
37
|
+
import { z } from "zod";
|
|
30
38
|
import type { ComponentFn } from "@xmachines/play-dom";
|
|
31
39
|
|
|
32
|
-
// 1.
|
|
33
|
-
const
|
|
34
|
-
|
|
40
|
+
// 1. Define catalog
|
|
41
|
+
const catalog = defineCatalog(schema, {
|
|
42
|
+
components: {
|
|
43
|
+
Home: { props: z.object({ title: z.string() }) },
|
|
44
|
+
Login: { props: z.object({ title: z.string(), username: z.string().optional() }) },
|
|
45
|
+
},
|
|
46
|
+
actions: {
|
|
47
|
+
login: { params: z.object({ username: z.string() }) },
|
|
48
|
+
logout: {},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
type AppCatalog = typeof catalog;
|
|
35
52
|
|
|
36
|
-
// 2. Implement
|
|
37
|
-
const Home: ComponentFn<
|
|
53
|
+
// 2. Implement components
|
|
54
|
+
const Home: ComponentFn<AppCatalog, "Home"> = ({ props }) => {
|
|
38
55
|
const el = document.createElement("section");
|
|
39
56
|
el.textContent = props.title;
|
|
40
57
|
return el;
|
|
41
58
|
};
|
|
42
59
|
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
60
|
+
const Login: ComponentFn<AppCatalog, "Login"> = ({ props, on }) => {
|
|
61
|
+
const section = document.createElement("section");
|
|
62
|
+
const input = document.createElement("input");
|
|
63
|
+
input.value = props.username ?? "";
|
|
64
|
+
input.addEventListener("input", () =>
|
|
65
|
+
ctx.store.update((s) => ({ ...s, username: input.value })),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const button = document.createElement("button");
|
|
69
|
+
button.textContent = "Log In";
|
|
70
|
+
const submit = on("submit");
|
|
71
|
+
button.addEventListener("click", () => submit.emit());
|
|
72
|
+
|
|
73
|
+
section.append(input, button);
|
|
74
|
+
return section;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// 3. Create the renderer factory (once, at module level)
|
|
78
|
+
const mount = createRenderer(catalog, { Home, Login });
|
|
79
|
+
|
|
80
|
+
// 4. Mount when actor and container are ready
|
|
81
|
+
const actor = createPlayer()();
|
|
82
|
+
actor.start();
|
|
83
|
+
|
|
84
|
+
const disconnect = mount(actor, document.getElementById("app")!);
|
|
85
|
+
|
|
86
|
+
// Cleanup:
|
|
87
|
+
disconnect();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## `defineRegistry` — Full Control
|
|
91
|
+
|
|
92
|
+
When you need `registryResult.executeAction()` or want to share the registry with `connectRenderer`:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { defineRegistry, connectRenderer, schema } from "@xmachines/play-dom";
|
|
96
|
+
import { defineCatalog } from "@json-render/core";
|
|
97
|
+
import { z } from "zod";
|
|
98
|
+
|
|
99
|
+
const catalog = defineCatalog(schema, {
|
|
100
|
+
components: {
|
|
101
|
+
Home: { props: z.object({ title: z.string() }) },
|
|
102
|
+
},
|
|
103
|
+
actions: {
|
|
104
|
+
login: { params: z.object({ username: z.string() }) },
|
|
105
|
+
logout: {},
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Action handlers receive (params, setState, state)
|
|
110
|
+
// - params: resolved from the spec's on.submit.params (e.g. { $state: "/username" })
|
|
111
|
+
// - setState: write to the local state store (e.g. to clear a form)
|
|
112
|
+
// - state: current local state store snapshot
|
|
113
|
+
const registryResult = defineRegistry(catalog, {
|
|
114
|
+
components: {
|
|
115
|
+
Home: ({ props }) => {
|
|
116
|
+
const el = document.createElement("section");
|
|
117
|
+
el.textContent = props.title;
|
|
118
|
+
return el;
|
|
119
|
+
},
|
|
120
|
+
},
|
|
46
121
|
actions: {
|
|
47
|
-
login: async (params) => {
|
|
122
|
+
login: async (params, setState) => {
|
|
48
123
|
if (!params) return;
|
|
49
124
|
actor.send({ type: "auth.login", username: params.username });
|
|
125
|
+
setState((prev) => ({ ...prev, username: "" })); // clear the form
|
|
50
126
|
},
|
|
51
127
|
logout: async () => actor.send({ type: "auth.logout" }),
|
|
52
128
|
},
|
|
53
129
|
});
|
|
54
130
|
|
|
55
|
-
// 4. Resolve handlers and connect
|
|
56
|
-
const actor = definePlayer({ machine: authMachine })();
|
|
57
|
-
actor.start();
|
|
58
|
-
|
|
59
|
-
const handlers = registryResult.handlers(
|
|
60
|
-
() => undefined,
|
|
61
|
-
() => ({}),
|
|
62
|
-
);
|
|
63
|
-
const container = document.getElementById("app")!;
|
|
64
131
|
const disconnect = connectRenderer({
|
|
65
132
|
actor,
|
|
66
133
|
registry: registryResult.registry,
|
|
67
|
-
|
|
68
|
-
|
|
134
|
+
registryResult, // wires setState/state from xstate store automatically
|
|
135
|
+
container: document.getElementById("app")!,
|
|
69
136
|
});
|
|
70
137
|
```
|
|
71
138
|
|
|
139
|
+
## Component API
|
|
140
|
+
|
|
141
|
+
### `ComponentFn<C, K>` — component function signature
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
const MyCard: ComponentFn<AppCatalog, "Card"> = ({
|
|
145
|
+
props, // catalog-typed props for this component
|
|
146
|
+
children, // Node[] — rendered child nodes
|
|
147
|
+
on, // (eventName) => EventHandle — get emit() for catalog-declared events
|
|
148
|
+
emit, // (eventName) => void — fire an event directly
|
|
149
|
+
bindings, // Record<string, string> — $bindState paths for two-way bindings
|
|
150
|
+
ctx, // DomRenderContext — store, send, handlers, loading, functions
|
|
151
|
+
}) => {
|
|
152
|
+
const el = document.createElement("div");
|
|
153
|
+
el.append(...children);
|
|
154
|
+
return el;
|
|
155
|
+
};
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Two-way binding with `$bindState`
|
|
159
|
+
|
|
160
|
+
In the view spec:
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{ "username": { "$bindState": "/username" } }
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
In the component:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
const Login: ComponentFn<AppCatalog, "Login"> = ({ props, ctx }) => {
|
|
170
|
+
const input = document.createElement("input");
|
|
171
|
+
input.value = props.username ?? "";
|
|
172
|
+
// Write back to the store on user input
|
|
173
|
+
input.addEventListener("input", () => {
|
|
174
|
+
ctx.store.update((s) => ({ ...s, username: input.value }));
|
|
175
|
+
});
|
|
176
|
+
return input;
|
|
177
|
+
};
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### `on()` — event handle
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
const submit = on("submit"); // EventHandle
|
|
184
|
+
if (submit.bound) {
|
|
185
|
+
button.addEventListener("click", (e) => {
|
|
186
|
+
if (submit.shouldPreventDefault) e.preventDefault();
|
|
187
|
+
submit.emit(); // resolves params from store, calls action handler
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### `ActionFn` — action handler signature
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// Full signature — all three params are available
|
|
196
|
+
login: async (params, setState, state) => {
|
|
197
|
+
actor.send({ type: "auth.login", username: params!.username });
|
|
198
|
+
setState(prev => ({ ...prev, username: "" }));
|
|
199
|
+
console.log("previous state was:", state);
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
// Params-only — setState/state can be omitted if unused
|
|
203
|
+
logout: async () => actor.send({ type: "auth.logout" }),
|
|
204
|
+
route: async (params) => actor.send({ type: "play.route", to: params!.to }),
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Spec Features
|
|
208
|
+
|
|
209
|
+
`renderSpec` / `renderElement` supports these spec directives:
|
|
210
|
+
|
|
211
|
+
| Directive | Description |
|
|
212
|
+
| ------------------------------------------------------ | ------------------------------------------------------------------------------------- |
|
|
213
|
+
| `visible` | Boolean or `{ $state: "/path" }` — hide element when false |
|
|
214
|
+
| `on.submit` / `on.click` | Action binding — `{ action: "login", params: { username: { $state: "/username" } } }` |
|
|
215
|
+
| `repeat: { statePath, key? }` | Render children once per item in the state array at `statePath` |
|
|
216
|
+
| `watch: { "/path": actionBinding }` | Fire action when store path changes after mount |
|
|
217
|
+
| `props.username: { $bindState: "/username" }` | Two-way binding — read from store, write back via `ctx.store.update()` |
|
|
218
|
+
| `props.value: { $state: "/value" }` | Read-only store reference |
|
|
219
|
+
| `props.label: { $computed: "computeFn", args: [...] }` | Computed prop via `functions` map |
|
|
220
|
+
|
|
221
|
+
## `PlayRenderer` — class API
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
import { PlayRenderer, defineRegistry } from "@xmachines/play-dom";
|
|
225
|
+
|
|
226
|
+
const { registry, registryResult } = defineRegistry(catalog, { components, actions });
|
|
227
|
+
|
|
228
|
+
const renderer = new PlayRenderer(document.getElementById("app")!, actor, registry, {
|
|
229
|
+
registryResult,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
renderer.connect(); // starts watching actor.currentView
|
|
233
|
+
renderer.disconnect(); // stops watching, clears container
|
|
234
|
+
|
|
235
|
+
// double-connect is safe — connect() calls disconnect() internally if already connected
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Options Reference
|
|
239
|
+
|
|
240
|
+
### `ConnectRendererOptions` / `PlayDomOptions`
|
|
241
|
+
|
|
242
|
+
| Option | Type | Description |
|
|
243
|
+
| ---------------- | ------------------------------- | ----------------------------------------------------------------------------- |
|
|
244
|
+
| `actor` | `AbstractActor & Viewable` | Actor providing `currentView` signal |
|
|
245
|
+
| `registry` | `DomRegistry` | Component renderer map from `defineRegistry` |
|
|
246
|
+
| `registryResult` | `DefineRegistryResult` | Preferred — auto-wires `setState`/`state` from xstate store |
|
|
247
|
+
| `handlers` | `Record<string, ActionHandler>` | Pre-resolved handlers (legacy / advanced) |
|
|
248
|
+
| `container` | `HTMLElement` | DOM element to render into |
|
|
249
|
+
| `fallback` | `HTMLElement \| null` | Shown on initial mount when view is `null` (initial mount only) |
|
|
250
|
+
| `store` | `StateStore` | External store — controlled mode, overrides `spec.state` |
|
|
251
|
+
| `loading` | `boolean` | Streaming mode — suppresses missing-child warnings, exposes `ctx.ctx.loading` |
|
|
252
|
+
|
|
72
253
|
## Learn More
|
|
73
254
|
|
|
74
255
|
- [DOM Router adapter `@xmachines/play-dom-router`](../play-dom-router/README.md)
|
|
256
|
+
- [API reference](../../packages/docs/api/@xmachines/play-dom/README.md)
|
|
257
|
+
- [Play RFC](../../packages/docs/rfc/play.md)
|
package/dist/PlayRenderer.d.ts
CHANGED
|
@@ -13,24 +13,37 @@ import type { AnyActorLogic } from "xstate";
|
|
|
13
13
|
import type { DomRegistry } from "./json-render/types.js";
|
|
14
14
|
import type { PlayDomOptions } from "./xm-types.js";
|
|
15
15
|
/**
|
|
16
|
-
* PlayRenderer connects an actor's currentView signal to the DOM renderer.
|
|
16
|
+
* PlayRenderer connects an actor's `currentView` signal to the DOM renderer.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
18
|
+
* Watches `actor.currentView` via TC39 Signals and renders `DomComponentRenderer`
|
|
19
|
+
* functions into `container` on every view transition. Cleared on `disconnect()`.
|
|
20
|
+
*
|
|
21
|
+
* **Preferred usage — via `registryResult`:**
|
|
19
22
|
* ```typescript
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* const
|
|
23
|
+
* import { PlayRenderer, defineRegistry } from "@xmachines/play-dom";
|
|
24
|
+
*
|
|
25
|
+
* const registryResult = defineRegistry(catalog, { components, actions });
|
|
26
|
+
* const renderer = new PlayRenderer(container, actor, registryResult.registry, {
|
|
27
|
+
* registryResult, // wires setState/getState from xstate store automatically
|
|
28
|
+
* });
|
|
23
29
|
* renderer.connect();
|
|
30
|
+
* // Later:
|
|
31
|
+
* renderer.disconnect();
|
|
32
|
+
* ```
|
|
24
33
|
*
|
|
25
|
-
*
|
|
34
|
+
* **Controlled store mode** — bring your own `StateStore`:
|
|
35
|
+
* ```typescript
|
|
26
36
|
* import { createAtom } from "@xstate/store";
|
|
27
37
|
* import { xstateStoreStateStore } from "@json-render/xstate";
|
|
28
|
-
* const store = xstateStoreStateStore({ atom: createAtom({ username: "" }) });
|
|
29
|
-
* const renderer = new PlayRenderer(container, actor, registry, { store, handlers: resolvedHandlers });
|
|
30
38
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
39
|
+
* const atom = createAtom({ username: "" });
|
|
40
|
+
* const store = xstateStoreStateStore({ atom });
|
|
41
|
+
* const renderer = new PlayRenderer(container, actor, registry, { registryResult, store });
|
|
42
|
+
* renderer.connect();
|
|
33
43
|
* ```
|
|
44
|
+
*
|
|
45
|
+
* Double `connect()` is safe — calling `connect()` while already connected
|
|
46
|
+
* automatically disconnects first, preventing double-render subscriptions.
|
|
34
47
|
*/
|
|
35
48
|
export declare class PlayRenderer {
|
|
36
49
|
private readonly container;
|
|
@@ -39,18 +52,34 @@ export declare class PlayRenderer {
|
|
|
39
52
|
private readonly options;
|
|
40
53
|
private _unwatch;
|
|
41
54
|
private _storeUnsubscribe;
|
|
55
|
+
private _watchCleanups;
|
|
56
|
+
/**
|
|
57
|
+
* Set to `false` in `disconnect()` before calling `storeUnsub()`.
|
|
58
|
+
* The `rerender` closure checks this flag at entry so that any synchronous
|
|
59
|
+
* callback fired by the `StateStore` implementation within its own
|
|
60
|
+
* `unsubscribe()` call (an edge-case but valid contract) does not mutate a
|
|
61
|
+
* detached DOM tree.
|
|
62
|
+
*/
|
|
63
|
+
private _alive;
|
|
42
64
|
/**
|
|
43
|
-
* @param container -
|
|
44
|
-
* @param actor
|
|
45
|
-
* @param registry
|
|
46
|
-
* @param options
|
|
47
|
-
*
|
|
48
|
-
*
|
|
65
|
+
* @param container - `HTMLElement` to render into. Cleared and repopulated on every view transition.
|
|
66
|
+
* @param actor - Actor providing the `currentView` signal (must implement `Viewable`).
|
|
67
|
+
* @param registry - Component renderer map — typically `registryResult.registry` from `defineRegistry`.
|
|
68
|
+
* @param options - Configuration:
|
|
69
|
+
* - `registryResult` — preferred; auto-wires `setState`/`state` from the xstate store.
|
|
70
|
+
* - `handlers` — pre-resolved handler map (legacy; used when `registryResult` is absent).
|
|
71
|
+
* - `store` — external `StateStore` (controlled mode; overrides `spec.state` seeding).
|
|
72
|
+
* - `loading` — streaming mode flag; suppresses missing-child warnings.
|
|
49
73
|
*/
|
|
50
74
|
constructor(container: HTMLElement, actor: AbstractActor<AnyActorLogic> & Viewable, registry: DomRegistry, options?: PlayDomOptions);
|
|
51
75
|
/**
|
|
52
76
|
* Start watching actor.currentView and render to container.
|
|
53
77
|
* Renders the initial view synchronously, then subscribes to signal changes.
|
|
78
|
+
*
|
|
79
|
+
* Calling `connect()` on an already-connected renderer (where a previous
|
|
80
|
+
* `connect()` was never followed by `disconnect()`) would silently install a
|
|
81
|
+
* second `watchSignal` subscription, causing double-renders on every view
|
|
82
|
+
* change. Guard against this by auto-disconnecting first.
|
|
54
83
|
*/
|
|
55
84
|
connect(): void;
|
|
56
85
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlayRenderer.d.ts","sourceRoot":"","sources":["../src/PlayRenderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAgB,MAAM,uBAAuB,CAAC;AACnF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAE5C,OAAO,KAAK,EAAE,WAAW,
|
|
1
|
+
{"version":3,"file":"PlayRenderer.d.ts","sourceRoot":"","sources":["../src/PlayRenderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAgB,MAAM,uBAAuB,CAAC;AACnF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAE5C,OAAO,KAAK,EAAE,WAAW,EAAY,MAAM,wBAAwB,CAAC;AACpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAyBpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,qBAAa,YAAY;IAwBvB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IA1BzB,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,cAAc,CAAsB;IAC5C;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,CAAQ;IAEtB;;;;;;;;;OASG;gBAEe,SAAS,EAAE,WAAW,EACtB,KAAK,EAAE,aAAa,CAAC,aAAa,CAAC,GAAG,QAAQ,EAC9C,QAAQ,EAAE,WAAW,EACrB,OAAO,GAAE,cAAmB;IAG9C;;;;;;;;OAQG;IACH,OAAO,IAAI,IAAI;IAMf;;OAEG;IACH,UAAU,IAAI,IAAI;IAmBlB,OAAO,CAAC,OAAO;CAuFf"}
|
package/dist/PlayRenderer.js
CHANGED
|
@@ -20,34 +20,52 @@ import { renderSpec } from "./json-render/renderer.js";
|
|
|
20
20
|
* machine author put in the view spec. `createAtom` requires a plain object as
|
|
21
21
|
* its initial value — anything else produces a broken store at runtime.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* Only plain objects (prototype is `Object.prototype` or `null`) are accepted.
|
|
24
|
+
* Class instances and built-in objects (Date, Map, Set, etc.) are rejected and
|
|
25
|
+
* fall back to `{}` — this prevents silent broken-store bugs at runtime.
|
|
26
|
+
* Arrays are also rejected (they have `Array.prototype`).
|
|
25
27
|
*/
|
|
26
28
|
function toAtomState(state) {
|
|
27
29
|
if (state !== null && typeof state === "object" && !Array.isArray(state)) {
|
|
28
|
-
|
|
30
|
+
const proto = Object.getPrototypeOf(state);
|
|
31
|
+
if (proto === Object.prototype || proto === null) {
|
|
32
|
+
return state;
|
|
33
|
+
}
|
|
29
34
|
}
|
|
30
35
|
return {};
|
|
31
36
|
}
|
|
32
37
|
/**
|
|
33
|
-
* PlayRenderer connects an actor's currentView signal to the DOM renderer.
|
|
38
|
+
* PlayRenderer connects an actor's `currentView` signal to the DOM renderer.
|
|
34
39
|
*
|
|
35
|
-
*
|
|
40
|
+
* Watches `actor.currentView` via TC39 Signals and renders `DomComponentRenderer`
|
|
41
|
+
* functions into `container` on every view transition. Cleared on `disconnect()`.
|
|
42
|
+
*
|
|
43
|
+
* **Preferred usage — via `registryResult`:**
|
|
36
44
|
* ```typescript
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* const
|
|
45
|
+
* import { PlayRenderer, defineRegistry } from "@xmachines/play-dom";
|
|
46
|
+
*
|
|
47
|
+
* const registryResult = defineRegistry(catalog, { components, actions });
|
|
48
|
+
* const renderer = new PlayRenderer(container, actor, registryResult.registry, {
|
|
49
|
+
* registryResult, // wires setState/getState from xstate store automatically
|
|
50
|
+
* });
|
|
40
51
|
* renderer.connect();
|
|
52
|
+
* // Later:
|
|
53
|
+
* renderer.disconnect();
|
|
54
|
+
* ```
|
|
41
55
|
*
|
|
42
|
-
*
|
|
56
|
+
* **Controlled store mode** — bring your own `StateStore`:
|
|
57
|
+
* ```typescript
|
|
43
58
|
* import { createAtom } from "@xstate/store";
|
|
44
59
|
* import { xstateStoreStateStore } from "@json-render/xstate";
|
|
45
|
-
* const store = xstateStoreStateStore({ atom: createAtom({ username: "" }) });
|
|
46
|
-
* const renderer = new PlayRenderer(container, actor, registry, { store, handlers: resolvedHandlers });
|
|
47
60
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
61
|
+
* const atom = createAtom({ username: "" });
|
|
62
|
+
* const store = xstateStoreStateStore({ atom });
|
|
63
|
+
* const renderer = new PlayRenderer(container, actor, registry, { registryResult, store });
|
|
64
|
+
* renderer.connect();
|
|
50
65
|
* ```
|
|
66
|
+
*
|
|
67
|
+
* Double `connect()` is safe — calling `connect()` while already connected
|
|
68
|
+
* automatically disconnects first, preventing double-render subscriptions.
|
|
51
69
|
*/
|
|
52
70
|
export class PlayRenderer {
|
|
53
71
|
container;
|
|
@@ -56,13 +74,24 @@ export class PlayRenderer {
|
|
|
56
74
|
options;
|
|
57
75
|
_unwatch = null;
|
|
58
76
|
_storeUnsubscribe = null;
|
|
77
|
+
_watchCleanups = [];
|
|
78
|
+
/**
|
|
79
|
+
* Set to `false` in `disconnect()` before calling `storeUnsub()`.
|
|
80
|
+
* The `rerender` closure checks this flag at entry so that any synchronous
|
|
81
|
+
* callback fired by the `StateStore` implementation within its own
|
|
82
|
+
* `unsubscribe()` call (an edge-case but valid contract) does not mutate a
|
|
83
|
+
* detached DOM tree.
|
|
84
|
+
*/
|
|
85
|
+
_alive = true;
|
|
59
86
|
/**
|
|
60
|
-
* @param container -
|
|
61
|
-
* @param actor
|
|
62
|
-
* @param registry
|
|
63
|
-
* @param options
|
|
64
|
-
*
|
|
65
|
-
*
|
|
87
|
+
* @param container - `HTMLElement` to render into. Cleared and repopulated on every view transition.
|
|
88
|
+
* @param actor - Actor providing the `currentView` signal (must implement `Viewable`).
|
|
89
|
+
* @param registry - Component renderer map — typically `registryResult.registry` from `defineRegistry`.
|
|
90
|
+
* @param options - Configuration:
|
|
91
|
+
* - `registryResult` — preferred; auto-wires `setState`/`state` from the xstate store.
|
|
92
|
+
* - `handlers` — pre-resolved handler map (legacy; used when `registryResult` is absent).
|
|
93
|
+
* - `store` — external `StateStore` (controlled mode; overrides `spec.state` seeding).
|
|
94
|
+
* - `loading` — streaming mode flag; suppresses missing-child warnings.
|
|
66
95
|
*/
|
|
67
96
|
constructor(container, actor, registry, options = {}) {
|
|
68
97
|
this.container = container;
|
|
@@ -73,8 +102,15 @@ export class PlayRenderer {
|
|
|
73
102
|
/**
|
|
74
103
|
* Start watching actor.currentView and render to container.
|
|
75
104
|
* Renders the initial view synchronously, then subscribes to signal changes.
|
|
105
|
+
*
|
|
106
|
+
* Calling `connect()` on an already-connected renderer (where a previous
|
|
107
|
+
* `connect()` was never followed by `disconnect()`) would silently install a
|
|
108
|
+
* second `watchSignal` subscription, causing double-renders on every view
|
|
109
|
+
* change. Guard against this by auto-disconnecting first.
|
|
76
110
|
*/
|
|
77
111
|
connect() {
|
|
112
|
+
if (this._unwatch !== null)
|
|
113
|
+
this.disconnect();
|
|
78
114
|
this._render(this.actor.currentView.get());
|
|
79
115
|
this._unwatch = watchSignal(this.actor.currentView, (view) => this._render(view));
|
|
80
116
|
}
|
|
@@ -84,14 +120,38 @@ export class PlayRenderer {
|
|
|
84
120
|
disconnect() {
|
|
85
121
|
this._unwatch?.();
|
|
86
122
|
this._unwatch = null;
|
|
87
|
-
|
|
123
|
+
// Null the refs first so any in-flight rerender callback sees null and skips.
|
|
124
|
+
// Capture the arrays/functions before clearing so we call the correct cleanups
|
|
125
|
+
// even if unsubscribing the store triggers a synchronous rerender that would
|
|
126
|
+
// otherwise overwrite _watchCleanups before we iterate them.
|
|
127
|
+
const storeUnsub = this._storeUnsubscribe;
|
|
128
|
+
const watchCleanups = [...this._watchCleanups];
|
|
88
129
|
this._storeUnsubscribe = null;
|
|
130
|
+
this._watchCleanups = [];
|
|
131
|
+
// Set alive = false before calling storeUnsub() so that any synchronous
|
|
132
|
+
// rerender callback triggered within unsubscribe() exits immediately.
|
|
133
|
+
this._alive = false;
|
|
134
|
+
storeUnsub?.();
|
|
135
|
+
for (const cleanup of watchCleanups)
|
|
136
|
+
cleanup();
|
|
89
137
|
this.container.replaceChildren();
|
|
90
138
|
}
|
|
91
139
|
_render(view) {
|
|
140
|
+
// Reset alive before releasing old subscriptions — any synchronous callback
|
|
141
|
+
// from the old store's unsubscribe sees alive=true and will be accepted.
|
|
142
|
+
// This differs from disconnect() which sets alive=false BEFORE calling storeUnsub.
|
|
143
|
+
this._alive = true;
|
|
92
144
|
// Clean up previous store subscription before re-rendering
|
|
93
145
|
this._storeUnsubscribe?.();
|
|
94
146
|
this._storeUnsubscribe = null;
|
|
147
|
+
// Release watch cleanups from the previous view's initial render.
|
|
148
|
+
// Without this, watch subscriptions from the prior view remain active
|
|
149
|
+
// between a view transition and the first store update in the new view,
|
|
150
|
+
// holding references to stale DOM elements and preventing GC.
|
|
151
|
+
const prevWatchCleanups = this._watchCleanups;
|
|
152
|
+
this._watchCleanups = [];
|
|
153
|
+
for (const cleanup of prevWatchCleanups)
|
|
154
|
+
cleanup();
|
|
95
155
|
this.container.replaceChildren();
|
|
96
156
|
if (!view)
|
|
97
157
|
return;
|
|
@@ -105,10 +165,39 @@ export class PlayRenderer {
|
|
|
105
165
|
atom: createAtom(toAtomState(view.spec.state)),
|
|
106
166
|
});
|
|
107
167
|
const send = this.actor.send.bind(this.actor);
|
|
108
|
-
|
|
168
|
+
// Build a SetState function backed by the xstate store.
|
|
169
|
+
// Called lazily at action-invocation time (not at handlers() call time)
|
|
170
|
+
// so actions always see the latest store state.
|
|
171
|
+
const setState = (updater) => {
|
|
172
|
+
const prev = store.getSnapshot();
|
|
173
|
+
const next = updater(prev);
|
|
174
|
+
// store.update() is called with a full state snapshot (not a patch map).
|
|
175
|
+
// This is compatible with createStoreAdapter/createStateStore where update()
|
|
176
|
+
// performs a shallow merge of the provided top-level keys, effectively
|
|
177
|
+
// replacing the entire state model. Do not change this to a path-keyed
|
|
178
|
+
// patch unless the StateStore contract is updated to match.
|
|
179
|
+
store.update(next);
|
|
180
|
+
};
|
|
181
|
+
// Resolve handlers: prefer registryResult (wires setState/getState from store),
|
|
182
|
+
// fall back to pre-resolved handlers map for backward compatibility.
|
|
183
|
+
const handlers = this.options.registryResult
|
|
184
|
+
? this.options.registryResult.handlers(() => setState, () => store.getSnapshot())
|
|
185
|
+
: (this.options.handlers ?? {});
|
|
186
|
+
const loading = this.options.loading;
|
|
109
187
|
const rerender = () => {
|
|
188
|
+
// Guard: skip if PlayRenderer has been disconnected.
|
|
189
|
+
// Handles the edge case where a StateStore implementation fires its
|
|
190
|
+
// subscribed callbacks synchronously within its own unsubscribe() call.
|
|
191
|
+
if (!this._alive)
|
|
192
|
+
return;
|
|
193
|
+
// Clear watch subscriptions from previous render pass before re-rendering.
|
|
194
|
+
for (const cleanup of this._watchCleanups)
|
|
195
|
+
cleanup();
|
|
196
|
+
this._watchCleanups = [];
|
|
110
197
|
this.container.replaceChildren();
|
|
111
|
-
const node = renderSpec(view.spec, store, this.registry, send, handlers)
|
|
198
|
+
const node = renderSpec(view.spec, store, this.registry, send, handlers, undefined, undefined, undefined, loading, (cleanup) => {
|
|
199
|
+
this._watchCleanups.push(cleanup);
|
|
200
|
+
});
|
|
112
201
|
if (node)
|
|
113
202
|
this.container.appendChild(node);
|
|
114
203
|
};
|
package/dist/PlayRenderer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlayRenderer.js","sourceRoot":"","sources":["../src/PlayRenderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAG5D,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAIvD
|
|
1
|
+
{"version":3,"file":"PlayRenderer.js","sourceRoot":"","sources":["../src/PlayRenderer.ts"],"names":[],"mappings":"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"}
|