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.
@@ -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 Component.
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
- * @return {*} */
157
- function getArg(el, name, val=null, type=ArgType.String) {
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 : undefined;
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
- return !['false', '0', false, 0, null, undefined].includes(lAttrVal);
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
- return parseInt(val);
209
+ result = parseInt(val);
210
+ return isNaN(result) ? fallback : result;
178
211
  case ArgType.Float:
179
- return parseFloat(val);
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 val;
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
- currentExprPath: [],
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
- // Get all values for a key
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
- Util.saveOrphans(oldNodeGroups, oldNodes);
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 = {[eventName]: new Array(3)};
1506
+ nodeEvents = {[key]: new Array(3)};
1384
1507
  Globals.nodeEvents.set(node, nodeEvents);
1385
1508
  }
1386
- let nodeEvent = nodeEvents[eventName];
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) // put "event", "el", and "this" in scope for the event code.
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[eventName][2] = args;
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
- node.addEventListener('input', () => {
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
- }, true); // We use capture so we update the values before other events added by the user.
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
- if (exact) {
1623
- result = this.nodeGroupsFree.delete(template.getExactKey());
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.delete(template.getCloseKey());
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 };