@xmachines/play-dom 2.0.0 → 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 +99 -65
- package/dist/PlayRenderer.d.ts +82 -77
- package/dist/PlayRenderer.d.ts.map +1 -1
- package/dist/PlayRenderer.js +173 -161
- 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 +19 -19
- package/dist/index.js +21 -21
- 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 +6 -5
- 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,12 +1,14 @@
|
|
|
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";
|
|
@@ -14,26 +16,27 @@ import { xstateStoreStateStore } from "@xmachines/json-render-xstate";
|
|
|
14
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,56 +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
|
-
* viewKey of the
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* A null emission
|
|
81
|
-
* the DOM
|
|
82
|
-
* the same view
|
|
83
|
-
* the framework providers.
|
|
84
|
-
* not outlive the connection.
|
|
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.
|
|
85
89
|
*/
|
|
86
90
|
lastViewKey = undefined;
|
|
87
91
|
currentStore = null;
|
|
88
92
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
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.
|
|
94
99
|
*/
|
|
95
100
|
storeLifecycle = createViewStoreLifecycle((seed) => xstateStoreStateStore({ atom: createAtom(seed) }));
|
|
96
101
|
/**
|
|
97
|
-
*
|
|
98
|
-
* The `rerender` closure
|
|
99
|
-
* callback
|
|
100
|
-
*
|
|
101
|
-
* detached DOM tree.
|
|
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.
|
|
102
106
|
*/
|
|
103
107
|
alive = true;
|
|
104
108
|
/**
|
|
105
|
-
* @param container - `HTMLElement` to render into.
|
|
106
|
-
* @param actor -
|
|
107
|
-
* @param registry -
|
|
108
|
-
* @param options -
|
|
109
|
-
* - `registryResult` —
|
|
110
|
-
* - `store` — external `StateStore
|
|
111
|
-
* - `loading` — streaming mode
|
|
112
|
-
* - `functions` — named compute functions
|
|
113
|
-
* - `directives` —
|
|
114
|
-
* - `validationFunctions` —
|
|
115
|
-
* - `navigate` — navigation callback
|
|
116
|
-
* - `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.
|
|
117
121
|
*/
|
|
118
122
|
constructor(container, actor, registry, options = {}) {
|
|
119
123
|
this.container = container;
|
|
@@ -122,13 +126,13 @@ export class PlayRenderer {
|
|
|
122
126
|
this.options = options;
|
|
123
127
|
}
|
|
124
128
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
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.
|
|
127
131
|
*
|
|
128
|
-
*
|
|
129
|
-
* `connect()
|
|
130
|
-
*
|
|
131
|
-
*
|
|
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.
|
|
132
136
|
*/
|
|
133
137
|
connect() {
|
|
134
138
|
if (this.unwatch !== null)
|
|
@@ -137,53 +141,55 @@ export class PlayRenderer {
|
|
|
137
141
|
this.unwatch = watchSignal(this.actor.currentView, (view) => this.render(view));
|
|
138
142
|
}
|
|
139
143
|
/**
|
|
140
|
-
*
|
|
144
|
+
* Stops the watch and clears the container.
|
|
141
145
|
*/
|
|
142
146
|
disconnect() {
|
|
143
147
|
this.unwatch?.();
|
|
144
148
|
this.unwatch = null;
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
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.
|
|
149
155
|
const storeUnsub = this.storeUnsubscribe;
|
|
150
156
|
const watchCleanups = [...this.watchCleanups];
|
|
151
157
|
this.storeUnsubscribe = null;
|
|
152
158
|
this.watchCleanups = new Set();
|
|
153
|
-
// Set alive = false before
|
|
154
|
-
//
|
|
159
|
+
// Set alive = false before the storeUnsub() call. A synchronous rerender callback
|
|
160
|
+
// from inside unsubscribe() therefore returns at once.
|
|
155
161
|
this.alive = false;
|
|
156
162
|
storeUnsub?.();
|
|
157
163
|
for (const cleanup of watchCleanups)
|
|
158
164
|
cleanup();
|
|
159
165
|
this.container.replaceChildren();
|
|
160
|
-
//
|
|
166
|
+
// The rule of the lifetime: the store that stays alive dies with the connection.
|
|
161
167
|
this.lastViewKey = undefined;
|
|
162
168
|
this.currentStore = null;
|
|
163
169
|
this.storeLifecycle.reset();
|
|
164
170
|
}
|
|
165
171
|
/**
|
|
166
|
-
*
|
|
172
|
+
* The number of live watch-subscription cleanup functions now.
|
|
167
173
|
*
|
|
168
|
-
* @internal
|
|
169
|
-
* collection
|
|
170
|
-
* 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.
|
|
171
177
|
*/
|
|
172
178
|
get watchCleanupCount() {
|
|
173
179
|
return this.watchCleanups.size;
|
|
174
180
|
}
|
|
175
181
|
render(view) {
|
|
176
|
-
// ──
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
// authored spec is static
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
// createIncrementalRenderer binds the SPEC at construction
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
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.
|
|
187
193
|
if (view !== null &&
|
|
188
194
|
view.viewKey !== undefined &&
|
|
189
195
|
this.lastViewKey === view.viewKey &&
|
|
@@ -193,37 +199,40 @@ export class PlayRenderer {
|
|
|
193
199
|
return;
|
|
194
200
|
}
|
|
195
201
|
this.alive = true;
|
|
196
|
-
// Capture and clear before
|
|
197
|
-
//
|
|
198
|
-
// storeUnsubscribe and
|
|
199
|
-
//
|
|
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.
|
|
200
207
|
const oldUnsub = this.storeUnsubscribe;
|
|
201
208
|
this.storeUnsubscribe = null;
|
|
202
209
|
oldUnsub?.();
|
|
203
|
-
// Release watch cleanups
|
|
204
|
-
// Without this, watch subscriptions
|
|
205
|
-
//
|
|
206
|
-
//
|
|
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.
|
|
207
215
|
const prevWatchCleanups = [...this.watchCleanups];
|
|
208
216
|
this.watchCleanups = new Set();
|
|
209
217
|
for (const cleanup of prevWatchCleanups)
|
|
210
218
|
cleanup();
|
|
211
219
|
this.container.replaceChildren();
|
|
212
|
-
// A null emission is a gap between views, not a new view: keep
|
|
213
|
-
//
|
|
214
|
-
//
|
|
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.
|
|
215
223
|
if (!view)
|
|
216
224
|
return;
|
|
217
|
-
// Resolve the store through the shared lifecycle:
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
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
|
|
223
232
|
// state.
|
|
224
233
|
const resolution = this.storeLifecycle.resolve(this.actor, view, this.options.store);
|
|
225
234
|
this.lastViewKey = view.viewKey;
|
|
226
|
-
//
|
|
235
|
+
// The code keeps the store without a guard, for the refresh of the fast path of a slice.
|
|
227
236
|
this.currentStore = resolution.store;
|
|
228
237
|
try {
|
|
229
238
|
this.renderView(view, resolution.guardedStore);
|
|
@@ -234,10 +243,10 @@ export class PlayRenderer {
|
|
|
234
243
|
}
|
|
235
244
|
}
|
|
236
245
|
/**
|
|
237
|
-
* A
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
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.
|
|
241
250
|
*/
|
|
242
251
|
resetFailedRebuild() {
|
|
243
252
|
const failedUnsub = this.storeUnsubscribe;
|
|
@@ -248,30 +257,32 @@ export class PlayRenderer {
|
|
|
248
257
|
this.storeLifecycle.reset();
|
|
249
258
|
}
|
|
250
259
|
/**
|
|
251
|
-
* The
|
|
260
|
+
* The tail of a complete rebuild, which can throw. See the catch block of render().
|
|
252
261
|
*
|
|
253
|
-
* @param guardedStore - Everything the spec can write through
|
|
254
|
-
* setState, chained set
|
|
255
|
-
* the spec
|
|
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.
|
|
256
266
|
*/
|
|
257
267
|
renderView(view, guardedStore) {
|
|
258
268
|
const send = this.actor.send.bind(this.actor);
|
|
259
|
-
// Build a SetState function
|
|
260
|
-
//
|
|
261
|
-
//
|
|
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.
|
|
262
272
|
const setState = (updater) => {
|
|
263
273
|
const prev = guardedStore.getSnapshot();
|
|
264
274
|
const next = updater(prev);
|
|
265
|
-
// store.update()
|
|
266
|
-
// This
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
// The
|
|
271
|
-
//
|
|
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.
|
|
272
283
|
guardedStore.update(next);
|
|
273
284
|
};
|
|
274
|
-
// Resolve handlers from registryResult
|
|
285
|
+
// Resolve the handlers from registryResult. This connects setState and getState of the store.
|
|
275
286
|
const handlers = this.options.registryResult
|
|
276
287
|
? this.options.registryResult.handlers(() => setState, () => guardedStore.getSnapshot())
|
|
277
288
|
: {};
|
|
@@ -282,28 +293,29 @@ export class PlayRenderer {
|
|
|
282
293
|
const navigate = this.options.navigate;
|
|
283
294
|
const onConfirm = this.options.onConfirm;
|
|
284
295
|
const onRenderError = this.options.onRenderError;
|
|
285
|
-
// ──
|
|
286
|
-
//
|
|
287
|
-
// renderer
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
// the same skip-on-mount baseline
|
|
291
|
-
//
|
|
292
|
-
//
|
|
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().
|
|
293
305
|
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
// in
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
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.
|
|
303
315
|
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
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.
|
|
307
319
|
const renderOptions = {
|
|
308
320
|
send,
|
|
309
321
|
handlers,
|
|
@@ -312,11 +324,11 @@ export class PlayRenderer {
|
|
|
312
324
|
...(directives !== undefined ? { directives } : {}),
|
|
313
325
|
...(loading !== undefined ? { loading } : {}),
|
|
314
326
|
onWatchSetup: (cleanup) => {
|
|
315
|
-
// Wrap
|
|
316
|
-
// here, OR from the incremental renderer when the
|
|
317
|
-
// unmounts
|
|
318
|
-
// renderer registers THIS wrapper and calls it on
|
|
319
|
-
// 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.
|
|
320
332
|
const entry = () => {
|
|
321
333
|
this.watchCleanups.delete(entry);
|
|
322
334
|
cleanup();
|
|
@@ -328,20 +340,20 @@ export class PlayRenderer {
|
|
|
328
340
|
...(navigate !== undefined ? { navigate } : {}),
|
|
329
341
|
...(onConfirm !== undefined ? { onConfirm } : {}),
|
|
330
342
|
};
|
|
331
|
-
// ──
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
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.
|
|
338
350
|
const incremental = createIncrementalRenderer(view, guardedStore, this.registry, this.container, renderOptions);
|
|
339
|
-
// subscribe passes through the guard
|
|
340
|
-
// reference
|
|
351
|
+
// subscribe passes through the guard without a change, and it keeps the same function
|
|
352
|
+
// reference. The guard wraps set and update only.
|
|
341
353
|
this.storeUnsubscribe = guardedStore.subscribe(() => {
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
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.
|
|
345
357
|
if (!this.alive)
|
|
346
358
|
return;
|
|
347
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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"}
|