thunderous 1.0.2 → 1.1.1

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/index.cjs CHANGED
@@ -358,8 +358,6 @@ if (typeof window !== "undefined") {
358
358
  installScopedCreationMethod(ShadowRoot, "createElement", document);
359
359
  installScopedCreationMethod(ShadowRoot, "importNode", document);
360
360
  installScopedCreationMethod(Element, "insertAdjacentHTML");
361
- installScopedCreationMethod(Node, "appendChild");
362
- installScopedCreationMethod(Element, "append");
363
361
  const installScopedCreationSetter = (ctor, name) => {
364
362
  const descriptor = Object.getOwnPropertyDescriptor(ctor.prototype, name);
365
363
  Object.defineProperty(ctor.prototype, name, {
@@ -701,20 +699,19 @@ var clientOnlyCallback = (fn) => {
701
699
  };
702
700
 
703
701
  // src/render.ts
702
+ var renderState = {
703
+ currentShadowRoot: null
704
+ };
704
705
  var clearHTML = (element) => {
705
706
  while (element.childNodes.length > 0) {
706
707
  element.childNodes[0].remove();
707
708
  }
708
709
  };
709
710
  var parseFragment = (htmlStr) => {
710
- const range = document.createRange();
711
- range.selectNode(document.body);
712
- return range.createContextualFragment(htmlStr);
713
- };
714
- var setInnerHTML = (element, html2) => {
715
- clearHTML(element);
716
- const fragment = typeof html2 === "string" ? parseFragment(html2) : html2;
717
- element.append(fragment);
711
+ const template = document.createElement("template");
712
+ template.innerHTML = htmlStr;
713
+ const fragment = renderState.currentShadowRoot === null ? template.content : renderState.currentShadowRoot.importNode(template.content, true);
714
+ return fragment;
718
715
  };
719
716
  var logValueError = (value) => {
720
717
  console.error(
@@ -722,21 +719,82 @@ var logValueError = (value) => {
722
719
  value
723
720
  );
724
721
  };
722
+ var arrayToDocumentFragment = (array, parent) => {
723
+ const documentFragment = new DocumentFragment();
724
+ let count = 0;
725
+ const keys = /* @__PURE__ */ new Set();
726
+ for (const item of array) {
727
+ const node = getNewNode(item, parent).cloneNode(true);
728
+ if (node instanceof DocumentFragment) {
729
+ const child = node.firstElementChild;
730
+ if (node.children.length > 1) {
731
+ console.error(
732
+ "When rendering arrays, fragments must contain only one top-level element at a time. Error occured in:",
733
+ parent
734
+ );
735
+ }
736
+ if (child === null) continue;
737
+ let key = child.getAttribute("key");
738
+ if (key === null) {
739
+ console.warn(
740
+ "When rendering arrays, a `key` attribute should be provided on each child element. An index was automatically applied, but this could result in unexpected behavior:",
741
+ child
742
+ );
743
+ key = String(count);
744
+ child.setAttribute("key", key);
745
+ }
746
+ if (keys.has(key)) {
747
+ console.warn(
748
+ `When rendering arrays, each child should have a unique \`key\` attribute. Duplicate key "${key}" found on:`,
749
+ child
750
+ );
751
+ }
752
+ keys.add(key);
753
+ count++;
754
+ }
755
+ documentFragment.append(node);
756
+ }
757
+ return documentFragment;
758
+ };
759
+ var getNewNode = (value, parent) => {
760
+ if (typeof value === "string") return new Text(value);
761
+ if (Array.isArray(value)) return arrayToDocumentFragment(value, parent);
762
+ if (value instanceof DocumentFragment) return value;
763
+ return new Text("");
764
+ };
725
765
  var html = (strings, ...values) => {
726
766
  let innerHTML = "";
727
767
  const signalMap = /* @__PURE__ */ new Map();
728
- strings.forEach((string, i) => {
729
- let value = values[i] ?? "";
768
+ const processValue = (value) => {
769
+ if (!isServer && value instanceof DocumentFragment) {
770
+ const tempDiv = document.createElement("div");
771
+ tempDiv.append(value.cloneNode(true));
772
+ return tempDiv.innerHTML;
773
+ }
730
774
  if (typeof value === "function") {
775
+ const getter = value;
731
776
  const uniqueKey = crypto.randomUUID();
732
- signalMap.set(uniqueKey, value);
733
- value = isServer ? value() : `{{signal:${uniqueKey}}}`;
777
+ signalMap.set(uniqueKey, getter);
778
+ let result = getter();
779
+ if (Array.isArray(result)) {
780
+ result = result.map((item) => processValue(item)).join("");
781
+ }
782
+ return isServer ? String(result) : `{{signal:${uniqueKey}}}`;
734
783
  }
735
784
  if (typeof value === "object" && value !== null) {
736
785
  logValueError(value);
737
- value = "";
786
+ return "";
787
+ }
788
+ return String(value);
789
+ };
790
+ strings.forEach((string, i) => {
791
+ let value = values[i] ?? "";
792
+ if (Array.isArray(value)) {
793
+ value = value.map((item) => processValue(item)).join("");
794
+ } else {
795
+ value = processValue(value);
738
796
  }
739
- innerHTML += string + String(value);
797
+ innerHTML += string + String(value === null ? "" : value);
740
798
  });
741
799
  if (isServer) {
742
800
  return innerHTML;
@@ -748,24 +806,40 @@ var html = (strings, ...values) => {
748
806
  for (const child of element.childNodes) {
749
807
  if (child instanceof Text && signalBindingRegex.test(child.data)) {
750
808
  const textList = child.data.split(signalBindingRegex);
809
+ const sibling = child.nextSibling;
751
810
  textList.forEach((text, i) => {
752
811
  const uniqueKey = text.replace(/\{\{signal:(.+)\}\}/, "$1");
753
812
  const signal = uniqueKey !== text ? signalMap.get(uniqueKey) : void 0;
754
813
  const newValue = signal !== void 0 ? signal() : text;
755
- const newNode = (() => {
756
- if (typeof newValue === "string") return new Text(newValue);
757
- if (newValue instanceof DocumentFragment) return newValue;
758
- return new Text("");
759
- })();
814
+ const newNode = getNewNode(newValue, element);
760
815
  if (i === 0) {
761
816
  child.replaceWith(newNode);
762
817
  } else {
763
- element.insertBefore(newNode, child.nextSibling);
818
+ element.insertBefore(newNode, sibling);
764
819
  }
765
820
  if (signal !== void 0 && newNode instanceof Text) {
766
821
  createEffect(() => {
767
822
  newNode.data = signal();
768
823
  });
824
+ } else if (signal !== void 0 && newNode instanceof DocumentFragment) {
825
+ createEffect(() => {
826
+ const result = signal();
827
+ const nextNode = getNewNode(result, element);
828
+ if (nextNode instanceof Text) {
829
+ throw new TypeError(
830
+ "Signal mismatch: expected DocumentFragment or Array<DocumentFragment>, but got Text"
831
+ );
832
+ }
833
+ let lastSibling = element.lastChild;
834
+ for (const child2 of nextNode.children) {
835
+ const key = child2.getAttribute("key");
836
+ const matchingNode = element.querySelector(`[key="${key}"]`);
837
+ if (matchingNode === null) continue;
838
+ lastSibling = matchingNode.nextSibling;
839
+ child2.replaceWith(matchingNode);
840
+ }
841
+ element.insertBefore(nextNode, lastSibling);
842
+ });
769
843
  }
770
844
  });
771
845
  }
@@ -947,6 +1021,7 @@ var customElement = (render, options) => {
947
1021
  });
948
1022
  #render() {
949
1023
  const root = this.#shadowRoot ?? this;
1024
+ renderState.currentShadowRoot = this.#shadowRoot;
950
1025
  const fragment = render({
951
1026
  elementRef: this,
952
1027
  root,
@@ -1043,25 +1118,11 @@ You must set an initial value before calling a property signal's getter.
1043
1118
  }
1044
1119
  });
1045
1120
  fragment.host = this;
1046
- const registry = shadowRootOptions.registry instanceof CustomElementRegistry ? shadowRootOptions.registry : shadowRootOptions.registry?.eject();
1047
- const tempContainer = document.createElement("div");
1048
- tempContainer.append(fragment.cloneNode(true));
1049
- const fragmentContent = tempContainer.innerHTML;
1050
- root.innerHTML = fragmentContent;
1051
- if (registry?.__tagNames !== void 0) {
1052
- for (const tagName of registry.__tagNames) {
1053
- const upgradedElements = root.querySelectorAll(tagName);
1054
- const nonUpgradedElements = fragment.querySelectorAll(tagName);
1055
- upgradedElements.forEach((upgradedElement, index) => {
1056
- const nonUpgradedElement = nonUpgradedElements[index];
1057
- nonUpgradedElement.replaceWith(upgradedElement);
1058
- });
1059
- }
1060
- }
1061
1121
  for (const fn of this.#clientOnlyCallbackFns) {
1062
1122
  fn();
1063
1123
  }
1064
- setInnerHTML(root, fragment);
1124
+ clearHTML(root);
1125
+ root.append(fragment);
1065
1126
  }
1066
1127
  static get formAssociated() {
1067
1128
  return formAssociated;
package/dist/index.d.cts CHANGED
@@ -439,9 +439,6 @@ if (typeof window !== 'undefined') {
439
439
  installScopedCreationMethod(ShadowRoot, 'createElement', document);
440
440
  installScopedCreationMethod(ShadowRoot, 'importNode', document);
441
441
  installScopedCreationMethod(Element, 'insertAdjacentHTML');
442
- installScopedCreationMethod(Node, 'appendChild');
443
- installScopedCreationMethod(Element, 'append');
444
-
445
442
 
446
443
  // Install scoped innerHTML on Element & ShadowRoot
447
444
  const installScopedCreationSetter = (ctor, name) => {
@@ -589,6 +586,10 @@ declare global {
589
586
  interface CustomElementRegistry {
590
587
  __tagNames: Set<string>;
591
588
  }
589
+ interface ShadowRoot {
590
+ // missing from typescript but present in the spec
591
+ importNode: <T = Node>(node: T, deep: boolean) => T;
592
+ }
592
593
  }
593
594
 
594
595
  type TagName = `${string}-${string}`;
package/dist/index.d.ts CHANGED
@@ -439,9 +439,6 @@ if (typeof window !== 'undefined') {
439
439
  installScopedCreationMethod(ShadowRoot, 'createElement', document);
440
440
  installScopedCreationMethod(ShadowRoot, 'importNode', document);
441
441
  installScopedCreationMethod(Element, 'insertAdjacentHTML');
442
- installScopedCreationMethod(Node, 'appendChild');
443
- installScopedCreationMethod(Element, 'append');
444
-
445
442
 
446
443
  // Install scoped innerHTML on Element & ShadowRoot
447
444
  const installScopedCreationSetter = (ctor, name) => {
@@ -589,6 +586,10 @@ declare global {
589
586
  interface CustomElementRegistry {
590
587
  __tagNames: Set<string>;
591
588
  }
589
+ interface ShadowRoot {
590
+ // missing from typescript but present in the spec
591
+ importNode: <T = Node>(node: T, deep: boolean) => T;
592
+ }
592
593
  }
593
594
 
594
595
  type TagName = `${string}-${string}`;
package/dist/index.js CHANGED
@@ -323,8 +323,6 @@ if (typeof window !== "undefined") {
323
323
  installScopedCreationMethod(ShadowRoot, "createElement", document);
324
324
  installScopedCreationMethod(ShadowRoot, "importNode", document);
325
325
  installScopedCreationMethod(Element, "insertAdjacentHTML");
326
- installScopedCreationMethod(Node, "appendChild");
327
- installScopedCreationMethod(Element, "append");
328
326
  const installScopedCreationSetter = (ctor, name) => {
329
327
  const descriptor = Object.getOwnPropertyDescriptor(ctor.prototype, name);
330
328
  Object.defineProperty(ctor.prototype, name, {
@@ -666,20 +664,19 @@ var clientOnlyCallback = (fn) => {
666
664
  };
667
665
 
668
666
  // src/render.ts
667
+ var renderState = {
668
+ currentShadowRoot: null
669
+ };
669
670
  var clearHTML = (element) => {
670
671
  while (element.childNodes.length > 0) {
671
672
  element.childNodes[0].remove();
672
673
  }
673
674
  };
674
675
  var parseFragment = (htmlStr) => {
675
- const range = document.createRange();
676
- range.selectNode(document.body);
677
- return range.createContextualFragment(htmlStr);
678
- };
679
- var setInnerHTML = (element, html2) => {
680
- clearHTML(element);
681
- const fragment = typeof html2 === "string" ? parseFragment(html2) : html2;
682
- element.append(fragment);
676
+ const template = document.createElement("template");
677
+ template.innerHTML = htmlStr;
678
+ const fragment = renderState.currentShadowRoot === null ? template.content : renderState.currentShadowRoot.importNode(template.content, true);
679
+ return fragment;
683
680
  };
684
681
  var logValueError = (value) => {
685
682
  console.error(
@@ -687,21 +684,82 @@ var logValueError = (value) => {
687
684
  value
688
685
  );
689
686
  };
687
+ var arrayToDocumentFragment = (array, parent) => {
688
+ const documentFragment = new DocumentFragment();
689
+ let count = 0;
690
+ const keys = /* @__PURE__ */ new Set();
691
+ for (const item of array) {
692
+ const node = getNewNode(item, parent).cloneNode(true);
693
+ if (node instanceof DocumentFragment) {
694
+ const child = node.firstElementChild;
695
+ if (node.children.length > 1) {
696
+ console.error(
697
+ "When rendering arrays, fragments must contain only one top-level element at a time. Error occured in:",
698
+ parent
699
+ );
700
+ }
701
+ if (child === null) continue;
702
+ let key = child.getAttribute("key");
703
+ if (key === null) {
704
+ console.warn(
705
+ "When rendering arrays, a `key` attribute should be provided on each child element. An index was automatically applied, but this could result in unexpected behavior:",
706
+ child
707
+ );
708
+ key = String(count);
709
+ child.setAttribute("key", key);
710
+ }
711
+ if (keys.has(key)) {
712
+ console.warn(
713
+ `When rendering arrays, each child should have a unique \`key\` attribute. Duplicate key "${key}" found on:`,
714
+ child
715
+ );
716
+ }
717
+ keys.add(key);
718
+ count++;
719
+ }
720
+ documentFragment.append(node);
721
+ }
722
+ return documentFragment;
723
+ };
724
+ var getNewNode = (value, parent) => {
725
+ if (typeof value === "string") return new Text(value);
726
+ if (Array.isArray(value)) return arrayToDocumentFragment(value, parent);
727
+ if (value instanceof DocumentFragment) return value;
728
+ return new Text("");
729
+ };
690
730
  var html = (strings, ...values) => {
691
731
  let innerHTML = "";
692
732
  const signalMap = /* @__PURE__ */ new Map();
693
- strings.forEach((string, i) => {
694
- let value = values[i] ?? "";
733
+ const processValue = (value) => {
734
+ if (!isServer && value instanceof DocumentFragment) {
735
+ const tempDiv = document.createElement("div");
736
+ tempDiv.append(value.cloneNode(true));
737
+ return tempDiv.innerHTML;
738
+ }
695
739
  if (typeof value === "function") {
740
+ const getter = value;
696
741
  const uniqueKey = crypto.randomUUID();
697
- signalMap.set(uniqueKey, value);
698
- value = isServer ? value() : `{{signal:${uniqueKey}}}`;
742
+ signalMap.set(uniqueKey, getter);
743
+ let result = getter();
744
+ if (Array.isArray(result)) {
745
+ result = result.map((item) => processValue(item)).join("");
746
+ }
747
+ return isServer ? String(result) : `{{signal:${uniqueKey}}}`;
699
748
  }
700
749
  if (typeof value === "object" && value !== null) {
701
750
  logValueError(value);
702
- value = "";
751
+ return "";
752
+ }
753
+ return String(value);
754
+ };
755
+ strings.forEach((string, i) => {
756
+ let value = values[i] ?? "";
757
+ if (Array.isArray(value)) {
758
+ value = value.map((item) => processValue(item)).join("");
759
+ } else {
760
+ value = processValue(value);
703
761
  }
704
- innerHTML += string + String(value);
762
+ innerHTML += string + String(value === null ? "" : value);
705
763
  });
706
764
  if (isServer) {
707
765
  return innerHTML;
@@ -713,24 +771,40 @@ var html = (strings, ...values) => {
713
771
  for (const child of element.childNodes) {
714
772
  if (child instanceof Text && signalBindingRegex.test(child.data)) {
715
773
  const textList = child.data.split(signalBindingRegex);
774
+ const sibling = child.nextSibling;
716
775
  textList.forEach((text, i) => {
717
776
  const uniqueKey = text.replace(/\{\{signal:(.+)\}\}/, "$1");
718
777
  const signal = uniqueKey !== text ? signalMap.get(uniqueKey) : void 0;
719
778
  const newValue = signal !== void 0 ? signal() : text;
720
- const newNode = (() => {
721
- if (typeof newValue === "string") return new Text(newValue);
722
- if (newValue instanceof DocumentFragment) return newValue;
723
- return new Text("");
724
- })();
779
+ const newNode = getNewNode(newValue, element);
725
780
  if (i === 0) {
726
781
  child.replaceWith(newNode);
727
782
  } else {
728
- element.insertBefore(newNode, child.nextSibling);
783
+ element.insertBefore(newNode, sibling);
729
784
  }
730
785
  if (signal !== void 0 && newNode instanceof Text) {
731
786
  createEffect(() => {
732
787
  newNode.data = signal();
733
788
  });
789
+ } else if (signal !== void 0 && newNode instanceof DocumentFragment) {
790
+ createEffect(() => {
791
+ const result = signal();
792
+ const nextNode = getNewNode(result, element);
793
+ if (nextNode instanceof Text) {
794
+ throw new TypeError(
795
+ "Signal mismatch: expected DocumentFragment or Array<DocumentFragment>, but got Text"
796
+ );
797
+ }
798
+ let lastSibling = element.lastChild;
799
+ for (const child2 of nextNode.children) {
800
+ const key = child2.getAttribute("key");
801
+ const matchingNode = element.querySelector(`[key="${key}"]`);
802
+ if (matchingNode === null) continue;
803
+ lastSibling = matchingNode.nextSibling;
804
+ child2.replaceWith(matchingNode);
805
+ }
806
+ element.insertBefore(nextNode, lastSibling);
807
+ });
734
808
  }
735
809
  });
736
810
  }
@@ -912,6 +986,7 @@ var customElement = (render, options) => {
912
986
  });
913
987
  #render() {
914
988
  const root = this.#shadowRoot ?? this;
989
+ renderState.currentShadowRoot = this.#shadowRoot;
915
990
  const fragment = render({
916
991
  elementRef: this,
917
992
  root,
@@ -1008,25 +1083,11 @@ You must set an initial value before calling a property signal's getter.
1008
1083
  }
1009
1084
  });
1010
1085
  fragment.host = this;
1011
- const registry = shadowRootOptions.registry instanceof CustomElementRegistry ? shadowRootOptions.registry : shadowRootOptions.registry?.eject();
1012
- const tempContainer = document.createElement("div");
1013
- tempContainer.append(fragment.cloneNode(true));
1014
- const fragmentContent = tempContainer.innerHTML;
1015
- root.innerHTML = fragmentContent;
1016
- if (registry?.__tagNames !== void 0) {
1017
- for (const tagName of registry.__tagNames) {
1018
- const upgradedElements = root.querySelectorAll(tagName);
1019
- const nonUpgradedElements = fragment.querySelectorAll(tagName);
1020
- upgradedElements.forEach((upgradedElement, index) => {
1021
- const nonUpgradedElement = nonUpgradedElements[index];
1022
- nonUpgradedElement.replaceWith(upgradedElement);
1023
- });
1024
- }
1025
- }
1026
1086
  for (const fn of this.#clientOnlyCallbackFns) {
1027
1087
  fn();
1028
1088
  }
1029
- setInnerHTML(root, fragment);
1089
+ clearHTML(root);
1090
+ root.append(fragment);
1030
1091
  }
1031
1092
  static get formAssociated() {
1032
1093
  return formAssociated;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thunderous",
3
- "version": "1.0.2",
3
+ "version": "1.1.1",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -31,6 +31,7 @@
31
31
  "license": "MIT",
32
32
  "scripts": {
33
33
  "demo": "cd demo && npm start",
34
+ "demo:ssr": "cd demo && npm run ssr",
34
35
  "www": "cd www && npm run dev",
35
36
  "build": "tsup src/index.ts --format cjs,esm --dts --no-clean",
36
37
  "test": "find src -name '*.test.ts' | xargs c8 node --import tsx --test",