thunderous 2.3.11 → 2.3.13

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.
@@ -0,0 +1,391 @@
1
+ import { DEFAULT_RENDER_OPTIONS } from './constants';
2
+ import { isCSSStyleSheet, renderState } from './render';
3
+ import { isServer, serverDefine } from './server-side';
4
+ import { createEffect, createSignal } from './signals';
5
+ const ANY_BINDING_REGEX = /(\{\{.+:.+\}\})/;
6
+ /**
7
+ * Create a custom element that can be defined for use in the DOM.
8
+ * @example
9
+ * ```ts
10
+ * const MyElement = customElement(() => {
11
+ * return html`<h1>Hello, World!</h1>`;
12
+ * });
13
+ * MyElement.define('my-element');
14
+ * ```
15
+ */
16
+ export const customElement = (render, options) => {
17
+ const _options = { ...DEFAULT_RENDER_OPTIONS, ...options };
18
+ const { formAssociated, observedAttributes: _observedAttributes, attributesAsProperties, attachShadow, shadowRootOptions: _shadowRootOptions, } = _options;
19
+ const shadowRootOptions = { ...DEFAULT_RENDER_OPTIONS.shadowRootOptions, ..._shadowRootOptions };
20
+ const allOptions = { ..._options, shadowRootOptions };
21
+ if (isServer) {
22
+ const serverRender = render;
23
+ let _tagName;
24
+ let _registry;
25
+ const scopedRegistry = (() => {
26
+ if (shadowRootOptions.registry !== undefined &&
27
+ 'scoped' in shadowRootOptions.registry &&
28
+ shadowRootOptions.registry.scoped) {
29
+ return shadowRootOptions.registry;
30
+ }
31
+ })();
32
+ return {
33
+ define(tagName) {
34
+ _tagName = tagName;
35
+ serverDefine({
36
+ tagName,
37
+ serverRender,
38
+ options: allOptions,
39
+ scopedRegistry,
40
+ parentRegistry: _registry,
41
+ elementResult: this,
42
+ });
43
+ return this;
44
+ },
45
+ register(registry) {
46
+ if (_tagName !== undefined && 'eject' in registry && registry.scoped) {
47
+ console.error('Must call `register()` before `define()` for scoped registries.');
48
+ return this;
49
+ }
50
+ if ('eject' in registry) {
51
+ _registry = registry;
52
+ }
53
+ else {
54
+ console.error('Registry must be created with `createRegistry()` for SSR.');
55
+ }
56
+ return this;
57
+ },
58
+ eject() {
59
+ const error = new Error('Cannot eject a custom element on the server.');
60
+ console.error(error);
61
+ throw error;
62
+ },
63
+ };
64
+ }
65
+ shadowRootOptions.registry = shadowRootOptions.customElements =
66
+ shadowRootOptions.registry instanceof CustomElementRegistry
67
+ ? shadowRootOptions.registry
68
+ : shadowRootOptions.registry?.eject();
69
+ // must set observedAttributes prior to defining the class
70
+ const observedAttributesSet = new Set(_observedAttributes);
71
+ const attributesAsPropertiesMap = new Map();
72
+ for (const [attrName, coerce] of attributesAsProperties) {
73
+ observedAttributesSet.add(attrName);
74
+ attributesAsPropertiesMap.set(attrName, {
75
+ // convert kebab-case attribute names to camelCase property names
76
+ prop: attrName.replace(/(?<=-|_)([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/(-|_)/g, ''),
77
+ coerce,
78
+ value: null,
79
+ });
80
+ }
81
+ const observedAttributes = Array.from(observedAttributesSet);
82
+ class CustomElement extends HTMLElement {
83
+ #attributesAsPropertiesMap = new Map(attributesAsPropertiesMap);
84
+ #attrSignals = {};
85
+ #propSignals = {};
86
+ #attributeChangedFns = new Set();
87
+ #connectedFns = new Set();
88
+ #disconnectedFns = new Set();
89
+ #adoptedCallbackFns = new Set();
90
+ #formAssociatedCallbackFns = new Set();
91
+ #formDisabledCallbackFns = new Set();
92
+ #formResetCallbackFns = new Set();
93
+ #formStateRestoreCallbackFns = new Set();
94
+ #clientOnlyCallbackFns = new Set();
95
+ #shadowRoot = attachShadow ? this.attachShadow(shadowRootOptions) : null;
96
+ #internals = this.attachInternals();
97
+ #observer = options?.observedAttributes !== undefined
98
+ ? null
99
+ : new MutationObserver((mutations) => {
100
+ for (const mutation of mutations) {
101
+ const attrName = mutation.attributeName;
102
+ if (mutation.type !== 'attributes' || attrName === null)
103
+ continue;
104
+ if (!(attrName in this.#attrSignals))
105
+ this.#attrSignals[attrName] = createSignal(null);
106
+ const [getter, setter] = this.#attrSignals[attrName];
107
+ const oldValue = getter();
108
+ const newValue = this.getAttribute(attrName);
109
+ setter(newValue);
110
+ for (const fn of this.#attributeChangedFns) {
111
+ fn(attrName, oldValue, newValue);
112
+ }
113
+ }
114
+ });
115
+ #getPropSignal = ((prop, { allowUndefined = false } = {}) => {
116
+ if (!(prop in this.#propSignals))
117
+ this.#propSignals[prop] = createSignal();
118
+ const [_getter, __setter] = this.#propSignals[prop];
119
+ let stackLength = 0;
120
+ const _setter = (newValue, options) => {
121
+ stackLength++;
122
+ queueMicrotask(() => stackLength--);
123
+ if (stackLength > 999) {
124
+ console.error(new Error(`Property signal setter stack overflow detected. Possible infinite loop. Bailing out.
125
+
126
+ Property: ${prop}
127
+
128
+ New value: ${JSON.stringify(newValue, null, 2)}
129
+
130
+ Element: <${this.tagName.toLowerCase()}>
131
+ `));
132
+ stackLength = 0;
133
+ return;
134
+ }
135
+ __setter(newValue, options);
136
+ };
137
+ const descriptor = Object.getOwnPropertyDescriptor(this, prop);
138
+ if (descriptor === undefined) {
139
+ Object.defineProperty(this, prop, {
140
+ get: _getter,
141
+ set: _setter,
142
+ configurable: false,
143
+ enumerable: true,
144
+ });
145
+ }
146
+ const setter = (newValue, options) => {
147
+ // @ts-expect-error // TODO: look into this
148
+ this[prop] = newValue;
149
+ _setter(newValue, options);
150
+ };
151
+ const getter = ((options) => {
152
+ const value = _getter(options);
153
+ if (value === undefined && !allowUndefined) {
154
+ const error = new Error(`Error accessing property: "${prop}"\nYou must set an initial value before calling a property signal's getter.\n`);
155
+ console.error(error);
156
+ throw error;
157
+ }
158
+ return value;
159
+ });
160
+ getter.getter = true;
161
+ const publicSignal = [getter, setter];
162
+ publicSignal.init = (value) => {
163
+ _setter(value);
164
+ return [getter, setter];
165
+ };
166
+ return publicSignal;
167
+ }).bind(this);
168
+ #render = (() => {
169
+ const root = this.#shadowRoot ?? this;
170
+ renderState.currentShadowRoot = this.#shadowRoot;
171
+ renderState.registry = shadowRootOptions.customElements ?? customElements;
172
+ const fragment = render({
173
+ elementRef: this,
174
+ root,
175
+ internals: this.#internals,
176
+ attributeChangedCallback: (fn) => this.#attributeChangedFns.add(fn),
177
+ connectedCallback: (fn) => this.#connectedFns.add(fn),
178
+ disconnectedCallback: (fn) => this.#disconnectedFns.add(fn),
179
+ adoptedCallback: (fn) => this.#adoptedCallbackFns.add(fn),
180
+ formAssociatedCallback: (fn) => this.#formAssociatedCallbackFns.add(fn),
181
+ formDisabledCallback: (fn) => this.#formDisabledCallbackFns.add(fn),
182
+ formResetCallback: (fn) => this.#formResetCallbackFns.add(fn),
183
+ formStateRestoreCallback: (fn) => this.#formStateRestoreCallbackFns.add(fn),
184
+ clientOnlyCallback: (fn) => this.#clientOnlyCallbackFns.add(fn),
185
+ getter: (fn) => {
186
+ const _fn = () => fn();
187
+ _fn.getter = true;
188
+ return _fn;
189
+ },
190
+ customCallback: (fn) => {
191
+ const key = crypto.randomUUID();
192
+ this.__customCallbackFns?.set(key, fn);
193
+ return `this.getRootNode().host.__customCallbackFns.get('${key}')(event)`;
194
+ },
195
+ attrSignals: new Proxy({}, {
196
+ get: (_, prop) => {
197
+ if (!(prop in this.#attrSignals))
198
+ this.#attrSignals[prop] = createSignal(null);
199
+ const [getter] = this.#attrSignals[prop];
200
+ const setter = (newValue) => this.setAttribute(prop, newValue);
201
+ return [getter, setter];
202
+ },
203
+ set: () => {
204
+ console.error('Signals must be assigned via setters.');
205
+ return false;
206
+ },
207
+ }),
208
+ propSignals: new Proxy({}, {
209
+ get: (_, prop) => this.#getPropSignal(prop),
210
+ set: () => {
211
+ console.error('Signals must be assigned via setters.');
212
+ return false;
213
+ },
214
+ }),
215
+ refs: new Proxy({}, {
216
+ get: (_, prop) => root.querySelector(`[ref=${prop}]`),
217
+ set: () => {
218
+ console.error('Refs are readonly and cannot be assigned.');
219
+ return false;
220
+ },
221
+ }),
222
+ adoptStyleSheet: (stylesheet) => {
223
+ if (!attachShadow) {
224
+ console.warn('Styles are only encapsulated when using shadow DOM. The stylesheet will be applied to the global document instead.');
225
+ }
226
+ if (isCSSStyleSheet(stylesheet)) {
227
+ if (this.#shadowRoot === null) {
228
+ for (const rule of stylesheet.cssRules) {
229
+ if (rule instanceof CSSStyleRule && rule.selectorText.includes(':host')) {
230
+ console.error('Styles with :host are not supported when not using shadow DOM.');
231
+ }
232
+ }
233
+ document.adoptedStyleSheets.push(stylesheet);
234
+ return;
235
+ }
236
+ this.#shadowRoot.adoptedStyleSheets.push(stylesheet);
237
+ }
238
+ else {
239
+ requestAnimationFrame(() => {
240
+ root.appendChild(stylesheet);
241
+ });
242
+ }
243
+ },
244
+ });
245
+ fragment.host = this;
246
+ for (const fn of this.#clientOnlyCallbackFns) {
247
+ fn();
248
+ }
249
+ root.replaceChildren(fragment);
250
+ renderState.currentShadowRoot = null;
251
+ renderState.registry = customElements;
252
+ }).bind(this);
253
+ static get formAssociated() {
254
+ return formAssociated;
255
+ }
256
+ static get observedAttributes() {
257
+ return observedAttributes;
258
+ }
259
+ constructor() {
260
+ try {
261
+ super();
262
+ if (!Object.prototype.hasOwnProperty.call(this, '__customCallbackFns')) {
263
+ this.__customCallbackFns = new Map();
264
+ }
265
+ for (const attr of this.attributes) {
266
+ this.#attrSignals[attr.name] = createSignal(attr.value);
267
+ }
268
+ this.#render();
269
+ }
270
+ catch (error) {
271
+ const _error = new Error('Error instantiating element:\nThis usually occurs if you have errors in the function body of your component. Check prior logs for possible causes.\n', { cause: error });
272
+ console.error(_error);
273
+ throw _error;
274
+ }
275
+ }
276
+ connectedCallback() {
277
+ for (const [attrName, attr] of this.#attributesAsPropertiesMap) {
278
+ if (!(attrName in this.#attrSignals))
279
+ this.#attrSignals[attrName] = createSignal(null);
280
+ const propName = attr.prop;
281
+ const [getter] = this.#getPropSignal(propName, { allowUndefined: true });
282
+ let busy = false;
283
+ createEffect(() => {
284
+ if (busy)
285
+ return;
286
+ busy = true;
287
+ const value = getter();
288
+ if (value === undefined)
289
+ return;
290
+ if (value !== null && value !== false) {
291
+ this.setAttribute(attrName, String(value === true ? '' : value));
292
+ }
293
+ else {
294
+ this.removeAttribute(attrName);
295
+ }
296
+ busy = false;
297
+ });
298
+ }
299
+ if (this.#observer !== null) {
300
+ this.#observer.observe(this, { attributes: true });
301
+ }
302
+ for (const fn of this.#connectedFns) {
303
+ fn();
304
+ }
305
+ }
306
+ disconnectedCallback() {
307
+ if (this.#observer !== null) {
308
+ this.#observer.disconnect();
309
+ }
310
+ for (const fn of this.#disconnectedFns) {
311
+ fn();
312
+ }
313
+ }
314
+ #attributesBusy = false;
315
+ attributeChangedCallback(name, oldValue, newValue) {
316
+ if (this.#attributesBusy || ANY_BINDING_REGEX.test(newValue ?? ''))
317
+ return;
318
+ const [, attrSetter] = this.#attrSignals[name] ?? [];
319
+ attrSetter?.(newValue);
320
+ const prop = this.#attributesAsPropertiesMap.get(name);
321
+ if (prop !== undefined) {
322
+ const propName = prop.prop;
323
+ this.#attributesBusy = true; // prevent infinite loop
324
+ const [, propSetter] = this.#getPropSignal(propName);
325
+ if (prop.coerce === Boolean) {
326
+ const bool = newValue !== null && newValue !== 'false';
327
+ const propValue = (newValue === null ? null : bool);
328
+ propSetter(propValue);
329
+ }
330
+ else {
331
+ const propValue = (newValue === null ? null : prop.coerce(newValue));
332
+ propSetter(propValue);
333
+ }
334
+ this.#attributesBusy = false;
335
+ }
336
+ for (const fn of this.#attributeChangedFns) {
337
+ fn(name, oldValue, newValue);
338
+ }
339
+ }
340
+ adoptedCallback() {
341
+ for (const fn of this.#adoptedCallbackFns) {
342
+ fn();
343
+ }
344
+ }
345
+ formAssociatedCallback() {
346
+ for (const fn of this.#formAssociatedCallbackFns) {
347
+ fn();
348
+ }
349
+ }
350
+ formDisabledCallback() {
351
+ for (const fn of this.#formDisabledCallbackFns) {
352
+ fn();
353
+ }
354
+ }
355
+ formResetCallback() {
356
+ for (const fn of this.#formResetCallbackFns) {
357
+ fn();
358
+ }
359
+ }
360
+ formStateRestoreCallback() {
361
+ for (const fn of this.#formStateRestoreCallbackFns) {
362
+ fn();
363
+ }
364
+ }
365
+ }
366
+ let _tagName;
367
+ let _registry;
368
+ const elementResult = {
369
+ define(tagName, options) {
370
+ const registry = _registry ?? customElements;
371
+ const nativeRegistry = 'eject' in registry ? registry.eject() : registry;
372
+ if (nativeRegistry.get(tagName) !== undefined) {
373
+ console.warn(`Custom element "${tagName}" was already defined. Skipping...`);
374
+ return this;
375
+ }
376
+ registry.define(tagName, CustomElement, options);
377
+ _tagName = tagName;
378
+ return this;
379
+ },
380
+ register(registry) {
381
+ if (_tagName !== undefined && 'eject' in registry && registry.scoped) {
382
+ console.error('Must call `register()` before `define()` for scoped registries.');
383
+ return this;
384
+ }
385
+ _registry = registry;
386
+ return this;
387
+ },
388
+ eject: () => CustomElement,
389
+ };
390
+ return elementResult;
391
+ };
package/dist/index.cjs CHANGED
@@ -291,6 +291,7 @@ var renderState = {
291
291
  signalMap: /* @__PURE__ */ new Map(),
292
292
  callbackMap: /* @__PURE__ */ new Map(),
293
293
  fragmentMap: /* @__PURE__ */ new Map(),
294
+ propertyMap: /* @__PURE__ */ new Map(),
294
295
  registry: typeof customElements !== "undefined" ? customElements : {}
295
296
  };
296
297
  var logPropertyWarning = (propName, element) => {
@@ -455,14 +456,22 @@ var evaluateBindings = (element, fragment) => {
455
456
  newText += text;
456
457
  }
457
458
  }
458
- if (hasNull && newText === "null" || attrName.startsWith("prop:")) {
459
+ if (hasNull && newText === "null" || attrName.startsWith("prop-id:")) {
459
460
  if (child.hasAttribute(attrName)) child.removeAttribute(attrName);
460
461
  } else {
461
462
  if (newText !== prevText) child.setAttribute(attrName, newText);
462
463
  }
463
- if (attrName.startsWith("prop:")) {
464
+ if (attrName.startsWith("prop-id:")) {
464
465
  if (child.hasAttribute(attrName)) child.removeAttribute(attrName);
465
- const propName = attrName.replace("prop:", "");
466
+ const propId = attrName.replace("prop-id:", "");
467
+ const propName = renderState.propertyMap.get(propId);
468
+ if (propName === void 0) {
469
+ console.error(
470
+ `BRANCH:SIGNAL; Property ID "${propId}" does not exist in the property map. This is likely a problem with Thunderous. Report a bug if you see this message. https://github.com/Thunder-Solutions/Thunderous/issues`,
471
+ child
472
+ );
473
+ return;
474
+ }
466
475
  const newValue = hasNull && newText === "null" ? null : newText;
467
476
  if (!(propName in child)) logPropertyWarning(propName, child);
468
477
  child[propName] = signal !== void 0 ? signal() : newValue;
@@ -488,19 +497,33 @@ var evaluateBindings = (element, fragment) => {
488
497
  child.__customCallbackFns.set(uniqueKey, callback);
489
498
  }
490
499
  }
491
- if (uniqueKey !== "" && !attrName.startsWith("prop:")) {
500
+ if (uniqueKey !== "" && !attrName.startsWith("prop-id:")) {
492
501
  child.setAttribute(attrName, `this.__customCallbackFns.get('${uniqueKey}')(event)`);
493
- } else if (attrName.startsWith("prop:")) {
502
+ } else if (attrName.startsWith("prop-id:")) {
494
503
  child.removeAttribute(attrName);
495
- const propName = attrName.replace("prop:", "");
496
- if (!(propName in child)) logPropertyWarning(propName, child);
504
+ const propId = attrName.replace("prop-id:", "");
505
+ const propName = renderState.propertyMap.get(propId);
506
+ if (propName === void 0) {
507
+ console.error(
508
+ `BRANCH:CALLBACK; Property ID "${propId}" does not exist in the property map. This is likely a problem with Thunderous. Report a bug if you see this message. https://github.com/Thunder-Solutions/Thunderous/issues`,
509
+ child
510
+ );
511
+ return;
512
+ }
497
513
  child[propName] = child.__customCallbackFns.get(uniqueKey);
498
514
  }
499
515
  });
500
- } else if (attrName.startsWith("prop:")) {
516
+ } else if (attrName.startsWith("prop-id:")) {
501
517
  child.removeAttribute(attrName);
502
- const propName = attrName.replace("prop:", "");
503
- if (!(propName in child)) logPropertyWarning(propName, child);
518
+ const propId = attrName.replace("prop-id:", "");
519
+ const propName = renderState.propertyMap.get(propId);
520
+ if (propName === void 0) {
521
+ console.error(
522
+ `BRANCH:PROP; Property ID "${propId}" does not exist in the property map. This is likely a problem with Thunderous. Report a bug if you see this message. https://github.com/Thunder-Solutions/Thunderous/issues`,
523
+ child
524
+ );
525
+ return;
526
+ }
504
527
  child[propName] = attr.value;
505
528
  }
506
529
  }
@@ -509,7 +532,7 @@ var evaluateBindings = (element, fragment) => {
509
532
  }
510
533
  };
511
534
  var html = (strings, ...values) => {
512
- const innerHTML = strings.reduce((innerHTML2, str, i) => {
535
+ let innerHTML = strings.reduce((innerHTML2, str, i) => {
513
536
  let value = values[i] ?? "";
514
537
  if (Array.isArray(value)) {
515
538
  value = value.map((item) => processValue(item)).join("");
@@ -520,6 +543,16 @@ var html = (strings, ...values) => {
520
543
  return innerHTML2;
521
544
  }, "");
522
545
  if (isServer) return innerHTML;
546
+ const props = innerHTML.match(/prop:([^=]+)/g);
547
+ if (props !== null) {
548
+ for (const prop of props) {
549
+ const name = prop.split(":")[1].trim();
550
+ const id = crypto.randomUUID();
551
+ const newProp = `prop-id:${id}`;
552
+ renderState.propertyMap.set(id, name);
553
+ innerHTML = innerHTML.replace(`prop:${name}`, newProp);
554
+ }
555
+ }
523
556
  const template = document.createElement("template");
524
557
  template.innerHTML = innerHTML;
525
558
  const fragment = renderState.currentShadowRoot?.importNode?.(template.content, true) ?? document.importNode(template.content, true);
@@ -633,7 +666,7 @@ var customElement = (render, options) => {
633
666
  observedAttributesSet.add(attrName);
634
667
  attributesAsPropertiesMap.set(attrName, {
635
668
  // convert kebab-case attribute names to camelCase property names
636
- prop: attrName.replace(/^([A-Z]+)/, (_, letter) => letter.toLowerCase()).replace(/(-|_| )([a-zA-Z])/g, (_, letter) => letter.toUpperCase()),
669
+ prop: attrName.replace(/(?<=-|_)([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/(-|_)/g, ""),
637
670
  coerce,
638
671
  value: null
639
672
  });
@@ -887,8 +920,14 @@ You must set an initial value before calling a property signal's getter.
887
920
  const propName = prop.prop;
888
921
  this.#attributesBusy = true;
889
922
  const [, propSetter] = this.#getPropSignal(propName);
890
- const propValue = newValue === null ? null : prop.coerce(newValue);
891
- propSetter(propValue);
923
+ if (prop.coerce === Boolean) {
924
+ const bool = newValue !== null && newValue !== "false";
925
+ const propValue = newValue === null ? null : bool;
926
+ propSetter(propValue);
927
+ } else {
928
+ const propValue = newValue === null ? null : prop.coerce(newValue);
929
+ propSetter(propValue);
930
+ }
892
931
  this.#attributesBusy = false;
893
932
  }
894
933
  for (const fn of this.#attributeChangedFns) {