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-debug.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
|
}
|
|
@@ -334,8 +370,9 @@ var Globals = {
|
|
|
334
370
|
rendered: new WeakSet(),
|
|
335
371
|
|
|
336
372
|
/**
|
|
337
|
-
* Used by watch3 to see which expressions are being accessed.
|
|
338
|
-
|
|
373
|
+
* Used by watch3 to see which expressions are being accessed.
|
|
374
|
+
* @type {[]}*/
|
|
375
|
+
currentExprPath: null,
|
|
339
376
|
|
|
340
377
|
/**
|
|
341
378
|
* @type {Object<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
|
|
@@ -734,7 +771,10 @@ class MultiValueMap {
|
|
|
734
771
|
return false;
|
|
735
772
|
}
|
|
736
773
|
|
|
737
|
-
|
|
774
|
+
/**
|
|
775
|
+
* Get all values for a key.
|
|
776
|
+
* @param key {string}
|
|
777
|
+
* @returns {Set|*[]} */
|
|
738
778
|
getAll(key) {
|
|
739
779
|
return this.data[key] || [];
|
|
740
780
|
}
|
|
@@ -743,7 +783,7 @@ class MultiValueMap {
|
|
|
743
783
|
* Remove one value from a key, and return it.
|
|
744
784
|
* @param key {string}
|
|
745
785
|
* @param val If specified, make sure we delete this specific value, if a key exists more than once.
|
|
746
|
-
* @returns {
|
|
786
|
+
* @returns {*|undefined} The deleted item. */
|
|
747
787
|
delete(key, val=undefined) {
|
|
748
788
|
// if (key === '["Html2",[[["Html3",["F1","A"]],["Html3",["F1","B"]]]]]')
|
|
749
789
|
// debugger;
|
|
@@ -779,6 +819,36 @@ class MultiValueMap {
|
|
|
779
819
|
return result;
|
|
780
820
|
}
|
|
781
821
|
|
|
822
|
+
/**
|
|
823
|
+
* Try to delete an item that matches the key and the isPreferred function.
|
|
824
|
+
* if not the latter, just delete any item that matches the key.
|
|
825
|
+
* @param key {string}
|
|
826
|
+
* @param isPreferred {function}
|
|
827
|
+
* @returns {*|undefined} The deleted item. */
|
|
828
|
+
deletePreferred(key, isPreferred) {
|
|
829
|
+
let result;
|
|
830
|
+
let data = this.data;
|
|
831
|
+
let set = data[key];
|
|
832
|
+
if (!set)
|
|
833
|
+
return undefined;
|
|
834
|
+
|
|
835
|
+
for (let val of set)
|
|
836
|
+
if (isPreferred(val)) {
|
|
837
|
+
set.delete(val);
|
|
838
|
+
result = val;
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
if (!result) {
|
|
842
|
+
[result] = set;
|
|
843
|
+
set.delete(result);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (set.size === 0)
|
|
847
|
+
delete data[key];
|
|
848
|
+
|
|
849
|
+
return result;
|
|
850
|
+
}
|
|
851
|
+
|
|
782
852
|
hasValue(val) {
|
|
783
853
|
let data = this.data;
|
|
784
854
|
let names = [];
|
|
@@ -1085,6 +1155,9 @@ class ExprPath {
|
|
|
1085
1155
|
nodeMarkerPath;
|
|
1086
1156
|
|
|
1087
1157
|
|
|
1158
|
+
/** @type {?function} */
|
|
1159
|
+
watchFunction
|
|
1160
|
+
|
|
1088
1161
|
/**
|
|
1089
1162
|
* @param nodeBefore {Node}
|
|
1090
1163
|
* @param nodeMarker {?Node}
|
|
@@ -1154,6 +1227,11 @@ class ExprPath {
|
|
|
1154
1227
|
applyNodes(expr) {
|
|
1155
1228
|
let path = this;
|
|
1156
1229
|
|
|
1230
|
+
// This can be done at the beginning or the end of this function.
|
|
1231
|
+
// If at the end, we may get rendering done faster.
|
|
1232
|
+
// But when at the beginning, it leaves all the nodes in-use so we can do a renderWatched().
|
|
1233
|
+
path.freeNodeGroups();
|
|
1234
|
+
|
|
1157
1235
|
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
1158
1236
|
|
|
1159
1237
|
/** @type {(Node|NodeGroup|Expr)[]} */
|
|
@@ -1218,15 +1296,52 @@ class ExprPath {
|
|
|
1218
1296
|
// Rearrange nodes.
|
|
1219
1297
|
udomdiff(path.nodeMarker.parentNode, oldNodes, newNodes, path.nodeMarker);
|
|
1220
1298
|
|
|
1221
|
-
|
|
1299
|
+
// TODO: Put this in a remove() function of NodeGroup.
|
|
1300
|
+
// Then only run it on the old nodeGroups that were actually removed.
|
|
1301
|
+
//Util.saveOrphans(oldNodeGroups, oldNodes);
|
|
1302
|
+
|
|
1303
|
+
for (let ng of oldNodeGroups)
|
|
1304
|
+
if (!ng.startNode.parentNode)
|
|
1305
|
+
ng.saveOrphans();
|
|
1222
1306
|
}
|
|
1223
1307
|
|
|
1224
|
-
// Must happen after second pass.
|
|
1225
|
-
path.freeNodeGroups();
|
|
1226
1308
|
|
|
1227
1309
|
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
1228
1310
|
}
|
|
1229
1311
|
|
|
1312
|
+
/**
|
|
1313
|
+
* Used by watch() for replacing individual loop items. */
|
|
1314
|
+
applyLoopItemUpdate(index, template) {
|
|
1315
|
+
// At this point none of the nodes being used will be in nodeGroupsFree.
|
|
1316
|
+
let oldNg = this.nodeGroups[index];
|
|
1317
|
+
this.nodeGroupsFree.add(oldNg.exactKey, oldNg);
|
|
1318
|
+
this.nodeGroupsFree.add(oldNg.closeKey, oldNg);
|
|
1319
|
+
|
|
1320
|
+
let ng = this.getNodeGroup(template, true);
|
|
1321
|
+
if (ng) {
|
|
1322
|
+
return; // It's an exactl match, so replace nothing.
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
ng = this.getNodeGroup(template, false);
|
|
1328
|
+
|
|
1329
|
+
this.nodeGroups[index] = ng;
|
|
1330
|
+
|
|
1331
|
+
// Splice in the new nodes.
|
|
1332
|
+
for (let node of ng.getNodes()) {
|
|
1333
|
+
oldNg.startNode.parentNode.insertBefore(node, oldNg.startNode);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
if (oldNg !== ng) {
|
|
1337
|
+
for (let node of oldNg.getNodes())
|
|
1338
|
+
node.remove();
|
|
1339
|
+
oldNg.saveOrphans();
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// TODO: update or invalidate the nodes cache?
|
|
1343
|
+
this.nodesCache = null;
|
|
1344
|
+
}
|
|
1230
1345
|
|
|
1231
1346
|
|
|
1232
1347
|
/**
|
|
@@ -1273,7 +1388,11 @@ class ExprPath {
|
|
|
1273
1388
|
this.applyExact(subExpr, newNodes, secondPass);
|
|
1274
1389
|
|
|
1275
1390
|
else if (typeof expr === 'function') {
|
|
1391
|
+
// TODO: One ExprPath can have multiple expr functions.
|
|
1392
|
+
// But if using it as a watch, it should only have one at the top level.
|
|
1393
|
+
// So maybe this is ok.
|
|
1276
1394
|
Globals.currentExprPath = [this, expr]; // Used by watch3()
|
|
1395
|
+
this.watchFunction = expr; // TODO: Only do this if it's a top level function.
|
|
1277
1396
|
let result = expr();
|
|
1278
1397
|
Globals.currentExprPath = null;
|
|
1279
1398
|
|
|
@@ -1366,7 +1485,6 @@ class ExprPath {
|
|
|
1366
1485
|
args = expr.slice(1);
|
|
1367
1486
|
}
|
|
1368
1487
|
|
|
1369
|
-
// Undocumented.
|
|
1370
1488
|
// oninput=${[this, 'value']}
|
|
1371
1489
|
else {
|
|
1372
1490
|
func = setValue;
|
|
@@ -1378,20 +1496,27 @@ class ExprPath {
|
|
|
1378
1496
|
else
|
|
1379
1497
|
func = expr;
|
|
1380
1498
|
|
|
1499
|
+
this.bindEvent(node, root, eventName, eventName, func, args);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
|
|
1503
|
+
bindEvent(node, root, key, eventName, func, args, capture=false) {
|
|
1381
1504
|
let nodeEvents = Globals.nodeEvents.get(node);
|
|
1382
1505
|
if (!nodeEvents) {
|
|
1383
|
-
nodeEvents = {[
|
|
1506
|
+
nodeEvents = {[key]: new Array(3)};
|
|
1384
1507
|
Globals.nodeEvents.set(node, nodeEvents);
|
|
1385
1508
|
}
|
|
1386
|
-
let nodeEvent = nodeEvents[
|
|
1387
|
-
|
|
1509
|
+
let nodeEvent = nodeEvents[key];
|
|
1510
|
+
if (!nodeEvent)
|
|
1511
|
+
nodeEvents[key] = nodeEvent = new Array(3);
|
|
1388
1512
|
|
|
1389
1513
|
|
|
1390
1514
|
// If function has changed, remove and rebind the event.
|
|
1391
1515
|
if (nodeEvent[0] !== func) {
|
|
1516
|
+
|
|
1392
1517
|
let [existing, existingBound, _] = nodeEvent;
|
|
1393
1518
|
if (existing)
|
|
1394
|
-
node.removeEventListener(eventName, existingBound);
|
|
1519
|
+
node.removeEventListener(eventName, existingBound, capture);
|
|
1395
1520
|
|
|
1396
1521
|
let originalFunc = func;
|
|
1397
1522
|
|
|
@@ -1407,15 +1532,15 @@ class ExprPath {
|
|
|
1407
1532
|
nodeEvent[0] = originalFunc;
|
|
1408
1533
|
nodeEvent[1] = boundFunc;
|
|
1409
1534
|
|
|
1410
|
-
node.addEventListener(eventName, boundFunc);
|
|
1535
|
+
node.addEventListener(eventName, boundFunc, capture);
|
|
1411
1536
|
|
|
1412
1537
|
// TODO: classic event attribs?
|
|
1413
|
-
//el[attr.name] = e => // e.g. el.onclick = ...
|
|
1414
|
-
// (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el)
|
|
1538
|
+
//el[attr.name] = e => // e.g. el.onclick = ... // put "event", "el", and "this" in scope for the event code.
|
|
1539
|
+
// (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el)
|
|
1415
1540
|
}
|
|
1416
1541
|
|
|
1417
1542
|
// Otherwise just update the args to the function.
|
|
1418
|
-
nodeEvents[
|
|
1543
|
+
nodeEvents[key][2] = args;
|
|
1419
1544
|
}
|
|
1420
1545
|
|
|
1421
1546
|
applyValueAttrib(node, exprs, exprIndex) {
|
|
@@ -1433,9 +1558,14 @@ class ExprPath {
|
|
|
1433
1558
|
else if ((this.attrName === 'value' || this.attrName === 'data-value') && Util.isPath(expr)) {
|
|
1434
1559
|
let [obj, path] = [expr[0], expr.slice(1)];
|
|
1435
1560
|
node.value = delve(obj, path);
|
|
1436
|
-
|
|
1561
|
+
// TODO: We need to remove any old listeners, like in bindEventAttribute
|
|
1562
|
+
|
|
1563
|
+
let func = () => {
|
|
1437
1564
|
delve(obj, path, Util.getInputValue(node));
|
|
1438
|
-
}
|
|
1565
|
+
};
|
|
1566
|
+
|
|
1567
|
+
// We use capture so we update the values before other events added by the user.
|
|
1568
|
+
this.bindEvent(node, path[0], 'value', 'input', func, [], true);
|
|
1439
1569
|
}
|
|
1440
1570
|
|
|
1441
1571
|
// Regular attribute
|
|
@@ -1619,8 +1749,9 @@ class ExprPath {
|
|
|
1619
1749
|
|
|
1620
1750
|
let result;
|
|
1621
1751
|
|
|
1622
|
-
|
|
1623
|
-
|
|
1752
|
+
// TODO: Would it be faster to maintain a separate list of detached nodegroups?
|
|
1753
|
+
if (exact) { // [below] parentElement will be null if the parent is a DocumentFragment
|
|
1754
|
+
result = this.nodeGroupsFree.deletePreferred(template.getExactKey(), ng=>ng.startNode.parentElement);
|
|
1624
1755
|
if (result) // also delete the matching close key.
|
|
1625
1756
|
this.nodeGroupsFree.delete(template.getCloseKey(), result);
|
|
1626
1757
|
else
|
|
@@ -1631,7 +1762,7 @@ class ExprPath {
|
|
|
1631
1762
|
// This is a match that has matching html, but different expressions applied.
|
|
1632
1763
|
// We can then apply the expressions to make it an exact match.
|
|
1633
1764
|
else {
|
|
1634
|
-
result = this.nodeGroupsFree.
|
|
1765
|
+
result = this.nodeGroupsFree.deletePreferred(template.getCloseKey(), ng=>ng.startNode.parentElement);
|
|
1635
1766
|
if (result) {
|
|
1636
1767
|
/*#IFDEV*/assert(result.exactKey);/*#ENDIF*/
|
|
1637
1768
|
this.nodeGroupsFree.delete(result.exactKey, result);
|
|
@@ -1674,12 +1805,18 @@ class ExprPath {
|
|
|
1674
1805
|
* @type {MultiValueMap<key:string, value:NodeGroup>} */
|
|
1675
1806
|
nodeGroupsFree = new MultiValueMap();
|
|
1676
1807
|
|
|
1808
|
+
nodeGroupsDetached = new MultiValueMap();
|
|
1809
|
+
|
|
1677
1810
|
|
|
1678
1811
|
/**
|
|
1679
1812
|
* Move everything from this.nodeGroupsInUse to this.nodeGroupsFree.
|
|
1680
1813
|
* TODO: this could run as needed in getNodeGroup? */
|
|
1681
1814
|
freeNodeGroups() {
|
|
1682
1815
|
// old:
|
|
1816
|
+
|
|
1817
|
+
//this.nodeGroupsDetached = this.nodeGroupsFree;
|
|
1818
|
+
//this.nodeGroupsFree = new MultiValueMap();
|
|
1819
|
+
|
|
1683
1820
|
let ngf = this.nodeGroupsFree;
|
|
1684
1821
|
for (let ng of this.nodeGroupsInUse) {
|
|
1685
1822
|
ngf.add(ng.exactKey, ng);
|
|
@@ -2459,6 +2596,16 @@ class NodeGroup {
|
|
|
2459
2596
|
return this.rootNg;
|
|
2460
2597
|
}
|
|
2461
2598
|
|
|
2599
|
+
/**
|
|
2600
|
+
* Requires the nodeCache to be present. */
|
|
2601
|
+
saveOrphans() {
|
|
2602
|
+
/*#IFDEV*/assert(!this.startNode.parentNode);/*#ENDIF*/
|
|
2603
|
+
/*#IFDEV*/assert(this.nodesCache);/*#ENDIF*/
|
|
2604
|
+
let fragment = document.createDocumentFragment();
|
|
2605
|
+
for (let node of this.getNodes())
|
|
2606
|
+
fragment.append(node);
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2462
2609
|
|
|
2463
2610
|
updatePaths(fragment, paths, offset) {
|
|
2464
2611
|
// Update paths to point to the fragment.
|
|
@@ -2620,6 +2767,23 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2620
2767
|
* @type {HTMLElement} */
|
|
2621
2768
|
root;
|
|
2622
2769
|
|
|
2770
|
+
/**
|
|
2771
|
+
* Store the expressions that use this watched variable,
|
|
2772
|
+
* along with the functions used to get their values.
|
|
2773
|
+
* @type {Object<field:string, Set<ExprPath>>} */
|
|
2774
|
+
watchedExprPaths = {};
|
|
2775
|
+
|
|
2776
|
+
/**
|
|
2777
|
+
* Map from arrays where .map is called and their callback functions.
|
|
2778
|
+
* TODO: One array might be called with two different map functions in different places!
|
|
2779
|
+
* @type {Map<Array, function>} */
|
|
2780
|
+
mapCallbacks = new Map();
|
|
2781
|
+
|
|
2782
|
+
/**
|
|
2783
|
+
*
|
|
2784
|
+
* @type {Map<ExprPath, boolean|Array>} */
|
|
2785
|
+
exprsToRender = new Map();
|
|
2786
|
+
|
|
2623
2787
|
/**
|
|
2624
2788
|
*
|
|
2625
2789
|
* @param template
|
|
@@ -2638,6 +2802,7 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2638
2802
|
let offset = 0;
|
|
2639
2803
|
let root = fragment; // TODO: Rename so it's not confused with this.root.
|
|
2640
2804
|
if (el) {
|
|
2805
|
+
Globals.nodeGroups.set(el, this);
|
|
2641
2806
|
|
|
2642
2807
|
// Save slot children
|
|
2643
2808
|
let slotFragment;
|
|
@@ -2683,12 +2848,15 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2683
2848
|
}
|
|
2684
2849
|
|
|
2685
2850
|
root = el;
|
|
2851
|
+
|
|
2686
2852
|
this.startNode = el;
|
|
2687
2853
|
this.endNode = el;
|
|
2688
2854
|
}
|
|
2689
2855
|
else {
|
|
2690
2856
|
let singleEl = getSingleEl(fragment);
|
|
2691
2857
|
this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
|
|
2858
|
+
|
|
2859
|
+
Globals.nodeGroups.set(this.root, this);
|
|
2692
2860
|
if (singleEl) {
|
|
2693
2861
|
root = singleEl;
|
|
2694
2862
|
offset = 1;
|
|
@@ -2702,6 +2870,11 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2702
2870
|
// Apply exprs
|
|
2703
2871
|
this.applyExprs(template.exprs);
|
|
2704
2872
|
}
|
|
2873
|
+
|
|
2874
|
+
clearRenderWatched() {
|
|
2875
|
+
this.watchedExprPaths = {};
|
|
2876
|
+
this.mapCallbacks = new Map();
|
|
2877
|
+
}
|
|
2705
2878
|
}
|
|
2706
2879
|
|
|
2707
2880
|
function getSingleEl(fragment) {
|
|
@@ -2805,14 +2978,14 @@ class Template {
|
|
|
2805
2978
|
if (standalone) {
|
|
2806
2979
|
ng = new RootNodeGroup(this, null, options);
|
|
2807
2980
|
el = ng.getRootNode();
|
|
2808
|
-
Globals.nodeGroups.set(el, ng);
|
|
2981
|
+
//Globals.nodeGroups.set(el, ng);
|
|
2809
2982
|
firstTime = true;
|
|
2810
2983
|
}
|
|
2811
2984
|
else {
|
|
2812
2985
|
ng = Globals.nodeGroups.get(el);
|
|
2813
2986
|
if (!ng) {
|
|
2814
2987
|
ng = new RootNodeGroup(this, el, options);
|
|
2815
|
-
Globals.nodeGroups.set(el, ng);
|
|
2988
|
+
//Globals.nodeGroups.set(el, ng);
|
|
2816
2989
|
firstTime = true;
|
|
2817
2990
|
}
|
|
2818
2991
|
}
|
|
@@ -2822,8 +2995,10 @@ class Template {
|
|
|
2822
2995
|
if (!firstTime) {
|
|
2823
2996
|
if (this.html?.length === 1 && !this.html[0])
|
|
2824
2997
|
el.innerHTML = ''; // Fast path for empty component.
|
|
2825
|
-
else
|
|
2998
|
+
else {
|
|
2999
|
+
ng.clearRenderWatched();
|
|
2826
3000
|
ng.applyExprs(this.exprs);
|
|
3001
|
+
}
|
|
2827
3002
|
}
|
|
2828
3003
|
|
|
2829
3004
|
return el;
|
|
@@ -3270,6 +3445,152 @@ function createSolarite(extendsTag=null) {
|
|
|
3270
3445
|
}
|
|
3271
3446
|
}
|
|
3272
3447
|
|
|
3448
|
+
/**
|
|
3449
|
+
* Trying to be able to automatically watch primitive values.
|
|
3450
|
+
* TODO:
|
|
3451
|
+
* 1. Have get() return Proxies for nested updates.
|
|
3452
|
+
* 2. Override .map() for loops to capture changes.
|
|
3453
|
+
*/
|
|
3454
|
+
|
|
3455
|
+
let unusedArg = Symbol('unusedArg');
|
|
3456
|
+
|
|
3457
|
+
/**
|
|
3458
|
+
* Custom map function triggers the get() Proxy.
|
|
3459
|
+
* @param array {Array}
|
|
3460
|
+
* @param callback {function}
|
|
3461
|
+
* @returns {*[]} */
|
|
3462
|
+
function map(array, callback) {
|
|
3463
|
+
let result = [];
|
|
3464
|
+
for (let i=0; i<array.length; i++)
|
|
3465
|
+
result.push(callback(array[i], i, array));
|
|
3466
|
+
return result;
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
|
|
3470
|
+
/**
|
|
3471
|
+
*
|
|
3472
|
+
* @param root {HTMLElement}
|
|
3473
|
+
* @param field {string}
|
|
3474
|
+
* @param value {string|Symbol} */
|
|
3475
|
+
function watch3(root, field, value=unusedArg) {
|
|
3476
|
+
// Store internal value used by get/set.
|
|
3477
|
+
if (value !== unusedArg)
|
|
3478
|
+
root[field] = value;
|
|
3479
|
+
else
|
|
3480
|
+
value = root[field];
|
|
3481
|
+
|
|
3482
|
+
|
|
3483
|
+
// use a single object for both defineProperty and new Proxy's handler.
|
|
3484
|
+
const handler = {
|
|
3485
|
+
get(obj, prop, receiver) {
|
|
3486
|
+
|
|
3487
|
+
let result = (obj === receiver && field === prop)
|
|
3488
|
+
? value // top-level value.
|
|
3489
|
+
: Reflect.get(obj, prop, receiver); // avoid infinite recursion.
|
|
3490
|
+
|
|
3491
|
+
if (prop === 'map')
|
|
3492
|
+
|
|
3493
|
+
// Double function so the ExprPath calls it as a function,
|
|
3494
|
+
// instead of it being evaluated immediately when the Templat eis created.
|
|
3495
|
+
return (callback) => () => {
|
|
3496
|
+
let rootNg = Globals.nodeGroups.get(root);
|
|
3497
|
+
rootNg.mapCallbacks.set(obj, callback);
|
|
3498
|
+
return map(new Proxy(obj, handler), callback);
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
// Track which ExprPath is using this variable.
|
|
3502
|
+
if (Globals.currentExprPath) {
|
|
3503
|
+
let [exprPath, exprFunction] = Globals.currentExprPath; // Set in ExprPath.applyExact()
|
|
3504
|
+
|
|
3505
|
+
let rootNg = Globals.nodeGroups.get(root);
|
|
3506
|
+
|
|
3507
|
+
// Init for field.
|
|
3508
|
+
rootNg.watchedExprPaths[field] = rootNg.watchedExprPaths[field] || new Set();
|
|
3509
|
+
rootNg.watchedExprPaths[field].add(exprPath);
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
if (result && typeof result === 'object')
|
|
3513
|
+
return new Proxy(result, handler);
|
|
3514
|
+
|
|
3515
|
+
return result;
|
|
3516
|
+
},
|
|
3517
|
+
|
|
3518
|
+
|
|
3519
|
+
// TODO: Will fail for attribute w/ a value having multiple ExprPaths.
|
|
3520
|
+
// TODO: This won't update a component's expressions.
|
|
3521
|
+
set(obj, prop, val, receiver) {
|
|
3522
|
+
|
|
3523
|
+
// 1. Set the value.
|
|
3524
|
+
if (obj === receiver && field === prop)
|
|
3525
|
+
value = val; // top-level value.
|
|
3526
|
+
else // avoid infinite recursion.
|
|
3527
|
+
Reflect.set(obj, prop, val, receiver);
|
|
3528
|
+
|
|
3529
|
+
// 2. Add to the list of ExprPaths to re-render.
|
|
3530
|
+
let rootNg = Globals.nodeGroups.get(root);
|
|
3531
|
+
for (let exprPath of rootNg.watchedExprPaths[field]) {
|
|
3532
|
+
|
|
3533
|
+
// Update a single NodeGroup created by array.map()
|
|
3534
|
+
if (Array.isArray(obj) && parseInt(prop) == prop) {
|
|
3535
|
+
let exprsToRender = rootNg.exprsToRender.get(exprPath);
|
|
3536
|
+
|
|
3537
|
+
// If we're not re-rendering the whole thing.
|
|
3538
|
+
if (exprsToRender !== true)
|
|
3539
|
+
Util$1.mapAdd(rootNg.exprsToRender, exprPath, [obj, prop, val]);
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
// Reapply the whole expression.
|
|
3543
|
+
else
|
|
3544
|
+
rootNg.exprsToRender.set(exprPath, true);
|
|
3545
|
+
}
|
|
3546
|
+
return true;
|
|
3547
|
+
}
|
|
3548
|
+
};
|
|
3549
|
+
|
|
3550
|
+
Object.defineProperty(root, field, {
|
|
3551
|
+
get: () => handler.get(root, field, root),
|
|
3552
|
+
set: (val) => handler.set(root, field, val, root)
|
|
3553
|
+
});
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
/**
|
|
3557
|
+
* TODO: Rename so we have watch.add() and watch.render() ?
|
|
3558
|
+
* @param root
|
|
3559
|
+
* @returns {*[]} */
|
|
3560
|
+
function renderWatched(root) {
|
|
3561
|
+
let rootNg = Globals.nodeGroups.get(root);
|
|
3562
|
+
let modified = [];
|
|
3563
|
+
|
|
3564
|
+
for (let [exprPath, params] of rootNg.exprsToRender) {
|
|
3565
|
+
|
|
3566
|
+
// Reapply the whole expression.
|
|
3567
|
+
if (params === true) {
|
|
3568
|
+
exprPath.apply(exprPath.watchFunction);
|
|
3569
|
+
|
|
3570
|
+
// TODO: freeNodeGroups() could be skipped if applyExprs() never marked them as in-use.
|
|
3571
|
+
exprPath.freeNodeGroups();
|
|
3572
|
+
|
|
3573
|
+
modified.push(...exprPath.getNodes());
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
// Update a single NodeGroup created by array.map()
|
|
3577
|
+
else {
|
|
3578
|
+
for (let row of params) {
|
|
3579
|
+
let [obj, prop, value] = row;
|
|
3580
|
+
let callback = rootNg.mapCallbacks.get(obj);
|
|
3581
|
+
let template = callback(value);
|
|
3582
|
+
exprPath.applyLoopItemUpdate(prop, template);
|
|
3583
|
+
|
|
3584
|
+
modified.push(...exprPath.nodeGroups[prop].getNodes());
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
rootNg.exprsToRender = new Map(); // clear
|
|
3590
|
+
|
|
3591
|
+
return modified;
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3273
3594
|
/**
|
|
3274
3595
|
* Solarite JavasCript UI library.
|
|
3275
3596
|
* MIT License
|
|
@@ -3285,9 +3606,6 @@ let Solarite = new Proxy(createSolarite(), {
|
|
|
3285
3606
|
}
|
|
3286
3607
|
});
|
|
3287
3608
|
let getInputValue = Util.getInputValue;
|
|
3288
|
-
|
|
3289
|
-
//Experimental:
|
|
3290
|
-
//export {forEach, watchGet, watchSet} from './watch.js' // old, unfinished
|
|
3291
|
-
//export {watch} from './watch2.js'; // unfinished
|
|
3609
|
+
// unfinished
|
|
3292
3610
|
|
|
3293
|
-
export { ArgType, Globals, Solarite, Template, delve, getArg, getInputValue, r };
|
|
3611
|
+
export { ArgType, Globals, Solarite, Template, delve, getArg, getInputValue, r, renderWatched, watch3 as watch };
|