treeloom 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,6 +17,8 @@ npm install treeloom
17
17
  It is written in TypeScript and ships its types. In a browser, use `browser/treeloom.min.js` (it defines `treeloom`) -
18
18
  or, for a build that renders JSON to HTML and forwards fired events elsewhere instead of running local listener
19
19
  functions, `browser/treeloomRenderOnly.min.js` (it defines `treeloomRenderOnly`; see `setForwardEvents`).
20
+ The bundles are in the npm package (not in git), so a CDN serves them too, e.g.
21
+ `https://cdn.jsdelivr.net/npm/treeloom@2/browser/treeloom.min.js`.
20
22
 
21
23
  ## Example
22
24
 
@@ -44,6 +46,23 @@ const json = domItemToJson(button) // a string, without the element and the l
44
46
  const copy = jsonToDomItem(json) // an item again, ready to render
45
47
  ```
46
48
 
49
+ ### Updating from a new version of the tree
50
+
51
+ When a new version of a tree arrives as a whole (a server sending its state as JSON, say), `reconcile` updates the
52
+ rendered tree to match it instead of rendering everything again. Items are matched by id (otherwise by position and node
53
+ name). Matched items keep their elements, listeners and focus, and only what changed is written to the page.
54
+
55
+ ```js
56
+ const { reconcile } = require('treeloom')
57
+
58
+ reconcile(button, jsonToDomItem(nextJson)) // same <button> element, only the changed attributes written
59
+ ```
60
+
61
+ Updates compare each item with what treeloom last wrote to its element, not with the element itself, so values the
62
+ browser stores in another form (`#333` as `rgb(51, 51, 51)`) don't count as changes. Changes other scripts make directly
63
+ on an element are left alone; form state the user changes (`value`, `checked`, `selected`) is still put back to the
64
+ item's value, as with a controlled input.
65
+
47
66
  ## Documentation
48
67
 
49
68
  The reference for every function and type is in [`docs/`](docs/index.html) (open `docs/index.html` in a browser). It is
@@ -52,10 +71,10 @@ generated from the TypeScript source, and each module is a folder of `src/`:
52
71
  | Module | What it does |
53
72
  | --- | --- |
54
73
  | `domItem` | Create items, the traits they carry, and find or change items in the tree |
55
- | `render` | Mirror the tree onto real elements |
74
+ | `render` | Mirror the tree onto real elements, and update a rendered tree from a new version of it (`reconcile`) |
56
75
  | `sync` | Read the real elements back into the tree |
57
76
  | `inflate`, `deflate` | Turn a tree into JSON, and JSON back into a tree |
58
- | `diff` | Work out what differs between an item and its element |
77
+ | `diff` | Work out what differs between an item and what was last written to its element |
59
78
  | `events` | Attach listeners to the elements of items |
60
79
 
61
80
  ## Development
@@ -8438,6 +8438,104 @@ var treeloom = (() => {
8438
8438
  }
8439
8439
  });
8440
8440
 
8441
+ // dist/diff/appliedSnapshot.js
8442
+ var require_appliedSnapshot = __commonJS({
8443
+ "dist/diff/appliedSnapshot.js"(exports) {
8444
+ "use strict";
8445
+ init_process();
8446
+ require_es_array_includes();
8447
+ var __importDefault = function(mod) {
8448
+ return mod && mod.__esModule ? mod : {
8449
+ "default": mod
8450
+ };
8451
+ };
8452
+ Object.defineProperty(exports, "__esModule", {
8453
+ value: true
8454
+ });
8455
+ exports.changesFromApplied = exports.forgetApplied = exports.recordApplied = void 0;
8456
+ var si_funciona_1 = __importDefault(require_main());
8457
+ var createChangeList_1 = __importDefault(require_createChangeList());
8458
+ var LIVE_PROPERTIES = ["value", "checked", "selected", "indeterminate"];
8459
+ var snapshots = /* @__PURE__ */ new WeakMap();
8460
+ var canSnapshot = (domItem) => typeof domItem.attributes.style === "undefined" || si_funciona_1.default.isObject(domItem.attributes.style);
8461
+ var classNames = (className) => typeof className === "string" ? className.split(/\s+/).filter(Boolean) : [];
8462
+ var describe = (domItem) => {
8463
+ const snapshot = {
8464
+ attributes: {},
8465
+ classes: [],
8466
+ style: {}
8467
+ };
8468
+ for (const name in domItem.attributes) {
8469
+ const value = domItem.attributes[name];
8470
+ if (name === "className") {
8471
+ snapshot.classes = classNames(value);
8472
+ continue;
8473
+ }
8474
+ if (name === "style") {
8475
+ for (const property in value) {
8476
+ snapshot.style[si_funciona_1.default.kabobCase(property)] = value[property];
8477
+ }
8478
+ continue;
8479
+ }
8480
+ snapshot.attributes[si_funciona_1.default.kabobCase(name)] = value;
8481
+ }
8482
+ return snapshot;
8483
+ };
8484
+ var recordApplied = (domItem) => {
8485
+ if (!domItem.element || domItem.nodeValue !== null || !canSnapshot(domItem)) {
8486
+ return;
8487
+ }
8488
+ snapshots.set(domItem.element, describe(domItem));
8489
+ };
8490
+ exports.recordApplied = recordApplied;
8491
+ var forgetApplied = (element) => {
8492
+ if (element) {
8493
+ snapshots.delete(element);
8494
+ }
8495
+ };
8496
+ exports.forgetApplied = forgetApplied;
8497
+ var changesFromApplied = (domItem, propertyName) => {
8498
+ const previous = domItem.element ? snapshots.get(domItem.element) : void 0;
8499
+ if (!previous || domItem.nodeValue !== null || !canSnapshot(domItem)) {
8500
+ return void 0;
8501
+ }
8502
+ const next = describe(domItem);
8503
+ const changes = (0, createChangeList_1.default)();
8504
+ const element = domItem.element;
8505
+ for (const key in next.attributes) {
8506
+ const live = LIVE_PROPERTIES.includes(key) && key in element;
8507
+ const current = live ? element[key] : previous.attributes[key];
8508
+ const differs = live && typeof current === "string" ? String(next.attributes[key] ?? "") !== current : next.attributes[key] !== current;
8509
+ if (differs) {
8510
+ changes.added[propertyName(key)] = next.attributes[key];
8511
+ }
8512
+ }
8513
+ previous.classes.forEach((name, index) => {
8514
+ if (!next.classes.includes(name)) {
8515
+ changes.removed[`class.${index}`] = name;
8516
+ }
8517
+ });
8518
+ next.classes.forEach((name, index) => {
8519
+ if (!previous.classes.includes(name)) {
8520
+ changes.added[`class.${index}`] = name;
8521
+ }
8522
+ });
8523
+ for (const property in previous.style) {
8524
+ if (!(property in next.style)) {
8525
+ changes.removed[`style.${property}`] = previous.style[property];
8526
+ }
8527
+ }
8528
+ for (const property in next.style) {
8529
+ if (next.style[property] !== previous.style[property]) {
8530
+ changes.added[`style.${property}`] = next.style[property];
8531
+ }
8532
+ }
8533
+ return changes;
8534
+ };
8535
+ exports.changesFromApplied = changesFromApplied;
8536
+ }
8537
+ });
8538
+
8441
8539
  // dist/sync/updateDomItem.js
8442
8540
  var require_updateDomItem = __commonJS({
8443
8541
  "dist/sync/updateDomItem.js"(exports) {
@@ -8453,6 +8551,7 @@ var treeloom = (() => {
8453
8551
  });
8454
8552
  var si_funciona_1 = __importDefault(require_main());
8455
8553
  var elementChanges_1 = __importDefault(require_elementChanges());
8554
+ var appliedSnapshot_1 = require_appliedSnapshot();
8456
8555
  var updateDomItem = (config) => {
8457
8556
  if (!config.element) {
8458
8557
  return config;
@@ -8478,6 +8577,7 @@ var treeloom = (() => {
8478
8577
  }
8479
8578
  si_funciona_1.default.dotSet(config, dotKey, changes.added[dotKey]);
8480
8579
  }
8580
+ (0, appliedSnapshot_1.forgetApplied)(config.element);
8481
8581
  return config;
8482
8582
  };
8483
8583
  exports.default = updateDomItem;
@@ -10539,6 +10639,7 @@ var treeloom = (() => {
10539
10639
  var si_funciona_1 = __importDefault(require_main());
10540
10640
  var elementHasAttribute_1 = __importDefault(require_elementHasAttribute());
10541
10641
  var createChangeList_1 = __importDefault(require_createChangeList());
10642
+ var appliedSnapshot_1 = require_appliedSnapshot();
10542
10643
  var checkAttribute = (element) => (attr, key) => {
10543
10644
  if (typeof attr === "object") {
10544
10645
  const styles = attr;
@@ -10591,6 +10692,16 @@ var treeloom = (() => {
10591
10692
  }
10592
10693
  return changeList;
10593
10694
  }
10695
+ if (!changeList.added.nodeName) {
10696
+ const fromApplied = (0, appliedSnapshot_1.changesFromApplied)(domItem, (kabobKey) => {
10697
+ const name = attributeName(domItem.attributes, kabobKey);
10698
+ const element = domItem.element;
10699
+ return element && name in element && name !== "form" ? name : kabobKey;
10700
+ });
10701
+ if (fromApplied) {
10702
+ return fromApplied;
10703
+ }
10704
+ }
10594
10705
  const domItemAttributes = kabobCaseAttributes(domItem.attributes);
10595
10706
  const diffMap = si_funciona_1.default.mapObject(domItemAttributes, (attr, key) => checkAttribute(domItem.element)(attr, key));
10596
10707
  return si_funciona_1.default.reduceObject(diffMap, (changed, result, key) => {
@@ -10678,31 +10789,24 @@ var treeloom = (() => {
10678
10789
  return domItem;
10679
10790
  }
10680
10791
  const element = domItem.element;
10681
- const previousChildNodes = Array.from(element.childNodes);
10682
- domItem.children.map((child, index) => {
10683
- const existing = previousChildNodes.find((childNode2) => childNode2 === child.element);
10684
- let childNode = existing ?? child.element;
10685
- if (!childNode) {
10686
- childNode = (0, updateElement_1.default)(child).element;
10687
- }
10688
- if (!previousChildNodes[index]) {
10689
- element.appendChild(childNode);
10690
- return childNode;
10691
- }
10692
- if (childNode !== previousChildNodes[index]) {
10693
- element.replaceChild(childNode, previousChildNodes[index]);
10694
- }
10792
+ const childNodes = domItem.children.map((child) => {
10793
+ const childNode = child.element ?? (0, updateElement_1.default)(child).element;
10794
+ child.element = childNode;
10695
10795
  return childNode;
10696
- }).forEach((childNode, index) => {
10697
- domItem.children[index].element = childNode;
10698
10796
  });
10699
- if (element.childNodes.length > domItem.children.length) {
10700
- const maxLength = domItem.children.length;
10701
- let nodeToRemove = element.childNodes[maxLength];
10702
- while (nodeToRemove) {
10703
- element.removeChild(nodeToRemove);
10704
- nodeToRemove = element.childNodes[maxLength];
10797
+ const wanted = new Set(childNodes);
10798
+ Array.from(element.childNodes).forEach((node) => {
10799
+ if (!wanted.has(node)) {
10800
+ element.removeChild(node);
10705
10801
  }
10802
+ });
10803
+ let following = null;
10804
+ for (let index = childNodes.length - 1; index >= 0; index--) {
10805
+ const childNode = childNodes[index];
10806
+ if (childNode.parentNode !== element || childNode.nextSibling !== following) {
10807
+ element.insertBefore(childNode, following);
10808
+ }
10809
+ following = childNode;
10706
10810
  }
10707
10811
  return domItem;
10708
10812
  };
@@ -10727,6 +10831,7 @@ var treeloom = (() => {
10727
10831
  var bindListeners_1 = __importDefault(require_bindListeners());
10728
10832
  var deactivateListeners_1 = __importDefault(require_deactivateListeners());
10729
10833
  var domItemChanges_1 = __importDefault(require_domItemChanges());
10834
+ var appliedSnapshot_1 = require_appliedSnapshot();
10730
10835
  var si_funciona_1 = __importDefault(require_main());
10731
10836
  var generateElement_1 = __importDefault(require_generateElement());
10732
10837
  var updateChildNodes_1 = __importDefault(require_updateChildNodes());
@@ -10794,6 +10899,7 @@ var treeloom = (() => {
10794
10899
  }
10795
10900
  element.style.setProperty(si_funciona_1.default.kabobCase(si_funciona_1.default.strAfter(dotKey, ".")), value);
10796
10901
  }
10902
+ (0, appliedSnapshot_1.recordApplied)(domItem);
10797
10903
  return (0, bindListeners_1.default)(domItem);
10798
10904
  };
10799
10905
  exports.default = updateElement;
@@ -10864,6 +10970,124 @@ var treeloom = (() => {
10864
10970
  }
10865
10971
  });
10866
10972
 
10973
+ // dist/render/reconcile.js
10974
+ var require_reconcile = __commonJS({
10975
+ "dist/render/reconcile.js"(exports) {
10976
+ "use strict";
10977
+ init_process();
10978
+ require_es_array_includes();
10979
+ require_es_json_stringify();
10980
+ var __importDefault = function(mod) {
10981
+ return mod && mod.__esModule ? mod : {
10982
+ "default": mod
10983
+ };
10984
+ };
10985
+ Object.defineProperty(exports, "__esModule", {
10986
+ value: true
10987
+ });
10988
+ var removeEventListener_1 = __importDefault(require_removeEventListener());
10989
+ var itemIdIndex_1 = require_itemIdIndex();
10990
+ var updateElements_1 = __importDefault(require_updateElements());
10991
+ var STRUCTURE = /* @__PURE__ */ new Set(["element", "parentItem", "children", "eventListeners", "activeListeners"]);
10992
+ var listenerKey = (listener) => typeof listener.listenerFunc === "string" ? JSON.stringify([listener.listenerFunc, listener.listenerArgs ?? null, listener.listenerOptions ?? null]) : null;
10993
+ var reconcileListeners = (existing, next) => {
10994
+ const nextListeners = next.eventListeners ?? {};
10995
+ existing.eventListeners = existing.eventListeners ?? {};
10996
+ for (const eventName of Object.keys(existing.eventListeners)) {
10997
+ const wanted = (nextListeners[eventName] ?? []).map(listenerKey);
10998
+ for (const listener of [...existing.eventListeners[eventName]]) {
10999
+ const key = listenerKey(listener);
11000
+ if (key !== null && !wanted.includes(key)) {
11001
+ (0, removeEventListener_1.default)(eventName, listener.listenerFunc, listener.listenerArgs, listener.listenerOptions, existing);
11002
+ }
11003
+ }
11004
+ }
11005
+ for (const eventName of Object.keys(nextListeners)) {
11006
+ const have = (existing.eventListeners[eventName] ?? []).map(listenerKey);
11007
+ for (const listener of nextListeners[eventName]) {
11008
+ const key = listenerKey(listener);
11009
+ if (key !== null && !have.includes(key)) {
11010
+ existing.eventListeners[eventName] = existing.eventListeners[eventName] ?? [];
11011
+ existing.eventListeners[eventName].push({
11012
+ ...listener,
11013
+ listenerActive: false,
11014
+ activeResponder: null
11015
+ });
11016
+ }
11017
+ }
11018
+ }
11019
+ };
11020
+ var reconcileData = (existing, next) => {
11021
+ const target = existing;
11022
+ const source = next;
11023
+ for (const key of Object.keys(target)) {
11024
+ if (!STRUCTURE.has(key) && !(key in source) && typeof target[key] !== "function") {
11025
+ delete target[key];
11026
+ }
11027
+ }
11028
+ for (const key of Object.keys(source)) {
11029
+ if (!STRUCTURE.has(key)) {
11030
+ target[key] = source[key];
11031
+ }
11032
+ }
11033
+ };
11034
+ var pairChildren = (existing, next) => {
11035
+ const byId = /* @__PURE__ */ new Map();
11036
+ existing.forEach((child) => {
11037
+ if (typeof child.id === "number") {
11038
+ byId.set(child.id, child);
11039
+ }
11040
+ });
11041
+ const claimed = new Set(next.map((child) => typeof child.id === "number" ? byId.get(child.id) : void 0).filter(Boolean));
11042
+ const taken = /* @__PURE__ */ new Set();
11043
+ return next.map((nextChild, index) => {
11044
+ const sameId = typeof nextChild.id === "number" ? byId.get(nextChild.id) : void 0;
11045
+ const atPosition = existing[index] && !claimed.has(existing[index]) ? existing[index] : void 0;
11046
+ const candidate = sameId ?? atPosition;
11047
+ if (candidate && !taken.has(candidate) && candidate.nodeName === nextChild.nodeName) {
11048
+ taken.add(candidate);
11049
+ return candidate;
11050
+ }
11051
+ return void 0;
11052
+ });
11053
+ };
11054
+ var reconcileItem = (existing, next) => {
11055
+ reconcileData(existing, next);
11056
+ reconcileListeners(existing, next);
11057
+ const previous = existing.children ?? [];
11058
+ const nextChildren = next.children ?? [];
11059
+ const pairs = pairChildren(previous, nextChildren);
11060
+ const kept = new Set(pairs.filter(Boolean));
11061
+ previous.filter((child) => !kept.has(child)).forEach((child) => {
11062
+ ;
11063
+ child.element?.remove?.();
11064
+ });
11065
+ existing.children = nextChildren.map((nextChild, index) => {
11066
+ const child = pairs[index];
11067
+ if (child) {
11068
+ reconcileItem(child, nextChild);
11069
+ return child;
11070
+ }
11071
+ nextChild.parentItem = existing;
11072
+ return nextChild;
11073
+ });
11074
+ existing.children.forEach((child) => {
11075
+ child.parentItem = existing;
11076
+ });
11077
+ };
11078
+ var reconcile = (existing, next) => {
11079
+ if (existing.nodeName.toLowerCase() !== next.nodeName.toLowerCase()) {
11080
+ throw new Error(`reconcile can't turn a ${existing.nodeName} into a ${next.nodeName}: give it a tree of the same node, or reconcile its parent.`);
11081
+ }
11082
+ (0, itemIdIndex_1.unregisterItemIds)(existing);
11083
+ reconcileItem(existing, next);
11084
+ (0, itemIdIndex_1.registerItemIds)(existing);
11085
+ return (0, updateElements_1.default)(existing);
11086
+ };
11087
+ exports.default = reconcile;
11088
+ }
11089
+ });
11090
+
10867
11091
  // dist/render/renderHtml.js
10868
11092
  var require_renderHtml = __commonJS({
10869
11093
  "dist/render/renderHtml.js"(exports) {
@@ -10912,12 +11136,14 @@ var treeloom = (() => {
10912
11136
  value: true
10913
11137
  });
10914
11138
  var generateElement_1 = __importDefault(require_generateElement());
11139
+ var reconcile_1 = __importDefault(require_reconcile());
10915
11140
  var renderHtml_1 = __importDefault(require_renderHtml());
10916
11141
  var updateChildNodes_1 = __importDefault(require_updateChildNodes());
10917
11142
  var updateElement_1 = __importDefault(require_updateElement());
10918
11143
  var updateElements_1 = __importDefault(require_updateElements());
10919
11144
  exports.default = {
10920
11145
  generateElement: generateElement_1.default,
11146
+ reconcile: reconcile_1.default,
10921
11147
  renderHtml: renderHtml_1.default,
10922
11148
  updateChildNodes: updateChildNodes_1.default,
10923
11149
  updateElement: updateElement_1.default,