thunderous 2.4.4 → 2.4.5-next.1776738548

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
@@ -102,13 +102,14 @@ var createSignal = (initVal, options) => {
102
102
  const effectRef = effects.get(sym);
103
103
  if (effectRef !== void 0) {
104
104
  try {
105
- effectRef.fn({
105
+ const result = effectRef.fn({
106
106
  lastValue: effectRef.value,
107
107
  destroy: () => {
108
108
  effects.delete(sym);
109
109
  queueMicrotask(() => subscribers.delete(sym));
110
110
  }
111
111
  });
112
+ if (result !== void 0) effectRef.value = result;
112
113
  } catch (error) {
113
114
  console.error("Error in subscriber:", { error, oldValue, newValue, fn: effectRef.fn });
114
115
  }
@@ -149,14 +150,16 @@ var derived = (fn, options) => {
149
150
  };
150
151
  var createEffect = (fn, value) => {
151
152
  const privateIdent = ident = {};
152
- effects.set(ident, { fn, value });
153
+ const effectRef = { fn, value };
154
+ effects.set(ident, effectRef);
153
155
  try {
154
- fn({
156
+ const result = fn({
155
157
  lastValue: value,
156
158
  destroy: () => {
157
159
  effects.delete(privateIdent);
158
160
  }
159
161
  });
162
+ if (result !== void 0) effectRef.value = result;
160
163
  } catch (error) {
161
164
  console.error("Error in effect:", { error, fn });
162
165
  }
@@ -165,12 +168,6 @@ var createEffect = (fn, value) => {
165
168
 
166
169
  // src/utilities.ts
167
170
  var NOOP = () => void 0;
168
- var queryComment = (node, comment) => {
169
- const walker = document.createTreeWalker(node, NodeFilter.SHOW_COMMENT, {
170
- acceptNode: (n) => n.nodeValue === comment ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
171
- });
172
- return walker.nextNode();
173
- };
174
171
  var queryChildren = (children, selector) => {
175
172
  for (const child of children) {
176
173
  if (child instanceof Element && child.matches(selector)) {
@@ -326,7 +323,7 @@ var clientOnlyCallback = (fn) => {
326
323
  // src/render.ts
327
324
  var CALLBACK_BINDING_REGEX = /(\{\{callback:.+\}\})/;
328
325
  var LEGACY_CALLBACK_BINDING_REGEX = /(this.getRootNode\(\).host.__customCallbackFns.get\('.+'\)\(event\))/;
329
- var SIGNAL_BINDING_REGEX = /(\{\{signal:.+\}\})/;
326
+ var SIGNAL_BINDING_REGEX = /(\{\{signal:.+?\}\})/;
330
327
  var FRAGMENT_ATTRIBUTE = "___thunderous-fragment";
331
328
  var renderState = {
332
329
  currentShadowRoot: null,
@@ -351,19 +348,34 @@ var logPropertyWarning = (propName, element) => {
351
348
  "\n\nThunderous will attempt to set the property anyway, but this may result in unexpected behavior. Please make sure the property exists on the element prior to setting it."
352
349
  );
353
350
  };
354
- var asNodeList = (value, parent) => {
351
+ var asNodeList = (value, parent, autoKey) => {
352
+ if (value === null || value === void 0) return [];
355
353
  if (typeof value === "string") return [new Text(value)];
356
- if (value instanceof DocumentFragment) return Array.from(value.children);
354
+ if (typeof value === "number" || typeof value === "boolean") return [new Text(String(value))];
355
+ if (value instanceof DocumentFragment) {
356
+ const children = Array.from(value.children);
357
+ if (autoKey !== void 0 && children.length > 0) {
358
+ const child = children[0];
359
+ if (child instanceof Element && child.getAttribute("key") === null) {
360
+ child.setAttribute("key", String(autoKey));
361
+ }
362
+ }
363
+ return children;
364
+ }
357
365
  if (Array.isArray(value)) {
358
366
  const nodeList = [];
359
367
  let count = 0;
360
368
  const keys = /* @__PURE__ */ new Set();
361
369
  for (const item of value) {
362
370
  const cachedItem = item instanceof DocumentFragment ? renderState.childrenMap.get(item) : void 0;
363
- const children = cachedItem ?? asNodeList(item, parent);
371
+ const children = cachedItem ?? asNodeList(item, parent, item instanceof DocumentFragment ? count : void 0);
364
372
  if (cachedItem === void 0 && item instanceof DocumentFragment) {
365
373
  renderState.childrenMap.set(item, children);
366
374
  }
375
+ if (!(item instanceof DocumentFragment)) {
376
+ nodeList.push(...children);
377
+ continue;
378
+ }
367
379
  if (children.length > 1) {
368
380
  console.error(
369
381
  "When rendering arrays, fragments must contain only one top-level element at a time. Error occured in:",
@@ -416,73 +428,68 @@ var processValue = (value) => {
416
428
  renderState.callbackMap.set(uniqueKey, value);
417
429
  return isServer ? String(value()) : `{{callback:${uniqueKey}}}`;
418
430
  }
419
- return String(value);
431
+ return value === null || value === void 0 ? "" : String(value);
420
432
  };
421
433
  var evaluateBindings = (element, fragment) => {
422
434
  for (const child of Array.from(element.childNodes)) {
423
435
  if (child instanceof Text && SIGNAL_BINDING_REGEX.test(child.data)) {
424
436
  const textList = child.data.split(SIGNAL_BINDING_REGEX);
425
- const nextSibling = child.nextSibling;
426
- const prevSibling = child.previousSibling;
427
- textList.forEach((text, i) => {
437
+ const allInitialChildren = [];
438
+ const signalEntries = [];
439
+ let signalIndex = 0;
440
+ const totalSignals = textList.filter((t) => SIGNAL_BINDING_REGEX.test(t)).length;
441
+ textList.forEach((text) => {
428
442
  const uniqueKey = SIGNAL_BINDING_REGEX.test(text) ? text.replace(/\{\{signal:(.+)\}\}/, "$1") : void 0;
429
443
  const signal = uniqueKey !== void 0 ? renderState.signalMap.get(uniqueKey) : void 0;
430
444
  const newValue = signal !== void 0 ? signal() : text;
431
- const initialChildren = asNodeList(newValue, element);
432
- if (i === 0) {
433
- child.replaceWith(...initialChildren);
434
- } else {
435
- const endAnchor2 = queryComment(element, `${uniqueKey}:end`) ?? nextSibling;
436
- if (endAnchor2 !== null) {
437
- endAnchor2.before(...initialChildren);
438
- } else {
439
- element.append(...initialChildren);
440
- }
445
+ const autoKey = signal !== void 0 && totalSignals > 1 ? signalIndex++ : void 0;
446
+ const initialChildren = asNodeList(newValue, element, autoKey);
447
+ allInitialChildren.push(...initialChildren);
448
+ if (uniqueKey !== void 0 && signal !== void 0) {
449
+ signalEntries.push({ uniqueKey, signal, initialChildren, autoKey });
441
450
  }
442
- if (uniqueKey === void 0) return;
451
+ });
452
+ child.replaceWith(...allInitialChildren);
453
+ signalEntries.forEach(({ uniqueKey, signal, initialChildren, autoKey }) => {
454
+ const firstChild = initialChildren[0];
455
+ const lastChild = initialChildren[initialChildren.length - 1];
456
+ if (firstChild === void 0) return;
443
457
  const startAnchor = document.createComment(`${uniqueKey}:start`);
444
- if (prevSibling !== null) {
445
- prevSibling.after(startAnchor);
446
- } else {
447
- element.prepend(startAnchor);
448
- }
458
+ firstChild.before(startAnchor);
449
459
  const endAnchor = document.createComment(`${uniqueKey}:end`);
450
- if (nextSibling !== null) {
451
- nextSibling.before(endAnchor);
452
- } else {
453
- element.append(endAnchor);
454
- }
460
+ lastChild.after(endAnchor);
455
461
  const bindText = (node, signal2) => {
456
462
  createEffect(({ destroy }) => {
457
463
  const result = signal2();
458
464
  if (Array.isArray(result)) {
459
465
  destroy();
460
- bindArray(signal2);
466
+ bindArray(signal2, autoKey);
461
467
  return;
462
468
  }
463
469
  if (result instanceof DocumentFragment) {
464
470
  destroy();
465
- bindFragment(signal2);
471
+ bindFragment(signal2, initialChildren, autoKey);
466
472
  return;
467
473
  }
468
- node.data = result === null ? "" : String(result);
474
+ node.data = result === null || result === void 0 ? "" : String(result);
469
475
  });
470
476
  };
471
- const bindArray = (signal2) => {
477
+ const bindArray = (signal2, autoKey2) => {
472
478
  createEffect(
473
479
  ({ lastValue: oldChildren, destroy }) => {
474
480
  const result = signal2();
475
- const newChildren = asNodeList(result, element);
476
- const firstChild = newChildren[0];
477
- if (!Array.isArray(result) && newChildren.length === 1 && firstChild instanceof DocumentFragment) {
478
- destroy();
479
- bindFragment(signal2);
480
- return;
481
- }
482
- if (newChildren.length === 1 && firstChild instanceof Text) {
483
- destroy();
484
- bindText(firstChild, signal2);
485
- return;
481
+ const newChildren = asNodeList(result, element, autoKey2);
482
+ const firstChild2 = newChildren[0];
483
+ if (!Array.isArray(result)) {
484
+ if (newChildren.length === 1 && firstChild2 instanceof Text) {
485
+ while (startAnchor.nextSibling !== endAnchor) {
486
+ startAnchor.nextSibling?.remove();
487
+ }
488
+ startAnchor.after(firstChild2);
489
+ destroy();
490
+ bindText(firstChild2, signal2);
491
+ return;
492
+ }
486
493
  }
487
494
  while (startAnchor.nextSibling !== endAnchor) {
488
495
  startAnchor.nextSibling?.remove();
@@ -492,7 +499,6 @@ var evaluateBindings = (element, fragment) => {
492
499
  for (const persistedChild of oldChildren) {
493
500
  if (persistedChild instanceof Element) {
494
501
  const key = persistedChild.getAttribute("key");
495
- if (key === null) continue;
496
502
  const newChild = queryChildren(newChildren, `[key="${key}"]`);
497
503
  if (newChild === null) {
498
504
  persistedChild.remove();
@@ -514,40 +520,52 @@ var evaluateBindings = (element, fragment) => {
514
520
  null
515
521
  );
516
522
  };
517
- const bindFragment = (signal2) => {
523
+ const bindFragment = (signal2, initialChildren2, autoKey2) => {
518
524
  const initialFragment = signal2();
519
- renderState.childrenMap.set(initialFragment, Array.from(initialFragment.childNodes));
525
+ const firstInitialChild = initialChildren2[0];
526
+ if (firstInitialChild instanceof Element) {
527
+ renderState.childrenMap.set(initialFragment, initialChildren2);
528
+ }
520
529
  createEffect(({ destroy }) => {
521
530
  const result = signal2();
522
- const cachedChildren = renderState.childrenMap.get(initialFragment);
523
- const children = cachedChildren ?? asNodeList(result, element);
531
+ const cachedChildren = result instanceof DocumentFragment ? renderState.childrenMap.get(result) : void 0;
532
+ const children = cachedChildren ?? asNodeList(result, element, autoKey2);
533
+ if (result instanceof DocumentFragment && !renderState.childrenMap.has(result)) {
534
+ renderState.childrenMap.set(result, children);
535
+ }
524
536
  if (Array.isArray(result)) {
525
537
  destroy();
526
- bindArray(signal2);
538
+ bindArray(signal2, autoKey2);
527
539
  return;
528
540
  }
529
- if (result instanceof Text) {
530
- const children2 = asNodeList(result, element);
531
- const text2 = children2[0];
541
+ if (!(result instanceof DocumentFragment) && result !== null && result !== void 0) {
542
+ while (startAnchor.nextSibling !== endAnchor) {
543
+ startAnchor.nextSibling?.remove();
544
+ }
545
+ const children2 = asNodeList(result, element, autoKey2);
546
+ const text = children2[0];
547
+ startAnchor.after(text);
532
548
  destroy();
533
- bindText(text2, signal2);
549
+ bindText(text, signal2);
534
550
  return;
535
551
  }
536
552
  while (startAnchor.nextSibling !== endAnchor) {
537
553
  startAnchor.nextSibling?.remove();
538
554
  }
555
+ if (result === null || result === void 0) {
556
+ return;
557
+ }
539
558
  startAnchor.after(...children);
540
559
  });
541
560
  };
542
- if (signal !== void 0) {
543
- if (Array.isArray(newValue)) {
544
- bindArray(signal);
545
- } else if (initialChildren instanceof DocumentFragment) {
546
- bindFragment(signal);
547
- } else {
548
- const initialChild = initialChildren[0];
549
- bindText(initialChild, signal);
550
- }
561
+ const currentValue = signal();
562
+ if (Array.isArray(currentValue)) {
563
+ bindArray(signal, autoKey);
564
+ } else if (currentValue instanceof DocumentFragment) {
565
+ bindFragment(signal, initialChildren, autoKey);
566
+ } else {
567
+ const initialChild = initialChildren[0];
568
+ bindText(initialChild, signal);
551
569
  }
552
570
  });
553
571
  }
@@ -584,7 +602,6 @@ var evaluateBindings = (element, fragment) => {
584
602
  if (newText !== prevText) child.setAttribute(attrName, newText);
585
603
  }
586
604
  if (attrName.startsWith("prop-id:")) {
587
- if (child.hasAttribute(attrName)) child.removeAttribute(attrName);
588
605
  const propId = attrName.replace("prop-id:", "");
589
606
  const propName = renderState.propertyMap.get(propId);
590
607
  if (propName === void 0) {
@@ -619,9 +636,7 @@ var evaluateBindings = (element, fragment) => {
619
636
  child.__customCallbackFns.set(uniqueKey, callback);
620
637
  }
621
638
  }
622
- if (uniqueKey !== "" && !attrName.startsWith("prop-id:")) {
623
- child.setAttribute(attrName, `this.__customCallbackFns.get('${uniqueKey}')(event)`);
624
- } else if (attrName.startsWith("prop-id:")) {
639
+ if (attrName.startsWith("prop-id:")) {
625
640
  child.removeAttribute(attrName);
626
641
  const propId = attrName.replace("prop-id:", "");
627
642
  const propName = renderState.propertyMap.get(propId);
@@ -634,6 +649,8 @@ var evaluateBindings = (element, fragment) => {
634
649
  }
635
650
  if (!(propName in child)) logPropertyWarning(propName, child);
636
651
  child[propName] = child.__customCallbackFns.get(uniqueKey);
652
+ } else {
653
+ child.setAttribute(attrName, `this.__customCallbackFns.get('${uniqueKey}')(event)`);
637
654
  }
638
655
  });
639
656
  } else if (attrName.startsWith("prop-id:")) {
@@ -663,7 +680,7 @@ var html = (strings, ...values) => {
663
680
  } else {
664
681
  value = processValue(value);
665
682
  }
666
- innerHTML2 += str + String(value === null ? "" : value);
683
+ innerHTML2 += str + String(value);
667
684
  return innerHTML2;
668
685
  }, "");
669
686
  if (isServer) return innerHTML;
@@ -684,7 +701,7 @@ var html = (strings, ...values) => {
684
701
  evaluateBindings(fragment, fragment);
685
702
  return fragment;
686
703
  };
687
- var adoptedStylesSupported = typeof window !== "undefined" && window.ShadowRoot?.prototype.hasOwnProperty("adoptedStyleSheets") && window.CSSStyleSheet?.prototype.hasOwnProperty("replace");
704
+ var isAdoptedStylesSupported = () => typeof window !== "undefined" && window.ShadowRoot?.prototype.hasOwnProperty("adoptedStyleSheets") && window.CSSStyleSheet?.prototype.hasOwnProperty("replace");
688
705
  var isCSSStyleSheet = (stylesheet) => {
689
706
  return typeof CSSStyleSheet !== "undefined" && stylesheet instanceof CSSStyleSheet;
690
707
  };
@@ -708,7 +725,7 @@ var css = (strings, ...values) => {
708
725
  if (isServer) {
709
726
  return cssText;
710
727
  }
711
- const stylesheet = adoptedStylesSupported ? new CSSStyleSheet() : document.createElement("style");
728
+ const stylesheet = isAdoptedStylesSupported() ? new CSSStyleSheet() : document.createElement("style");
712
729
  const textList = cssText.split(signalBindingRegex);
713
730
  createEffect(() => {
714
731
  const newCSSTextList = [];
@@ -815,8 +832,9 @@ var customElement = (render, options) => {
815
832
  #observer = options?.observedAttributes !== void 0 ? null : new MutationObserver((mutations) => {
816
833
  for (const mutation of mutations) {
817
834
  const attrName = mutation.attributeName;
818
- if (mutation.type !== "attributes" || attrName === null) continue;
819
- if (!(attrName in this.#attrSignals)) this.#attrSignals[attrName] = createSignal(null);
835
+ if (!(attrName in this.#attrSignals)) {
836
+ this.#attrSignals[attrName] = createSignal(this.getAttribute(attrName));
837
+ }
820
838
  const [getter, setter] = this.#attrSignals[attrName];
821
839
  const oldValue = getter();
822
840
  const newValue = this.getAttribute(attrName);
@@ -916,7 +934,9 @@ You must set an initial value before calling a property signal's getter.
916
934
  {},
917
935
  {
918
936
  get: (_, prop) => {
919
- if (!(prop in this.#attrSignals)) this.#attrSignals[prop] = createSignal(null);
937
+ if (!(prop in this.#attrSignals)) {
938
+ this.#attrSignals[prop] = createSignal(this.getAttribute(prop));
939
+ }
920
940
  const [getter] = this.#attrSignals[prop];
921
941
  const setter = (newValue) => this.setAttribute(prop, newValue);
922
942
  return [getter, setter];
@@ -985,9 +1005,7 @@ You must set an initial value before calling a property signal's getter.
985
1005
  constructor() {
986
1006
  try {
987
1007
  super();
988
- if (!Object.prototype.hasOwnProperty.call(this, "__customCallbackFns")) {
989
- this.__customCallbackFns = /* @__PURE__ */ new Map();
990
- }
1008
+ this.__customCallbackFns = /* @__PURE__ */ new Map();
991
1009
  for (const attr of this.attributes) {
992
1010
  this.#attrSignals[attr.name] = createSignal(attr.value);
993
1011
  }
@@ -998,12 +1016,13 @@ You must set an initial value before calling a property signal's getter.
998
1016
  { cause: error }
999
1017
  );
1000
1018
  console.error(_error);
1001
- throw _error;
1002
1019
  }
1003
1020
  }
1004
1021
  connectedCallback() {
1005
1022
  for (const [attrName, attr] of this.#attributesAsPropertiesMap) {
1006
- if (!(attrName in this.#attrSignals)) this.#attrSignals[attrName] = createSignal(null);
1023
+ if (!(attrName in this.#attrSignals)) {
1024
+ this.#attrSignals[attrName] = createSignal(this.getAttribute(attrName));
1025
+ }
1007
1026
  const propName = attr.prop;
1008
1027
  const [getter] = this.#getPropSignal(propName, { allowUndefined: true });
1009
1028
  let busy = false;
@@ -1020,6 +1039,14 @@ You must set an initial value before calling a property signal's getter.
1020
1039
  busy = false;
1021
1040
  });
1022
1041
  }
1042
+ for (const attrName of Object.keys(this.#attrSignals)) {
1043
+ const signal = this.#attrSignals[attrName];
1044
+ const [getter, setter] = signal;
1045
+ const currentValue = this.getAttribute(attrName);
1046
+ if (getter() !== currentValue) {
1047
+ setter(currentValue);
1048
+ }
1049
+ }
1023
1050
  if (this.#observer !== null) {
1024
1051
  this.#observer.observe(this, { attributes: true });
1025
1052
  }
package/dist/index.d.cts CHANGED
@@ -94,7 +94,7 @@ type RegistryArgs = {
94
94
 
95
95
  type Styles = CSSStyleSheet | HTMLStyleElement;
96
96
 
97
- type SignalOptions = { debugMode: boolean; label?: string };
97
+ type SignalOptions = { debugMode?: boolean; label?: string };
98
98
  type SignalGetter<T> = {
99
99
  (options?: SignalOptions): T;
100
100
  getter: true;
package/dist/index.d.ts CHANGED
@@ -94,7 +94,7 @@ type RegistryArgs = {
94
94
 
95
95
  type Styles = CSSStyleSheet | HTMLStyleElement;
96
96
 
97
- type SignalOptions = { debugMode: boolean; label?: string };
97
+ type SignalOptions = { debugMode?: boolean; label?: string };
98
98
  type SignalGetter<T> = {
99
99
  (options?: SignalOptions): T;
100
100
  getter: true;
package/dist/index.js CHANGED
@@ -65,13 +65,14 @@ var createSignal = (initVal, options) => {
65
65
  const effectRef = effects.get(sym);
66
66
  if (effectRef !== void 0) {
67
67
  try {
68
- effectRef.fn({
68
+ const result = effectRef.fn({
69
69
  lastValue: effectRef.value,
70
70
  destroy: () => {
71
71
  effects.delete(sym);
72
72
  queueMicrotask(() => subscribers.delete(sym));
73
73
  }
74
74
  });
75
+ if (result !== void 0) effectRef.value = result;
75
76
  } catch (error) {
76
77
  console.error("Error in subscriber:", { error, oldValue, newValue, fn: effectRef.fn });
77
78
  }
@@ -112,14 +113,16 @@ var derived = (fn, options) => {
112
113
  };
113
114
  var createEffect = (fn, value) => {
114
115
  const privateIdent = ident = {};
115
- effects.set(ident, { fn, value });
116
+ const effectRef = { fn, value };
117
+ effects.set(ident, effectRef);
116
118
  try {
117
- fn({
119
+ const result = fn({
118
120
  lastValue: value,
119
121
  destroy: () => {
120
122
  effects.delete(privateIdent);
121
123
  }
122
124
  });
125
+ if (result !== void 0) effectRef.value = result;
123
126
  } catch (error) {
124
127
  console.error("Error in effect:", { error, fn });
125
128
  }
@@ -128,12 +131,6 @@ var createEffect = (fn, value) => {
128
131
 
129
132
  // src/utilities.ts
130
133
  var NOOP = () => void 0;
131
- var queryComment = (node, comment) => {
132
- const walker = document.createTreeWalker(node, NodeFilter.SHOW_COMMENT, {
133
- acceptNode: (n) => n.nodeValue === comment ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
134
- });
135
- return walker.nextNode();
136
- };
137
134
  var queryChildren = (children, selector) => {
138
135
  for (const child of children) {
139
136
  if (child instanceof Element && child.matches(selector)) {
@@ -289,7 +286,7 @@ var clientOnlyCallback = (fn) => {
289
286
  // src/render.ts
290
287
  var CALLBACK_BINDING_REGEX = /(\{\{callback:.+\}\})/;
291
288
  var LEGACY_CALLBACK_BINDING_REGEX = /(this.getRootNode\(\).host.__customCallbackFns.get\('.+'\)\(event\))/;
292
- var SIGNAL_BINDING_REGEX = /(\{\{signal:.+\}\})/;
289
+ var SIGNAL_BINDING_REGEX = /(\{\{signal:.+?\}\})/;
293
290
  var FRAGMENT_ATTRIBUTE = "___thunderous-fragment";
294
291
  var renderState = {
295
292
  currentShadowRoot: null,
@@ -314,19 +311,34 @@ var logPropertyWarning = (propName, element) => {
314
311
  "\n\nThunderous will attempt to set the property anyway, but this may result in unexpected behavior. Please make sure the property exists on the element prior to setting it."
315
312
  );
316
313
  };
317
- var asNodeList = (value, parent) => {
314
+ var asNodeList = (value, parent, autoKey) => {
315
+ if (value === null || value === void 0) return [];
318
316
  if (typeof value === "string") return [new Text(value)];
319
- if (value instanceof DocumentFragment) return Array.from(value.children);
317
+ if (typeof value === "number" || typeof value === "boolean") return [new Text(String(value))];
318
+ if (value instanceof DocumentFragment) {
319
+ const children = Array.from(value.children);
320
+ if (autoKey !== void 0 && children.length > 0) {
321
+ const child = children[0];
322
+ if (child instanceof Element && child.getAttribute("key") === null) {
323
+ child.setAttribute("key", String(autoKey));
324
+ }
325
+ }
326
+ return children;
327
+ }
320
328
  if (Array.isArray(value)) {
321
329
  const nodeList = [];
322
330
  let count = 0;
323
331
  const keys = /* @__PURE__ */ new Set();
324
332
  for (const item of value) {
325
333
  const cachedItem = item instanceof DocumentFragment ? renderState.childrenMap.get(item) : void 0;
326
- const children = cachedItem ?? asNodeList(item, parent);
334
+ const children = cachedItem ?? asNodeList(item, parent, item instanceof DocumentFragment ? count : void 0);
327
335
  if (cachedItem === void 0 && item instanceof DocumentFragment) {
328
336
  renderState.childrenMap.set(item, children);
329
337
  }
338
+ if (!(item instanceof DocumentFragment)) {
339
+ nodeList.push(...children);
340
+ continue;
341
+ }
330
342
  if (children.length > 1) {
331
343
  console.error(
332
344
  "When rendering arrays, fragments must contain only one top-level element at a time. Error occured in:",
@@ -379,73 +391,68 @@ var processValue = (value) => {
379
391
  renderState.callbackMap.set(uniqueKey, value);
380
392
  return isServer ? String(value()) : `{{callback:${uniqueKey}}}`;
381
393
  }
382
- return String(value);
394
+ return value === null || value === void 0 ? "" : String(value);
383
395
  };
384
396
  var evaluateBindings = (element, fragment) => {
385
397
  for (const child of Array.from(element.childNodes)) {
386
398
  if (child instanceof Text && SIGNAL_BINDING_REGEX.test(child.data)) {
387
399
  const textList = child.data.split(SIGNAL_BINDING_REGEX);
388
- const nextSibling = child.nextSibling;
389
- const prevSibling = child.previousSibling;
390
- textList.forEach((text, i) => {
400
+ const allInitialChildren = [];
401
+ const signalEntries = [];
402
+ let signalIndex = 0;
403
+ const totalSignals = textList.filter((t) => SIGNAL_BINDING_REGEX.test(t)).length;
404
+ textList.forEach((text) => {
391
405
  const uniqueKey = SIGNAL_BINDING_REGEX.test(text) ? text.replace(/\{\{signal:(.+)\}\}/, "$1") : void 0;
392
406
  const signal = uniqueKey !== void 0 ? renderState.signalMap.get(uniqueKey) : void 0;
393
407
  const newValue = signal !== void 0 ? signal() : text;
394
- const initialChildren = asNodeList(newValue, element);
395
- if (i === 0) {
396
- child.replaceWith(...initialChildren);
397
- } else {
398
- const endAnchor2 = queryComment(element, `${uniqueKey}:end`) ?? nextSibling;
399
- if (endAnchor2 !== null) {
400
- endAnchor2.before(...initialChildren);
401
- } else {
402
- element.append(...initialChildren);
403
- }
408
+ const autoKey = signal !== void 0 && totalSignals > 1 ? signalIndex++ : void 0;
409
+ const initialChildren = asNodeList(newValue, element, autoKey);
410
+ allInitialChildren.push(...initialChildren);
411
+ if (uniqueKey !== void 0 && signal !== void 0) {
412
+ signalEntries.push({ uniqueKey, signal, initialChildren, autoKey });
404
413
  }
405
- if (uniqueKey === void 0) return;
414
+ });
415
+ child.replaceWith(...allInitialChildren);
416
+ signalEntries.forEach(({ uniqueKey, signal, initialChildren, autoKey }) => {
417
+ const firstChild = initialChildren[0];
418
+ const lastChild = initialChildren[initialChildren.length - 1];
419
+ if (firstChild === void 0) return;
406
420
  const startAnchor = document.createComment(`${uniqueKey}:start`);
407
- if (prevSibling !== null) {
408
- prevSibling.after(startAnchor);
409
- } else {
410
- element.prepend(startAnchor);
411
- }
421
+ firstChild.before(startAnchor);
412
422
  const endAnchor = document.createComment(`${uniqueKey}:end`);
413
- if (nextSibling !== null) {
414
- nextSibling.before(endAnchor);
415
- } else {
416
- element.append(endAnchor);
417
- }
423
+ lastChild.after(endAnchor);
418
424
  const bindText = (node, signal2) => {
419
425
  createEffect(({ destroy }) => {
420
426
  const result = signal2();
421
427
  if (Array.isArray(result)) {
422
428
  destroy();
423
- bindArray(signal2);
429
+ bindArray(signal2, autoKey);
424
430
  return;
425
431
  }
426
432
  if (result instanceof DocumentFragment) {
427
433
  destroy();
428
- bindFragment(signal2);
434
+ bindFragment(signal2, initialChildren, autoKey);
429
435
  return;
430
436
  }
431
- node.data = result === null ? "" : String(result);
437
+ node.data = result === null || result === void 0 ? "" : String(result);
432
438
  });
433
439
  };
434
- const bindArray = (signal2) => {
440
+ const bindArray = (signal2, autoKey2) => {
435
441
  createEffect(
436
442
  ({ lastValue: oldChildren, destroy }) => {
437
443
  const result = signal2();
438
- const newChildren = asNodeList(result, element);
439
- const firstChild = newChildren[0];
440
- if (!Array.isArray(result) && newChildren.length === 1 && firstChild instanceof DocumentFragment) {
441
- destroy();
442
- bindFragment(signal2);
443
- return;
444
- }
445
- if (newChildren.length === 1 && firstChild instanceof Text) {
446
- destroy();
447
- bindText(firstChild, signal2);
448
- return;
444
+ const newChildren = asNodeList(result, element, autoKey2);
445
+ const firstChild2 = newChildren[0];
446
+ if (!Array.isArray(result)) {
447
+ if (newChildren.length === 1 && firstChild2 instanceof Text) {
448
+ while (startAnchor.nextSibling !== endAnchor) {
449
+ startAnchor.nextSibling?.remove();
450
+ }
451
+ startAnchor.after(firstChild2);
452
+ destroy();
453
+ bindText(firstChild2, signal2);
454
+ return;
455
+ }
449
456
  }
450
457
  while (startAnchor.nextSibling !== endAnchor) {
451
458
  startAnchor.nextSibling?.remove();
@@ -455,7 +462,6 @@ var evaluateBindings = (element, fragment) => {
455
462
  for (const persistedChild of oldChildren) {
456
463
  if (persistedChild instanceof Element) {
457
464
  const key = persistedChild.getAttribute("key");
458
- if (key === null) continue;
459
465
  const newChild = queryChildren(newChildren, `[key="${key}"]`);
460
466
  if (newChild === null) {
461
467
  persistedChild.remove();
@@ -477,40 +483,52 @@ var evaluateBindings = (element, fragment) => {
477
483
  null
478
484
  );
479
485
  };
480
- const bindFragment = (signal2) => {
486
+ const bindFragment = (signal2, initialChildren2, autoKey2) => {
481
487
  const initialFragment = signal2();
482
- renderState.childrenMap.set(initialFragment, Array.from(initialFragment.childNodes));
488
+ const firstInitialChild = initialChildren2[0];
489
+ if (firstInitialChild instanceof Element) {
490
+ renderState.childrenMap.set(initialFragment, initialChildren2);
491
+ }
483
492
  createEffect(({ destroy }) => {
484
493
  const result = signal2();
485
- const cachedChildren = renderState.childrenMap.get(initialFragment);
486
- const children = cachedChildren ?? asNodeList(result, element);
494
+ const cachedChildren = result instanceof DocumentFragment ? renderState.childrenMap.get(result) : void 0;
495
+ const children = cachedChildren ?? asNodeList(result, element, autoKey2);
496
+ if (result instanceof DocumentFragment && !renderState.childrenMap.has(result)) {
497
+ renderState.childrenMap.set(result, children);
498
+ }
487
499
  if (Array.isArray(result)) {
488
500
  destroy();
489
- bindArray(signal2);
501
+ bindArray(signal2, autoKey2);
490
502
  return;
491
503
  }
492
- if (result instanceof Text) {
493
- const children2 = asNodeList(result, element);
494
- const text2 = children2[0];
504
+ if (!(result instanceof DocumentFragment) && result !== null && result !== void 0) {
505
+ while (startAnchor.nextSibling !== endAnchor) {
506
+ startAnchor.nextSibling?.remove();
507
+ }
508
+ const children2 = asNodeList(result, element, autoKey2);
509
+ const text = children2[0];
510
+ startAnchor.after(text);
495
511
  destroy();
496
- bindText(text2, signal2);
512
+ bindText(text, signal2);
497
513
  return;
498
514
  }
499
515
  while (startAnchor.nextSibling !== endAnchor) {
500
516
  startAnchor.nextSibling?.remove();
501
517
  }
518
+ if (result === null || result === void 0) {
519
+ return;
520
+ }
502
521
  startAnchor.after(...children);
503
522
  });
504
523
  };
505
- if (signal !== void 0) {
506
- if (Array.isArray(newValue)) {
507
- bindArray(signal);
508
- } else if (initialChildren instanceof DocumentFragment) {
509
- bindFragment(signal);
510
- } else {
511
- const initialChild = initialChildren[0];
512
- bindText(initialChild, signal);
513
- }
524
+ const currentValue = signal();
525
+ if (Array.isArray(currentValue)) {
526
+ bindArray(signal, autoKey);
527
+ } else if (currentValue instanceof DocumentFragment) {
528
+ bindFragment(signal, initialChildren, autoKey);
529
+ } else {
530
+ const initialChild = initialChildren[0];
531
+ bindText(initialChild, signal);
514
532
  }
515
533
  });
516
534
  }
@@ -547,7 +565,6 @@ var evaluateBindings = (element, fragment) => {
547
565
  if (newText !== prevText) child.setAttribute(attrName, newText);
548
566
  }
549
567
  if (attrName.startsWith("prop-id:")) {
550
- if (child.hasAttribute(attrName)) child.removeAttribute(attrName);
551
568
  const propId = attrName.replace("prop-id:", "");
552
569
  const propName = renderState.propertyMap.get(propId);
553
570
  if (propName === void 0) {
@@ -582,9 +599,7 @@ var evaluateBindings = (element, fragment) => {
582
599
  child.__customCallbackFns.set(uniqueKey, callback);
583
600
  }
584
601
  }
585
- if (uniqueKey !== "" && !attrName.startsWith("prop-id:")) {
586
- child.setAttribute(attrName, `this.__customCallbackFns.get('${uniqueKey}')(event)`);
587
- } else if (attrName.startsWith("prop-id:")) {
602
+ if (attrName.startsWith("prop-id:")) {
588
603
  child.removeAttribute(attrName);
589
604
  const propId = attrName.replace("prop-id:", "");
590
605
  const propName = renderState.propertyMap.get(propId);
@@ -597,6 +612,8 @@ var evaluateBindings = (element, fragment) => {
597
612
  }
598
613
  if (!(propName in child)) logPropertyWarning(propName, child);
599
614
  child[propName] = child.__customCallbackFns.get(uniqueKey);
615
+ } else {
616
+ child.setAttribute(attrName, `this.__customCallbackFns.get('${uniqueKey}')(event)`);
600
617
  }
601
618
  });
602
619
  } else if (attrName.startsWith("prop-id:")) {
@@ -626,7 +643,7 @@ var html = (strings, ...values) => {
626
643
  } else {
627
644
  value = processValue(value);
628
645
  }
629
- innerHTML2 += str + String(value === null ? "" : value);
646
+ innerHTML2 += str + String(value);
630
647
  return innerHTML2;
631
648
  }, "");
632
649
  if (isServer) return innerHTML;
@@ -647,7 +664,7 @@ var html = (strings, ...values) => {
647
664
  evaluateBindings(fragment, fragment);
648
665
  return fragment;
649
666
  };
650
- var adoptedStylesSupported = typeof window !== "undefined" && window.ShadowRoot?.prototype.hasOwnProperty("adoptedStyleSheets") && window.CSSStyleSheet?.prototype.hasOwnProperty("replace");
667
+ var isAdoptedStylesSupported = () => typeof window !== "undefined" && window.ShadowRoot?.prototype.hasOwnProperty("adoptedStyleSheets") && window.CSSStyleSheet?.prototype.hasOwnProperty("replace");
651
668
  var isCSSStyleSheet = (stylesheet) => {
652
669
  return typeof CSSStyleSheet !== "undefined" && stylesheet instanceof CSSStyleSheet;
653
670
  };
@@ -671,7 +688,7 @@ var css = (strings, ...values) => {
671
688
  if (isServer) {
672
689
  return cssText;
673
690
  }
674
- const stylesheet = adoptedStylesSupported ? new CSSStyleSheet() : document.createElement("style");
691
+ const stylesheet = isAdoptedStylesSupported() ? new CSSStyleSheet() : document.createElement("style");
675
692
  const textList = cssText.split(signalBindingRegex);
676
693
  createEffect(() => {
677
694
  const newCSSTextList = [];
@@ -778,8 +795,9 @@ var customElement = (render, options) => {
778
795
  #observer = options?.observedAttributes !== void 0 ? null : new MutationObserver((mutations) => {
779
796
  for (const mutation of mutations) {
780
797
  const attrName = mutation.attributeName;
781
- if (mutation.type !== "attributes" || attrName === null) continue;
782
- if (!(attrName in this.#attrSignals)) this.#attrSignals[attrName] = createSignal(null);
798
+ if (!(attrName in this.#attrSignals)) {
799
+ this.#attrSignals[attrName] = createSignal(this.getAttribute(attrName));
800
+ }
783
801
  const [getter, setter] = this.#attrSignals[attrName];
784
802
  const oldValue = getter();
785
803
  const newValue = this.getAttribute(attrName);
@@ -879,7 +897,9 @@ You must set an initial value before calling a property signal's getter.
879
897
  {},
880
898
  {
881
899
  get: (_, prop) => {
882
- if (!(prop in this.#attrSignals)) this.#attrSignals[prop] = createSignal(null);
900
+ if (!(prop in this.#attrSignals)) {
901
+ this.#attrSignals[prop] = createSignal(this.getAttribute(prop));
902
+ }
883
903
  const [getter] = this.#attrSignals[prop];
884
904
  const setter = (newValue) => this.setAttribute(prop, newValue);
885
905
  return [getter, setter];
@@ -948,9 +968,7 @@ You must set an initial value before calling a property signal's getter.
948
968
  constructor() {
949
969
  try {
950
970
  super();
951
- if (!Object.prototype.hasOwnProperty.call(this, "__customCallbackFns")) {
952
- this.__customCallbackFns = /* @__PURE__ */ new Map();
953
- }
971
+ this.__customCallbackFns = /* @__PURE__ */ new Map();
954
972
  for (const attr of this.attributes) {
955
973
  this.#attrSignals[attr.name] = createSignal(attr.value);
956
974
  }
@@ -961,12 +979,13 @@ You must set an initial value before calling a property signal's getter.
961
979
  { cause: error }
962
980
  );
963
981
  console.error(_error);
964
- throw _error;
965
982
  }
966
983
  }
967
984
  connectedCallback() {
968
985
  for (const [attrName, attr] of this.#attributesAsPropertiesMap) {
969
- if (!(attrName in this.#attrSignals)) this.#attrSignals[attrName] = createSignal(null);
986
+ if (!(attrName in this.#attrSignals)) {
987
+ this.#attrSignals[attrName] = createSignal(this.getAttribute(attrName));
988
+ }
970
989
  const propName = attr.prop;
971
990
  const [getter] = this.#getPropSignal(propName, { allowUndefined: true });
972
991
  let busy = false;
@@ -983,6 +1002,14 @@ You must set an initial value before calling a property signal's getter.
983
1002
  busy = false;
984
1003
  });
985
1004
  }
1005
+ for (const attrName of Object.keys(this.#attrSignals)) {
1006
+ const signal = this.#attrSignals[attrName];
1007
+ const [getter, setter] = signal;
1008
+ const currentValue = this.getAttribute(attrName);
1009
+ if (getter() !== currentValue) {
1010
+ setter(currentValue);
1011
+ }
1012
+ }
986
1013
  if (this.#observer !== null) {
987
1014
  this.#observer.observe(this, { attributes: true });
988
1015
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thunderous",
3
- "version": "2.4.4",
3
+ "version": "2.4.5-next.1776738548",
4
4
  "description": "A lightweight, functional web components library that brings the power of signals to your UI.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -15,7 +15,8 @@
15
15
  "author": "Jonathan DeWitt <jon.dewitt@thunder.solutions>",
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "https://github.com/Thunder-Solutions/Thunderous"
18
+ "url": "https://github.com/Thunder-Solutions/Thunderous",
19
+ "directory": "packages/thunderous"
19
20
  },
20
21
  "keywords": [
21
22
  "thunderous",
@@ -27,7 +28,7 @@
27
28
  "bugs": {
28
29
  "url": "https://github.com/Thunder-Solutions/Thunderous/issues"
29
30
  },
30
- "homepage": "https://github.com/Thunder-Solutions/Thunderous#readme",
31
+ "homepage": "https://thunderous.dev",
31
32
  "license": "MIT",
32
33
  "peerDependencies": {
33
34
  "@webcomponents/scoped-custom-element-registry": "^0.0.10"
@@ -37,19 +38,29 @@
37
38
  "optional": true
38
39
  }
39
40
  },
41
+ "devDependencies": {
42
+ "@playwright/test": "^1.58.2",
43
+ "@vitest/browser": "^4.1.4",
44
+ "@vitest/browser-playwright": "^4.1.4",
45
+ "@vitest/coverage-istanbul": "^4.1.4",
46
+ "istanbul-merge": "^2.0.0",
47
+ "nyc": "^18.0.0",
48
+ "vitest": "^4.1.4"
49
+ },
40
50
  "scripts": {
41
51
  "demo": "cd demo && npm start",
42
- "demo:ssr": "cd demo && npm run ssr",
52
+ "demo:ssr": "cd demo && pnpm ssr",
43
53
  "build": "tsup src/index.ts --format cjs,esm --dts --no-clean",
44
- "test": "npm run test:server && npm run test:client",
45
- "test:server": "find src/__test__/server -name '*.test.ts' | xargs c8 tsx --test",
46
- "test:client": "PLAYWRIGHT_BROWSERS_PATH=../../.browsers playwright test",
54
+ "clean": "rm -rf dist",
55
+ "test": "pnpm test:server & pnpm test:client && pnpm coverage",
56
+ "test:server": "vitest run --config vitest.server.config.ts --coverage",
57
+ "test:client": "vitest run --config vitest.client.config.ts --coverage",
58
+ "coverage": "istanbul-merge --out ./coverage/merged/coverage-final.json ./coverage/server/coverage-final.json ./coverage/client/coverage-final.json && nyc report && nyc check-coverage",
47
59
  "typecheck": "tsc --noEmit",
48
60
  "lint": "eslint .",
49
61
  "lint:fix": "eslint . --fix",
50
62
  "format": "prettier --check . --ignore-path ../../.gitignore",
51
63
  "format:fix": "prettier --write . --ignore-path ../../.gitignore",
52
- "preversion": "npm run typecheck && npm run lint && npm test && npm run build",
53
64
  "version": "node postversion.js"
54
65
  }
55
66
  }