thunderous 2.4.3 → 2.4.5

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
@@ -20,6 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ clearRenderState: () => clearRenderState,
24
+ clearServerCss: () => clearServerCss,
23
25
  clientOnlyCallback: () => clientOnlyCallback,
24
26
  createEffect: () => createEffect,
25
27
  createRegistry: () => createRegistry,
@@ -163,12 +165,6 @@ var createEffect = (fn, value) => {
163
165
 
164
166
  // src/utilities.ts
165
167
  var NOOP = () => void 0;
166
- var queryComment = (node, comment) => {
167
- const walker = document.createTreeWalker(node, NodeFilter.SHOW_COMMENT, {
168
- acceptNode: (n) => n.nodeValue === comment ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
169
- });
170
- return walker.nextNode();
171
- };
172
168
  var queryChildren = (children, selector) => {
173
169
  for (const child of children) {
174
170
  if (child instanceof Element && child.matches(selector)) {
@@ -184,6 +180,9 @@ var serverDefineFns = /* @__PURE__ */ new Set();
184
180
  var onServerDefine = (fn) => {
185
181
  serverDefineFns.add(fn);
186
182
  };
183
+ var clearServerCss = () => {
184
+ serverCss.clear();
185
+ };
187
186
  var serverDefine = ({
188
187
  tagName,
189
188
  serverRender,
@@ -321,7 +320,7 @@ var clientOnlyCallback = (fn) => {
321
320
  // src/render.ts
322
321
  var CALLBACK_BINDING_REGEX = /(\{\{callback:.+\}\})/;
323
322
  var LEGACY_CALLBACK_BINDING_REGEX = /(this.getRootNode\(\).host.__customCallbackFns.get\('.+'\)\(event\))/;
324
- var SIGNAL_BINDING_REGEX = /(\{\{signal:.+\}\})/;
323
+ var SIGNAL_BINDING_REGEX = /(\{\{signal:.+?\}\})/;
325
324
  var FRAGMENT_ATTRIBUTE = "___thunderous-fragment";
326
325
  var renderState = {
327
326
  currentShadowRoot: null,
@@ -332,6 +331,13 @@ var renderState = {
332
331
  propertyMap: /* @__PURE__ */ new Map(),
333
332
  registry: typeof customElements !== "undefined" ? customElements : {}
334
333
  };
334
+ var clearRenderState = () => {
335
+ renderState.signalMap.clear();
336
+ renderState.callbackMap.clear();
337
+ renderState.propertyMap.clear();
338
+ renderState.fragmentMap.clear();
339
+ renderState.childrenMap.clear();
340
+ };
335
341
  var logPropertyWarning = (propName, element) => {
336
342
  console.warn(
337
343
  `Property "${propName}" does not exist on element:`,
@@ -339,19 +345,34 @@ var logPropertyWarning = (propName, element) => {
339
345
  "\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."
340
346
  );
341
347
  };
342
- var asNodeList = (value, parent) => {
348
+ var asNodeList = (value, parent, autoKey) => {
349
+ if (value === null || value === void 0) return [];
343
350
  if (typeof value === "string") return [new Text(value)];
344
- if (value instanceof DocumentFragment) return [...value.children];
351
+ if (typeof value === "number" || typeof value === "boolean") return [new Text(String(value))];
352
+ if (value instanceof DocumentFragment) {
353
+ const children = Array.from(value.children);
354
+ if (autoKey !== void 0 && children.length > 0) {
355
+ const child = children[0];
356
+ if (child instanceof Element && child.getAttribute("key") === null) {
357
+ child.setAttribute("key", String(autoKey));
358
+ }
359
+ }
360
+ return children;
361
+ }
345
362
  if (Array.isArray(value)) {
346
363
  const nodeList = [];
347
364
  let count = 0;
348
365
  const keys = /* @__PURE__ */ new Set();
349
366
  for (const item of value) {
350
367
  const cachedItem = item instanceof DocumentFragment ? renderState.childrenMap.get(item) : void 0;
351
- const children = cachedItem ?? asNodeList(item, parent);
368
+ const children = cachedItem ?? asNodeList(item, parent, item instanceof DocumentFragment ? count : void 0);
352
369
  if (cachedItem === void 0 && item instanceof DocumentFragment) {
353
370
  renderState.childrenMap.set(item, children);
354
371
  }
372
+ if (!(item instanceof DocumentFragment)) {
373
+ nodeList.push(...children);
374
+ continue;
375
+ }
355
376
  if (children.length > 1) {
356
377
  console.error(
357
378
  "When rendering arrays, fragments must contain only one top-level element at a time. Error occured in:",
@@ -404,73 +425,77 @@ var processValue = (value) => {
404
425
  renderState.callbackMap.set(uniqueKey, value);
405
426
  return isServer ? String(value()) : `{{callback:${uniqueKey}}}`;
406
427
  }
407
- return String(value);
428
+ return value === null || value === void 0 ? "" : String(value);
408
429
  };
409
430
  var evaluateBindings = (element, fragment) => {
410
- for (const child of [...element.childNodes]) {
431
+ for (const child of Array.from(element.childNodes)) {
411
432
  if (child instanceof Text && SIGNAL_BINDING_REGEX.test(child.data)) {
412
433
  const textList = child.data.split(SIGNAL_BINDING_REGEX);
413
- const nextSibling = child.nextSibling;
414
- const prevSibling = child.previousSibling;
415
- textList.forEach((text, i) => {
434
+ const allInitialChildren = [];
435
+ const signalEntries = [];
436
+ let signalIndex = 0;
437
+ const totalSignals = textList.filter((t) => SIGNAL_BINDING_REGEX.test(t)).length;
438
+ textList.forEach((text) => {
416
439
  const uniqueKey = SIGNAL_BINDING_REGEX.test(text) ? text.replace(/\{\{signal:(.+)\}\}/, "$1") : void 0;
417
440
  const signal = uniqueKey !== void 0 ? renderState.signalMap.get(uniqueKey) : void 0;
418
441
  const newValue = signal !== void 0 ? signal() : text;
419
- const initialChildren = asNodeList(newValue, element);
420
- if (i === 0) {
421
- child.replaceWith(...initialChildren);
422
- } else {
423
- const endAnchor2 = queryComment(element, `${uniqueKey}:end`) ?? nextSibling;
424
- if (endAnchor2 !== null) {
425
- endAnchor2.before(...initialChildren);
426
- } else {
427
- element.append(...initialChildren);
428
- }
442
+ const autoKey = signal !== void 0 && totalSignals > 1 ? signalIndex++ : void 0;
443
+ const initialChildren = asNodeList(newValue, element, autoKey);
444
+ allInitialChildren.push(...initialChildren);
445
+ if (uniqueKey !== void 0 && signal !== void 0) {
446
+ signalEntries.push({ uniqueKey, signal, initialChildren, autoKey });
429
447
  }
430
- if (uniqueKey === void 0) return;
448
+ });
449
+ child.replaceWith(...allInitialChildren);
450
+ signalEntries.forEach(({ uniqueKey, signal, initialChildren, autoKey }) => {
451
+ const firstChild = initialChildren[0];
452
+ const lastChild = initialChildren[initialChildren.length - 1];
453
+ if (uniqueKey === void 0 || firstChild === void 0) return;
431
454
  const startAnchor = document.createComment(`${uniqueKey}:start`);
432
- if (prevSibling !== null) {
433
- prevSibling.after(startAnchor);
434
- } else {
435
- element.prepend(startAnchor);
436
- }
455
+ firstChild.before(startAnchor);
437
456
  const endAnchor = document.createComment(`${uniqueKey}:end`);
438
- if (nextSibling !== null) {
439
- nextSibling.before(endAnchor);
457
+ if (lastChild !== void 0) {
458
+ lastChild.after(endAnchor);
440
459
  } else {
441
- element.append(endAnchor);
460
+ startAnchor.after(endAnchor);
442
461
  }
443
462
  const bindText = (node, signal2) => {
444
463
  createEffect(({ destroy }) => {
445
464
  const result = signal2();
446
465
  if (Array.isArray(result)) {
447
466
  destroy();
448
- bindArray(signal2);
467
+ bindArray(signal2, autoKey);
449
468
  return;
450
469
  }
451
470
  if (result instanceof DocumentFragment) {
452
471
  destroy();
453
- bindFragment(signal2);
472
+ bindFragment(signal2, initialChildren, autoKey);
454
473
  return;
455
474
  }
456
- node.data = result === null ? "" : String(result);
475
+ node.data = result === null || result === void 0 ? "" : String(result);
457
476
  });
458
477
  };
459
- const bindArray = (signal2) => {
478
+ const bindArray = (signal2, autoKey2) => {
460
479
  createEffect(
461
480
  ({ lastValue: oldChildren, destroy }) => {
462
481
  const result = signal2();
463
- const newChildren = asNodeList(result, element);
464
- const firstChild = newChildren[0];
465
- if (!Array.isArray(result) && newChildren.length === 1 && firstChild instanceof DocumentFragment) {
466
- destroy();
467
- bindFragment(signal2);
468
- return;
469
- }
470
- if (newChildren.length === 1 && firstChild instanceof Text) {
471
- destroy();
472
- bindText(firstChild, signal2);
473
- return;
482
+ const newChildren = asNodeList(result, element, autoKey2);
483
+ const firstChild2 = newChildren[0];
484
+ if (!Array.isArray(result)) {
485
+ if (newChildren.length === 1 && firstChild2 instanceof DocumentFragment) {
486
+ destroy();
487
+ bindFragment(signal2, initialChildren, autoKey2);
488
+ return;
489
+ }
490
+ if (newChildren.length === 1 && firstChild2 instanceof Text) {
491
+ while (startAnchor.nextSibling !== endAnchor) {
492
+ startAnchor.nextSibling?.remove();
493
+ }
494
+ startAnchor.after(firstChild2);
495
+ destroy();
496
+ bindText(firstChild2, signal2);
497
+ return;
498
+ }
474
499
  }
475
500
  while (startAnchor.nextSibling !== endAnchor) {
476
501
  startAnchor.nextSibling?.remove();
@@ -486,10 +511,10 @@ var evaluateBindings = (element, fragment) => {
486
511
  persistedChild.remove();
487
512
  continue;
488
513
  }
489
- for (const attr of [...persistedChild.attributes]) {
514
+ for (const attr of Array.from(persistedChild.attributes)) {
490
515
  if (!newChild.hasAttribute(attr.name)) persistedChild.removeAttribute(attr.name);
491
516
  }
492
- for (const newAttr of [...newChild.attributes]) {
517
+ for (const newAttr of Array.from(newChild.attributes)) {
493
518
  const oldAttrValue = persistedChild.getAttribute(newAttr.name);
494
519
  if (oldAttrValue?.startsWith("this.__customCallbackFns")) continue;
495
520
  persistedChild.setAttribute(newAttr.name, newAttr.value);
@@ -502,36 +527,50 @@ var evaluateBindings = (element, fragment) => {
502
527
  null
503
528
  );
504
529
  };
505
- const bindFragment = (signal2) => {
530
+ const bindFragment = (signal2, initialChildren2, autoKey2) => {
506
531
  const initialFragment = signal2();
507
- renderState.childrenMap.set(initialFragment, [...initialFragment.childNodes]);
532
+ const firstInitialChild = initialChildren2[0];
533
+ if (firstInitialChild instanceof Element) {
534
+ renderState.childrenMap.set(initialFragment, initialChildren2);
535
+ }
508
536
  createEffect(({ destroy }) => {
509
537
  const result = signal2();
510
- const cachedChildren = renderState.childrenMap.get(initialFragment);
511
- const children = cachedChildren ?? asNodeList(result, element);
538
+ const cachedChildren = result instanceof DocumentFragment ? renderState.childrenMap.get(result) : void 0;
539
+ const children = cachedChildren ?? asNodeList(result, element, autoKey2);
540
+ if (result instanceof DocumentFragment && !renderState.childrenMap.has(result)) {
541
+ renderState.childrenMap.set(result, children);
542
+ }
512
543
  if (Array.isArray(result)) {
513
544
  destroy();
514
- bindArray(signal2);
545
+ bindArray(signal2, autoKey2);
515
546
  return;
516
547
  }
517
- if (result instanceof Text) {
518
- const children2 = asNodeList(result, element);
519
- const text2 = children2[0];
548
+ if (!(result instanceof DocumentFragment) && result !== null && result !== void 0) {
549
+ while (startAnchor.nextSibling !== endAnchor) {
550
+ startAnchor.nextSibling?.remove();
551
+ }
552
+ const children2 = asNodeList(result, element, autoKey2);
553
+ const text = children2[0];
554
+ startAnchor.after(text);
520
555
  destroy();
521
- bindText(text2, signal2);
556
+ bindText(text, signal2);
522
557
  return;
523
558
  }
524
559
  while (startAnchor.nextSibling !== endAnchor) {
525
560
  startAnchor.nextSibling?.remove();
526
561
  }
562
+ if (result === null || result === void 0) {
563
+ return;
564
+ }
527
565
  startAnchor.after(...children);
528
566
  });
529
567
  };
530
568
  if (signal !== void 0) {
531
- if (Array.isArray(newValue)) {
532
- bindArray(signal);
533
- } else if (initialChildren instanceof DocumentFragment) {
534
- bindFragment(signal);
569
+ const currentValue = signal();
570
+ if (Array.isArray(currentValue)) {
571
+ bindArray(signal, autoKey);
572
+ } else if (currentValue instanceof DocumentFragment) {
573
+ bindFragment(signal, initialChildren, autoKey);
535
574
  } else {
536
575
  const initialChild = initialChildren[0];
537
576
  bindText(initialChild, signal);
@@ -546,7 +585,7 @@ var evaluateBindings = (element, fragment) => {
546
585
  child.replaceWith(childFragment);
547
586
  }
548
587
  } else if (child instanceof Element) {
549
- for (const attr of [...child.attributes]) {
588
+ for (const attr of Array.from(child.attributes)) {
550
589
  const attrName = attr.name;
551
590
  if (SIGNAL_BINDING_REGEX.test(attr.value)) {
552
591
  const textList = attr.value.split(SIGNAL_BINDING_REGEX);
@@ -804,7 +843,9 @@ var customElement = (render, options) => {
804
843
  for (const mutation of mutations) {
805
844
  const attrName = mutation.attributeName;
806
845
  if (mutation.type !== "attributes" || attrName === null) continue;
807
- if (!(attrName in this.#attrSignals)) this.#attrSignals[attrName] = createSignal(null);
846
+ if (!(attrName in this.#attrSignals)) {
847
+ this.#attrSignals[attrName] = createSignal(this.getAttribute(attrName));
848
+ }
808
849
  const [getter, setter] = this.#attrSignals[attrName];
809
850
  const oldValue = getter();
810
851
  const newValue = this.getAttribute(attrName);
@@ -904,7 +945,9 @@ You must set an initial value before calling a property signal's getter.
904
945
  {},
905
946
  {
906
947
  get: (_, prop) => {
907
- if (!(prop in this.#attrSignals)) this.#attrSignals[prop] = createSignal(null);
948
+ if (!(prop in this.#attrSignals)) {
949
+ this.#attrSignals[prop] = createSignal(this.getAttribute(prop));
950
+ }
908
951
  const [getter] = this.#attrSignals[prop];
909
952
  const setter = (newValue) => this.setAttribute(prop, newValue);
910
953
  return [getter, setter];
@@ -991,7 +1034,9 @@ You must set an initial value before calling a property signal's getter.
991
1034
  }
992
1035
  connectedCallback() {
993
1036
  for (const [attrName, attr] of this.#attributesAsPropertiesMap) {
994
- if (!(attrName in this.#attrSignals)) this.#attrSignals[attrName] = createSignal(null);
1037
+ if (!(attrName in this.#attrSignals)) {
1038
+ this.#attrSignals[attrName] = createSignal(this.getAttribute(attrName));
1039
+ }
995
1040
  const propName = attr.prop;
996
1041
  const [getter] = this.#getPropSignal(propName, { allowUndefined: true });
997
1042
  let busy = false;
@@ -1008,6 +1053,16 @@ You must set an initial value before calling a property signal's getter.
1008
1053
  busy = false;
1009
1054
  });
1010
1055
  }
1056
+ for (const attrName of Object.keys(this.#attrSignals)) {
1057
+ const signal = this.#attrSignals[attrName];
1058
+ if (signal) {
1059
+ const [getter, setter] = signal;
1060
+ const currentValue = this.getAttribute(attrName);
1061
+ if (getter() !== currentValue) {
1062
+ setter(currentValue);
1063
+ }
1064
+ }
1065
+ }
1011
1066
  if (this.#observer !== null) {
1012
1067
  this.#observer.observe(this, { attributes: true });
1013
1068
  }
@@ -1176,6 +1231,8 @@ var createRegistry = (args) => {
1176
1231
  };
1177
1232
  // Annotate the CommonJS export names for ESM import in node:
1178
1233
  0 && (module.exports = {
1234
+ clearRenderState,
1235
+ clearServerCss,
1179
1236
  clientOnlyCallback,
1180
1237
  createEffect,
1181
1238
  createRegistry,
package/dist/index.d.cts CHANGED
@@ -143,7 +143,41 @@ declare const customElement: <Props extends CustomElementProps>(render: RenderFu
143
143
  */
144
144
  declare const createRegistry: (args?: RegistryArgs) => RegistryResult;
145
145
 
146
+ /**
147
+ * Add a callback to handle each call to `define()` on the server.
148
+ *
149
+ * This enables you to intercept those definitions and respond to them,
150
+ * for example to inject declarative shadow DOM templates.
151
+ *
152
+ * @example
153
+ * ```ts
154
+ * let response = originalResponse;
155
+ * onServerDefine((tagName, htmlString) => {
156
+ * // ...
157
+ * response = htmlString.replace(tagName, `my-${tagName}`);
158
+ * });
159
+ * ```
160
+ */
146
161
  declare const onServerDefine: (fn: ServerDefineFn) => void;
162
+ /**
163
+ * Thunderous tracks its state using several maps to associate values with
164
+ * their respective elements.
165
+ *
166
+ * This function clears the map that tracks CSS on the server side, to prevent
167
+ * memory leaks and purge stale data from previous renders.
168
+ *
169
+ * If you are building a framework or plugin that depends on Thunderous, you
170
+ * should call this function before every render. Otherwise, the map will
171
+ * accumulate stale data and may create significant performance issues.
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * import { clearServerCss } from 'thunderous'
176
+ *
177
+ * clearServerCss();
178
+ * ```
179
+ */
180
+ declare const clearServerCss: () => void;
147
181
  declare const insertTemplates: (tagName: string, template: string, inputString: string) => string;
148
182
  declare const clientOnlyCallback: (fn: (() => void) | (() => Promise<void>)) => void | Promise<void>;
149
183
 
@@ -178,10 +212,29 @@ declare const derived: <T>(fn: () => T, options?: SignalOptions) => SignalGetter
178
212
  */
179
213
  declare const createEffect: <T = unknown>(fn: Effect<T>, value?: T) => void;
180
214
 
215
+ /**
216
+ * Thunderous tracks its state using several maps to associate values with
217
+ * their respective elements.
218
+ *
219
+ * This function clears the maps tracking render state, to prevent memory
220
+ * leaks and purge stale data from previous renders.
221
+ *
222
+ * If you are building a framework or plugin that depends on Thunderous, you
223
+ * should call this function before every render. Otherwise, the maps will
224
+ * accumulate stale data and may create significant performance issues.
225
+ *
226
+ * @example
227
+ * ```ts
228
+ * import { clearRenderState } from 'thunderous'
229
+ *
230
+ * clearRenderState();
231
+ * ```
232
+ */
233
+ declare const clearRenderState: () => void;
181
234
  /**
182
235
  * A tagged template function for creating DocumentFragment instances.
183
236
  */
184
237
  declare const html: (strings: TemplateStringsArray, ...values: unknown[]) => DocumentFragment;
185
238
  declare const css: (strings: TemplateStringsArray, ...values: unknown[]) => Styles;
186
239
 
187
- export { type ElementResult, type HTMLCustomElement, type RegistryResult, type RenderArgs, type RenderFunction, type Signal, type SignalGetter, type SignalSetter, clientOnlyCallback, createEffect, createRegistry, createSignal, css, customElement, derived, html, insertTemplates, onServerDefine };
240
+ export { type ElementResult, type HTMLCustomElement, type RegistryResult, type RenderArgs, type RenderFunction, type Signal, type SignalGetter, type SignalSetter, clearRenderState, clearServerCss, clientOnlyCallback, createEffect, createRegistry, createSignal, css, customElement, derived, html, insertTemplates, onServerDefine };
package/dist/index.d.ts CHANGED
@@ -143,7 +143,41 @@ declare const customElement: <Props extends CustomElementProps>(render: RenderFu
143
143
  */
144
144
  declare const createRegistry: (args?: RegistryArgs) => RegistryResult;
145
145
 
146
+ /**
147
+ * Add a callback to handle each call to `define()` on the server.
148
+ *
149
+ * This enables you to intercept those definitions and respond to them,
150
+ * for example to inject declarative shadow DOM templates.
151
+ *
152
+ * @example
153
+ * ```ts
154
+ * let response = originalResponse;
155
+ * onServerDefine((tagName, htmlString) => {
156
+ * // ...
157
+ * response = htmlString.replace(tagName, `my-${tagName}`);
158
+ * });
159
+ * ```
160
+ */
146
161
  declare const onServerDefine: (fn: ServerDefineFn) => void;
162
+ /**
163
+ * Thunderous tracks its state using several maps to associate values with
164
+ * their respective elements.
165
+ *
166
+ * This function clears the map that tracks CSS on the server side, to prevent
167
+ * memory leaks and purge stale data from previous renders.
168
+ *
169
+ * If you are building a framework or plugin that depends on Thunderous, you
170
+ * should call this function before every render. Otherwise, the map will
171
+ * accumulate stale data and may create significant performance issues.
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * import { clearServerCss } from 'thunderous'
176
+ *
177
+ * clearServerCss();
178
+ * ```
179
+ */
180
+ declare const clearServerCss: () => void;
147
181
  declare const insertTemplates: (tagName: string, template: string, inputString: string) => string;
148
182
  declare const clientOnlyCallback: (fn: (() => void) | (() => Promise<void>)) => void | Promise<void>;
149
183
 
@@ -178,10 +212,29 @@ declare const derived: <T>(fn: () => T, options?: SignalOptions) => SignalGetter
178
212
  */
179
213
  declare const createEffect: <T = unknown>(fn: Effect<T>, value?: T) => void;
180
214
 
215
+ /**
216
+ * Thunderous tracks its state using several maps to associate values with
217
+ * their respective elements.
218
+ *
219
+ * This function clears the maps tracking render state, to prevent memory
220
+ * leaks and purge stale data from previous renders.
221
+ *
222
+ * If you are building a framework or plugin that depends on Thunderous, you
223
+ * should call this function before every render. Otherwise, the maps will
224
+ * accumulate stale data and may create significant performance issues.
225
+ *
226
+ * @example
227
+ * ```ts
228
+ * import { clearRenderState } from 'thunderous'
229
+ *
230
+ * clearRenderState();
231
+ * ```
232
+ */
233
+ declare const clearRenderState: () => void;
181
234
  /**
182
235
  * A tagged template function for creating DocumentFragment instances.
183
236
  */
184
237
  declare const html: (strings: TemplateStringsArray, ...values: unknown[]) => DocumentFragment;
185
238
  declare const css: (strings: TemplateStringsArray, ...values: unknown[]) => Styles;
186
239
 
187
- export { type ElementResult, type HTMLCustomElement, type RegistryResult, type RenderArgs, type RenderFunction, type Signal, type SignalGetter, type SignalSetter, clientOnlyCallback, createEffect, createRegistry, createSignal, css, customElement, derived, html, insertTemplates, onServerDefine };
240
+ export { type ElementResult, type HTMLCustomElement, type RegistryResult, type RenderArgs, type RenderFunction, type Signal, type SignalGetter, type SignalSetter, clearRenderState, clearServerCss, clientOnlyCallback, createEffect, createRegistry, createSignal, css, customElement, derived, html, insertTemplates, onServerDefine };
package/dist/index.js CHANGED
@@ -128,12 +128,6 @@ var createEffect = (fn, value) => {
128
128
 
129
129
  // src/utilities.ts
130
130
  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
131
  var queryChildren = (children, selector) => {
138
132
  for (const child of children) {
139
133
  if (child instanceof Element && child.matches(selector)) {
@@ -149,6 +143,9 @@ var serverDefineFns = /* @__PURE__ */ new Set();
149
143
  var onServerDefine = (fn) => {
150
144
  serverDefineFns.add(fn);
151
145
  };
146
+ var clearServerCss = () => {
147
+ serverCss.clear();
148
+ };
152
149
  var serverDefine = ({
153
150
  tagName,
154
151
  serverRender,
@@ -286,7 +283,7 @@ var clientOnlyCallback = (fn) => {
286
283
  // src/render.ts
287
284
  var CALLBACK_BINDING_REGEX = /(\{\{callback:.+\}\})/;
288
285
  var LEGACY_CALLBACK_BINDING_REGEX = /(this.getRootNode\(\).host.__customCallbackFns.get\('.+'\)\(event\))/;
289
- var SIGNAL_BINDING_REGEX = /(\{\{signal:.+\}\})/;
286
+ var SIGNAL_BINDING_REGEX = /(\{\{signal:.+?\}\})/;
290
287
  var FRAGMENT_ATTRIBUTE = "___thunderous-fragment";
291
288
  var renderState = {
292
289
  currentShadowRoot: null,
@@ -297,6 +294,13 @@ var renderState = {
297
294
  propertyMap: /* @__PURE__ */ new Map(),
298
295
  registry: typeof customElements !== "undefined" ? customElements : {}
299
296
  };
297
+ var clearRenderState = () => {
298
+ renderState.signalMap.clear();
299
+ renderState.callbackMap.clear();
300
+ renderState.propertyMap.clear();
301
+ renderState.fragmentMap.clear();
302
+ renderState.childrenMap.clear();
303
+ };
300
304
  var logPropertyWarning = (propName, element) => {
301
305
  console.warn(
302
306
  `Property "${propName}" does not exist on element:`,
@@ -304,19 +308,34 @@ var logPropertyWarning = (propName, element) => {
304
308
  "\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."
305
309
  );
306
310
  };
307
- var asNodeList = (value, parent) => {
311
+ var asNodeList = (value, parent, autoKey) => {
312
+ if (value === null || value === void 0) return [];
308
313
  if (typeof value === "string") return [new Text(value)];
309
- if (value instanceof DocumentFragment) return [...value.children];
314
+ if (typeof value === "number" || typeof value === "boolean") return [new Text(String(value))];
315
+ if (value instanceof DocumentFragment) {
316
+ const children = Array.from(value.children);
317
+ if (autoKey !== void 0 && children.length > 0) {
318
+ const child = children[0];
319
+ if (child instanceof Element && child.getAttribute("key") === null) {
320
+ child.setAttribute("key", String(autoKey));
321
+ }
322
+ }
323
+ return children;
324
+ }
310
325
  if (Array.isArray(value)) {
311
326
  const nodeList = [];
312
327
  let count = 0;
313
328
  const keys = /* @__PURE__ */ new Set();
314
329
  for (const item of value) {
315
330
  const cachedItem = item instanceof DocumentFragment ? renderState.childrenMap.get(item) : void 0;
316
- const children = cachedItem ?? asNodeList(item, parent);
331
+ const children = cachedItem ?? asNodeList(item, parent, item instanceof DocumentFragment ? count : void 0);
317
332
  if (cachedItem === void 0 && item instanceof DocumentFragment) {
318
333
  renderState.childrenMap.set(item, children);
319
334
  }
335
+ if (!(item instanceof DocumentFragment)) {
336
+ nodeList.push(...children);
337
+ continue;
338
+ }
320
339
  if (children.length > 1) {
321
340
  console.error(
322
341
  "When rendering arrays, fragments must contain only one top-level element at a time. Error occured in:",
@@ -369,73 +388,77 @@ var processValue = (value) => {
369
388
  renderState.callbackMap.set(uniqueKey, value);
370
389
  return isServer ? String(value()) : `{{callback:${uniqueKey}}}`;
371
390
  }
372
- return String(value);
391
+ return value === null || value === void 0 ? "" : String(value);
373
392
  };
374
393
  var evaluateBindings = (element, fragment) => {
375
- for (const child of [...element.childNodes]) {
394
+ for (const child of Array.from(element.childNodes)) {
376
395
  if (child instanceof Text && SIGNAL_BINDING_REGEX.test(child.data)) {
377
396
  const textList = child.data.split(SIGNAL_BINDING_REGEX);
378
- const nextSibling = child.nextSibling;
379
- const prevSibling = child.previousSibling;
380
- textList.forEach((text, i) => {
397
+ const allInitialChildren = [];
398
+ const signalEntries = [];
399
+ let signalIndex = 0;
400
+ const totalSignals = textList.filter((t) => SIGNAL_BINDING_REGEX.test(t)).length;
401
+ textList.forEach((text) => {
381
402
  const uniqueKey = SIGNAL_BINDING_REGEX.test(text) ? text.replace(/\{\{signal:(.+)\}\}/, "$1") : void 0;
382
403
  const signal = uniqueKey !== void 0 ? renderState.signalMap.get(uniqueKey) : void 0;
383
404
  const newValue = signal !== void 0 ? signal() : text;
384
- const initialChildren = asNodeList(newValue, element);
385
- if (i === 0) {
386
- child.replaceWith(...initialChildren);
387
- } else {
388
- const endAnchor2 = queryComment(element, `${uniqueKey}:end`) ?? nextSibling;
389
- if (endAnchor2 !== null) {
390
- endAnchor2.before(...initialChildren);
391
- } else {
392
- element.append(...initialChildren);
393
- }
405
+ const autoKey = signal !== void 0 && totalSignals > 1 ? signalIndex++ : void 0;
406
+ const initialChildren = asNodeList(newValue, element, autoKey);
407
+ allInitialChildren.push(...initialChildren);
408
+ if (uniqueKey !== void 0 && signal !== void 0) {
409
+ signalEntries.push({ uniqueKey, signal, initialChildren, autoKey });
394
410
  }
395
- if (uniqueKey === void 0) return;
411
+ });
412
+ child.replaceWith(...allInitialChildren);
413
+ signalEntries.forEach(({ uniqueKey, signal, initialChildren, autoKey }) => {
414
+ const firstChild = initialChildren[0];
415
+ const lastChild = initialChildren[initialChildren.length - 1];
416
+ if (uniqueKey === void 0 || firstChild === void 0) return;
396
417
  const startAnchor = document.createComment(`${uniqueKey}:start`);
397
- if (prevSibling !== null) {
398
- prevSibling.after(startAnchor);
399
- } else {
400
- element.prepend(startAnchor);
401
- }
418
+ firstChild.before(startAnchor);
402
419
  const endAnchor = document.createComment(`${uniqueKey}:end`);
403
- if (nextSibling !== null) {
404
- nextSibling.before(endAnchor);
420
+ if (lastChild !== void 0) {
421
+ lastChild.after(endAnchor);
405
422
  } else {
406
- element.append(endAnchor);
423
+ startAnchor.after(endAnchor);
407
424
  }
408
425
  const bindText = (node, signal2) => {
409
426
  createEffect(({ destroy }) => {
410
427
  const result = signal2();
411
428
  if (Array.isArray(result)) {
412
429
  destroy();
413
- bindArray(signal2);
430
+ bindArray(signal2, autoKey);
414
431
  return;
415
432
  }
416
433
  if (result instanceof DocumentFragment) {
417
434
  destroy();
418
- bindFragment(signal2);
435
+ bindFragment(signal2, initialChildren, autoKey);
419
436
  return;
420
437
  }
421
- node.data = result === null ? "" : String(result);
438
+ node.data = result === null || result === void 0 ? "" : String(result);
422
439
  });
423
440
  };
424
- const bindArray = (signal2) => {
441
+ const bindArray = (signal2, autoKey2) => {
425
442
  createEffect(
426
443
  ({ lastValue: oldChildren, destroy }) => {
427
444
  const result = signal2();
428
- const newChildren = asNodeList(result, element);
429
- const firstChild = newChildren[0];
430
- if (!Array.isArray(result) && newChildren.length === 1 && firstChild instanceof DocumentFragment) {
431
- destroy();
432
- bindFragment(signal2);
433
- return;
434
- }
435
- if (newChildren.length === 1 && firstChild instanceof Text) {
436
- destroy();
437
- bindText(firstChild, signal2);
438
- return;
445
+ const newChildren = asNodeList(result, element, autoKey2);
446
+ const firstChild2 = newChildren[0];
447
+ if (!Array.isArray(result)) {
448
+ if (newChildren.length === 1 && firstChild2 instanceof DocumentFragment) {
449
+ destroy();
450
+ bindFragment(signal2, initialChildren, autoKey2);
451
+ return;
452
+ }
453
+ if (newChildren.length === 1 && firstChild2 instanceof Text) {
454
+ while (startAnchor.nextSibling !== endAnchor) {
455
+ startAnchor.nextSibling?.remove();
456
+ }
457
+ startAnchor.after(firstChild2);
458
+ destroy();
459
+ bindText(firstChild2, signal2);
460
+ return;
461
+ }
439
462
  }
440
463
  while (startAnchor.nextSibling !== endAnchor) {
441
464
  startAnchor.nextSibling?.remove();
@@ -451,10 +474,10 @@ var evaluateBindings = (element, fragment) => {
451
474
  persistedChild.remove();
452
475
  continue;
453
476
  }
454
- for (const attr of [...persistedChild.attributes]) {
477
+ for (const attr of Array.from(persistedChild.attributes)) {
455
478
  if (!newChild.hasAttribute(attr.name)) persistedChild.removeAttribute(attr.name);
456
479
  }
457
- for (const newAttr of [...newChild.attributes]) {
480
+ for (const newAttr of Array.from(newChild.attributes)) {
458
481
  const oldAttrValue = persistedChild.getAttribute(newAttr.name);
459
482
  if (oldAttrValue?.startsWith("this.__customCallbackFns")) continue;
460
483
  persistedChild.setAttribute(newAttr.name, newAttr.value);
@@ -467,36 +490,50 @@ var evaluateBindings = (element, fragment) => {
467
490
  null
468
491
  );
469
492
  };
470
- const bindFragment = (signal2) => {
493
+ const bindFragment = (signal2, initialChildren2, autoKey2) => {
471
494
  const initialFragment = signal2();
472
- renderState.childrenMap.set(initialFragment, [...initialFragment.childNodes]);
495
+ const firstInitialChild = initialChildren2[0];
496
+ if (firstInitialChild instanceof Element) {
497
+ renderState.childrenMap.set(initialFragment, initialChildren2);
498
+ }
473
499
  createEffect(({ destroy }) => {
474
500
  const result = signal2();
475
- const cachedChildren = renderState.childrenMap.get(initialFragment);
476
- const children = cachedChildren ?? asNodeList(result, element);
501
+ const cachedChildren = result instanceof DocumentFragment ? renderState.childrenMap.get(result) : void 0;
502
+ const children = cachedChildren ?? asNodeList(result, element, autoKey2);
503
+ if (result instanceof DocumentFragment && !renderState.childrenMap.has(result)) {
504
+ renderState.childrenMap.set(result, children);
505
+ }
477
506
  if (Array.isArray(result)) {
478
507
  destroy();
479
- bindArray(signal2);
508
+ bindArray(signal2, autoKey2);
480
509
  return;
481
510
  }
482
- if (result instanceof Text) {
483
- const children2 = asNodeList(result, element);
484
- const text2 = children2[0];
511
+ if (!(result instanceof DocumentFragment) && result !== null && result !== void 0) {
512
+ while (startAnchor.nextSibling !== endAnchor) {
513
+ startAnchor.nextSibling?.remove();
514
+ }
515
+ const children2 = asNodeList(result, element, autoKey2);
516
+ const text = children2[0];
517
+ startAnchor.after(text);
485
518
  destroy();
486
- bindText(text2, signal2);
519
+ bindText(text, signal2);
487
520
  return;
488
521
  }
489
522
  while (startAnchor.nextSibling !== endAnchor) {
490
523
  startAnchor.nextSibling?.remove();
491
524
  }
525
+ if (result === null || result === void 0) {
526
+ return;
527
+ }
492
528
  startAnchor.after(...children);
493
529
  });
494
530
  };
495
531
  if (signal !== void 0) {
496
- if (Array.isArray(newValue)) {
497
- bindArray(signal);
498
- } else if (initialChildren instanceof DocumentFragment) {
499
- bindFragment(signal);
532
+ const currentValue = signal();
533
+ if (Array.isArray(currentValue)) {
534
+ bindArray(signal, autoKey);
535
+ } else if (currentValue instanceof DocumentFragment) {
536
+ bindFragment(signal, initialChildren, autoKey);
500
537
  } else {
501
538
  const initialChild = initialChildren[0];
502
539
  bindText(initialChild, signal);
@@ -511,7 +548,7 @@ var evaluateBindings = (element, fragment) => {
511
548
  child.replaceWith(childFragment);
512
549
  }
513
550
  } else if (child instanceof Element) {
514
- for (const attr of [...child.attributes]) {
551
+ for (const attr of Array.from(child.attributes)) {
515
552
  const attrName = attr.name;
516
553
  if (SIGNAL_BINDING_REGEX.test(attr.value)) {
517
554
  const textList = attr.value.split(SIGNAL_BINDING_REGEX);
@@ -769,7 +806,9 @@ var customElement = (render, options) => {
769
806
  for (const mutation of mutations) {
770
807
  const attrName = mutation.attributeName;
771
808
  if (mutation.type !== "attributes" || attrName === null) continue;
772
- if (!(attrName in this.#attrSignals)) this.#attrSignals[attrName] = createSignal(null);
809
+ if (!(attrName in this.#attrSignals)) {
810
+ this.#attrSignals[attrName] = createSignal(this.getAttribute(attrName));
811
+ }
773
812
  const [getter, setter] = this.#attrSignals[attrName];
774
813
  const oldValue = getter();
775
814
  const newValue = this.getAttribute(attrName);
@@ -869,7 +908,9 @@ You must set an initial value before calling a property signal's getter.
869
908
  {},
870
909
  {
871
910
  get: (_, prop) => {
872
- if (!(prop in this.#attrSignals)) this.#attrSignals[prop] = createSignal(null);
911
+ if (!(prop in this.#attrSignals)) {
912
+ this.#attrSignals[prop] = createSignal(this.getAttribute(prop));
913
+ }
873
914
  const [getter] = this.#attrSignals[prop];
874
915
  const setter = (newValue) => this.setAttribute(prop, newValue);
875
916
  return [getter, setter];
@@ -956,7 +997,9 @@ You must set an initial value before calling a property signal's getter.
956
997
  }
957
998
  connectedCallback() {
958
999
  for (const [attrName, attr] of this.#attributesAsPropertiesMap) {
959
- if (!(attrName in this.#attrSignals)) this.#attrSignals[attrName] = createSignal(null);
1000
+ if (!(attrName in this.#attrSignals)) {
1001
+ this.#attrSignals[attrName] = createSignal(this.getAttribute(attrName));
1002
+ }
960
1003
  const propName = attr.prop;
961
1004
  const [getter] = this.#getPropSignal(propName, { allowUndefined: true });
962
1005
  let busy = false;
@@ -973,6 +1016,16 @@ You must set an initial value before calling a property signal's getter.
973
1016
  busy = false;
974
1017
  });
975
1018
  }
1019
+ for (const attrName of Object.keys(this.#attrSignals)) {
1020
+ const signal = this.#attrSignals[attrName];
1021
+ if (signal) {
1022
+ const [getter, setter] = signal;
1023
+ const currentValue = this.getAttribute(attrName);
1024
+ if (getter() !== currentValue) {
1025
+ setter(currentValue);
1026
+ }
1027
+ }
1028
+ }
976
1029
  if (this.#observer !== null) {
977
1030
  this.#observer.observe(this, { attributes: true });
978
1031
  }
@@ -1140,6 +1193,8 @@ var createRegistry = (args) => {
1140
1193
  };
1141
1194
  };
1142
1195
  export {
1196
+ clearRenderState,
1197
+ clearServerCss,
1143
1198
  clientOnlyCallback,
1144
1199
  createEffect,
1145
1200
  createRegistry,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thunderous",
3
- "version": "2.4.3",
3
+ "version": "2.4.5",
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",