@schukai/monster 4.148.8 → 4.148.9
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/package.json +1 -1
- package/source/dom/customelement.mjs +105 -40
- package/source/dom/updater.mjs +381 -119
- package/test/cases/dom/customelement.mjs +136 -1
- package/test/cases/dom/updater.mjs +174 -0
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.148.
|
|
1
|
+
{"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.148.9"}
|
|
@@ -122,6 +122,20 @@ const managedShadowRootSymbol = Symbol("managedShadowRoot");
|
|
|
122
122
|
const visibilityStateSymbol = Symbol("visibilityState");
|
|
123
123
|
let hostVisibilityStyleSheet = null;
|
|
124
124
|
|
|
125
|
+
class RevisionObserver extends Observer {
|
|
126
|
+
#onNotification;
|
|
127
|
+
|
|
128
|
+
constructor(callback, onNotification) {
|
|
129
|
+
super(callback);
|
|
130
|
+
this.#onNotification = onNotification;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
update(subject) {
|
|
134
|
+
this.#onNotification();
|
|
135
|
+
return super.update(subject);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
125
139
|
/**
|
|
126
140
|
* The `CustomElement` class provides a way to define a new HTML element using the power of Custom Elements.
|
|
127
141
|
*
|
|
@@ -1207,20 +1221,42 @@ function initOptionObserver() {
|
|
|
1207
1221
|
|
|
1208
1222
|
self[internalSymbol].syncDisabledState = syncDisabledState;
|
|
1209
1223
|
|
|
1224
|
+
let updaterSyncRevision = 0;
|
|
1210
1225
|
self.attachObserver(
|
|
1211
|
-
new
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1226
|
+
new RevisionObserver(
|
|
1227
|
+
async function () {
|
|
1228
|
+
let processedRevision;
|
|
1229
|
+
do {
|
|
1230
|
+
processedRevision = updaterSyncRevision;
|
|
1231
|
+
if (hasObjectLink(self, customElementUpdaterLinkSymbol)) {
|
|
1232
|
+
const source = clone(
|
|
1233
|
+
self[internalSymbol].getRealSubject()["options"],
|
|
1234
|
+
);
|
|
1235
|
+
const notifications = [];
|
|
1236
|
+
const updaters = getLinkedObjects(
|
|
1237
|
+
self,
|
|
1238
|
+
customElementUpdaterLinkSymbol,
|
|
1239
|
+
);
|
|
1240
|
+
|
|
1241
|
+
for (const list of updaters) {
|
|
1242
|
+
for (const updater of list) {
|
|
1243
|
+
syncUpdaterSubject(updater.getSubject(), source);
|
|
1244
|
+
const subject = updater?.[internalSymbol]?.subject;
|
|
1245
|
+
if (subject instanceof ProxyObserver) {
|
|
1246
|
+
notifications.push(subject.notifyObservers());
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
await Promise.all(notifications);
|
|
1251
|
+
}
|
|
1216
1252
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1253
|
+
await Promise.resolve();
|
|
1254
|
+
} while (processedRevision !== updaterSyncRevision);
|
|
1255
|
+
},
|
|
1256
|
+
() => {
|
|
1257
|
+
updaterSyncRevision++;
|
|
1258
|
+
},
|
|
1259
|
+
),
|
|
1224
1260
|
);
|
|
1225
1261
|
|
|
1226
1262
|
self[attributeObserverSymbol][ATTRIBUTE_DISABLED] = () => {
|
|
@@ -1258,39 +1294,68 @@ function syncUpdaterSubject(target, source) {
|
|
|
1258
1294
|
}
|
|
1259
1295
|
|
|
1260
1296
|
for (const [key, value] of Object.entries(source)) {
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
(typeof DocumentFragment !== "undefined" &&
|
|
1265
|
-
value instanceof DocumentFragment)
|
|
1266
|
-
) {
|
|
1267
|
-
target[key] = value;
|
|
1268
|
-
continue;
|
|
1269
|
-
}
|
|
1297
|
+
syncUpdaterValue(target, key, value);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1270
1300
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1301
|
+
/**
|
|
1302
|
+
* Synchronizes an updater value while preserving proxied array and plain-object
|
|
1303
|
+
* identities. Equal primitive entries therefore do not notify observers again.
|
|
1304
|
+
* @private
|
|
1305
|
+
* @param {object|array} target
|
|
1306
|
+
* @param {string|number} key
|
|
1307
|
+
* @param {*} value
|
|
1308
|
+
* @return {void}
|
|
1309
|
+
*/
|
|
1310
|
+
function syncUpdaterValue(target, key, value) {
|
|
1311
|
+
if (
|
|
1312
|
+
isElement(value) ||
|
|
1313
|
+
(typeof Document !== "undefined" && value instanceof Document) ||
|
|
1314
|
+
(typeof DocumentFragment !== "undefined" &&
|
|
1315
|
+
value instanceof DocumentFragment)
|
|
1316
|
+
) {
|
|
1317
|
+
target[key] = value;
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
if (isArray(value)) {
|
|
1322
|
+
if (!isArray(target?.[key])) {
|
|
1323
|
+
target[key] = clone(value);
|
|
1324
|
+
return;
|
|
1278
1325
|
}
|
|
1326
|
+
syncUpdaterArray(target[key], value);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1279
1329
|
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
}
|
|
1286
|
-
if (!isObject(target?.[key]) || isArray(target?.[key])) {
|
|
1287
|
-
target[key] = {};
|
|
1288
|
-
}
|
|
1289
|
-
syncUpdaterSubject(target[key], value);
|
|
1290
|
-
continue;
|
|
1330
|
+
if (isObject(value)) {
|
|
1331
|
+
const proto = Object.getPrototypeOf(value);
|
|
1332
|
+
if (proto && proto !== Object.prototype) {
|
|
1333
|
+
target[key] = value;
|
|
1334
|
+
return;
|
|
1291
1335
|
}
|
|
1336
|
+
if (!isObject(target?.[key]) || isArray(target?.[key])) {
|
|
1337
|
+
target[key] = {};
|
|
1338
|
+
}
|
|
1339
|
+
syncUpdaterSubject(target[key], value);
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1292
1342
|
|
|
1293
|
-
|
|
1343
|
+
target[key] = value;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/**
|
|
1347
|
+
* @private
|
|
1348
|
+
* @param {array} target
|
|
1349
|
+
* @param {array} source
|
|
1350
|
+
* @return {void}
|
|
1351
|
+
*/
|
|
1352
|
+
function syncUpdaterArray(target, source) {
|
|
1353
|
+
for (let index = 0; index < source.length; index++) {
|
|
1354
|
+
syncUpdaterValue(target, index, source[index]);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
if (target.length !== source.length) {
|
|
1358
|
+
target.length = source.length;
|
|
1294
1359
|
}
|
|
1295
1360
|
}
|
|
1296
1361
|
|
package/source/dom/updater.mjs
CHANGED
|
@@ -91,6 +91,8 @@ const subjectObserverSymbol = Symbol("subjectObserver");
|
|
|
91
91
|
const subjectRevisionSymbol = Symbol("subjectRevision");
|
|
92
92
|
const patchNodeKeySymbol = Symbol("patchNodeKey");
|
|
93
93
|
const queuedSnapshotSymbol = Symbol("queuedSnapshot");
|
|
94
|
+
const fullRefreshRequestedSymbol = Symbol("fullRefreshRequested");
|
|
95
|
+
const replacementValueCacheSymbol = Symbol("replacementValueCache");
|
|
94
96
|
|
|
95
97
|
/**
|
|
96
98
|
* Tracks every notification request, including requests that Observer deduplicates
|
|
@@ -188,6 +190,8 @@ class Updater extends Base {
|
|
|
188
190
|
this[disposedSymbol] = false;
|
|
189
191
|
this[subjectRevisionSymbol] = 0;
|
|
190
192
|
this[queuedSnapshotSymbol] = clone(this[internalSymbol].last);
|
|
193
|
+
this[fullRefreshRequestedSymbol] = false;
|
|
194
|
+
this[replacementValueCacheSymbol] = new WeakMap();
|
|
191
195
|
this[controlEventTimersSymbol] = new Map();
|
|
192
196
|
this[registeredEventTypesSymbol] = new Set();
|
|
193
197
|
|
|
@@ -201,7 +205,11 @@ class Updater extends Base {
|
|
|
201
205
|
}
|
|
202
206
|
|
|
203
207
|
const real = this[internalSymbol].subject.getRealSubject();
|
|
204
|
-
const
|
|
208
|
+
const previous = this[fullRefreshRequestedSymbol]
|
|
209
|
+
? { __init__: true }
|
|
210
|
+
: this[queuedSnapshotSymbol];
|
|
211
|
+
this[fullRefreshRequestedSymbol] = false;
|
|
212
|
+
const diffResult = diff(previous, real);
|
|
205
213
|
if (diffResult.length > 0) {
|
|
206
214
|
const snapshot = clone(real);
|
|
207
215
|
this[queuedSnapshotSymbol] = snapshot;
|
|
@@ -239,11 +247,15 @@ class Updater extends Base {
|
|
|
239
247
|
}
|
|
240
248
|
|
|
241
249
|
const { diffResult, snapshot } = this[pendingDiffsSymbol].shift();
|
|
250
|
+
const renderContext = createRenderContext();
|
|
242
251
|
if (this[internalSymbol].features.batchUpdates === true) {
|
|
243
252
|
const updatePaths = new Map();
|
|
244
253
|
for (const change of Object.values(diffResult)) {
|
|
245
|
-
removeElement.call(this, change);
|
|
246
|
-
insertElement.call(this, change);
|
|
254
|
+
const removed = removeElement.call(this, change, renderContext);
|
|
255
|
+
const inserted = insertElement.call(this, change, renderContext);
|
|
256
|
+
if (removed || inserted) {
|
|
257
|
+
invalidateRenderCandidates(renderContext);
|
|
258
|
+
}
|
|
247
259
|
|
|
248
260
|
const path = isArray(change?.["path"]) ? change["path"] : null;
|
|
249
261
|
if (!path) {
|
|
@@ -260,13 +272,13 @@ class Updater extends Base {
|
|
|
260
272
|
}
|
|
261
273
|
|
|
262
274
|
for (const path of updatePaths.values()) {
|
|
263
|
-
updateContent.call(this, { path });
|
|
264
|
-
updateAttributes.call(this, { path });
|
|
265
|
-
updateProperties.call(this, { path });
|
|
275
|
+
updateContent.call(this, { path }, renderContext);
|
|
276
|
+
updateAttributes.call(this, { path }, renderContext);
|
|
277
|
+
updateProperties.call(this, { path }, renderContext);
|
|
266
278
|
}
|
|
267
279
|
} else {
|
|
268
280
|
for (const change of Object.values(diffResult)) {
|
|
269
|
-
await this[applyChangeSymbol](change);
|
|
281
|
+
await this[applyChangeSymbol](change, renderContext);
|
|
270
282
|
}
|
|
271
283
|
}
|
|
272
284
|
this[internalSymbol].last = clone(snapshot);
|
|
@@ -279,17 +291,20 @@ class Updater extends Base {
|
|
|
279
291
|
}
|
|
280
292
|
|
|
281
293
|
/** @private **/
|
|
282
|
-
async [applyChangeSymbol](change) {
|
|
294
|
+
async [applyChangeSymbol](change, renderContext) {
|
|
283
295
|
if (this[disposedSymbol] === true) {
|
|
284
|
-
return
|
|
296
|
+
return;
|
|
285
297
|
}
|
|
286
298
|
|
|
287
|
-
removeElement.call(this, change);
|
|
288
|
-
insertElement.call(this, change);
|
|
289
|
-
|
|
299
|
+
const removed = removeElement.call(this, change, renderContext);
|
|
300
|
+
const inserted = insertElement.call(this, change, renderContext);
|
|
301
|
+
if (removed || inserted) {
|
|
302
|
+
invalidateRenderCandidates(renderContext);
|
|
303
|
+
}
|
|
304
|
+
updateContent.call(this, change, renderContext);
|
|
290
305
|
await Promise.resolve();
|
|
291
|
-
updateAttributes.call(this, change);
|
|
292
|
-
updateProperties.call(this, change);
|
|
306
|
+
updateAttributes.call(this, change, renderContext);
|
|
307
|
+
updateProperties.call(this, change, renderContext);
|
|
293
308
|
}
|
|
294
309
|
|
|
295
310
|
/**
|
|
@@ -391,6 +406,7 @@ class Updater extends Base {
|
|
|
391
406
|
}
|
|
392
407
|
|
|
393
408
|
this[disposedSymbol] = true;
|
|
409
|
+
this[fullRefreshRequestedSymbol] = false;
|
|
394
410
|
this.disableEventProcessing();
|
|
395
411
|
this[pendingDiffsSymbol].length = 0;
|
|
396
412
|
|
|
@@ -423,10 +439,10 @@ class Updater extends Base {
|
|
|
423
439
|
return Promise.resolve();
|
|
424
440
|
}
|
|
425
441
|
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
this[
|
|
442
|
+
// The key __init__ has no further meaning and is only used to create
|
|
443
|
+
// a full diff. A flag keeps repeated run requests from resetting an
|
|
444
|
+
// active queue more than once.
|
|
445
|
+
this[fullRefreshRequestedSymbol] = true;
|
|
430
446
|
return this[internalSymbol].subject.notifyObservers();
|
|
431
447
|
}
|
|
432
448
|
|
|
@@ -803,15 +819,25 @@ function retrieveFromBindings() {
|
|
|
803
819
|
* @license AGPLv3
|
|
804
820
|
* @since 1.8.0
|
|
805
821
|
* @param {object} change
|
|
806
|
-
* @return {
|
|
822
|
+
* @return {boolean}
|
|
807
823
|
*/
|
|
808
|
-
function removeElement(
|
|
824
|
+
function removeElement(_change, renderContext) {
|
|
825
|
+
if (renderContext?.removeProcessed === true) {
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
if (renderContext) {
|
|
829
|
+
renderContext.removeProcessed = true;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
let changed = false;
|
|
809
833
|
for (const [, element] of this[internalSymbol].element
|
|
810
834
|
.querySelectorAll(`:scope [${ATTRIBUTE_UPDATER_REMOVE}]`)
|
|
811
835
|
.entries()) {
|
|
812
836
|
teardownManagedSubtree(element);
|
|
813
837
|
element.parentNode.removeChild(element);
|
|
838
|
+
changed = true;
|
|
814
839
|
}
|
|
840
|
+
return changed;
|
|
815
841
|
}
|
|
816
842
|
|
|
817
843
|
/**
|
|
@@ -819,27 +845,36 @@ function removeElement(change) {
|
|
|
819
845
|
* @license AGPLv3
|
|
820
846
|
* @since 1.8.0
|
|
821
847
|
* @param {object} change
|
|
822
|
-
* @return {
|
|
848
|
+
* @return {boolean}
|
|
823
849
|
* @throws {Error} the value is not iterable
|
|
824
850
|
* @throws {Error} pipes are not allowed when cloning a node.
|
|
825
851
|
* @throws {Error} no template was found with the specified key.
|
|
826
852
|
* @throws {Error} the maximum depth for the recursion is reached.
|
|
827
853
|
* @this Updater
|
|
828
854
|
*/
|
|
829
|
-
function insertElement(change) {
|
|
855
|
+
function insertElement(change, renderContext) {
|
|
830
856
|
const subject = this[internalSymbol].subject.getRealSubject();
|
|
831
857
|
|
|
832
858
|
const mem = new WeakSet();
|
|
833
859
|
let wd = 0;
|
|
860
|
+
let changed = false;
|
|
834
861
|
|
|
835
862
|
const container = this[internalSymbol].element;
|
|
863
|
+
if (renderContext && renderContext.insertBindingsPresent === undefined) {
|
|
864
|
+
const query = `[${ATTRIBUTE_UPDATER_INSERT}]`;
|
|
865
|
+
renderContext.insertBindingsPresent =
|
|
866
|
+
container.matches(query) || container.querySelector(query) !== null;
|
|
867
|
+
}
|
|
868
|
+
if (renderContext?.insertBindingsPresent === false) {
|
|
869
|
+
return false;
|
|
870
|
+
}
|
|
836
871
|
|
|
837
872
|
while (true) {
|
|
838
873
|
let found = false;
|
|
839
874
|
wd++;
|
|
840
875
|
|
|
841
876
|
const p = clone(change?.["path"]);
|
|
842
|
-
if (!isArray(p)) return;
|
|
877
|
+
if (!isArray(p)) return changed;
|
|
843
878
|
|
|
844
879
|
while (p.length > 0) {
|
|
845
880
|
const current = p.join(".");
|
|
@@ -920,6 +955,7 @@ function insertElement(change) {
|
|
|
920
955
|
}
|
|
921
956
|
|
|
922
957
|
appendNewDocumentFragment(containerElement, key, ref, currentPath);
|
|
958
|
+
changed = true;
|
|
923
959
|
}
|
|
924
960
|
|
|
925
961
|
const nodes = containerElement.querySelectorAll(
|
|
@@ -935,6 +971,7 @@ function insertElement(change) {
|
|
|
935
971
|
try {
|
|
936
972
|
teardownManagedSubtree(node);
|
|
937
973
|
containerElement.removeChild(node);
|
|
974
|
+
changed = true;
|
|
938
975
|
} catch (e) {
|
|
939
976
|
addErrorAttribute(containerElement, e);
|
|
940
977
|
}
|
|
@@ -950,6 +987,8 @@ function insertElement(change) {
|
|
|
950
987
|
throw new Error("the maximum depth for the recursion is reached.");
|
|
951
988
|
}
|
|
952
989
|
}
|
|
990
|
+
|
|
991
|
+
return changed;
|
|
953
992
|
}
|
|
954
993
|
|
|
955
994
|
/**
|
|
@@ -1064,25 +1103,147 @@ function applyRecursive(node, key, path) {
|
|
|
1064
1103
|
|
|
1065
1104
|
/**
|
|
1066
1105
|
* @private
|
|
1067
|
-
*
|
|
1068
|
-
* @
|
|
1106
|
+
* Creates state shared by every change in one queued subject snapshot.
|
|
1107
|
+
* @private
|
|
1108
|
+
* @return {object}
|
|
1109
|
+
*/
|
|
1110
|
+
function createRenderContext() {
|
|
1111
|
+
return {
|
|
1112
|
+
replace: new WeakSet(),
|
|
1113
|
+
patch: new WeakSet(),
|
|
1114
|
+
attributes: new WeakSet(),
|
|
1115
|
+
properties: new WeakSet(),
|
|
1116
|
+
removeProcessed: false,
|
|
1117
|
+
insertBindingsPresent: undefined,
|
|
1118
|
+
candidates: {
|
|
1119
|
+
replace: new WeakMap(),
|
|
1120
|
+
patch: new WeakMap(),
|
|
1121
|
+
attributes: new WeakMap(),
|
|
1122
|
+
properties: new WeakMap(),
|
|
1123
|
+
slots: new WeakMap(),
|
|
1124
|
+
},
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Invalidates DOM-derived candidates after structural or rendered DOM changes.
|
|
1130
|
+
* @private
|
|
1131
|
+
* @param {object} renderContext
|
|
1132
|
+
* @return {void}
|
|
1133
|
+
*/
|
|
1134
|
+
function invalidateRenderCandidates(renderContext) {
|
|
1135
|
+
renderContext.removeProcessed = false;
|
|
1136
|
+
renderContext.insertBindingsPresent = undefined;
|
|
1137
|
+
renderContext.candidates.replace = new WeakMap();
|
|
1138
|
+
renderContext.candidates.patch = new WeakMap();
|
|
1139
|
+
renderContext.candidates.attributes = new WeakMap();
|
|
1140
|
+
renderContext.candidates.properties = new WeakMap();
|
|
1141
|
+
renderContext.candidates.slots = new WeakMap();
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* @private
|
|
1146
|
+
* @param {HTMLElement} container
|
|
1147
|
+
* @param {string} attribute
|
|
1148
|
+
* @param {WeakMap} cache
|
|
1149
|
+
* @return {Set<HTMLElement>}
|
|
1150
|
+
*/
|
|
1151
|
+
function getBindingCandidates(container, attribute, cache) {
|
|
1152
|
+
if (!(container instanceof HTMLElement)) {
|
|
1153
|
+
return new Set();
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
if (cache.has(container)) {
|
|
1157
|
+
return cache.get(container);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
const candidates = new Set(container.querySelectorAll(`[${attribute}]`));
|
|
1161
|
+
if (container.hasAttribute(attribute)) {
|
|
1162
|
+
candidates.add(container);
|
|
1163
|
+
}
|
|
1164
|
+
cache.set(container, candidates);
|
|
1165
|
+
return candidates;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* @private
|
|
1170
|
+
* @param {HTMLElement} container
|
|
1171
|
+
* @param {WeakMap} cache
|
|
1172
|
+
* @return {NodeList}
|
|
1173
|
+
*/
|
|
1174
|
+
function getSlots(container, cache) {
|
|
1175
|
+
if (cache.has(container)) {
|
|
1176
|
+
return cache.get(container);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const slots = container.querySelectorAll("slot");
|
|
1180
|
+
cache.set(container, slots);
|
|
1181
|
+
return slots;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* @private
|
|
1069
1186
|
* @param {object} change
|
|
1187
|
+
* @param {object} renderContext
|
|
1070
1188
|
* @return {void}
|
|
1071
1189
|
* @this Updater
|
|
1072
1190
|
*/
|
|
1073
|
-
function updateContent(change) {
|
|
1191
|
+
function updateContent(change, renderContext = createRenderContext()) {
|
|
1074
1192
|
const subject = this[internalSymbol].subject.getRealSubject();
|
|
1075
1193
|
|
|
1076
1194
|
const p = clone(change?.["path"]);
|
|
1077
|
-
runUpdateContent.call(
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1195
|
+
const replacementChanged = runUpdateContent.call(
|
|
1196
|
+
this,
|
|
1197
|
+
this[internalSymbol].element,
|
|
1198
|
+
p,
|
|
1199
|
+
subject,
|
|
1200
|
+
renderContext.replace,
|
|
1201
|
+
renderContext.candidates.replace,
|
|
1202
|
+
);
|
|
1203
|
+
if (replacementChanged) {
|
|
1204
|
+
invalidateRenderCandidates(renderContext);
|
|
1205
|
+
}
|
|
1206
|
+
const patchProcessed = runUpdatePatch.call(
|
|
1207
|
+
this,
|
|
1208
|
+
this[internalSymbol].element,
|
|
1209
|
+
p,
|
|
1210
|
+
subject,
|
|
1211
|
+
renderContext.patch,
|
|
1212
|
+
renderContext.candidates.patch,
|
|
1213
|
+
);
|
|
1214
|
+
if (patchProcessed) {
|
|
1215
|
+
invalidateRenderCandidates(renderContext);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const slots = getSlots(
|
|
1219
|
+
this[internalSymbol].element,
|
|
1220
|
+
renderContext.candidates.slots,
|
|
1221
|
+
);
|
|
1081
1222
|
if (slots.length > 0) {
|
|
1082
1223
|
for (const [, slot] of Object.entries(slots)) {
|
|
1083
1224
|
for (const [, element] of Object.entries(slot.assignedNodes())) {
|
|
1084
|
-
runUpdateContent.call(
|
|
1085
|
-
|
|
1225
|
+
const slottedReplacementChanged = runUpdateContent.call(
|
|
1226
|
+
this,
|
|
1227
|
+
element,
|
|
1228
|
+
p,
|
|
1229
|
+
subject,
|
|
1230
|
+
renderContext.replace,
|
|
1231
|
+
renderContext.candidates.replace,
|
|
1232
|
+
);
|
|
1233
|
+
if (slottedReplacementChanged) {
|
|
1234
|
+
invalidateRenderCandidates(renderContext);
|
|
1235
|
+
}
|
|
1236
|
+
const slottedPatchProcessed = runUpdatePatch.call(
|
|
1237
|
+
this,
|
|
1238
|
+
element,
|
|
1239
|
+
p,
|
|
1240
|
+
subject,
|
|
1241
|
+
renderContext.patch,
|
|
1242
|
+
renderContext.candidates.patch,
|
|
1243
|
+
);
|
|
1244
|
+
if (slottedPatchProcessed) {
|
|
1245
|
+
invalidateRenderCandidates(renderContext);
|
|
1246
|
+
}
|
|
1086
1247
|
}
|
|
1087
1248
|
}
|
|
1088
1249
|
}
|
|
@@ -1097,35 +1258,47 @@ function updateContent(change) {
|
|
|
1097
1258
|
* @param {object} subject
|
|
1098
1259
|
* @return {void}
|
|
1099
1260
|
*/
|
|
1100
|
-
function runUpdateContent(
|
|
1261
|
+
function runUpdateContent(
|
|
1262
|
+
container,
|
|
1263
|
+
parts,
|
|
1264
|
+
subject,
|
|
1265
|
+
mem = new WeakSet(),
|
|
1266
|
+
candidateCache = new WeakMap(),
|
|
1267
|
+
) {
|
|
1101
1268
|
if (!isArray(parts)) return;
|
|
1102
1269
|
if (!(container instanceof HTMLElement)) return;
|
|
1103
1270
|
parts = clone(parts);
|
|
1104
|
-
|
|
1105
|
-
const mem = new WeakSet();
|
|
1271
|
+
let changed = false;
|
|
1106
1272
|
|
|
1107
1273
|
while (parts.length > 0) {
|
|
1108
1274
|
const current = parts.join(".");
|
|
1109
1275
|
parts.pop();
|
|
1110
1276
|
|
|
1111
1277
|
// Unfortunately, static data is always changed as well, since it is not possible to react to changes here.
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
if (container.matches(query)) {
|
|
1118
|
-
iterator.add(container);
|
|
1119
|
-
}
|
|
1278
|
+
const iterator = getBindingCandidates(
|
|
1279
|
+
container,
|
|
1280
|
+
ATTRIBUTE_UPDATER_REPLACE,
|
|
1281
|
+
candidateCache,
|
|
1282
|
+
);
|
|
1120
1283
|
|
|
1121
1284
|
/**
|
|
1122
1285
|
* @type {HTMLElement}
|
|
1123
1286
|
*/
|
|
1124
1287
|
for (const [element] of iterator.entries()) {
|
|
1125
1288
|
if (mem.has(element)) continue;
|
|
1126
|
-
mem.add(element);
|
|
1127
1289
|
|
|
1128
1290
|
const attributes = element.getAttribute(ATTRIBUTE_UPDATER_REPLACE);
|
|
1291
|
+
if (!isString(attributes)) {
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
if (
|
|
1295
|
+
!attributes.startsWith(`path:${current}`) &&
|
|
1296
|
+
!attributes.startsWith("static:") &&
|
|
1297
|
+
!attributes.startsWith("i18n:")
|
|
1298
|
+
) {
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
mem.add(element);
|
|
1129
1302
|
const cmd = trimSpaces(attributes);
|
|
1130
1303
|
|
|
1131
1304
|
const pipe = new Pipe(cmd);
|
|
@@ -1142,49 +1315,101 @@ function runUpdateContent(container, parts, subject) {
|
|
|
1142
1315
|
continue;
|
|
1143
1316
|
}
|
|
1144
1317
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
element.removeChild(element.firstChild);
|
|
1149
|
-
}
|
|
1318
|
+
changed = applyReplacementValue.call(this, element, value) || changed;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1150
1321
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1322
|
+
return changed;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function applyReplacementValue(element, value) {
|
|
1326
|
+
if (!(element instanceof HTMLElement)) {
|
|
1327
|
+
return false;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
if (value instanceof HTMLElement) {
|
|
1331
|
+
if (element.childNodes.length === 1 && element.firstChild === value) {
|
|
1332
|
+
return false;
|
|
1160
1333
|
}
|
|
1334
|
+
|
|
1335
|
+
teardownChildNodes(element);
|
|
1336
|
+
while (element.firstChild) {
|
|
1337
|
+
element.removeChild(element.firstChild);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
try {
|
|
1341
|
+
element.appendChild(value);
|
|
1342
|
+
this[replacementValueCacheSymbol].delete(element);
|
|
1343
|
+
} catch (e) {
|
|
1344
|
+
addErrorAttribute(element, e);
|
|
1345
|
+
}
|
|
1346
|
+
return true;
|
|
1161
1347
|
}
|
|
1348
|
+
|
|
1349
|
+
const cacheable =
|
|
1350
|
+
value === null ||
|
|
1351
|
+
value === undefined ||
|
|
1352
|
+
(typeof value !== "object" && typeof value !== "function");
|
|
1353
|
+
const cached = this[replacementValueCacheSymbol].get(element);
|
|
1354
|
+
if (
|
|
1355
|
+
cacheable &&
|
|
1356
|
+
cached &&
|
|
1357
|
+
Object.is(cached.value, value) &&
|
|
1358
|
+
element.innerHTML === cached.html
|
|
1359
|
+
) {
|
|
1360
|
+
return false;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
teardownChildNodes(element);
|
|
1364
|
+
element.innerHTML = value;
|
|
1365
|
+
if (cacheable) {
|
|
1366
|
+
this[replacementValueCacheSymbol].set(element, {
|
|
1367
|
+
value,
|
|
1368
|
+
html: element.innerHTML,
|
|
1369
|
+
});
|
|
1370
|
+
} else {
|
|
1371
|
+
this[replacementValueCacheSymbol].delete(element);
|
|
1372
|
+
}
|
|
1373
|
+
return true;
|
|
1162
1374
|
}
|
|
1163
1375
|
|
|
1164
|
-
function runUpdatePatch(
|
|
1376
|
+
function runUpdatePatch(
|
|
1377
|
+
container,
|
|
1378
|
+
parts,
|
|
1379
|
+
subject,
|
|
1380
|
+
mem = new WeakSet(),
|
|
1381
|
+
candidateCache = new WeakMap(),
|
|
1382
|
+
) {
|
|
1165
1383
|
if (!isArray(parts)) return;
|
|
1166
1384
|
if (!(container instanceof HTMLElement)) return;
|
|
1167
1385
|
parts = clone(parts);
|
|
1168
|
-
|
|
1169
|
-
const mem = new WeakSet();
|
|
1386
|
+
let processed = false;
|
|
1170
1387
|
|
|
1171
1388
|
while (parts.length > 0) {
|
|
1172
1389
|
const current = parts.join(".");
|
|
1173
1390
|
parts.pop();
|
|
1174
1391
|
|
|
1175
|
-
const
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
iterator.add(container);
|
|
1181
|
-
}
|
|
1392
|
+
const iterator = getBindingCandidates(
|
|
1393
|
+
container,
|
|
1394
|
+
ATTRIBUTE_UPDATER_PATCH,
|
|
1395
|
+
candidateCache,
|
|
1396
|
+
);
|
|
1182
1397
|
|
|
1183
1398
|
for (const [element] of iterator.entries()) {
|
|
1184
1399
|
if (mem.has(element)) continue;
|
|
1185
|
-
mem.add(element);
|
|
1186
1400
|
|
|
1187
1401
|
const attributes = element.getAttribute(ATTRIBUTE_UPDATER_PATCH);
|
|
1402
|
+
if (!isString(attributes)) {
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
if (
|
|
1406
|
+
!attributes.startsWith(`path:${current}`) &&
|
|
1407
|
+
!attributes.startsWith("static:") &&
|
|
1408
|
+
!attributes.startsWith("i18n:")
|
|
1409
|
+
) {
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
mem.add(element);
|
|
1188
1413
|
const cmd = trimSpaces(attributes);
|
|
1189
1414
|
|
|
1190
1415
|
const pipe = new Pipe(cmd);
|
|
@@ -1202,8 +1427,11 @@ function runUpdatePatch(container, parts, subject) {
|
|
|
1202
1427
|
}
|
|
1203
1428
|
|
|
1204
1429
|
applyPatchValue.call(this, element, value);
|
|
1430
|
+
processed = true;
|
|
1205
1431
|
}
|
|
1206
1432
|
}
|
|
1433
|
+
|
|
1434
|
+
return processed;
|
|
1207
1435
|
}
|
|
1208
1436
|
|
|
1209
1437
|
function applyPatchValue(element, value) {
|
|
@@ -1253,6 +1481,9 @@ function applyPatchValue(element, value) {
|
|
|
1253
1481
|
}
|
|
1254
1482
|
|
|
1255
1483
|
const nextValue = value === null || value === undefined ? "" : String(value);
|
|
1484
|
+
if (element.children.length === 0 && element.textContent === nextValue) {
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1256
1487
|
|
|
1257
1488
|
if (element.children.length > 0) {
|
|
1258
1489
|
teardownChildNodes(element);
|
|
@@ -1518,10 +1749,20 @@ function hasObjectLinkSafe(element, symbol) {
|
|
|
1518
1749
|
* @param {object} change
|
|
1519
1750
|
* @return {void}
|
|
1520
1751
|
*/
|
|
1521
|
-
function updateAttributes(change) {
|
|
1752
|
+
function updateAttributes(change, renderContext = createRenderContext()) {
|
|
1522
1753
|
const subject = this[internalSymbol].subject.getRealSubject();
|
|
1523
1754
|
const p = clone(change?.["path"]);
|
|
1524
|
-
runUpdateAttributes.call(
|
|
1755
|
+
const processed = runUpdateAttributes.call(
|
|
1756
|
+
this,
|
|
1757
|
+
this[internalSymbol].element,
|
|
1758
|
+
p,
|
|
1759
|
+
subject,
|
|
1760
|
+
renderContext.attributes,
|
|
1761
|
+
renderContext.candidates.attributes,
|
|
1762
|
+
);
|
|
1763
|
+
if (processed) {
|
|
1764
|
+
invalidateRenderCandidates(renderContext);
|
|
1765
|
+
}
|
|
1525
1766
|
}
|
|
1526
1767
|
|
|
1527
1768
|
/**
|
|
@@ -1530,10 +1771,20 @@ function updateAttributes(change) {
|
|
|
1530
1771
|
* @param {object} change
|
|
1531
1772
|
* @return {void}
|
|
1532
1773
|
*/
|
|
1533
|
-
function updateProperties(change) {
|
|
1774
|
+
function updateProperties(change, renderContext = createRenderContext()) {
|
|
1534
1775
|
const subject = this[internalSymbol].subject.getRealSubject();
|
|
1535
1776
|
const p = clone(change?.["path"]);
|
|
1536
|
-
runUpdateProperties.call(
|
|
1777
|
+
const processed = runUpdateProperties.call(
|
|
1778
|
+
this,
|
|
1779
|
+
this[internalSymbol].element,
|
|
1780
|
+
p,
|
|
1781
|
+
subject,
|
|
1782
|
+
renderContext.properties,
|
|
1783
|
+
renderContext.candidates.properties,
|
|
1784
|
+
);
|
|
1785
|
+
if (processed) {
|
|
1786
|
+
invalidateRenderCandidates(renderContext);
|
|
1787
|
+
}
|
|
1537
1788
|
}
|
|
1538
1789
|
|
|
1539
1790
|
/**
|
|
@@ -1544,40 +1795,44 @@ function updateProperties(change) {
|
|
|
1544
1795
|
* @return {void}
|
|
1545
1796
|
* @this Updater
|
|
1546
1797
|
*/
|
|
1547
|
-
function runUpdateAttributes(
|
|
1798
|
+
function runUpdateAttributes(
|
|
1799
|
+
container,
|
|
1800
|
+
parts,
|
|
1801
|
+
subject,
|
|
1802
|
+
mem = new WeakSet(),
|
|
1803
|
+
candidateCache = new WeakMap(),
|
|
1804
|
+
) {
|
|
1548
1805
|
if (!isArray(parts)) return;
|
|
1549
1806
|
parts = clone(parts);
|
|
1550
|
-
|
|
1551
|
-
const mem = new WeakSet();
|
|
1807
|
+
let processed = false;
|
|
1552
1808
|
|
|
1553
1809
|
while (parts.length > 0) {
|
|
1554
1810
|
const current = parts.join(".");
|
|
1555
1811
|
parts.pop();
|
|
1556
1812
|
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
if (e.length > 0) {
|
|
1564
|
-
iterator = new Set([...e]);
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
if (container.matches(query)) {
|
|
1568
|
-
iterator.add(container);
|
|
1569
|
-
}
|
|
1813
|
+
const iterator = getBindingCandidates(
|
|
1814
|
+
container,
|
|
1815
|
+
ATTRIBUTE_UPDATER_ATTRIBUTES,
|
|
1816
|
+
candidateCache,
|
|
1817
|
+
);
|
|
1570
1818
|
|
|
1571
1819
|
for (const [element] of iterator.entries()) {
|
|
1572
1820
|
if (mem.has(element)) continue;
|
|
1573
|
-
mem.add(element);
|
|
1574
1821
|
|
|
1575
|
-
|
|
1576
|
-
if (!
|
|
1822
|
+
const attributes = element.getAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES);
|
|
1823
|
+
if (!isString(attributes)) {
|
|
1577
1824
|
continue;
|
|
1578
1825
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1826
|
+
if (
|
|
1827
|
+
!element.hasAttribute(ATTRIBUTE_UPDATER_SELECT_THIS) &&
|
|
1828
|
+
!attributes.includes(`path:${current}`) &&
|
|
1829
|
+
!attributes.startsWith("static:") &&
|
|
1830
|
+
!attributes.startsWith("i18n:")
|
|
1831
|
+
) {
|
|
1832
|
+
continue;
|
|
1833
|
+
}
|
|
1834
|
+
mem.add(element);
|
|
1835
|
+
processed = true;
|
|
1581
1836
|
element.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
|
|
1582
1837
|
|
|
1583
1838
|
for (let [, def] of Object.entries(attributes.split(","))) {
|
|
@@ -1610,6 +1865,8 @@ function runUpdateAttributes(container, parts, subject) {
|
|
|
1610
1865
|
}
|
|
1611
1866
|
}
|
|
1612
1867
|
}
|
|
1868
|
+
|
|
1869
|
+
return processed;
|
|
1613
1870
|
}
|
|
1614
1871
|
|
|
1615
1872
|
/**
|
|
@@ -1620,40 +1877,44 @@ function runUpdateAttributes(container, parts, subject) {
|
|
|
1620
1877
|
* @return {void}
|
|
1621
1878
|
* @this Updater
|
|
1622
1879
|
*/
|
|
1623
|
-
function runUpdateProperties(
|
|
1880
|
+
function runUpdateProperties(
|
|
1881
|
+
container,
|
|
1882
|
+
parts,
|
|
1883
|
+
subject,
|
|
1884
|
+
mem = new WeakSet(),
|
|
1885
|
+
candidateCache = new WeakMap(),
|
|
1886
|
+
) {
|
|
1624
1887
|
if (!isArray(parts)) return;
|
|
1625
1888
|
parts = clone(parts);
|
|
1626
|
-
|
|
1627
|
-
const mem = new WeakSet();
|
|
1889
|
+
let processed = false;
|
|
1628
1890
|
|
|
1629
1891
|
while (parts.length > 0) {
|
|
1630
1892
|
const current = parts.join(".");
|
|
1631
1893
|
parts.pop();
|
|
1632
1894
|
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
if (e.length > 0) {
|
|
1640
|
-
iterator = new Set([...e]);
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
if (container.matches(query)) {
|
|
1644
|
-
iterator.add(container);
|
|
1645
|
-
}
|
|
1895
|
+
const iterator = getBindingCandidates(
|
|
1896
|
+
container,
|
|
1897
|
+
ATTRIBUTE_UPDATER_PROPERTIES,
|
|
1898
|
+
candidateCache,
|
|
1899
|
+
);
|
|
1646
1900
|
|
|
1647
1901
|
for (const [element] of iterator.entries()) {
|
|
1648
1902
|
if (mem.has(element)) continue;
|
|
1649
|
-
mem.add(element);
|
|
1650
1903
|
|
|
1651
|
-
|
|
1652
|
-
if (!
|
|
1904
|
+
const properties = element.getAttribute(ATTRIBUTE_UPDATER_PROPERTIES);
|
|
1905
|
+
if (!isString(properties)) {
|
|
1653
1906
|
continue;
|
|
1654
1907
|
}
|
|
1655
|
-
|
|
1656
|
-
|
|
1908
|
+
if (
|
|
1909
|
+
!element.hasAttribute(ATTRIBUTE_UPDATER_SELECT_THIS) &&
|
|
1910
|
+
!properties.includes(`path:${current}`) &&
|
|
1911
|
+
!properties.startsWith("static:") &&
|
|
1912
|
+
!properties.startsWith("i18n:")
|
|
1913
|
+
) {
|
|
1914
|
+
continue;
|
|
1915
|
+
}
|
|
1916
|
+
mem.add(element);
|
|
1917
|
+
processed = true;
|
|
1657
1918
|
element.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
|
|
1658
1919
|
|
|
1659
1920
|
for (let [, def] of Object.entries(properties.split(","))) {
|
|
@@ -1680,6 +1941,8 @@ function runUpdateProperties(container, parts, subject) {
|
|
|
1680
1941
|
}
|
|
1681
1942
|
}
|
|
1682
1943
|
}
|
|
1944
|
+
|
|
1945
|
+
return processed;
|
|
1683
1946
|
}
|
|
1684
1947
|
|
|
1685
1948
|
/**
|
|
@@ -2026,12 +2289,11 @@ function addObjectWithUpdaterToElement(elements, symbol, object, config = {}) {
|
|
|
2026
2289
|
u.setCallback(name, callback);
|
|
2027
2290
|
}
|
|
2028
2291
|
}
|
|
2029
|
-
|
|
2292
|
+
if (config.batchUpdates === true) {
|
|
2293
|
+
u.setBatchUpdates(true);
|
|
2294
|
+
}
|
|
2030
2295
|
result.push(
|
|
2031
2296
|
u.run().then(() => {
|
|
2032
|
-
if (config.batchUpdates === true) {
|
|
2033
|
-
u.setBatchUpdates(true);
|
|
2034
|
-
}
|
|
2035
2297
|
if (config.eventProcessing === true) {
|
|
2036
2298
|
u.enableEventProcessing();
|
|
2037
2299
|
}
|
|
@@ -4,6 +4,7 @@ import * as chai from 'chai';
|
|
|
4
4
|
import {internalSymbol} from "../../../source/constants.mjs";
|
|
5
5
|
import {customElementUpdaterLinkSymbol} from "../../../source/dom/constants.mjs";
|
|
6
6
|
import {getDocument} from "../../../source/dom/util.mjs";
|
|
7
|
+
import {Observer} from "../../../source/types/observer.mjs";
|
|
7
8
|
import {ProxyObserver} from "../../../source/types/proxyobserver.mjs";
|
|
8
9
|
import {chaiDom} from "../../util/chai-dom.mjs";
|
|
9
10
|
import {initJSDOM} from "../../util/jsdom.mjs";
|
|
@@ -28,7 +29,7 @@ const updaterSymbolSymbol = Symbol.for(updaterSymbolKey);
|
|
|
28
29
|
|
|
29
30
|
describe('DOM', function () {
|
|
30
31
|
|
|
31
|
-
let CustomElement, registerCustomElement, TestComponent, document, TestComponent2, TestStateComponent, TestMutationComponent, HiddenStateClass, assignUpdaterToElement,
|
|
32
|
+
let CustomElement, registerCustomElement, updaterTransformerMethodsSymbol, TestComponent, document, TestComponent2, TestStateComponent, TestMutationComponent, HiddenStateClass, assignUpdaterToElement,
|
|
32
33
|
addObjectWithUpdaterToElement;
|
|
33
34
|
|
|
34
35
|
describe("assignUpdaterToElement", function () {
|
|
@@ -154,6 +155,7 @@ describe('DOM', function () {
|
|
|
154
155
|
try {
|
|
155
156
|
CustomElement = m['CustomElement'];
|
|
156
157
|
registerCustomElement = m['registerCustomElement'];
|
|
158
|
+
updaterTransformerMethodsSymbol = m['updaterTransformerMethodsSymbol'];
|
|
157
159
|
TestComponent = class extends CustomElement {
|
|
158
160
|
static getTag() {
|
|
159
161
|
return "monster-testclass"
|
|
@@ -510,6 +512,81 @@ describe('DOM', function () {
|
|
|
510
512
|
}, 20);
|
|
511
513
|
}, 10);
|
|
512
514
|
});
|
|
515
|
+
|
|
516
|
+
it('should synchronize option arrays without unchanged proxy churn issue #517', async function () {
|
|
517
|
+
const htmlTAG = 'monster-testclass-array-sync-517';
|
|
518
|
+
|
|
519
|
+
if (!customElements.get(htmlTAG)) {
|
|
520
|
+
class ArraySyncComponent extends CustomElement {
|
|
521
|
+
static getTag() {
|
|
522
|
+
return htmlTAG;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
get defaults() {
|
|
526
|
+
return Object.assign({}, super.defaults, {
|
|
527
|
+
items: Array.from({length: 100}, (_, index) => `item-${index}`),
|
|
528
|
+
unrelated: 0,
|
|
529
|
+
templates: {
|
|
530
|
+
main: '<span id="array-sync-value" data-monster-replace="path:items | call:joinItems"></span><span id="array-sync-value-second" data-monster-replace="path:items | call:joinItems"></span>',
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
[updaterTransformerMethodsSymbol]() {
|
|
536
|
+
return {
|
|
537
|
+
joinItems: (items) => items.join(','),
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
registerCustomElement(ArraySyncComponent);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const element = document.createElement(htmlTAG);
|
|
546
|
+
document.getElementById('test1').appendChild(element);
|
|
547
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
548
|
+
|
|
549
|
+
const updaterGroups = element[customElementUpdaterLinkSymbol];
|
|
550
|
+
let linkedUpdater = null;
|
|
551
|
+
for (const group of updaterGroups) {
|
|
552
|
+
for (const updater of group) {
|
|
553
|
+
linkedUpdater = updater;
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
if (linkedUpdater) break;
|
|
557
|
+
}
|
|
558
|
+
expect(linkedUpdater).to.exist;
|
|
559
|
+
|
|
560
|
+
let notificationCount = 0;
|
|
561
|
+
class CountingObserver extends Observer {
|
|
562
|
+
update(subject) {
|
|
563
|
+
notificationCount++;
|
|
564
|
+
return super.update(subject);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const observer = new CountingObserver(() => {});
|
|
568
|
+
linkedUpdater[internalSymbol].subject.attachObserver(observer);
|
|
569
|
+
|
|
570
|
+
try {
|
|
571
|
+
element.setOption('unrelated', 1);
|
|
572
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
573
|
+
expect(notificationCount).to.be.at.most(3);
|
|
574
|
+
|
|
575
|
+
notificationCount = 0;
|
|
576
|
+
element.setOption('items.50', 'changed');
|
|
577
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
578
|
+
|
|
579
|
+
expect(notificationCount).to.be.at.most(3);
|
|
580
|
+
expect(
|
|
581
|
+
element.shadowRoot.querySelector('#array-sync-value').textContent,
|
|
582
|
+
).to.contain('changed');
|
|
583
|
+
expect(
|
|
584
|
+
element.shadowRoot.querySelector('#array-sync-value-second').textContent,
|
|
585
|
+
).to.contain('changed');
|
|
586
|
+
} finally {
|
|
587
|
+
linkedUpdater[internalSymbol].subject.detachObserver(observer);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
513
590
|
})
|
|
514
591
|
|
|
515
592
|
describe('setOptions()', function () {
|
|
@@ -860,6 +937,64 @@ describe('DOM', function () {
|
|
|
860
937
|
})
|
|
861
938
|
|
|
862
939
|
describe('mutation observer lifecycle', function () {
|
|
940
|
+
it('should coalesce child mutations without a refresh loop issue #517', async function () {
|
|
941
|
+
const htmlTAG = 'monster-testclass-mutation-loop-517';
|
|
942
|
+
|
|
943
|
+
if (!customElements.get(htmlTAG)) {
|
|
944
|
+
class MutationLoopComponent extends CustomElement {
|
|
945
|
+
static getTag() {
|
|
946
|
+
return htmlTAG;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
get defaults() {
|
|
950
|
+
return Object.assign({}, super.defaults, {
|
|
951
|
+
shadowMode: false,
|
|
952
|
+
features: {
|
|
953
|
+
mutationObserver: true,
|
|
954
|
+
},
|
|
955
|
+
templates: {
|
|
956
|
+
main: '<span data-monster-replace="static:stable"></span>',
|
|
957
|
+
},
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
registerCustomElement(MutationLoopComponent);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const element = document.createElement(htmlTAG);
|
|
966
|
+
document.getElementById('test1').appendChild(element);
|
|
967
|
+
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
968
|
+
|
|
969
|
+
const updaterGroups = element[customElementUpdaterLinkSymbol];
|
|
970
|
+
let linkedUpdater = null;
|
|
971
|
+
for (const group of updaterGroups) {
|
|
972
|
+
for (const updater of group) {
|
|
973
|
+
linkedUpdater = updater;
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
if (linkedUpdater) break;
|
|
977
|
+
}
|
|
978
|
+
expect(linkedUpdater).to.exist;
|
|
979
|
+
|
|
980
|
+
let runCount = 0;
|
|
981
|
+
const originalRun = linkedUpdater.run.bind(linkedUpdater);
|
|
982
|
+
linkedUpdater.run = function () {
|
|
983
|
+
runCount++;
|
|
984
|
+
return originalRun();
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
element.appendChild(document.createElement('i'));
|
|
988
|
+
element.appendChild(document.createElement('b'));
|
|
989
|
+
element.appendChild(document.createElement('em'));
|
|
990
|
+
await new Promise((resolve) => setTimeout(resolve, 160));
|
|
991
|
+
|
|
992
|
+
expect(runCount).to.equal(1);
|
|
993
|
+
expect(element.querySelector('[data-monster-replace]').textContent).to.equal(
|
|
994
|
+
'stable',
|
|
995
|
+
);
|
|
996
|
+
});
|
|
997
|
+
|
|
863
998
|
it('should not rerun linked updaters after disconnect', function (done) {
|
|
864
999
|
let mocks = document.getElementById('mocks');
|
|
865
1000
|
mocks.innerHTML = `<monster-testclass-mutation id="mutation-case"></monster-testclass-mutation>`;
|
|
@@ -170,6 +170,7 @@ let htmlPatchVsReplace = `
|
|
|
170
170
|
|
|
171
171
|
describe("DOM", function () {
|
|
172
172
|
let Updater = null;
|
|
173
|
+
let addObjectWithUpdaterToElement = null;
|
|
173
174
|
|
|
174
175
|
before(function (done) {
|
|
175
176
|
const options = {};
|
|
@@ -177,6 +178,7 @@ describe("DOM", function () {
|
|
|
177
178
|
import("../../../source/dom/updater.mjs")
|
|
178
179
|
.then((m) => {
|
|
179
180
|
Updater = m.Updater;
|
|
181
|
+
addObjectWithUpdaterToElement = m.addObjectWithUpdaterToElement;
|
|
180
182
|
|
|
181
183
|
if (!customElements.get("monster-test-property")) {
|
|
182
184
|
class MonsterTestProperty extends HTMLElement {
|
|
@@ -1907,6 +1909,50 @@ describe("DOM", function () {
|
|
|
1907
1909
|
});
|
|
1908
1910
|
|
|
1909
1911
|
describe("Updater reactive updates", function () {
|
|
1912
|
+
it("should enable configured batching before the initial run issue #517", async function () {
|
|
1913
|
+
const originalRun = Updater.prototype.run;
|
|
1914
|
+
const originalSetBatchUpdates = Updater.prototype.setBatchUpdates;
|
|
1915
|
+
const batchedUpdaters = new WeakSet();
|
|
1916
|
+
const batchStateAtRun = [];
|
|
1917
|
+
const symbol = Symbol("initial-batch-test");
|
|
1918
|
+
const element = document.createElement("div");
|
|
1919
|
+
element.setAttribute("data-monster-replace", "path:value");
|
|
1920
|
+
document.getElementById("mocks").appendChild(element);
|
|
1921
|
+
|
|
1922
|
+
Updater.prototype.setBatchUpdates = function (enabled) {
|
|
1923
|
+
if (enabled === true) {
|
|
1924
|
+
batchedUpdaters.add(this);
|
|
1925
|
+
}
|
|
1926
|
+
return originalSetBatchUpdates.call(this, enabled);
|
|
1927
|
+
};
|
|
1928
|
+
Updater.prototype.run = function () {
|
|
1929
|
+
batchStateAtRun.push(batchedUpdaters.has(this));
|
|
1930
|
+
return originalRun.call(this);
|
|
1931
|
+
};
|
|
1932
|
+
|
|
1933
|
+
let updaters = [];
|
|
1934
|
+
try {
|
|
1935
|
+
updaters = await Promise.all(
|
|
1936
|
+
addObjectWithUpdaterToElement.call(
|
|
1937
|
+
element,
|
|
1938
|
+
element,
|
|
1939
|
+
symbol,
|
|
1940
|
+
{ value: "initial" },
|
|
1941
|
+
{ batchUpdates: true },
|
|
1942
|
+
),
|
|
1943
|
+
);
|
|
1944
|
+
|
|
1945
|
+
expect(batchStateAtRun).to.deep.equal([true]);
|
|
1946
|
+
expect(element.textContent).to.equal("initial");
|
|
1947
|
+
} finally {
|
|
1948
|
+
Updater.prototype.run = originalRun;
|
|
1949
|
+
Updater.prototype.setBatchUpdates = originalSetBatchUpdates;
|
|
1950
|
+
for (const updater of updaters) {
|
|
1951
|
+
updater.dispose();
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
});
|
|
1955
|
+
|
|
1910
1956
|
[false, true].forEach((batchUpdates) => {
|
|
1911
1957
|
it(`should preserve microtask changes with batchUpdates=${batchUpdates} issue #496`, async function () {
|
|
1912
1958
|
let mocks = document.getElementById("mocks");
|
|
@@ -1938,6 +1984,134 @@ describe("DOM", function () {
|
|
|
1938
1984
|
});
|
|
1939
1985
|
});
|
|
1940
1986
|
|
|
1987
|
+
[false, true].forEach((batchUpdates) => {
|
|
1988
|
+
it(`should render each binding once per snapshot with batchUpdates=${batchUpdates} issue #517`, async function () {
|
|
1989
|
+
const mocks = document.getElementById("mocks");
|
|
1990
|
+
const staticBindings = Array.from(
|
|
1991
|
+
{ length: 20 },
|
|
1992
|
+
(_, index) =>
|
|
1993
|
+
`<span data-index="${index}" data-monster-replace="static:marker | call:trackStatic"></span>`,
|
|
1994
|
+
).join("");
|
|
1995
|
+
mocks.innerHTML = `
|
|
1996
|
+
<span id="snapshot-items" data-monster-replace="path:items | call:lastItem"></span>
|
|
1997
|
+
${staticBindings}
|
|
1998
|
+
<span id="snapshot-attribute" data-monster-attributes="title path:items | call:trackAttribute"></span>
|
|
1999
|
+
<input id="snapshot-property" data-monster-properties="value path:items | call:trackProperty">
|
|
2000
|
+
`;
|
|
2001
|
+
|
|
2002
|
+
const updater = new Updater(mocks, { items: [] });
|
|
2003
|
+
updater.setBatchUpdates(batchUpdates);
|
|
2004
|
+
let staticRenderCount = 0;
|
|
2005
|
+
let attributeRenderCount = 0;
|
|
2006
|
+
let propertyRenderCount = 0;
|
|
2007
|
+
updater.setCallback("trackStatic", (value) => {
|
|
2008
|
+
staticRenderCount++;
|
|
2009
|
+
return value;
|
|
2010
|
+
});
|
|
2011
|
+
updater.setCallback("lastItem", (value) => value.at(-1) ?? "");
|
|
2012
|
+
updater.setCallback("trackAttribute", (value) => {
|
|
2013
|
+
attributeRenderCount++;
|
|
2014
|
+
return value;
|
|
2015
|
+
});
|
|
2016
|
+
updater.setCallback("trackProperty", (value) => {
|
|
2017
|
+
propertyRenderCount++;
|
|
2018
|
+
return value;
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
let innerHTMLDescriptor = null;
|
|
2022
|
+
let replacementWrites = 0;
|
|
2023
|
+
try {
|
|
2024
|
+
await updater.run();
|
|
2025
|
+
staticRenderCount = 0;
|
|
2026
|
+
attributeRenderCount = 0;
|
|
2027
|
+
propertyRenderCount = 0;
|
|
2028
|
+
|
|
2029
|
+
innerHTMLDescriptor = Object.getOwnPropertyDescriptor(
|
|
2030
|
+
Element.prototype,
|
|
2031
|
+
"innerHTML",
|
|
2032
|
+
);
|
|
2033
|
+
Object.defineProperty(Element.prototype, "innerHTML", {
|
|
2034
|
+
configurable: innerHTMLDescriptor.configurable,
|
|
2035
|
+
enumerable: innerHTMLDescriptor.enumerable,
|
|
2036
|
+
get: innerHTMLDescriptor.get,
|
|
2037
|
+
set(value) {
|
|
2038
|
+
if (
|
|
2039
|
+
mocks.contains(this) &&
|
|
2040
|
+
this.hasAttribute("data-monster-replace")
|
|
2041
|
+
) {
|
|
2042
|
+
replacementWrites++;
|
|
2043
|
+
}
|
|
2044
|
+
return innerHTMLDescriptor.set.call(this, value);
|
|
2045
|
+
},
|
|
2046
|
+
});
|
|
2047
|
+
|
|
2048
|
+
const items = updater.getSubject().items;
|
|
2049
|
+
for (let index = 0; index < 500; index++) {
|
|
2050
|
+
items.push(`new-${index}`);
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
2054
|
+
|
|
2055
|
+
expect(document.getElementById("snapshot-items").textContent).to.equal(
|
|
2056
|
+
"new-499",
|
|
2057
|
+
);
|
|
2058
|
+
expect(staticRenderCount).to.equal(20);
|
|
2059
|
+
expect(attributeRenderCount).to.equal(1);
|
|
2060
|
+
expect(propertyRenderCount).to.equal(1);
|
|
2061
|
+
expect(replacementWrites).to.equal(1);
|
|
2062
|
+
} finally {
|
|
2063
|
+
updater.dispose();
|
|
2064
|
+
if (innerHTMLDescriptor) {
|
|
2065
|
+
Object.defineProperty(
|
|
2066
|
+
Element.prototype,
|
|
2067
|
+
"innerHTML",
|
|
2068
|
+
innerHTMLDescriptor,
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
});
|
|
2073
|
+
});
|
|
2074
|
+
|
|
2075
|
+
it("should not rewrite unchanged replacement content issue #517", async function () {
|
|
2076
|
+
const mocks = document.getElementById("mocks");
|
|
2077
|
+
mocks.innerHTML = `
|
|
2078
|
+
<span id="unchanged-replacement" data-monster-replace="static:same"></span>
|
|
2079
|
+
`;
|
|
2080
|
+
|
|
2081
|
+
const element = document.getElementById("unchanged-replacement");
|
|
2082
|
+
const descriptor = Object.getOwnPropertyDescriptor(
|
|
2083
|
+
Element.prototype,
|
|
2084
|
+
"innerHTML",
|
|
2085
|
+
);
|
|
2086
|
+
let writes = 0;
|
|
2087
|
+
|
|
2088
|
+
Object.defineProperty(Element.prototype, "innerHTML", {
|
|
2089
|
+
configurable: descriptor.configurable,
|
|
2090
|
+
enumerable: descriptor.enumerable,
|
|
2091
|
+
get: descriptor.get,
|
|
2092
|
+
set(value) {
|
|
2093
|
+
if (this === element) {
|
|
2094
|
+
writes++;
|
|
2095
|
+
}
|
|
2096
|
+
return descriptor.set.call(this, value);
|
|
2097
|
+
},
|
|
2098
|
+
});
|
|
2099
|
+
|
|
2100
|
+
const updater = new Updater(mocks, { trigger: 0 });
|
|
2101
|
+
try {
|
|
2102
|
+
await updater.run();
|
|
2103
|
+
writes = 0;
|
|
2104
|
+
updater.getSubject().trigger = 1;
|
|
2105
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
2106
|
+
|
|
2107
|
+
expect(element.textContent).to.equal("same");
|
|
2108
|
+
expect(writes).to.equal(0);
|
|
2109
|
+
} finally {
|
|
2110
|
+
updater.dispose();
|
|
2111
|
+
Object.defineProperty(Element.prototype, "innerHTML", descriptor);
|
|
2112
|
+
}
|
|
2113
|
+
});
|
|
2114
|
+
|
|
1941
2115
|
it("should update the DOM when the subject changes", function (done) {
|
|
1942
2116
|
let mocks = document.getElementById("mocks");
|
|
1943
2117
|
mocks.innerHTML = html1;
|