thunderous 1.0.2 → 1.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/dist/index.cjs CHANGED
@@ -722,21 +722,82 @@ var logValueError = (value) => {
722
722
  value
723
723
  );
724
724
  };
725
+ var arrayToDocumentFragment = (array, parent) => {
726
+ const documentFragment = new DocumentFragment();
727
+ let count = 0;
728
+ const keys = /* @__PURE__ */ new Set();
729
+ for (const item of array) {
730
+ const node = getNewNode(item, parent).cloneNode(true);
731
+ if (node instanceof DocumentFragment) {
732
+ const child = node.firstElementChild;
733
+ if (node.children.length > 1) {
734
+ console.error(
735
+ "When rendering arrays, fragments must contain only one top-level element at a time. Error occured in:",
736
+ parent
737
+ );
738
+ }
739
+ if (child === null) continue;
740
+ let key = child.getAttribute("key");
741
+ if (key === null) {
742
+ console.warn(
743
+ "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:",
744
+ child
745
+ );
746
+ key = String(count);
747
+ child.setAttribute("key", key);
748
+ }
749
+ if (keys.has(key)) {
750
+ console.warn(
751
+ `When rendering arrays, each child should have a unique \`key\` attribute. Duplicate key "${key}" found on:`,
752
+ child
753
+ );
754
+ }
755
+ keys.add(key);
756
+ count++;
757
+ }
758
+ documentFragment.append(node);
759
+ }
760
+ return documentFragment;
761
+ };
762
+ var getNewNode = (value, parent) => {
763
+ if (typeof value === "string") return new Text(value);
764
+ if (Array.isArray(value)) return arrayToDocumentFragment(value, parent);
765
+ if (value instanceof DocumentFragment) return value;
766
+ return new Text("");
767
+ };
725
768
  var html = (strings, ...values) => {
726
769
  let innerHTML = "";
727
770
  const signalMap = /* @__PURE__ */ new Map();
728
- strings.forEach((string, i) => {
729
- let value = values[i] ?? "";
771
+ const processValue = (value) => {
772
+ if (!isServer && value instanceof DocumentFragment) {
773
+ const tempDiv = document.createElement("div");
774
+ tempDiv.append(value.cloneNode(true));
775
+ return tempDiv.innerHTML;
776
+ }
730
777
  if (typeof value === "function") {
778
+ const getter = value;
731
779
  const uniqueKey = crypto.randomUUID();
732
- signalMap.set(uniqueKey, value);
733
- value = isServer ? value() : `{{signal:${uniqueKey}}}`;
780
+ signalMap.set(uniqueKey, getter);
781
+ let result = getter();
782
+ if (Array.isArray(result)) {
783
+ result = result.map((item) => processValue(item)).join("");
784
+ }
785
+ return isServer ? String(result) : `{{signal:${uniqueKey}}}`;
734
786
  }
735
787
  if (typeof value === "object" && value !== null) {
736
788
  logValueError(value);
737
- value = "";
789
+ return "";
738
790
  }
739
- innerHTML += string + String(value);
791
+ return String(value);
792
+ };
793
+ strings.forEach((string, i) => {
794
+ let value = values[i] ?? "";
795
+ if (Array.isArray(value)) {
796
+ value = value.map((item) => processValue(item)).join("");
797
+ } else {
798
+ value = processValue(value);
799
+ }
800
+ innerHTML += string + String(value === null ? "" : value);
740
801
  });
741
802
  if (isServer) {
742
803
  return innerHTML;
@@ -748,24 +809,40 @@ var html = (strings, ...values) => {
748
809
  for (const child of element.childNodes) {
749
810
  if (child instanceof Text && signalBindingRegex.test(child.data)) {
750
811
  const textList = child.data.split(signalBindingRegex);
812
+ const sibling = child.nextSibling;
751
813
  textList.forEach((text, i) => {
752
814
  const uniqueKey = text.replace(/\{\{signal:(.+)\}\}/, "$1");
753
815
  const signal = uniqueKey !== text ? signalMap.get(uniqueKey) : void 0;
754
816
  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
- })();
817
+ const newNode = getNewNode(newValue, element);
760
818
  if (i === 0) {
761
819
  child.replaceWith(newNode);
762
820
  } else {
763
- element.insertBefore(newNode, child.nextSibling);
821
+ element.insertBefore(newNode, sibling);
764
822
  }
765
823
  if (signal !== void 0 && newNode instanceof Text) {
766
824
  createEffect(() => {
767
825
  newNode.data = signal();
768
826
  });
827
+ } else if (signal !== void 0 && newNode instanceof DocumentFragment) {
828
+ createEffect(() => {
829
+ const result = signal();
830
+ const nextNode = getNewNode(result, element);
831
+ if (nextNode instanceof Text) {
832
+ throw new TypeError(
833
+ "Signal mismatch: expected DocumentFragment or Array<DocumentFragment>, but got Text"
834
+ );
835
+ }
836
+ let lastSibling = element.lastChild;
837
+ for (const child2 of nextNode.children) {
838
+ const key = child2.getAttribute("key");
839
+ const matchingNode = element.querySelector(`[key="${key}"]`);
840
+ if (matchingNode === null) continue;
841
+ lastSibling = matchingNode.nextSibling;
842
+ child2.replaceWith(matchingNode);
843
+ }
844
+ element.insertBefore(nextNode, lastSibling);
845
+ });
769
846
  }
770
847
  });
771
848
  }
package/dist/index.js CHANGED
@@ -687,21 +687,82 @@ var logValueError = (value) => {
687
687
  value
688
688
  );
689
689
  };
690
+ var arrayToDocumentFragment = (array, parent) => {
691
+ const documentFragment = new DocumentFragment();
692
+ let count = 0;
693
+ const keys = /* @__PURE__ */ new Set();
694
+ for (const item of array) {
695
+ const node = getNewNode(item, parent).cloneNode(true);
696
+ if (node instanceof DocumentFragment) {
697
+ const child = node.firstElementChild;
698
+ if (node.children.length > 1) {
699
+ console.error(
700
+ "When rendering arrays, fragments must contain only one top-level element at a time. Error occured in:",
701
+ parent
702
+ );
703
+ }
704
+ if (child === null) continue;
705
+ let key = child.getAttribute("key");
706
+ if (key === null) {
707
+ console.warn(
708
+ "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:",
709
+ child
710
+ );
711
+ key = String(count);
712
+ child.setAttribute("key", key);
713
+ }
714
+ if (keys.has(key)) {
715
+ console.warn(
716
+ `When rendering arrays, each child should have a unique \`key\` attribute. Duplicate key "${key}" found on:`,
717
+ child
718
+ );
719
+ }
720
+ keys.add(key);
721
+ count++;
722
+ }
723
+ documentFragment.append(node);
724
+ }
725
+ return documentFragment;
726
+ };
727
+ var getNewNode = (value, parent) => {
728
+ if (typeof value === "string") return new Text(value);
729
+ if (Array.isArray(value)) return arrayToDocumentFragment(value, parent);
730
+ if (value instanceof DocumentFragment) return value;
731
+ return new Text("");
732
+ };
690
733
  var html = (strings, ...values) => {
691
734
  let innerHTML = "";
692
735
  const signalMap = /* @__PURE__ */ new Map();
693
- strings.forEach((string, i) => {
694
- let value = values[i] ?? "";
736
+ const processValue = (value) => {
737
+ if (!isServer && value instanceof DocumentFragment) {
738
+ const tempDiv = document.createElement("div");
739
+ tempDiv.append(value.cloneNode(true));
740
+ return tempDiv.innerHTML;
741
+ }
695
742
  if (typeof value === "function") {
743
+ const getter = value;
696
744
  const uniqueKey = crypto.randomUUID();
697
- signalMap.set(uniqueKey, value);
698
- value = isServer ? value() : `{{signal:${uniqueKey}}}`;
745
+ signalMap.set(uniqueKey, getter);
746
+ let result = getter();
747
+ if (Array.isArray(result)) {
748
+ result = result.map((item) => processValue(item)).join("");
749
+ }
750
+ return isServer ? String(result) : `{{signal:${uniqueKey}}}`;
699
751
  }
700
752
  if (typeof value === "object" && value !== null) {
701
753
  logValueError(value);
702
- value = "";
754
+ return "";
703
755
  }
704
- innerHTML += string + String(value);
756
+ return String(value);
757
+ };
758
+ strings.forEach((string, i) => {
759
+ let value = values[i] ?? "";
760
+ if (Array.isArray(value)) {
761
+ value = value.map((item) => processValue(item)).join("");
762
+ } else {
763
+ value = processValue(value);
764
+ }
765
+ innerHTML += string + String(value === null ? "" : value);
705
766
  });
706
767
  if (isServer) {
707
768
  return innerHTML;
@@ -713,24 +774,40 @@ var html = (strings, ...values) => {
713
774
  for (const child of element.childNodes) {
714
775
  if (child instanceof Text && signalBindingRegex.test(child.data)) {
715
776
  const textList = child.data.split(signalBindingRegex);
777
+ const sibling = child.nextSibling;
716
778
  textList.forEach((text, i) => {
717
779
  const uniqueKey = text.replace(/\{\{signal:(.+)\}\}/, "$1");
718
780
  const signal = uniqueKey !== text ? signalMap.get(uniqueKey) : void 0;
719
781
  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
- })();
782
+ const newNode = getNewNode(newValue, element);
725
783
  if (i === 0) {
726
784
  child.replaceWith(newNode);
727
785
  } else {
728
- element.insertBefore(newNode, child.nextSibling);
786
+ element.insertBefore(newNode, sibling);
729
787
  }
730
788
  if (signal !== void 0 && newNode instanceof Text) {
731
789
  createEffect(() => {
732
790
  newNode.data = signal();
733
791
  });
792
+ } else if (signal !== void 0 && newNode instanceof DocumentFragment) {
793
+ createEffect(() => {
794
+ const result = signal();
795
+ const nextNode = getNewNode(result, element);
796
+ if (nextNode instanceof Text) {
797
+ throw new TypeError(
798
+ "Signal mismatch: expected DocumentFragment or Array<DocumentFragment>, but got Text"
799
+ );
800
+ }
801
+ let lastSibling = element.lastChild;
802
+ for (const child2 of nextNode.children) {
803
+ const key = child2.getAttribute("key");
804
+ const matchingNode = element.querySelector(`[key="${key}"]`);
805
+ if (matchingNode === null) continue;
806
+ lastSibling = matchingNode.nextSibling;
807
+ child2.replaceWith(matchingNode);
808
+ }
809
+ element.insertBefore(nextNode, lastSibling);
810
+ });
734
811
  }
735
812
  });
736
813
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thunderous",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
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",