@zubra/runtime 5.9.0 → 5.9.2
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 +7 -1
- package/dist/production/effect.js +13 -10
- package/dist/production/element.js +44 -14
- package/dist/production/signal-impl.js +235 -165
- package/dist/production/spread.js +15 -0
- package/package.json +1 -1
- package/src/devtools.js +1 -1
- package/src/effect.js +13 -10
- package/src/element.js +44 -14
- package/src/signal-impl.js +235 -165
- package/src/spread.js +15 -0
package/src/effect.js
CHANGED
|
@@ -31,7 +31,7 @@ let owner = null; // the Effect or Scope currently running
|
|
|
31
31
|
let nextId = 0;
|
|
32
32
|
let scheduled = null; // promise for the pending microtask flush
|
|
33
33
|
let flushing = false;
|
|
34
|
-
const
|
|
34
|
+
const EFFECT = Symbol('effect');
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* `Symbol.dispose`, or, where the engine doesn't have it yet, the symbol TypeScript's downlevel
|
|
@@ -92,15 +92,18 @@ function report(error) {
|
|
|
92
92
|
class Owner {
|
|
93
93
|
constructor(parent) {
|
|
94
94
|
this.parent = parent;
|
|
95
|
-
this.children =
|
|
95
|
+
this.children = null; // created on first child: most effects have none
|
|
96
96
|
this.disposed = false;
|
|
97
97
|
this.paused = false;
|
|
98
|
-
parent
|
|
98
|
+
if (parent) (parent.children ??= new Set()).add(this);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
disposeChildren() {
|
|
102
|
+
const children = this.children;
|
|
103
|
+
if (children === null || children.size === 0) return;
|
|
104
|
+
this.children = null;
|
|
102
105
|
batch(() => {
|
|
103
|
-
for (const child of
|
|
106
|
+
for (const child of children) { child.parent = null; child.dispose(); }
|
|
104
107
|
});
|
|
105
108
|
}
|
|
106
109
|
|
|
@@ -108,7 +111,7 @@ class Owner {
|
|
|
108
111
|
if (this.disposed) return;
|
|
109
112
|
this.disposed = true;
|
|
110
113
|
this.disposeChildren();
|
|
111
|
-
this.parent?.children
|
|
114
|
+
this.parent?.children?.delete(this);
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
[DISPOSE]() {
|
|
@@ -118,7 +121,7 @@ class Owner {
|
|
|
118
121
|
pause() {
|
|
119
122
|
if (this.paused || this.disposed) return;
|
|
120
123
|
this.paused = true;
|
|
121
|
-
batch(() => {
|
|
124
|
+
if (this.children) batch(() => {
|
|
122
125
|
for (const child of [...this.children]) child.pause();
|
|
123
126
|
});
|
|
124
127
|
}
|
|
@@ -128,7 +131,7 @@ class Owner {
|
|
|
128
131
|
this.paused = false;
|
|
129
132
|
// Children may be replaced while their parent resumes, so iterate a snapshot and skip
|
|
130
133
|
// anything that was disposed along the way.
|
|
131
|
-
for (const child of [...this.children]) if (!child.disposed) child.resume();
|
|
134
|
+
if (this.children) for (const child of [...this.children]) if (!child.disposed) child.resume();
|
|
132
135
|
}
|
|
133
136
|
}
|
|
134
137
|
|
|
@@ -152,7 +155,7 @@ class Effect extends Owner {
|
|
|
152
155
|
owner = previous;
|
|
153
156
|
}
|
|
154
157
|
});
|
|
155
|
-
|
|
158
|
+
this.computed[EFFECT] = this;
|
|
156
159
|
watcher.watch(this.computed);
|
|
157
160
|
}
|
|
158
161
|
|
|
@@ -187,7 +190,7 @@ class Effect extends Owner {
|
|
|
187
190
|
this.paused = false;
|
|
188
191
|
watchAgain(this.computed);
|
|
189
192
|
try { this.run(); } catch (error) { report(error); }
|
|
190
|
-
for (const child of [...this.children]) if (!child.disposed) child.resume();
|
|
193
|
+
if (this.children) for (const child of [...this.children]) if (!child.disposed) child.resume();
|
|
191
194
|
}
|
|
192
195
|
}
|
|
193
196
|
|
|
@@ -255,7 +258,7 @@ export function flush() {
|
|
|
255
258
|
report(new Error('zubra: effects keep re-triggering each other; stopped after 100 rounds'));
|
|
256
259
|
return;
|
|
257
260
|
}
|
|
258
|
-
const effects = pending.map((computed) =>
|
|
261
|
+
const effects = pending.map((computed) => computed[EFFECT]).sort((a, b) => a.id - b.id);
|
|
259
262
|
for (const e of effects) {
|
|
260
263
|
if (e.disposed || e.paused) continue;
|
|
261
264
|
try { e.run(); } catch (error) { report(error); }
|
package/src/element.js
CHANGED
|
@@ -559,6 +559,25 @@ function blueprint(source, parent = null) {
|
|
|
559
559
|
return content;
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
+
// For a blueprint with no custom elements: a copy owned by this document, made once, which rows
|
|
563
|
+
// clone. Its clones need no adopting when they're inserted, and nothing in it has to upgrade.
|
|
564
|
+
const imported = new WeakMap();
|
|
565
|
+
const upgrades = (content) =>
|
|
566
|
+
[...content.querySelectorAll('*')].some((element) => element.localName.includes('-') || element.hasAttribute('is'));
|
|
567
|
+
|
|
568
|
+
/** A fresh copy of `block`'s template for `parent`, ready for its parts to be found and bound. */
|
|
569
|
+
function freshCopy(block, parent) {
|
|
570
|
+
const content = blueprint(block, parent);
|
|
571
|
+
let copy = imported.get(content);
|
|
572
|
+
if (copy === undefined) {
|
|
573
|
+
// importNode: custom elements inside upgrade now, so their properties exist before parts
|
|
574
|
+
// bind (a plain clone stays in the template's inert document).
|
|
575
|
+
copy = upgrades(content) ? null : document.importNode(content, true);
|
|
576
|
+
imported.set(content, copy);
|
|
577
|
+
}
|
|
578
|
+
return copy === null ? document.importNode(content, true) : copy.cloneNode(true);
|
|
579
|
+
}
|
|
580
|
+
|
|
562
581
|
function clearBetween(open, close) {
|
|
563
582
|
while (open.nextSibling && open.nextSibling !== close) open.nextSibling.remove();
|
|
564
583
|
}
|
|
@@ -619,10 +638,8 @@ function mountBlock(block, args, open, close, hydrating, context) {
|
|
|
619
638
|
clearBetween(open, close);
|
|
620
639
|
// Find and bind the parts in the fragment, then insert it: child components inside see
|
|
621
640
|
// their props from the start, and can't move slotted nodes before they're found.
|
|
622
|
-
// importNode, not cloneNode: defined custom elements upgrade now, so their properties exist
|
|
623
|
-
// before parts bind (a clone stays in the template's inert document).
|
|
624
641
|
const parent = parentOf(close);
|
|
625
|
-
const fragment =
|
|
642
|
+
const fragment = freshCopy(block, parent);
|
|
626
643
|
fragmentParents.set(fragment, parent);
|
|
627
644
|
const fresh = locate(childRange(fragment), parts);
|
|
628
645
|
parts.forEach((part, index) => bind(part, fresh[index], false, context));
|
|
@@ -756,7 +773,12 @@ function bind(part, node, hydrating, context) {
|
|
|
756
773
|
if (part.text) {
|
|
757
774
|
effect(() => {
|
|
758
775
|
const text = part.text();
|
|
759
|
-
|
|
776
|
+
// A lone Text child is compared and written in place (`.data`): reading `textContent`
|
|
777
|
+
// walks the subtree, and setting it replaces the Text node.
|
|
778
|
+
const child = node.firstChild;
|
|
779
|
+
if (child !== null && child === node.lastChild && child.nodeType === TEXT) {
|
|
780
|
+
if (child.data !== text) child.data = text;
|
|
781
|
+
} else if (node.textContent !== text) node.textContent = text;
|
|
760
782
|
});
|
|
761
783
|
} else if (part.anchor) {
|
|
762
784
|
effect(() => {
|
|
@@ -1317,19 +1339,27 @@ function bindEach(part, open, hydrating, context) {
|
|
|
1317
1339
|
const rowsOwner = scope();
|
|
1318
1340
|
let rows = [];
|
|
1319
1341
|
|
|
1342
|
+
// A row is the nodes from `open` to `close`. Rows the server rendered keep the markers it put
|
|
1343
|
+
// around them. A row made here is its block's own nodes, from first to last, with no markers:
|
|
1344
|
+
// what a block's nodes are at the top never changes after it's bound (a region inside one keeps
|
|
1345
|
+
// its own markers), so the first and last stay the row's ends. Only an empty block needs one.
|
|
1320
1346
|
const createRow = (value, index, key, rowOpen, rowClose, hydrateRow) => {
|
|
1321
|
-
if (!rowOpen) {
|
|
1322
|
-
// A new row is assembled in a fragment, then placed.
|
|
1323
|
-
rowOpen = document.createComment('[');
|
|
1324
|
-
rowClose = document.createComment(']');
|
|
1325
|
-
const fragment = new DocumentFragment();
|
|
1326
|
-
fragmentParents.set(fragment, parentOf(open));
|
|
1327
|
-
fragment.append(rowOpen, rowClose);
|
|
1328
|
-
}
|
|
1329
1347
|
const item = new Signal.State(value);
|
|
1330
1348
|
const position = new Signal.State(index);
|
|
1331
|
-
|
|
1332
|
-
|
|
1349
|
+
if (rowOpen) {
|
|
1350
|
+
const owner = rowsOwner.run(() => scope(() => mountBlock(part.block, [item, position], rowOpen, rowClose, hydrateRow, context)));
|
|
1351
|
+
return { key, item, position, open: rowOpen, close: rowClose, owner };
|
|
1352
|
+
}
|
|
1353
|
+
const parent = parentOf(open);
|
|
1354
|
+
const fragment = freshCopy(part.block, parent);
|
|
1355
|
+
fragmentParents.set(fragment, parent);
|
|
1356
|
+
const owner = rowsOwner.run(() => scope(() => {
|
|
1357
|
+
const parts = part.block.parts(item, position);
|
|
1358
|
+
const nodes = locate(childRange(fragment), parts);
|
|
1359
|
+
parts.forEach((each, at) => bind(each, nodes[at], false, context));
|
|
1360
|
+
}));
|
|
1361
|
+
if (!fragment.firstChild) fragment.append(document.createComment(''));
|
|
1362
|
+
return { key, item, position, open: fragment.firstChild, close: fragment.lastChild, owner };
|
|
1333
1363
|
};
|
|
1334
1364
|
const nodesOf = (row) => {
|
|
1335
1365
|
const nodes = [];
|