solarite 0.2.3 → 0.2.4
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/dist/Solarite-debug.js +357 -39
- package/dist/Solarite.js +357 -39
- package/dist/Solarite.min.js +2 -2
- package/package.json +1 -1
- package/src/solarite/ExprPath.js +84 -17
- package/src/solarite/Globals.js +3 -2
- package/src/solarite/NodeGroup.js +37 -0
- package/src/solarite/Solarite.js +1 -1
- package/src/solarite/Template.js +5 -3
- package/src/solarite/getArg.js +18 -8
- package/src/solarite/watch3.js +119 -28
- package/src/util/MultiValueMap.js +35 -2
- package/src/util/Util.js +26 -1
package/dist/Solarite.js
CHANGED
|
@@ -76,10 +76,36 @@ var Util$1 = {
|
|
|
76
76
|
|
|
77
77
|
return result;
|
|
78
78
|
},
|
|
79
|
-
|
|
80
79
|
|
|
80
|
+
/**
|
|
81
|
+
* @param map {Map|WeakMap|Object}
|
|
82
|
+
* @param key
|
|
83
|
+
* @param value */
|
|
84
|
+
mapAdd(map, key, value) {
|
|
85
|
+
let isMap = map instanceof Map || map instanceof WeakMap;
|
|
86
|
+
let result = isMap ? map.get(key) : map[key];
|
|
87
|
+
if (!result) {
|
|
88
|
+
result = [value];
|
|
89
|
+
if (isMap)
|
|
90
|
+
map.set(key, result);
|
|
91
|
+
else
|
|
92
|
+
map[key] = result;
|
|
93
|
+
}
|
|
94
|
+
else
|
|
95
|
+
result.push(value);
|
|
96
|
+
},
|
|
81
97
|
|
|
82
|
-
|
|
98
|
+
weakMemoize(obj, callback) {
|
|
99
|
+
let result = weakMemoizeInputs.get(obj);
|
|
100
|
+
if (!result) {
|
|
101
|
+
result = callback(obj);
|
|
102
|
+
weakMemoizeInputs.set(obj, result);
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
let weakMemoizeInputs = new WeakMap();
|
|
83
109
|
|
|
84
110
|
/**
|
|
85
111
|
* Follow a path into an object.
|
|
@@ -128,7 +154,7 @@ let delveDontCreate = {};
|
|
|
128
154
|
/**
|
|
129
155
|
* There are three ways to create an instance of a Solarite Component:
|
|
130
156
|
* 1. new ComponentName(); // direct class instantiation
|
|
131
|
-
* 2. this.html = r`<div><component-name></component-name></div>; // as a child of another
|
|
157
|
+
* 2. this.html = r`<div><component-name></component-name></div>; // as a child of another RedComponent.
|
|
132
158
|
* 3. <body><component-name></component-name></body> // in the Document html.
|
|
133
159
|
*
|
|
134
160
|
* When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make
|
|
@@ -153,14 +179,15 @@ let delveDontCreate = {};
|
|
|
153
179
|
* @param type {ArgType|function|*[]}
|
|
154
180
|
* If an array, use the value if it's in the array, otherwise return undefined.
|
|
155
181
|
* If it's a function, pass the value to the function and return the result.
|
|
156
|
-
* @
|
|
157
|
-
|
|
182
|
+
* @param fallback {*} If the type can't be parsed as the given type, use this value.
|
|
183
|
+
* @return {*} Undefined if attribute isn't set. */
|
|
184
|
+
function getArg(el, name, val=undefined, type=ArgType.String, fallback=undefined) {
|
|
158
185
|
let attrVal = el.getAttribute(name);
|
|
159
186
|
if (attrVal !== null) // If attribute doesn't exist.
|
|
160
187
|
val = attrVal;
|
|
161
188
|
|
|
162
189
|
if (Array.isArray(type))
|
|
163
|
-
return type.includes(val) ? val :
|
|
190
|
+
return type.includes(val) ? val : fallback;
|
|
164
191
|
|
|
165
192
|
if (typeof type === 'function')
|
|
166
193
|
return type(val);
|
|
@@ -168,15 +195,22 @@ function getArg(el, name, val=null, type=ArgType.String) {
|
|
|
168
195
|
// If bool, it's true as long as it exists and its value isn't falsey.
|
|
169
196
|
if (type===ArgType.Bool) {
|
|
170
197
|
let lAttrVal = typeof val === 'string' ? val.toLowerCase() : val;
|
|
171
|
-
|
|
198
|
+
if (['false', '0', false, 0, null, undefined, NaN].includes(lAttrVal))
|
|
199
|
+
return false;
|
|
200
|
+
if (['true', true].includes(lAttrVal) || parseFloat(lAttrVal) !== 0)
|
|
201
|
+
return true;
|
|
202
|
+
return fallback;
|
|
172
203
|
}
|
|
173
204
|
|
|
174
205
|
// Attribute doesn't exist
|
|
206
|
+
let result;
|
|
175
207
|
switch (type) {
|
|
176
208
|
case ArgType.Int:
|
|
177
|
-
|
|
209
|
+
result = parseInt(val);
|
|
210
|
+
return isNaN(result) ? fallback : result;
|
|
178
211
|
case ArgType.Float:
|
|
179
|
-
|
|
212
|
+
result = parseFloat(val);
|
|
213
|
+
return isNaN(result) ? fallback : result;
|
|
180
214
|
case ArgType.String:
|
|
181
215
|
return [undefined, null, false].includes(val) ? '' : val+'';
|
|
182
216
|
case ArgType.JSON:
|
|
@@ -190,7 +224,9 @@ function getArg(el, name, val=null, type=ArgType.String) {
|
|
|
190
224
|
} catch (e) {
|
|
191
225
|
return val;
|
|
192
226
|
}
|
|
193
|
-
else return
|
|
227
|
+
else return fallback;
|
|
228
|
+
|
|
229
|
+
// type not provided
|
|
194
230
|
default:
|
|
195
231
|
return val;
|
|
196
232
|
}
|
|
@@ -326,8 +362,9 @@ var Globals = {
|
|
|
326
362
|
rendered: new WeakSet(),
|
|
327
363
|
|
|
328
364
|
/**
|
|
329
|
-
* Used by watch3 to see which expressions are being accessed.
|
|
330
|
-
|
|
365
|
+
* Used by watch3 to see which expressions are being accessed.
|
|
366
|
+
* @type {[]}*/
|
|
367
|
+
currentExprPath: null,
|
|
331
368
|
|
|
332
369
|
/**
|
|
333
370
|
* @type {Object<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
|
|
@@ -667,7 +704,10 @@ class MultiValueMap {
|
|
|
667
704
|
return false;
|
|
668
705
|
}
|
|
669
706
|
|
|
670
|
-
|
|
707
|
+
/**
|
|
708
|
+
* Get all values for a key.
|
|
709
|
+
* @param key {string}
|
|
710
|
+
* @returns {Set|*[]} */
|
|
671
711
|
getAll(key) {
|
|
672
712
|
return this.data[key] || [];
|
|
673
713
|
}
|
|
@@ -676,7 +716,7 @@ class MultiValueMap {
|
|
|
676
716
|
* Remove one value from a key, and return it.
|
|
677
717
|
* @param key {string}
|
|
678
718
|
* @param val If specified, make sure we delete this specific value, if a key exists more than once.
|
|
679
|
-
* @returns {
|
|
719
|
+
* @returns {*|undefined} The deleted item. */
|
|
680
720
|
delete(key, val=undefined) {
|
|
681
721
|
// if (key === '["Html2",[[["Html3",["F1","A"]],["Html3",["F1","B"]]]]]')
|
|
682
722
|
// debugger;
|
|
@@ -712,6 +752,36 @@ class MultiValueMap {
|
|
|
712
752
|
return result;
|
|
713
753
|
}
|
|
714
754
|
|
|
755
|
+
/**
|
|
756
|
+
* Try to delete an item that matches the key and the isPreferred function.
|
|
757
|
+
* if not the latter, just delete any item that matches the key.
|
|
758
|
+
* @param key {string}
|
|
759
|
+
* @param isPreferred {function}
|
|
760
|
+
* @returns {*|undefined} The deleted item. */
|
|
761
|
+
deletePreferred(key, isPreferred) {
|
|
762
|
+
let result;
|
|
763
|
+
let data = this.data;
|
|
764
|
+
let set = data[key];
|
|
765
|
+
if (!set)
|
|
766
|
+
return undefined;
|
|
767
|
+
|
|
768
|
+
for (let val of set)
|
|
769
|
+
if (isPreferred(val)) {
|
|
770
|
+
set.delete(val);
|
|
771
|
+
result = val;
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
if (!result) {
|
|
775
|
+
[result] = set;
|
|
776
|
+
set.delete(result);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (set.size === 0)
|
|
780
|
+
delete data[key];
|
|
781
|
+
|
|
782
|
+
return result;
|
|
783
|
+
}
|
|
784
|
+
|
|
715
785
|
hasValue(val) {
|
|
716
786
|
let data = this.data;
|
|
717
787
|
let names = [];
|
|
@@ -978,6 +1048,9 @@ class ExprPath {
|
|
|
978
1048
|
nodeMarkerPath;
|
|
979
1049
|
|
|
980
1050
|
|
|
1051
|
+
/** @type {?function} */
|
|
1052
|
+
watchFunction
|
|
1053
|
+
|
|
981
1054
|
/**
|
|
982
1055
|
* @param nodeBefore {Node}
|
|
983
1056
|
* @param nodeMarker {?Node}
|
|
@@ -1047,6 +1120,11 @@ class ExprPath {
|
|
|
1047
1120
|
applyNodes(expr) {
|
|
1048
1121
|
let path = this;
|
|
1049
1122
|
|
|
1123
|
+
// This can be done at the beginning or the end of this function.
|
|
1124
|
+
// If at the end, we may get rendering done faster.
|
|
1125
|
+
// But when at the beginning, it leaves all the nodes in-use so we can do a renderWatched().
|
|
1126
|
+
path.freeNodeGroups();
|
|
1127
|
+
|
|
1050
1128
|
|
|
1051
1129
|
|
|
1052
1130
|
/** @type {(Node|NodeGroup|Expr)[]} */
|
|
@@ -1111,15 +1189,52 @@ class ExprPath {
|
|
|
1111
1189
|
// Rearrange nodes.
|
|
1112
1190
|
udomdiff(path.nodeMarker.parentNode, oldNodes, newNodes, path.nodeMarker);
|
|
1113
1191
|
|
|
1114
|
-
|
|
1192
|
+
// TODO: Put this in a remove() function of NodeGroup.
|
|
1193
|
+
// Then only run it on the old nodeGroups that were actually removed.
|
|
1194
|
+
//Util.saveOrphans(oldNodeGroups, oldNodes);
|
|
1195
|
+
|
|
1196
|
+
for (let ng of oldNodeGroups)
|
|
1197
|
+
if (!ng.startNode.parentNode)
|
|
1198
|
+
ng.saveOrphans();
|
|
1115
1199
|
}
|
|
1116
1200
|
|
|
1117
|
-
// Must happen after second pass.
|
|
1118
|
-
path.freeNodeGroups();
|
|
1119
1201
|
|
|
1120
1202
|
|
|
1121
1203
|
}
|
|
1122
1204
|
|
|
1205
|
+
/**
|
|
1206
|
+
* Used by watch() for replacing individual loop items. */
|
|
1207
|
+
applyLoopItemUpdate(index, template) {
|
|
1208
|
+
// At this point none of the nodes being used will be in nodeGroupsFree.
|
|
1209
|
+
let oldNg = this.nodeGroups[index];
|
|
1210
|
+
this.nodeGroupsFree.add(oldNg.exactKey, oldNg);
|
|
1211
|
+
this.nodeGroupsFree.add(oldNg.closeKey, oldNg);
|
|
1212
|
+
|
|
1213
|
+
let ng = this.getNodeGroup(template, true);
|
|
1214
|
+
if (ng) {
|
|
1215
|
+
return; // It's an exactl match, so replace nothing.
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
ng = this.getNodeGroup(template, false);
|
|
1221
|
+
|
|
1222
|
+
this.nodeGroups[index] = ng;
|
|
1223
|
+
|
|
1224
|
+
// Splice in the new nodes.
|
|
1225
|
+
for (let node of ng.getNodes()) {
|
|
1226
|
+
oldNg.startNode.parentNode.insertBefore(node, oldNg.startNode);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
if (oldNg !== ng) {
|
|
1230
|
+
for (let node of oldNg.getNodes())
|
|
1231
|
+
node.remove();
|
|
1232
|
+
oldNg.saveOrphans();
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// TODO: update or invalidate the nodes cache?
|
|
1236
|
+
this.nodesCache = null;
|
|
1237
|
+
}
|
|
1123
1238
|
|
|
1124
1239
|
|
|
1125
1240
|
/**
|
|
@@ -1166,7 +1281,11 @@ class ExprPath {
|
|
|
1166
1281
|
this.applyExact(subExpr, newNodes, secondPass);
|
|
1167
1282
|
|
|
1168
1283
|
else if (typeof expr === 'function') {
|
|
1284
|
+
// TODO: One ExprPath can have multiple expr functions.
|
|
1285
|
+
// But if using it as a watch, it should only have one at the top level.
|
|
1286
|
+
// So maybe this is ok.
|
|
1169
1287
|
Globals.currentExprPath = [this, expr]; // Used by watch3()
|
|
1288
|
+
this.watchFunction = expr; // TODO: Only do this if it's a top level function.
|
|
1170
1289
|
let result = expr();
|
|
1171
1290
|
Globals.currentExprPath = null;
|
|
1172
1291
|
|
|
@@ -1256,7 +1375,6 @@ class ExprPath {
|
|
|
1256
1375
|
args = expr.slice(1);
|
|
1257
1376
|
}
|
|
1258
1377
|
|
|
1259
|
-
// Undocumented.
|
|
1260
1378
|
// oninput=${[this, 'value']}
|
|
1261
1379
|
else {
|
|
1262
1380
|
func = setValue;
|
|
@@ -1268,20 +1386,27 @@ class ExprPath {
|
|
|
1268
1386
|
else
|
|
1269
1387
|
func = expr;
|
|
1270
1388
|
|
|
1389
|
+
this.bindEvent(node, root, eventName, eventName, func, args);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
|
|
1393
|
+
bindEvent(node, root, key, eventName, func, args, capture=false) {
|
|
1271
1394
|
let nodeEvents = Globals.nodeEvents.get(node);
|
|
1272
1395
|
if (!nodeEvents) {
|
|
1273
|
-
nodeEvents = {[
|
|
1396
|
+
nodeEvents = {[key]: new Array(3)};
|
|
1274
1397
|
Globals.nodeEvents.set(node, nodeEvents);
|
|
1275
1398
|
}
|
|
1276
|
-
let nodeEvent = nodeEvents[
|
|
1277
|
-
|
|
1399
|
+
let nodeEvent = nodeEvents[key];
|
|
1400
|
+
if (!nodeEvent)
|
|
1401
|
+
nodeEvents[key] = nodeEvent = new Array(3);
|
|
1278
1402
|
|
|
1279
1403
|
|
|
1280
1404
|
// If function has changed, remove and rebind the event.
|
|
1281
1405
|
if (nodeEvent[0] !== func) {
|
|
1406
|
+
|
|
1282
1407
|
let [existing, existingBound, _] = nodeEvent;
|
|
1283
1408
|
if (existing)
|
|
1284
|
-
node.removeEventListener(eventName, existingBound);
|
|
1409
|
+
node.removeEventListener(eventName, existingBound, capture);
|
|
1285
1410
|
|
|
1286
1411
|
let originalFunc = func;
|
|
1287
1412
|
|
|
@@ -1297,15 +1422,15 @@ class ExprPath {
|
|
|
1297
1422
|
nodeEvent[0] = originalFunc;
|
|
1298
1423
|
nodeEvent[1] = boundFunc;
|
|
1299
1424
|
|
|
1300
|
-
node.addEventListener(eventName, boundFunc);
|
|
1425
|
+
node.addEventListener(eventName, boundFunc, capture);
|
|
1301
1426
|
|
|
1302
1427
|
// TODO: classic event attribs?
|
|
1303
|
-
//el[attr.name] = e => // e.g. el.onclick = ...
|
|
1304
|
-
// (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el)
|
|
1428
|
+
//el[attr.name] = e => // e.g. el.onclick = ... // put "event", "el", and "this" in scope for the event code.
|
|
1429
|
+
// (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el)
|
|
1305
1430
|
}
|
|
1306
1431
|
|
|
1307
1432
|
// Otherwise just update the args to the function.
|
|
1308
|
-
nodeEvents[
|
|
1433
|
+
nodeEvents[key][2] = args;
|
|
1309
1434
|
}
|
|
1310
1435
|
|
|
1311
1436
|
applyValueAttrib(node, exprs, exprIndex) {
|
|
@@ -1323,9 +1448,14 @@ class ExprPath {
|
|
|
1323
1448
|
else if ((this.attrName === 'value' || this.attrName === 'data-value') && Util.isPath(expr)) {
|
|
1324
1449
|
let [obj, path] = [expr[0], expr.slice(1)];
|
|
1325
1450
|
node.value = delve(obj, path);
|
|
1326
|
-
|
|
1451
|
+
// TODO: We need to remove any old listeners, like in bindEventAttribute
|
|
1452
|
+
|
|
1453
|
+
let func = () => {
|
|
1327
1454
|
delve(obj, path, Util.getInputValue(node));
|
|
1328
|
-
}
|
|
1455
|
+
};
|
|
1456
|
+
|
|
1457
|
+
// We use capture so we update the values before other events added by the user.
|
|
1458
|
+
this.bindEvent(node, path[0], 'value', 'input', func, [], true);
|
|
1329
1459
|
}
|
|
1330
1460
|
|
|
1331
1461
|
// Regular attribute
|
|
@@ -1502,8 +1632,9 @@ class ExprPath {
|
|
|
1502
1632
|
|
|
1503
1633
|
let result;
|
|
1504
1634
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1635
|
+
// TODO: Would it be faster to maintain a separate list of detached nodegroups?
|
|
1636
|
+
if (exact) { // [below] parentElement will be null if the parent is a DocumentFragment
|
|
1637
|
+
result = this.nodeGroupsFree.deletePreferred(template.getExactKey(), ng=>ng.startNode.parentElement);
|
|
1507
1638
|
if (result) // also delete the matching close key.
|
|
1508
1639
|
this.nodeGroupsFree.delete(template.getCloseKey(), result);
|
|
1509
1640
|
else
|
|
@@ -1514,7 +1645,7 @@ class ExprPath {
|
|
|
1514
1645
|
// This is a match that has matching html, but different expressions applied.
|
|
1515
1646
|
// We can then apply the expressions to make it an exact match.
|
|
1516
1647
|
else {
|
|
1517
|
-
result = this.nodeGroupsFree.
|
|
1648
|
+
result = this.nodeGroupsFree.deletePreferred(template.getCloseKey(), ng=>ng.startNode.parentElement);
|
|
1518
1649
|
if (result) {
|
|
1519
1650
|
|
|
1520
1651
|
this.nodeGroupsFree.delete(result.exactKey, result);
|
|
@@ -1557,12 +1688,18 @@ class ExprPath {
|
|
|
1557
1688
|
* @type {MultiValueMap<key:string, value:NodeGroup>} */
|
|
1558
1689
|
nodeGroupsFree = new MultiValueMap();
|
|
1559
1690
|
|
|
1691
|
+
nodeGroupsDetached = new MultiValueMap();
|
|
1692
|
+
|
|
1560
1693
|
|
|
1561
1694
|
/**
|
|
1562
1695
|
* Move everything from this.nodeGroupsInUse to this.nodeGroupsFree.
|
|
1563
1696
|
* TODO: this could run as needed in getNodeGroup? */
|
|
1564
1697
|
freeNodeGroups() {
|
|
1565
1698
|
// old:
|
|
1699
|
+
|
|
1700
|
+
//this.nodeGroupsDetached = this.nodeGroupsFree;
|
|
1701
|
+
//this.nodeGroupsFree = new MultiValueMap();
|
|
1702
|
+
|
|
1566
1703
|
let ngf = this.nodeGroupsFree;
|
|
1567
1704
|
for (let ng of this.nodeGroupsInUse) {
|
|
1568
1705
|
ngf.add(ng.exactKey, ng);
|
|
@@ -2274,6 +2411,16 @@ class NodeGroup {
|
|
|
2274
2411
|
return this.rootNg;
|
|
2275
2412
|
}
|
|
2276
2413
|
|
|
2414
|
+
/**
|
|
2415
|
+
* Requires the nodeCache to be present. */
|
|
2416
|
+
saveOrphans() {
|
|
2417
|
+
|
|
2418
|
+
|
|
2419
|
+
let fragment = document.createDocumentFragment();
|
|
2420
|
+
for (let node of this.getNodes())
|
|
2421
|
+
fragment.append(node);
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2277
2424
|
|
|
2278
2425
|
updatePaths(fragment, paths, offset) {
|
|
2279
2426
|
// Update paths to point to the fragment.
|
|
@@ -2370,6 +2517,23 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2370
2517
|
* @type {HTMLElement} */
|
|
2371
2518
|
root;
|
|
2372
2519
|
|
|
2520
|
+
/**
|
|
2521
|
+
* Store the expressions that use this watched variable,
|
|
2522
|
+
* along with the functions used to get their values.
|
|
2523
|
+
* @type {Object<field:string, Set<ExprPath>>} */
|
|
2524
|
+
watchedExprPaths = {};
|
|
2525
|
+
|
|
2526
|
+
/**
|
|
2527
|
+
* Map from arrays where .map is called and their callback functions.
|
|
2528
|
+
* TODO: One array might be called with two different map functions in different places!
|
|
2529
|
+
* @type {Map<Array, function>} */
|
|
2530
|
+
mapCallbacks = new Map();
|
|
2531
|
+
|
|
2532
|
+
/**
|
|
2533
|
+
*
|
|
2534
|
+
* @type {Map<ExprPath, boolean|Array>} */
|
|
2535
|
+
exprsToRender = new Map();
|
|
2536
|
+
|
|
2373
2537
|
/**
|
|
2374
2538
|
*
|
|
2375
2539
|
* @param template
|
|
@@ -2388,6 +2552,7 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2388
2552
|
let offset = 0;
|
|
2389
2553
|
let root = fragment; // TODO: Rename so it's not confused with this.root.
|
|
2390
2554
|
if (el) {
|
|
2555
|
+
Globals.nodeGroups.set(el, this);
|
|
2391
2556
|
|
|
2392
2557
|
// Save slot children
|
|
2393
2558
|
let slotFragment;
|
|
@@ -2433,12 +2598,15 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2433
2598
|
}
|
|
2434
2599
|
|
|
2435
2600
|
root = el;
|
|
2601
|
+
|
|
2436
2602
|
this.startNode = el;
|
|
2437
2603
|
this.endNode = el;
|
|
2438
2604
|
}
|
|
2439
2605
|
else {
|
|
2440
2606
|
let singleEl = getSingleEl(fragment);
|
|
2441
2607
|
this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
|
|
2608
|
+
|
|
2609
|
+
Globals.nodeGroups.set(this.root, this);
|
|
2442
2610
|
if (singleEl) {
|
|
2443
2611
|
root = singleEl;
|
|
2444
2612
|
offset = 1;
|
|
@@ -2452,6 +2620,11 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2452
2620
|
// Apply exprs
|
|
2453
2621
|
this.applyExprs(template.exprs);
|
|
2454
2622
|
}
|
|
2623
|
+
|
|
2624
|
+
clearRenderWatched() {
|
|
2625
|
+
this.watchedExprPaths = {};
|
|
2626
|
+
this.mapCallbacks = new Map();
|
|
2627
|
+
}
|
|
2455
2628
|
}
|
|
2456
2629
|
|
|
2457
2630
|
function getSingleEl(fragment) {
|
|
@@ -2546,14 +2719,14 @@ class Template {
|
|
|
2546
2719
|
if (standalone) {
|
|
2547
2720
|
ng = new RootNodeGroup(this, null, options);
|
|
2548
2721
|
el = ng.getRootNode();
|
|
2549
|
-
Globals.nodeGroups.set(el, ng);
|
|
2722
|
+
//Globals.nodeGroups.set(el, ng);
|
|
2550
2723
|
firstTime = true;
|
|
2551
2724
|
}
|
|
2552
2725
|
else {
|
|
2553
2726
|
ng = Globals.nodeGroups.get(el);
|
|
2554
2727
|
if (!ng) {
|
|
2555
2728
|
ng = new RootNodeGroup(this, el, options);
|
|
2556
|
-
Globals.nodeGroups.set(el, ng);
|
|
2729
|
+
//Globals.nodeGroups.set(el, ng);
|
|
2557
2730
|
firstTime = true;
|
|
2558
2731
|
}
|
|
2559
2732
|
}
|
|
@@ -2563,8 +2736,10 @@ class Template {
|
|
|
2563
2736
|
if (!firstTime) {
|
|
2564
2737
|
if (this.html?.length === 1 && !this.html[0])
|
|
2565
2738
|
el.innerHTML = ''; // Fast path for empty component.
|
|
2566
|
-
else
|
|
2739
|
+
else {
|
|
2740
|
+
ng.clearRenderWatched();
|
|
2567
2741
|
ng.applyExprs(this.exprs);
|
|
2742
|
+
}
|
|
2568
2743
|
}
|
|
2569
2744
|
|
|
2570
2745
|
return el;
|
|
@@ -2903,6 +3078,152 @@ function createSolarite(extendsTag=null) {
|
|
|
2903
3078
|
}
|
|
2904
3079
|
}
|
|
2905
3080
|
|
|
3081
|
+
/**
|
|
3082
|
+
* Trying to be able to automatically watch primitive values.
|
|
3083
|
+
* TODO:
|
|
3084
|
+
* 1. Have get() return Proxies for nested updates.
|
|
3085
|
+
* 2. Override .map() for loops to capture changes.
|
|
3086
|
+
*/
|
|
3087
|
+
|
|
3088
|
+
let unusedArg = Symbol('unusedArg');
|
|
3089
|
+
|
|
3090
|
+
/**
|
|
3091
|
+
* Custom map function triggers the get() Proxy.
|
|
3092
|
+
* @param array {Array}
|
|
3093
|
+
* @param callback {function}
|
|
3094
|
+
* @returns {*[]} */
|
|
3095
|
+
function map(array, callback) {
|
|
3096
|
+
let result = [];
|
|
3097
|
+
for (let i=0; i<array.length; i++)
|
|
3098
|
+
result.push(callback(array[i], i, array));
|
|
3099
|
+
return result;
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
|
|
3103
|
+
/**
|
|
3104
|
+
*
|
|
3105
|
+
* @param root {HTMLElement}
|
|
3106
|
+
* @param field {string}
|
|
3107
|
+
* @param value {string|Symbol} */
|
|
3108
|
+
function watch3(root, field, value=unusedArg) {
|
|
3109
|
+
// Store internal value used by get/set.
|
|
3110
|
+
if (value !== unusedArg)
|
|
3111
|
+
root[field] = value;
|
|
3112
|
+
else
|
|
3113
|
+
value = root[field];
|
|
3114
|
+
|
|
3115
|
+
|
|
3116
|
+
// use a single object for both defineProperty and new Proxy's handler.
|
|
3117
|
+
const handler = {
|
|
3118
|
+
get(obj, prop, receiver) {
|
|
3119
|
+
|
|
3120
|
+
let result = (obj === receiver && field === prop)
|
|
3121
|
+
? value // top-level value.
|
|
3122
|
+
: Reflect.get(obj, prop, receiver); // avoid infinite recursion.
|
|
3123
|
+
|
|
3124
|
+
if (prop === 'map')
|
|
3125
|
+
|
|
3126
|
+
// Double function so the ExprPath calls it as a function,
|
|
3127
|
+
// instead of it being evaluated immediately when the Templat eis created.
|
|
3128
|
+
return (callback) => () => {
|
|
3129
|
+
let rootNg = Globals.nodeGroups.get(root);
|
|
3130
|
+
rootNg.mapCallbacks.set(obj, callback);
|
|
3131
|
+
return map(new Proxy(obj, handler), callback);
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
// Track which ExprPath is using this variable.
|
|
3135
|
+
if (Globals.currentExprPath) {
|
|
3136
|
+
let [exprPath, exprFunction] = Globals.currentExprPath; // Set in ExprPath.applyExact()
|
|
3137
|
+
|
|
3138
|
+
let rootNg = Globals.nodeGroups.get(root);
|
|
3139
|
+
|
|
3140
|
+
// Init for field.
|
|
3141
|
+
rootNg.watchedExprPaths[field] = rootNg.watchedExprPaths[field] || new Set();
|
|
3142
|
+
rootNg.watchedExprPaths[field].add(exprPath);
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
if (result && typeof result === 'object')
|
|
3146
|
+
return new Proxy(result, handler);
|
|
3147
|
+
|
|
3148
|
+
return result;
|
|
3149
|
+
},
|
|
3150
|
+
|
|
3151
|
+
|
|
3152
|
+
// TODO: Will fail for attribute w/ a value having multiple ExprPaths.
|
|
3153
|
+
// TODO: This won't update a component's expressions.
|
|
3154
|
+
set(obj, prop, val, receiver) {
|
|
3155
|
+
|
|
3156
|
+
// 1. Set the value.
|
|
3157
|
+
if (obj === receiver && field === prop)
|
|
3158
|
+
value = val; // top-level value.
|
|
3159
|
+
else // avoid infinite recursion.
|
|
3160
|
+
Reflect.set(obj, prop, val, receiver);
|
|
3161
|
+
|
|
3162
|
+
// 2. Add to the list of ExprPaths to re-render.
|
|
3163
|
+
let rootNg = Globals.nodeGroups.get(root);
|
|
3164
|
+
for (let exprPath of rootNg.watchedExprPaths[field]) {
|
|
3165
|
+
|
|
3166
|
+
// Update a single NodeGroup created by array.map()
|
|
3167
|
+
if (Array.isArray(obj) && parseInt(prop) == prop) {
|
|
3168
|
+
let exprsToRender = rootNg.exprsToRender.get(exprPath);
|
|
3169
|
+
|
|
3170
|
+
// If we're not re-rendering the whole thing.
|
|
3171
|
+
if (exprsToRender !== true)
|
|
3172
|
+
Util$1.mapAdd(rootNg.exprsToRender, exprPath, [obj, prop, val]);
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
// Reapply the whole expression.
|
|
3176
|
+
else
|
|
3177
|
+
rootNg.exprsToRender.set(exprPath, true);
|
|
3178
|
+
}
|
|
3179
|
+
return true;
|
|
3180
|
+
}
|
|
3181
|
+
};
|
|
3182
|
+
|
|
3183
|
+
Object.defineProperty(root, field, {
|
|
3184
|
+
get: () => handler.get(root, field, root),
|
|
3185
|
+
set: (val) => handler.set(root, field, val, root)
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
/**
|
|
3190
|
+
* TODO: Rename so we have watch.add() and watch.render() ?
|
|
3191
|
+
* @param root
|
|
3192
|
+
* @returns {*[]} */
|
|
3193
|
+
function renderWatched(root) {
|
|
3194
|
+
let rootNg = Globals.nodeGroups.get(root);
|
|
3195
|
+
let modified = [];
|
|
3196
|
+
|
|
3197
|
+
for (let [exprPath, params] of rootNg.exprsToRender) {
|
|
3198
|
+
|
|
3199
|
+
// Reapply the whole expression.
|
|
3200
|
+
if (params === true) {
|
|
3201
|
+
exprPath.apply(exprPath.watchFunction);
|
|
3202
|
+
|
|
3203
|
+
// TODO: freeNodeGroups() could be skipped if applyExprs() never marked them as in-use.
|
|
3204
|
+
exprPath.freeNodeGroups();
|
|
3205
|
+
|
|
3206
|
+
modified.push(...exprPath.getNodes());
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
// Update a single NodeGroup created by array.map()
|
|
3210
|
+
else {
|
|
3211
|
+
for (let row of params) {
|
|
3212
|
+
let [obj, prop, value] = row;
|
|
3213
|
+
let callback = rootNg.mapCallbacks.get(obj);
|
|
3214
|
+
let template = callback(value);
|
|
3215
|
+
exprPath.applyLoopItemUpdate(prop, template);
|
|
3216
|
+
|
|
3217
|
+
modified.push(...exprPath.nodeGroups[prop].getNodes());
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
rootNg.exprsToRender = new Map(); // clear
|
|
3223
|
+
|
|
3224
|
+
return modified;
|
|
3225
|
+
}
|
|
3226
|
+
|
|
2906
3227
|
/**
|
|
2907
3228
|
* Solarite JavasCript UI library.
|
|
2908
3229
|
* MIT License
|
|
@@ -2918,9 +3239,6 @@ let Solarite = new Proxy(createSolarite(), {
|
|
|
2918
3239
|
}
|
|
2919
3240
|
});
|
|
2920
3241
|
let getInputValue = Util.getInputValue;
|
|
2921
|
-
|
|
2922
|
-
//Experimental:
|
|
2923
|
-
//export {forEach, watchGet, watchSet} from './watch.js' // old, unfinished
|
|
2924
|
-
//export {watch} from './watch2.js'; // unfinished
|
|
3242
|
+
// unfinished
|
|
2925
3243
|
|
|
2926
|
-
export { ArgType, Globals, Solarite, Template, delve, getArg, getInputValue, r };
|
|
3244
|
+
export { ArgType, Globals, Solarite, Template, delve, getArg, getInputValue, r, renderWatched, watch3 as watch };
|