@schukai/monster 4.150.6 → 4.150.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.150.6"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.150.7"}
@@ -854,7 +854,10 @@ function removeElement(_change, renderContext) {
854
854
  */
855
855
  function insertElement(change, renderContext) {
856
856
  const subject = this[internalSymbol].subject.getRealSubject();
857
-
857
+ const changePath = clone(change?.["path"]);
858
+ if (!isArray(changePath)) {
859
+ return false;
860
+ }
858
861
  const mem = new WeakSet();
859
862
  let wd = 0;
860
863
  let changed = false;
@@ -873,36 +876,41 @@ function insertElement(change, renderContext) {
873
876
  let found = false;
874
877
  wd++;
875
878
 
876
- const p = clone(change?.["path"]);
877
- if (!isArray(p)) return changed;
879
+ const p = clone(changePath);
878
880
 
879
881
  while (p.length > 0) {
880
882
  const current = p.join(".");
881
883
 
882
884
  let iterator = new Set();
883
- const query = `[${ATTRIBUTE_UPDATER_INSERT}*="path:${current}"]`;
884
-
885
- const e = container.querySelectorAll(query);
886
-
887
- if (e.length > 0) {
888
- iterator = new Set([...e]);
889
- }
890
-
891
- if (container.matches(query)) {
892
- iterator.add(container);
885
+ const candidates = getBindingCandidates(
886
+ container,
887
+ ATTRIBUTE_UPDATER_INSERT,
888
+ renderContext?.candidates.insert ?? new WeakMap(),
889
+ );
890
+ for (const candidate of candidates) {
891
+ const definition = candidate.getAttribute(ATTRIBUTE_UPDATER_INSERT);
892
+ if (definition?.includes(`path:${current}`)) {
893
+ iterator.add(candidate);
894
+ }
893
895
  }
894
896
 
895
897
  for (const [, containerElement] of iterator.entries()) {
896
- if (mem.has(containerElement)) continue;
897
- mem.add(containerElement);
898
-
899
- found = true;
900
-
901
898
  const attributes = containerElement.getAttribute(
902
899
  ATTRIBUTE_UPDATER_INSERT,
903
900
  );
904
901
  if (attributes === null) continue;
905
902
 
903
+ if (
904
+ mem.has(containerElement) ||
905
+ renderContext?.insert.get(containerElement) === attributes
906
+ ) {
907
+ continue;
908
+ }
909
+ mem.add(containerElement);
910
+ renderContext?.insert.set(containerElement, attributes);
911
+
912
+ found = true;
913
+
906
914
  const def = trimSpaces(attributes);
907
915
  const i = def.indexOf(" ");
908
916
  const key = trimSpaces(def.substr(0, i));
@@ -929,40 +937,53 @@ function insertElement(change, renderContext) {
929
937
 
930
938
  const dataPath = cmd.split(":").pop();
931
939
 
932
- let insertPoint;
933
- if (containerElement.hasChildNodes()) {
934
- insertPoint = containerElement.lastChild;
935
- }
936
-
937
940
  if (!isIterable(value)) {
938
941
  throw new Error("the value is not iterable");
939
942
  }
940
943
 
941
944
  const available = new Set();
945
+ const existing = new Map();
946
+ const managedNodes = [];
947
+ for (const node of containerElement.querySelectorAll(
948
+ `[${ATTRIBUTE_UPDATER_INSERT_REFERENCE}]`,
949
+ )) {
950
+ const ref = node.getAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE);
951
+ if (!existing.has(ref)) {
952
+ existing.set(ref, node);
953
+ }
954
+ if (ref.includes(refPrefix)) {
955
+ managedNodes.push(node);
956
+ }
957
+ }
958
+
959
+ let template;
960
+ const pending = containerElement.ownerDocument.createDocumentFragment();
961
+ let structureChanged = false;
942
962
 
943
963
  for (const [i] of Object.entries(value)) {
944
964
  const ref = refPrefix + i;
945
965
  const currentPath = `${dataPath}.${i}`;
946
966
 
947
967
  available.add(ref);
948
- const refElement = containerElement.querySelector(
949
- `[${ATTRIBUTE_UPDATER_INSERT_REFERENCE}="${ref}"]`,
950
- );
968
+ const refElement = existing.get(ref);
951
969
 
952
970
  if (refElement instanceof HTMLElement) {
953
- insertPoint = refElement;
954
971
  continue;
955
972
  }
956
973
 
957
- appendNewDocumentFragment(containerElement, key, ref, currentPath);
974
+ template ??= findDocumentTemplate(key, containerElement);
975
+ pending.appendChild(
976
+ createNewDocumentFragment(template, key, ref, currentPath),
977
+ );
958
978
  changed = true;
959
979
  }
960
980
 
961
- const nodes = containerElement.querySelectorAll(
962
- `[${ATTRIBUTE_UPDATER_INSERT_REFERENCE}*="${refPrefix}"]`,
963
- );
981
+ if (pending.hasChildNodes()) {
982
+ containerElement.appendChild(pending);
983
+ structureChanged = true;
984
+ }
964
985
 
965
- for (const [, node] of Object.entries(nodes)) {
986
+ for (const node of managedNodes) {
966
987
  if (
967
988
  !available.has(
968
989
  node.getAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE),
@@ -972,11 +993,16 @@ function insertElement(change, renderContext) {
972
993
  teardownManagedSubtree(node);
973
994
  containerElement.removeChild(node);
974
995
  changed = true;
996
+ structureChanged = true;
975
997
  } catch (e) {
976
998
  addErrorAttribute(containerElement, e);
977
999
  }
978
1000
  }
979
1001
  }
1002
+
1003
+ if (structureChanged && renderContext) {
1004
+ renderContext.candidates.insert = new WeakMap();
1005
+ }
980
1006
  }
981
1007
 
982
1008
  p.pop();
@@ -987,7 +1013,6 @@ function insertElement(change, renderContext) {
987
1013
  throw new Error("the maximum depth for the recursion is reached.");
988
1014
  }
989
1015
  }
990
-
991
1016
  return changed;
992
1017
  }
993
1018
 
@@ -996,24 +1021,21 @@ function insertElement(change, renderContext) {
996
1021
  * @private
997
1022
  * @license AGPLv3
998
1023
  * @since 1.8.0
999
- * @param {HTMLElement} container
1024
+ * @param {Template} template
1000
1025
  * @param {string} key
1001
1026
  * @param {string} ref
1002
1027
  * @param {string} path
1003
1028
  * @throws {Error} no template was found with the specified key.
1004
1029
  */
1005
- function appendNewDocumentFragment(container, key, ref, path) {
1006
- const template = findDocumentTemplate(key, container);
1007
-
1030
+ function createNewDocumentFragment(template, key, ref, path) {
1008
1031
  const nodes = template.createDocumentFragment();
1009
- for (const [, node] of Object.entries(nodes.childNodes)) {
1032
+ for (const node of nodes.childNodes) {
1010
1033
  if (node instanceof HTMLElement) {
1011
1034
  applyRecursive(node, key, path);
1012
1035
  node.setAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE, ref);
1013
1036
  }
1014
-
1015
- container.appendChild(node);
1016
1037
  }
1038
+ return nodes;
1017
1039
  }
1018
1040
 
1019
1041
  /**
@@ -1113,9 +1135,11 @@ function createRenderContext() {
1113
1135
  patch: new WeakSet(),
1114
1136
  attributes: new WeakSet(),
1115
1137
  properties: new WeakSet(),
1138
+ insert: new WeakMap(),
1116
1139
  removeProcessed: false,
1117
1140
  insertBindingsPresent: undefined,
1118
1141
  candidates: {
1142
+ insert: new WeakMap(),
1119
1143
  replace: new WeakMap(),
1120
1144
  patch: new WeakMap(),
1121
1145
  attributes: new WeakMap(),
@@ -1134,6 +1158,7 @@ function createRenderContext() {
1134
1158
  function invalidateRenderCandidates(renderContext) {
1135
1159
  renderContext.removeProcessed = false;
1136
1160
  renderContext.insertBindingsPresent = undefined;
1161
+ renderContext.candidates.insert = new WeakMap();
1137
1162
  renderContext.candidates.replace = new WeakMap();
1138
1163
  renderContext.candidates.patch = new WeakMap();
1139
1164
  renderContext.candidates.attributes = new WeakMap();
@@ -725,6 +725,14 @@ describe("DOM", function () {
725
725
 
726
726
  describe("Updater()", function () {
727
727
  describe("Repeat", function () {
728
+ const waitUntil = async (predicate, message, timeout = 5000) => {
729
+ const deadline = Date.now() + timeout;
730
+ while (!predicate() && Date.now() < deadline) {
731
+ await new Promise((resolve) => setTimeout(resolve, 5));
732
+ }
733
+ expect(predicate(), message).to.equal(true);
734
+ };
735
+
728
736
  it("should build 6 li elements from an array", function (done) {
729
737
  let element = document.getElementById("test1");
730
738
  const testData = {
@@ -769,6 +777,244 @@ describe("DOM", function () {
769
777
  })
770
778
  .catch(done);
771
779
  });
780
+
781
+ [false, true].forEach((batchUpdates) => {
782
+ it(`should bulk insert and reuse indexed entries with batchUpdates=${batchUpdates} issue #548`, async function () {
783
+ this.timeout(5000);
784
+ const mocks = document.getElementById("mocks");
785
+ mocks.innerHTML = `
786
+ <template id="bulk-row">
787
+ <li data-monster-replace="path:bulk-row.label"></li>
788
+ </template>
789
+ <ul id="bulk-rows" data-monster-insert="bulk-row path:items"></ul>
790
+ `;
791
+
792
+ const rows = document.getElementById("bulk-rows");
793
+ const updater = new Updater(rows, { items: [] });
794
+ updater.setBatchUpdates(batchUpdates);
795
+
796
+ const originalAppendChild = rows.appendChild;
797
+ const originalQuerySelector = rows.querySelector;
798
+ let appendCalls = 0;
799
+ let appendedFragments = 0;
800
+ let exactReferenceLookups = 0;
801
+
802
+ rows.appendChild = function (node) {
803
+ appendCalls++;
804
+ if (node instanceof DocumentFragment) {
805
+ appendedFragments++;
806
+ }
807
+ return originalAppendChild.call(this, node);
808
+ };
809
+ rows.querySelector = function (selector) {
810
+ if (selector.startsWith('[data-monster-insert-reference="')) {
811
+ exactReferenceLookups++;
812
+ }
813
+ return originalQuerySelector.call(this, selector);
814
+ };
815
+
816
+ const waitForLength = (length) =>
817
+ waitUntil(
818
+ () => rows.children.length === length,
819
+ `expected ${length} bulk rows`,
820
+ );
821
+
822
+ try {
823
+ await updater.run();
824
+ const items = updater.getSubject().items;
825
+ for (let index = 0; index < 872; index++) {
826
+ items.push({ label: `Row ${index}` });
827
+ }
828
+ await waitForLength(872);
829
+
830
+ expect(appendCalls).to.equal(1);
831
+ expect(appendedFragments).to.equal(1);
832
+ expect(exactReferenceLookups).to.equal(0);
833
+ expect(rows.firstElementChild.textContent).to.equal("Row 0");
834
+ expect(rows.lastElementChild.textContent).to.equal("Row 871");
835
+
836
+ const firstRow = rows.firstElementChild;
837
+ items.splice(400, 0, { label: "Inserted middle" });
838
+ await waitForLength(873);
839
+ expect(rows.children[400].textContent).to.equal("Inserted middle");
840
+ expect(rows.lastElementChild.textContent).to.equal("Row 871");
841
+
842
+ items.splice(400, 1);
843
+ await waitForLength(872);
844
+ expect(rows.children[400].textContent).to.equal("Row 400");
845
+
846
+ items[0].label = "Updated row";
847
+ items.splice(870, 2);
848
+ await waitForLength(870);
849
+
850
+ expect(rows.firstElementChild).to.equal(firstRow);
851
+ expect(rows.firstElementChild.textContent).to.equal("Updated row");
852
+ expect(rows.lastElementChild.textContent).to.equal("Row 869");
853
+ expect(appendCalls).to.equal(2);
854
+ expect(appendedFragments).to.equal(2);
855
+ } finally {
856
+ updater.dispose();
857
+ rows.appendChild = originalAppendChild;
858
+ rows.querySelector = originalQuerySelector;
859
+ }
860
+ });
861
+ });
862
+
863
+ it("should process two insert containers with the same data path issue #548", async function () {
864
+ const mocks = document.getElementById("mocks");
865
+ mocks.innerHTML = `
866
+ <template id="first-row"><li data-monster-replace="path:first-row"></li></template>
867
+ <template id="second-row"><li data-monster-replace="path:second-row"></li></template>
868
+ <div id="bulk-groups">
869
+ <ul id="first-rows" data-monster-insert="first-row path:items"></ul>
870
+ <ul id="second-rows" data-monster-insert="second-row path:items"></ul>
871
+ </div>
872
+ `;
873
+
874
+ const updater = new Updater(document.getElementById("bulk-groups"), {
875
+ items: [],
876
+ });
877
+ updater.setBatchUpdates(true);
878
+
879
+ try {
880
+ await updater.run();
881
+ updater.getSubject().items.push("Shared");
882
+ await waitUntil(
883
+ () =>
884
+ document.getElementById("first-rows").textContent === "Shared" &&
885
+ document.getElementById("second-rows").textContent === "Shared",
886
+ "expected both insert containers to render",
887
+ );
888
+
889
+ expect(document.getElementById("first-rows").textContent).to.equal(
890
+ "Shared",
891
+ );
892
+ expect(document.getElementById("second-rows").textContent).to.equal(
893
+ "Shared",
894
+ );
895
+ } finally {
896
+ updater.dispose();
897
+ }
898
+ });
899
+
900
+ it("should discover an insert binding created by replace in non-batch mode issue #548", async function () {
901
+ const mocks = document.getElementById("mocks");
902
+ mocks.innerHTML = `
903
+ <template id="dynamic-row">
904
+ <article data-monster-replace="path:dynamic-row.html"></article>
905
+ </template>
906
+ <template id="dynamic-child">
907
+ <span data-monster-replace="path:dynamic-child.label"></span>
908
+ </template>
909
+ <div id="dynamic-root" data-monster-insert="dynamic-row path:items"></div>
910
+ `;
911
+
912
+ const updater = new Updater(document.getElementById("dynamic-root"), {
913
+ items: [{ html: "", children: [] }],
914
+ });
915
+ updater.setBatchUpdates(false);
916
+
917
+ try {
918
+ await updater.run();
919
+ const item = updater.getSubject().items[0];
920
+ item.html =
921
+ '<div id="dynamic-children" data-monster-insert="dynamic-child path:items.0.children"></div>';
922
+ item.children.push({ label: "Child" });
923
+
924
+ await waitUntil(
925
+ () =>
926
+ document.getElementById("dynamic-children")?.textContent.trim() ===
927
+ "Child",
928
+ "expected the dynamically created insert binding to render",
929
+ );
930
+ } finally {
931
+ updater.dispose();
932
+ }
933
+ });
934
+
935
+ it("should process a changed insert definition in the same snapshot issue #548", async function () {
936
+ this.timeout(6000);
937
+ const mocks = document.getElementById("mocks");
938
+ mocks.innerHTML = `
939
+ <template id="old-row">
940
+ <span data-monster-replace="path:old-row"></span>
941
+ </template>
942
+ <template id="new-row">
943
+ <span data-monster-replace="path:new-row"></span>
944
+ </template>
945
+ <div id="rebound-insert"
946
+ data-monster-insert="old-row path:oldItems"
947
+ data-monster-attributes="data-monster-insert path:insertDefinition"></div>
948
+ `;
949
+
950
+ const updater = new Updater(mocks, {
951
+ oldItems: [],
952
+ insertDefinition: "old-row path:oldItems",
953
+ newItems: [],
954
+ });
955
+ updater.setBatchUpdates(false);
956
+
957
+ try {
958
+ await updater.run();
959
+ const container = document.getElementById("rebound-insert");
960
+ const subject = updater.getSubject();
961
+ subject.oldItems.push("Old");
962
+ subject.insertDefinition = "new-row path:newItems";
963
+ subject.newItems.push("New");
964
+
965
+ await waitUntil(
966
+ () =>
967
+ Array.from(container.children, (node) => node.textContent).join("") ===
968
+ "OldNew",
969
+ "expected the changed insert definition to render",
970
+ );
971
+
972
+ expect(container.getAttribute("data-monster-insert")).to.equal(
973
+ "new-row path:newItems",
974
+ );
975
+ expect(container.children.length).to.equal(2);
976
+ } finally {
977
+ updater.dispose();
978
+ }
979
+ });
980
+
981
+ it("should preserve multi-root template entries issue #548", async function () {
982
+ const mocks = document.getElementById("mocks");
983
+ mocks.innerHTML = `
984
+ <template id="multi-row">
985
+ <strong data-monster-replace="path:multi-row.label"></strong>
986
+ <em data-monster-replace="path:multi-row.label"></em>
987
+ </template>
988
+ <div id="multi-rows" data-monster-insert="multi-row path:items"></div>
989
+ `;
990
+
991
+ const rows = document.getElementById("multi-rows");
992
+ const updater = new Updater(rows, {
993
+ items: [{ label: "First" }, { label: "Second" }],
994
+ });
995
+
996
+ try {
997
+ await updater.run();
998
+ expect(Array.from(rows.children, (node) => node.textContent)).to.deep.equal([
999
+ "First",
1000
+ "First",
1001
+ "Second",
1002
+ "Second",
1003
+ ]);
1004
+
1005
+ updater.getSubject().items.splice(0, 1);
1006
+ await waitUntil(
1007
+ () => rows.children.length === 2,
1008
+ "expected both top-level nodes of the removed entry to be removed",
1009
+ );
1010
+ expect(Array.from(rows.children, (node) => node.textContent)).to.deep.equal([
1011
+ "Second",
1012
+ "Second",
1013
+ ]);
1014
+ } finally {
1015
+ updater.dispose();
1016
+ }
1017
+ });
772
1018
  });
773
1019
  });
774
1020