@xmachines/play-dom 2.0.0-alpha.1 → 2.1.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 +100 -66
- package/dist/PlayRenderer.d.ts +96 -55
- package/dist/PlayRenderer.d.ts.map +1 -1
- package/dist/PlayRenderer.js +223 -128
- package/dist/PlayRenderer.js.map +1 -1
- package/dist/create-play-ui.d.ts +38 -36
- package/dist/create-play-ui.d.ts.map +1 -1
- package/dist/create-play-ui.js +30 -28
- package/dist/create-play-ui.js.map +1 -1
- package/dist/create-renderer.d.ts +26 -22
- package/dist/create-renderer.d.ts.map +1 -1
- package/dist/create-renderer.js +26 -22
- package/dist/create-renderer.js.map +1 -1
- package/dist/index.d.ts +21 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -22
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +38 -35
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -3
- package/package.json +27 -24
- package/dist/errors.d.ts +0 -46
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -52
- package/dist/errors.js.map +0 -1
package/dist/PlayRenderer.js
CHANGED
|
@@ -1,39 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PlayRenderer.ts — XMachines wrapper
|
|
2
|
+
* PlayRenderer.ts — the XMachines wrapper of @xmachines/json-render-dom.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* play-react, play-vue, and
|
|
4
|
+
* It connects actor.currentView, a TC39 Signal, to the DOM renderer below it, with
|
|
5
|
+
* watchSignal. It gives you the connect() method and the disconnect() method, in
|
|
6
|
+
* the same pattern as the PlayRenderer class of play-react, of play-vue, and of
|
|
7
|
+
* play-solid.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* The state store: the renderer uses the external `store` option when the caller
|
|
10
|
+
* gives one, and this is the controlled mode. Without that option, it makes a new
|
|
11
|
+
* @xstate/store atom for each view transition, with the values of spec.state.
|
|
10
12
|
*/
|
|
11
13
|
import { watchSignal } from "@xmachines/play-signals";
|
|
12
14
|
import { createAtom } from "@xstate/store";
|
|
13
15
|
import { xstateStoreStateStore } from "@xmachines/json-render-xstate";
|
|
14
|
-
import {
|
|
16
|
+
import { createViewStoreLifecycle, refreshContextSubtree } from "@xmachines/play-actor";
|
|
15
17
|
import { createIncrementalRenderer } from "@xmachines/json-render-dom";
|
|
16
18
|
/**
|
|
17
|
-
* PlayRenderer connects
|
|
19
|
+
* PlayRenderer connects the `currentView` signal of an actor to the DOM renderer.
|
|
18
20
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
+
* It watches `actor.currentView` through the TC39 Signals. It renders each
|
|
22
|
+
* `DomComponentRenderer` function into `container` on every view transition.
|
|
23
|
+
* `disconnect()` clears the container.
|
|
21
24
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* - `functions` — named compute functions
|
|
25
|
-
* - `directives` —
|
|
26
|
-
* - `validationFunctions` —
|
|
27
|
-
* - `navigate` — navigation callback
|
|
28
|
-
* - `onRenderError` —
|
|
25
|
+
* Every option of `PlayDomOptions`, which extends `UIProviderOptions`, goes into
|
|
26
|
+
* `DomRenderContext` on each render pass:
|
|
27
|
+
* - `functions` — the named compute functions of a `{ $computed: "name" }` prop expression
|
|
28
|
+
* - `directives` — your own dynamic values with a `$` prefix, from `defineDirective`. The renderer resolves them with the props
|
|
29
|
+
* - `validationFunctions` — your own check functions. They are available at `ctx.ctx.validationFunctions`
|
|
30
|
+
* - `navigate` — the navigation callback. The renderer calls it for an `onSuccess: { navigate: "..." }` action binding
|
|
31
|
+
* - `onRenderError` — the renderer calls it with `(error, name)` for a component render error and for an action handler rejection
|
|
29
32
|
*
|
|
30
|
-
* **
|
|
33
|
+
* **The preferred use, with `registryResult`:**
|
|
31
34
|
* ```typescript
|
|
32
35
|
* import { PlayRenderer, defineRegistry } from "@xmachines/play-dom";
|
|
33
36
|
*
|
|
34
37
|
* const registryResult = defineRegistry(catalog, { components, actions });
|
|
35
38
|
* const renderer = new PlayRenderer(container, actor, registryResult.registry, {
|
|
36
|
-
* registryResult, //
|
|
39
|
+
* registryResult, // it connects setState and getState of the xstate store for you
|
|
37
40
|
* navigate: (path) => myRouter.push(path),
|
|
38
41
|
* functions: { fullName: (args) => `${args.first} ${args.last}` },
|
|
39
42
|
* });
|
|
@@ -42,7 +45,7 @@ import { createIncrementalRenderer } from "@xmachines/json-render-dom";
|
|
|
42
45
|
* renderer.disconnect();
|
|
43
46
|
* ```
|
|
44
47
|
*
|
|
45
|
-
* **
|
|
48
|
+
* **The controlled store mode** — you give your own `StateStore`:
|
|
46
49
|
* ```typescript
|
|
47
50
|
* import { createAtom } from "@xstate/store";
|
|
48
51
|
* import { xstateStoreStateStore } from "@xmachines/json-render-xstate";
|
|
@@ -53,8 +56,9 @@ import { createIncrementalRenderer } from "@xmachines/json-render-dom";
|
|
|
53
56
|
* renderer.connect();
|
|
54
57
|
* ```
|
|
55
58
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
59
|
+
* A second `connect()` is safe. A `connect()` call on a connected renderer
|
|
60
|
+
* disconnects it first. Therefore the renderer holds no second render
|
|
61
|
+
* subscription.
|
|
58
62
|
*/
|
|
59
63
|
export class PlayRenderer {
|
|
60
64
|
container;
|
|
@@ -64,35 +68,56 @@ export class PlayRenderer {
|
|
|
64
68
|
unwatch = null;
|
|
65
69
|
storeUnsubscribe = null;
|
|
66
70
|
/**
|
|
67
|
-
*
|
|
68
|
-
* registered cleanup can delete ITSELF on
|
|
69
|
-
* renderer releases a watcher when its element unmounts,
|
|
70
|
-
*
|
|
71
|
-
* subscriptions
|
|
72
|
-
*
|
|
71
|
+
* The cleanup functions of the live watch subscriptions. This field is a `Set`,
|
|
72
|
+
* and not an array, so that each registered cleanup function can delete ITSELF on
|
|
73
|
+
* its call: the incremental renderer releases a watcher when its element unmounts,
|
|
74
|
+
* and it calls the registered wrapper. The Set therefore shrinks with the live
|
|
75
|
+
* subscriptions. It does not collect one spent closure for each historical mount of
|
|
76
|
+
* a view with a long life.
|
|
73
77
|
*/
|
|
74
78
|
watchCleanups = new Set();
|
|
75
79
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
80
|
+
* The viewKey of the view on the screen now, and the store behind it, without the
|
|
81
|
+
* guard. An emission with the same key means that only the /context projection
|
|
82
|
+
* moved: the fast path for a slice in render() refreshes the store in place, and it
|
|
83
|
+
* does not remove the tree. The ephemeral view state, such as a draft or a toggle,
|
|
84
|
+
* therefore stays. A null emission, from a state with no meta.view, is a GAP, and
|
|
85
|
+
* not a new view: the DOM goes away, but the identity and the store stay. A return
|
|
86
|
+
* to the same view therefore builds again around the ephemeral state that survived,
|
|
87
|
+
* in the same way as the framework providers. `disconnect()` resets both fields,
|
|
88
|
+
* because a store that stays alive must not outlive the connection.
|
|
89
|
+
*/
|
|
90
|
+
lastViewKey = undefined;
|
|
91
|
+
currentStore = null;
|
|
92
|
+
/**
|
|
93
|
+
* The shared coordinator of the store lifecycle, from @xmachines/play-actor. It
|
|
94
|
+
* seeds the store again on a change of the viewKey, it refreshes /context in place
|
|
95
|
+
* in every other case, and it guards the identity cache. The two fields above,
|
|
96
|
+
* lastViewKey and currentStore, stay as the DOM-side record for the fast path of a
|
|
97
|
+
* slice. That path is about the live TREE, which is the spec and the subscription,
|
|
98
|
+
* and not about the store.
|
|
99
|
+
*/
|
|
100
|
+
storeLifecycle = createViewStoreLifecycle((seed) => xstateStoreStateStore({ atom: createAtom(seed) }));
|
|
101
|
+
/**
|
|
102
|
+
* `disconnect()` sets this field to `false` before it calls `storeUnsub()`.
|
|
103
|
+
* The `rerender` closure reads the flag at its start. Therefore a synchronous
|
|
104
|
+
* callback from the `StateStore` implementation inside its own `unsubscribe()`
|
|
105
|
+
* call, which is rare but correct, changes no detached DOM tree.
|
|
81
106
|
*/
|
|
82
107
|
alive = true;
|
|
83
108
|
/**
|
|
84
|
-
* @param container - `HTMLElement` to render into.
|
|
85
|
-
* @param actor -
|
|
86
|
-
* @param registry -
|
|
87
|
-
* @param options -
|
|
88
|
-
* - `registryResult` —
|
|
89
|
-
* - `store` — external `StateStore
|
|
90
|
-
* - `loading` — streaming mode
|
|
91
|
-
* - `functions` — named compute functions
|
|
92
|
-
* - `directives` —
|
|
93
|
-
* - `validationFunctions` —
|
|
94
|
-
* - `navigate` — navigation callback
|
|
95
|
-
* - `onRenderError` — `(error, name)` handler
|
|
109
|
+
* @param container - The `HTMLElement` to render into. Each view transition clears it and fills it again.
|
|
110
|
+
* @param actor - The actor with the `currentView` signal. It must implement `Viewable`.
|
|
111
|
+
* @param registry - The map of the component renderers, usually `registryResult.registry` from `defineRegistry`.
|
|
112
|
+
* @param options - The configuration. See {@link PlayDomOptions}:
|
|
113
|
+
* - `registryResult` — it connects `setState` and `state` of the xstate store for you.
|
|
114
|
+
* - `store` — an external `StateStore`, which is the controlled mode. It replaces the values of `spec.state`.
|
|
115
|
+
* - `loading` — the flag of the streaming mode. It stops each warning about an absent child.
|
|
116
|
+
* - `functions` — the named compute functions of a `$computed` prop expression.
|
|
117
|
+
* - `directives` — your own dynamic values with a `$` prefix. The renderer resolves them with the props.
|
|
118
|
+
* - `validationFunctions` — your own check functions. They are available at `ctx.ctx.validationFunctions`.
|
|
119
|
+
* - `navigate` — the navigation callback. The renderer calls it for `onSuccess: { navigate: "..." }`.
|
|
120
|
+
* - `onRenderError` — the `(error, name)` handler of a component render error and of an action handler rejection. It stops the `console.error` fallback.
|
|
96
121
|
*/
|
|
97
122
|
constructor(container, actor, registry, options = {}) {
|
|
98
123
|
this.container = container;
|
|
@@ -101,13 +126,13 @@ export class PlayRenderer {
|
|
|
101
126
|
this.options = options;
|
|
102
127
|
}
|
|
103
128
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
129
|
+
* Starts the watch of actor.currentView, and renders into the container.
|
|
130
|
+
* It renders the first view synchronously, then it subscribes to the signal changes.
|
|
106
131
|
*
|
|
107
|
-
*
|
|
108
|
-
* `connect()
|
|
109
|
-
*
|
|
110
|
-
*
|
|
132
|
+
* A `connect()` call on a connected renderer, where a `disconnect()` call did not
|
|
133
|
+
* follow the previous `connect()`, installs a second `watchSignal` subscription.
|
|
134
|
+
* Each view change then renders two times. Therefore this method disconnects the
|
|
135
|
+
* renderer first.
|
|
111
136
|
*/
|
|
112
137
|
connect() {
|
|
113
138
|
if (this.unwatch !== null)
|
|
@@ -116,83 +141,150 @@ export class PlayRenderer {
|
|
|
116
141
|
this.unwatch = watchSignal(this.actor.currentView, (view) => this.render(view));
|
|
117
142
|
}
|
|
118
143
|
/**
|
|
119
|
-
*
|
|
144
|
+
* Stops the watch and clears the container.
|
|
120
145
|
*/
|
|
121
146
|
disconnect() {
|
|
122
147
|
this.unwatch?.();
|
|
123
148
|
this.unwatch = null;
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
149
|
+
// Set the refs to null first. A rerender callback that is still in flight therefore
|
|
150
|
+
// sees null, and it stops.
|
|
151
|
+
// Capture each array and each function before the code clears them. Therefore the
|
|
152
|
+
// code calls the correct cleanup functions, also when the cancellation of the store
|
|
153
|
+
// subscription starts a synchronous rerender. Without the capture, that rerender
|
|
154
|
+
// overwrites watchCleanups before the loop over them.
|
|
128
155
|
const storeUnsub = this.storeUnsubscribe;
|
|
129
156
|
const watchCleanups = [...this.watchCleanups];
|
|
130
157
|
this.storeUnsubscribe = null;
|
|
131
158
|
this.watchCleanups = new Set();
|
|
132
|
-
// Set alive = false before
|
|
133
|
-
//
|
|
159
|
+
// Set alive = false before the storeUnsub() call. A synchronous rerender callback
|
|
160
|
+
// from inside unsubscribe() therefore returns at once.
|
|
134
161
|
this.alive = false;
|
|
135
162
|
storeUnsub?.();
|
|
136
163
|
for (const cleanup of watchCleanups)
|
|
137
164
|
cleanup();
|
|
138
165
|
this.container.replaceChildren();
|
|
166
|
+
// The rule of the lifetime: the store that stays alive dies with the connection.
|
|
167
|
+
this.lastViewKey = undefined;
|
|
168
|
+
this.currentStore = null;
|
|
169
|
+
this.storeLifecycle.reset();
|
|
139
170
|
}
|
|
140
171
|
/**
|
|
141
|
-
*
|
|
172
|
+
* The number of live watch-subscription cleanup functions now.
|
|
142
173
|
*
|
|
143
|
-
* @internal
|
|
144
|
-
* collection
|
|
145
|
-
* public API and
|
|
174
|
+
* @internal This accessor is for a test only. A test asserts with it that the
|
|
175
|
+
* collection of the watch cleanups holds the live registrations, and that no mount
|
|
176
|
+
* leaks. It is not part of the public API, and it can change without a notice.
|
|
146
177
|
*/
|
|
147
178
|
get watchCleanupCount() {
|
|
148
179
|
return this.watchCleanups.size;
|
|
149
180
|
}
|
|
150
181
|
render(view) {
|
|
182
|
+
// ── The fast path of a slice ────────────────────────────────────────────
|
|
183
|
+
// The viewKey of the live tree is the same → only the /context projection changed.
|
|
184
|
+
// deriveCurrentView uses the selected meta entry as the key of each emission, and the
|
|
185
|
+
// authored spec of a state is static. Refresh the subtree of the store in place: the
|
|
186
|
+
// store subscription below calls incremental.update() already, and that function
|
|
187
|
+
// compares the snapshots and patches the elements of the change only. Skip the
|
|
188
|
+
// teardown completely, so that the ephemeral view state stays.
|
|
189
|
+
// createIncrementalRenderer binds the SPEC at its construction. Therefore this path
|
|
190
|
+
// is safe only because the spec is the same. A real view change, which means a
|
|
191
|
+
// different viewKey or no viewKey for a comparison, still rebuilds the tree below, as
|
|
192
|
+
// the discard-on-view-transition contract of the incremental renderer says.
|
|
193
|
+
if (view !== null &&
|
|
194
|
+
view.viewKey !== undefined &&
|
|
195
|
+
this.lastViewKey === view.viewKey &&
|
|
196
|
+
this.currentStore !== null &&
|
|
197
|
+
this.storeUnsubscribe !== null) {
|
|
198
|
+
refreshContextSubtree(this.currentStore, view);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
151
201
|
this.alive = true;
|
|
152
|
-
// Capture and clear before
|
|
153
|
-
//
|
|
154
|
-
// storeUnsubscribe and
|
|
155
|
-
//
|
|
202
|
+
// Capture the values and clear them before the call. Some store implementations
|
|
203
|
+
// fire a synchronous callback during their unsubscribe(). Without this order, that
|
|
204
|
+
// callback sees a live storeUnsubscribe, and it enters the rerender path again with
|
|
205
|
+
// an old view and an old store.
|
|
206
|
+
// disconnect() uses the same capture-then-clear pattern.
|
|
156
207
|
const oldUnsub = this.storeUnsubscribe;
|
|
157
208
|
this.storeUnsubscribe = null;
|
|
158
209
|
oldUnsub?.();
|
|
159
|
-
// Release watch cleanups
|
|
160
|
-
// Without this, watch subscriptions
|
|
161
|
-
//
|
|
162
|
-
//
|
|
210
|
+
// Release the watch cleanups of the first render of the previous view.
|
|
211
|
+
// Without this step, the watch subscriptions of the previous view stay active between
|
|
212
|
+
// a view transition and the first store update of the new view. They then hold a
|
|
213
|
+
// reference to a DOM element that is gone, and the garbage collector cannot take
|
|
214
|
+
// it.
|
|
163
215
|
const prevWatchCleanups = [...this.watchCleanups];
|
|
164
216
|
this.watchCleanups = new Set();
|
|
165
217
|
for (const cleanup of prevWatchCleanups)
|
|
166
218
|
cleanup();
|
|
167
219
|
this.container.replaceChildren();
|
|
220
|
+
// A null emission is a gap between two views, and not a new view: keep lastViewKey
|
|
221
|
+
// and currentStore. A return to the same identity therefore uses the store below
|
|
222
|
+
// again. disconnect() stays the boundary of the lifetime.
|
|
168
223
|
if (!view)
|
|
169
224
|
return;
|
|
170
|
-
// Resolve the store
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
225
|
+
// Resolve the store through the shared lifecycle. There are three results: the
|
|
226
|
+
// external store, which is the controlled mode, and the coordinator refreshes its
|
|
227
|
+
// /context, because the machinery owns /context in both modes; the store that the
|
|
228
|
+
// coordinator kept, when this emission returns to the live identity (a view, then
|
|
229
|
+
// null, then the same view — the fast path could not take this case, because the DOM
|
|
230
|
+
// is gone, and the coordinator refreshes /context in place, because the context can
|
|
231
|
+
// move during the gap); or a new store of this view, with the values of the composed
|
|
232
|
+
// state.
|
|
233
|
+
const resolution = this.storeLifecycle.resolve(this.actor, view, this.options.store);
|
|
234
|
+
this.lastViewKey = view.viewKey;
|
|
235
|
+
// The code keeps the store without a guard, for the refresh of the fast path of a slice.
|
|
236
|
+
this.currentStore = resolution.store;
|
|
237
|
+
try {
|
|
238
|
+
this.renderView(view, resolution.guardedStore);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
this.resetFailedRebuild();
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* A rebuild that fails must not leave the guard of the fast path armed on a dead
|
|
247
|
+
* tree, because the container is empty already. Therefore this code resets the view
|
|
248
|
+
* identity. The next emission, also one with the same viewKey, then makes a
|
|
249
|
+
* complete render again. It does not patch a store that nothing shows.
|
|
250
|
+
*/
|
|
251
|
+
resetFailedRebuild() {
|
|
252
|
+
const failedUnsub = this.storeUnsubscribe;
|
|
253
|
+
this.storeUnsubscribe = null;
|
|
254
|
+
failedUnsub?.();
|
|
255
|
+
this.lastViewKey = undefined;
|
|
256
|
+
this.currentStore = null;
|
|
257
|
+
this.storeLifecycle.reset();
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The tail of a complete rebuild, which can throw. See the catch block of render().
|
|
261
|
+
*
|
|
262
|
+
* @param guardedStore - Everything that the spec can write through, which includes
|
|
263
|
+
* $bindState, setState, and a chained set, receives the GUARDED store: /context is
|
|
264
|
+
* read-only to the spec, because the machine context changes through an event
|
|
265
|
+
* only.
|
|
266
|
+
*/
|
|
267
|
+
renderView(view, guardedStore) {
|
|
179
268
|
const send = this.actor.send.bind(this.actor);
|
|
180
|
-
// Build a SetState function
|
|
181
|
-
//
|
|
182
|
-
//
|
|
269
|
+
// Build a SetState function on the xstate store.
|
|
270
|
+
// The code calls it late, at the moment of an action, and not at the moment of the
|
|
271
|
+
// handlers() call. Therefore each action sees the newest state of the store.
|
|
183
272
|
const setState = (updater) => {
|
|
184
|
-
const prev =
|
|
273
|
+
const prev = guardedStore.getSnapshot();
|
|
185
274
|
const next = updater(prev);
|
|
186
|
-
// store.update()
|
|
187
|
-
// This
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
store
|
|
275
|
+
// The code calls store.update() with a complete snapshot of the state, and not with
|
|
276
|
+
// a map of a patch. This works with createStoreAdapter and with createStateStore,
|
|
277
|
+
// where update() merges the given top-level keys in a shallow way, and therefore
|
|
278
|
+
// replaces the complete model of the state. Change this call to a patch with a path
|
|
279
|
+
// as its key only after a change of the StateStore contract.
|
|
280
|
+
// The store with the guard lets the context key of this read-modify-write round trip
|
|
281
|
+
// pass, because the code touches that key not. A real mutation of /context
|
|
282
|
+
// throws.
|
|
283
|
+
guardedStore.update(next);
|
|
192
284
|
};
|
|
193
|
-
// Resolve handlers from registryResult
|
|
285
|
+
// Resolve the handlers from registryResult. This connects setState and getState of the store.
|
|
194
286
|
const handlers = this.options.registryResult
|
|
195
|
-
? this.options.registryResult.handlers(() => setState, () =>
|
|
287
|
+
? this.options.registryResult.handlers(() => setState, () => guardedStore.getSnapshot())
|
|
196
288
|
: {};
|
|
197
289
|
const loading = this.options.loading;
|
|
198
290
|
const functions = this.options.functions;
|
|
@@ -201,28 +293,29 @@ export class PlayRenderer {
|
|
|
201
293
|
const navigate = this.options.navigate;
|
|
202
294
|
const onConfirm = this.options.onConfirm;
|
|
203
295
|
const onRenderError = this.options.onRenderError;
|
|
204
|
-
// ──
|
|
205
|
-
//
|
|
206
|
-
// renderer
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
// the same skip-on-mount baseline
|
|
210
|
-
//
|
|
211
|
-
//
|
|
296
|
+
// ── The lifecycle of a watch ─────────────────────────────────────────────
|
|
297
|
+
// A watch subscription follows the MOUNT of an element, and the registry of each
|
|
298
|
+
// instance in the incremental renderer manages it: an element of the first render
|
|
299
|
+
// registers then, and an element that appears later, from a visibility that becomes
|
|
300
|
+
// true or from a repeat item that the code appends, registers at that moment, with
|
|
301
|
+
// the same skip-on-mount baseline. The renderer releases a subscription when its
|
|
302
|
+
// element unmounts, from a visibility that becomes false or from a repeat item that
|
|
303
|
+
// the code removes. It also releases each subscription on a view change, at the top of
|
|
304
|
+
// this method, and on a disconnect().
|
|
212
305
|
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
// in
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
306
|
+
// A stable element does NOT receive a new watch on each rerender. The code registers
|
|
307
|
+
// the update subscription below with the store before the watch subscriptions of
|
|
308
|
+
// renderSpec, and @xstate/store notifies in the order of the insertion. If a render
|
|
309
|
+
// in place removed the watchers and made them again, each old watcher would die in
|
|
310
|
+
// the queue, before it fires, and each new watcher would take the snapshot after the
|
|
311
|
+
// update as its baseline. A watch action would then fire never. Therefore the registry
|
|
312
|
+
// keeps the ORIGINAL subscription, with its baseline and its position in the
|
|
313
|
+
// notification, whenever the code only renders an instance again. It registers a
|
|
314
|
+
// real new mount only.
|
|
222
315
|
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
316
|
+
// Each registered cleanup is idempotent, and the Set watchCleanups also holds it.
|
|
317
|
+
// Therefore a second call here, after the incremental renderer released it already, is
|
|
318
|
+
// safe.
|
|
226
319
|
const renderOptions = {
|
|
227
320
|
send,
|
|
228
321
|
handlers,
|
|
@@ -231,11 +324,11 @@ export class PlayRenderer {
|
|
|
231
324
|
...(directives !== undefined ? { directives } : {}),
|
|
232
325
|
...(loading !== undefined ? { loading } : {}),
|
|
233
326
|
onWatchSetup: (cleanup) => {
|
|
234
|
-
// Wrap
|
|
235
|
-
// here, OR from the incremental renderer when the
|
|
236
|
-
// unmounts
|
|
237
|
-
// renderer registers THIS wrapper and calls it on
|
|
238
|
-
// the Set in step with live subscriptions.
|
|
327
|
+
// Wrap each cleanup. A call of the wrapper, from the disconnect() here or from the
|
|
328
|
+
// view change here, OR from the incremental renderer when the element of the cleanup
|
|
329
|
+
// unmounts, also removes the cleanup from the Set. The code returns the wrapper.
|
|
330
|
+
// Therefore the incremental renderer registers THIS wrapper and calls it on a
|
|
331
|
+
// release, and the Set stays in step with the live subscriptions.
|
|
239
332
|
const entry = () => {
|
|
240
333
|
this.watchCleanups.delete(entry);
|
|
241
334
|
cleanup();
|
|
@@ -247,18 +340,20 @@ export class PlayRenderer {
|
|
|
247
340
|
...(navigate !== undefined ? { navigate } : {}),
|
|
248
341
|
...(onConfirm !== undefined ? { onConfirm } : {}),
|
|
249
342
|
};
|
|
250
|
-
// ──
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
const incremental = createIncrementalRenderer(view,
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
//
|
|
343
|
+
// ── The incremental updates ─────────────────────────────────────────────
|
|
344
|
+
// A store update rebuilds the complete tree no longer: the incremental renderer
|
|
345
|
+
// compares the previous snapshot and the next snapshot on the tracked $state paths of
|
|
346
|
+
// each element, and it renders the elements of the change again, in place. An element
|
|
347
|
+
// without a state binding keeps the identity of its DOM node. Therefore the focus, the
|
|
348
|
+
// selection, and the scroll position survive an update of another element. A view
|
|
349
|
+
// transition still rebuilds everything, because this method runs again.
|
|
350
|
+
const incremental = createIncrementalRenderer(view, guardedStore, this.registry, this.container, renderOptions);
|
|
351
|
+
// subscribe passes through the guard without a change, and it keeps the same function
|
|
352
|
+
// reference. The guard wraps set and update only.
|
|
353
|
+
this.storeUnsubscribe = guardedStore.subscribe(() => {
|
|
354
|
+
// The guard: stop when a caller disconnected the PlayRenderer.
|
|
355
|
+
// This handles the case of a StateStore implementation that fires its subscribed
|
|
356
|
+
// callbacks synchronously inside its own unsubscribe() call.
|
|
262
357
|
if (!this.alive)
|
|
263
358
|
return;
|
|
264
359
|
incremental.update();
|
package/dist/PlayRenderer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlayRenderer.js","sourceRoot":"","sources":["../src/PlayRenderer.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"PlayRenderer.js","sourceRoot":"","sources":["../src/PlayRenderer.ts"],"names":[],"mappings":"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"}
|