thunderous 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -91,23 +91,55 @@ You can always define the internals the same as you usually would, and if for so
91
91
 
92
92
  <!-- prettier-ignore-start -->
93
93
  ```ts
94
- const MyElement = customElement(({ internals, elementRef, root }) => {
95
- internals.ariaRequired = 'true';
96
- const childLink = elementRef.querySelector('a[href]'); // light DOM
97
- const innerLink = root.querySelector('a[href]'); // shadow DOM
94
+ const MyElement = customElement((params) => {
95
+ const {
96
+ internals,
97
+ elementRef,
98
+ root,
99
+ connectedCallback,
100
+ } = params;
101
+
102
+ internals.setFormValue('hello world');
103
+ connectedCallback(() => {
104
+ const childLink = elementRef.querySelector('a[href]'); // light DOM
105
+ const innerLink = root.querySelector('a[href]'); // shadow DOM
106
+ /* ... */
107
+ });
98
108
  /* ... */
99
109
  }, { formAssociated: true });
100
110
  ```
101
111
  <!-- prettier-ignore-end -->
102
112
 
113
+ If you need to pass certain options to the `attachShadow()` call, you can do so by passing `shadowRootOptions`.
114
+
115
+ <!-- prettier-ignore-start -->
116
+ ```ts
117
+ const MyElement = customElement(() => {
118
+ /* ... */
119
+ }, { shadowRootOptions: { delegatesFocus: true } });
120
+ ```
121
+ <!-- prettier-ignore-end -->
122
+
123
+ To opt out of using shadow DOM at all, pass `false` to `attachShadow`. This will change `root` above to reference the element itself, and stylesheets will apply to the global document rather than being encapsulated within the component.
124
+
125
+ <!-- prettier-ignore-start -->
126
+ ```ts
127
+ const MyElement = customElement(() => {
128
+ /* ... */
129
+ }, { attachShadow: false });
130
+ ```
131
+ <!-- prettier-ignore-end -->
132
+
103
133
  #### Adopted Style Sheets
104
134
 
105
135
  This one diverges from native slightly, since the native approach is a bit manual. For convenience, you can use the `adoptStyleSheet()` function.
106
136
 
107
- > If you prefer the manual approach, `root.adoptedStyleSheets = []`, you can always do that with the `root` property listed above.
137
+ > If you prefer the manual approach, `root.adoptedStyleSheets = []`, you can always do that with the `root` property above, or the global `document`.
108
138
 
109
139
  The `css` tagged template function will construct a `CSSStyleSheet` object that can be adopted by documents and shadow roots.
110
140
 
141
+ > When `attachShadow` is set to `false`, `adoptStyleSheet()` will use the global document to adopt the styles.
142
+
111
143
  <!-- prettier-ignore-start -->
112
144
  ```ts
113
145
  import { customElement, css } from 'thunderous';
@@ -175,7 +207,7 @@ By default, each element is observed with a `MutationObserver` watching all attr
175
207
  ```ts
176
208
  const MyElement = customElement(({ attrSignals }) => {
177
209
  const [heading, setHeading] = attrSignals['my-heading'];
178
- // setHeading() will also update the attribute in the DOM.
210
+ // setHeading() will also update the attribute in the HTML.
179
211
  return html`<h2>${heading}</h2>`;
180
212
  });
181
213
  ```
@@ -200,6 +232,53 @@ Usage:
200
232
 
201
233
  > NOTICE: Since `attrSignals` is a `Proxy` object, _any_ property will return a signal and auto-bind it to the attribute it corresponds with.
202
234
 
235
+ ##### Property Signals
236
+
237
+ In addition to attributes, there are also properties. Though often conflated, there is an important distinction: _attributes_ are strings defined in HTML, and _properties_ can be any type of data, strictly in JavaScript and completely invisible to HTML.
238
+
239
+ Modern templating solutions often allow developers to assign properties via HTML attribute _syntax_, even though they are not actually attributes. While this distinction may seem trivial for those working with modern frameworks, it becomes much more relevant when defining custom elements that may be used in plain HTML.
240
+
241
+ Thunderous supports properties for cases where strings are not sufficient. These are also reflected as signals _within_ the component, but the _consumer_ of the component will not directly interact with this signal. `myElement.count = 1` will update the internal signal.
242
+
243
+ <!-- prettier-ignore-start -->
244
+ ```ts
245
+ const MyElement = customElement(({ propSignals }) => {
246
+ const [count, setCount] = propSignals['prop'];
247
+ // setCount() will also update the property in the DOM
248
+ return html`<output>${count}</output>`;
249
+ });
250
+ ```
251
+ <!-- prettier-ignore-end -->
252
+
253
+ There is also a way to sync attributes with properties, though it should be used deliberately, with caution. Since it requires some coercion to occur, it may introduce some performance overhead. It's best not to use this to parse JSON strings, for example.
254
+
255
+ To use this feature, pass `attributesAsProperties` in the options. It accepts an array of `[propertyName, coerceFn]` pairs. For primitive types, you can use their constructors for coercion, like `['count', Number]`.
256
+
257
+ <!-- prettier-ignore-start -->
258
+ ```ts
259
+ const MyElement = customElement(({ propSignals }) => {
260
+ const [count, setCount] = propSignals['prop'];
261
+ // setCount() will also update the property in the DOM
262
+ return html`<output>${count}</output>`;
263
+ }, { attributesAsProperties: [['count', Number]] });
264
+ ```
265
+ <!-- prettier-ignore-end -->
266
+
267
+ With the above snippet, `count` may be controlled by setting the attribute, like so:
268
+
269
+ ```html
270
+ <my-element count="1"></my-element>
271
+ ```
272
+
273
+ ...and the attribute will reflect changes made to the property as well:
274
+
275
+ ```ts
276
+ const myElement = document.querySelector('my-element');
277
+ myElement.count = 1;
278
+ ```
279
+
280
+ In both cases, the `count()` signal will be updated.
281
+
203
282
  ##### Derived Signals
204
283
 
205
284
  If you want to calculate a value based on another signal's value, you should use the `derived()` function. This signal will trigger its subscribers each time the signals inside change.
@@ -278,9 +357,14 @@ const MyElement = customElement(({ customCallback }) => {
278
357
  `;
279
358
  });
280
359
  ```
281
- <!-- prettier-ignore-end -->
282
360
 
283
- > NOTICE: This uses the native HTML inline event-binding syntax. There is no special syntax for `on` attributes, because it simply renders a reference to `this.getRootNode().host` and extracts the callback from there.
361
+ > NOTICE: This uses the native HTML inline event-binding syntax. There is no special syntax for `on` attributes, because it simply renders the reference to the host element.
362
+ >
363
+ > You will see something like this when you inspect the element in the browser:
364
+ > ```html
365
+ > <my-element onclick="this.getRootNode().host.__customCallbackFns.get('d7121610-e89d-4629-ab00-d4e22644f16b')(event)">
366
+ > ```
367
+ <!-- prettier-ignore-end -->
284
368
 
285
369
  ### Defining Custom Elements
286
370
 
package/dist/index.cjs CHANGED
@@ -182,6 +182,11 @@ var html = (strings, ...values) => {
182
182
  child.setAttribute(attr.name, newText);
183
183
  });
184
184
  } else if (callbackBindingRegex.test(attr.value)) {
185
+ const getRootNode = child.getRootNode.bind(child);
186
+ child.getRootNode = () => {
187
+ const rootNode = getRootNode();
188
+ return rootNode instanceof ShadowRoot ? rootNode : fragment;
189
+ };
185
190
  const uniqueKey = attr.value.replace(/\{\{callback:(.+)\}\}/, "$1");
186
191
  child.setAttribute(attr.name, `this.getRootNode().host.__customCallbackFns.get('${uniqueKey}')(event)`);
187
192
  }
@@ -234,12 +239,38 @@ var css = (strings, ...values) => {
234
239
  // src/custom-element.ts
235
240
  var DEFAULT_RENDER_OPTIONS = {
236
241
  formAssociated: false,
237
- observedAttributes: []
242
+ observedAttributes: [],
243
+ attributesAsProperties: [],
244
+ attachShadow: true,
245
+ shadowRootOptions: {
246
+ mode: "closed"
247
+ }
238
248
  };
249
+ var getPropName = (attrName) => attrName.replace(/^([A-Z]+)/, (_, letter) => letter.toLowerCase()).replace(/(-|_| )([a-zA-Z])/g, (_, letter) => letter.toUpperCase());
239
250
  var customElement = (render, options) => {
240
- const { formAssociated, observedAttributes } = { ...DEFAULT_RENDER_OPTIONS, ...options };
251
+ const {
252
+ formAssociated,
253
+ observedAttributes: _observedAttributes,
254
+ attributesAsProperties,
255
+ attachShadow,
256
+ shadowRootOptions: _shadowRootOptions
257
+ } = { ...DEFAULT_RENDER_OPTIONS, ...options };
258
+ const shadowRootOptions = { ...DEFAULT_RENDER_OPTIONS.shadowRootOptions, ..._shadowRootOptions };
259
+ const observedAttributesSet = new Set(_observedAttributes);
260
+ const attributesAsPropertiesMap = /* @__PURE__ */ new Map();
261
+ for (const [attrName, coerce] of attributesAsProperties) {
262
+ observedAttributesSet.add(attrName);
263
+ attributesAsPropertiesMap.set(attrName, {
264
+ prop: getPropName(attrName),
265
+ coerce,
266
+ value: null
267
+ });
268
+ }
269
+ const observedAttributes = Array.from(observedAttributesSet);
241
270
  class CustomElement extends HTMLElement {
271
+ #attributesAsPropertiesMap = new Map(attributesAsPropertiesMap);
242
272
  #attrSignals = {};
273
+ #propSignals = {};
243
274
  #attributeChangedFns = /* @__PURE__ */ new Set();
244
275
  #connectedFns = /* @__PURE__ */ new Set();
245
276
  #disconnectedFns = /* @__PURE__ */ new Set();
@@ -248,26 +279,28 @@ var customElement = (render, options) => {
248
279
  #formResetCallbackFns = /* @__PURE__ */ new Set();
249
280
  #formStateRestoreCallbackFns = /* @__PURE__ */ new Set();
250
281
  __customCallbackFns = /* @__PURE__ */ new Map();
251
- #shadowRoot = this.attachShadow({ mode: "closed" });
282
+ #shadowRoot = attachShadow ? this.attachShadow(shadowRootOptions) : null;
252
283
  #internals = this.attachInternals();
253
- #observer = observedAttributes.length > 0 ? null : new MutationObserver((mutations) => {
284
+ #observer = (options == null ? void 0 : options.observedAttributes) !== void 0 ? null : new MutationObserver((mutations) => {
254
285
  for (const mutation of mutations) {
255
286
  const attrName = mutation.attributeName;
256
287
  if (mutation.type !== "attributes" || attrName === null) continue;
257
- const [value, setValue] = this.#attrSignals[attrName];
258
- const _oldValue = value();
288
+ if (!(attrName in this.#attrSignals)) this.#attrSignals[attrName] = createSignal(null);
289
+ const [getter, setter] = this.#attrSignals[attrName];
290
+ const _oldValue = getter();
259
291
  const oldValue = _oldValue === null ? null : _oldValue;
260
292
  const newValue = this.getAttribute(attrName);
261
- setValue(newValue);
293
+ setter(newValue);
262
294
  for (const fn of this.#attributeChangedFns) {
263
295
  fn(attrName, oldValue, newValue);
264
296
  }
265
297
  }
266
298
  });
267
299
  #render() {
300
+ const root = this.#shadowRoot ?? this;
268
301
  const fragment = render({
269
302
  elementRef: this,
270
- root: this.#shadowRoot,
303
+ root,
271
304
  internals: this.#internals,
272
305
  attributeChangedCallback: (fn) => this.#attributeChangedFns.add(fn),
273
306
  connectedCallback: (fn) => this.#connectedFns.add(fn),
@@ -296,10 +329,28 @@ var customElement = (render, options) => {
296
329
  }
297
330
  }
298
331
  ),
332
+ propSignals: new Proxy(
333
+ {},
334
+ {
335
+ get: (_, prop) => {
336
+ if (!(prop in this.#propSignals)) this.#propSignals[prop] = createSignal(null);
337
+ const [getter, _setter] = this.#propSignals[prop];
338
+ const setter = (newValue) => {
339
+ this[prop] = newValue;
340
+ _setter(newValue);
341
+ };
342
+ return [getter, setter];
343
+ },
344
+ set: () => {
345
+ console.error("Signals must be assigned via setters.");
346
+ return false;
347
+ }
348
+ }
349
+ ),
299
350
  refs: new Proxy(
300
351
  {},
301
352
  {
302
- get: (_, prop) => this.#shadowRoot.querySelector(`[ref=${prop}]`),
353
+ get: (_, prop) => root.querySelector(`[ref=${prop}]`),
303
354
  set: () => {
304
355
  console.error("Refs are readonly and cannot be assigned.");
305
356
  return false;
@@ -307,16 +358,31 @@ var customElement = (render, options) => {
307
358
  }
308
359
  ),
309
360
  adoptStyleSheet: (stylesheet) => {
361
+ if (!attachShadow) {
362
+ console.warn(
363
+ "Styles are only encapsulated when using shadow DOM. The stylesheet will be applied to the global document instead."
364
+ );
365
+ }
310
366
  if (isCSSStyleSheet(stylesheet)) {
367
+ if (this.#shadowRoot === null) {
368
+ for (const rule of stylesheet.cssRules) {
369
+ if (rule instanceof CSSStyleRule && rule.selectorText.includes(":host")) {
370
+ console.error("Styles with :host are not supported when not using shadow DOM.");
371
+ }
372
+ }
373
+ document.adoptedStyleSheets.push(stylesheet);
374
+ return;
375
+ }
311
376
  this.#shadowRoot.adoptedStyleSheets.push(stylesheet);
312
377
  } else {
313
378
  requestAnimationFrame(() => {
314
- this.#shadowRoot.appendChild(stylesheet);
379
+ root.appendChild(stylesheet);
315
380
  });
316
381
  }
317
382
  }
318
383
  });
319
- setInnerHTML(this.#shadowRoot, fragment);
384
+ fragment.host = this;
385
+ setInnerHTML(root, fragment);
320
386
  }
321
387
  static get formAssociated() {
322
388
  return formAssociated;
@@ -326,6 +392,34 @@ var customElement = (render, options) => {
326
392
  }
327
393
  constructor() {
328
394
  super();
395
+ for (const [attrName, attr] of this.#attributesAsPropertiesMap) {
396
+ this.#attrSignals[attrName] = createSignal(null);
397
+ Object.defineProperty(this, attr.prop, {
398
+ get: () => {
399
+ if (!(attrName in this.#attrSignals)) this.#attrSignals[attrName] = createSignal(null);
400
+ const [getter] = this.#attrSignals[attrName];
401
+ const raw = getter();
402
+ const rawOnly = raw !== null && attr.value === null;
403
+ const value = rawOnly ? attr.coerce(raw) : attr.value;
404
+ return value === null ? null : value;
405
+ },
406
+ set: (newValue) => {
407
+ const oldValue = attr.value;
408
+ attr.value = newValue;
409
+ if (!(attrName in this.#attrSignals)) this.#attrSignals[attrName] = createSignal(null);
410
+ const [, attrSetter] = this.#attrSignals[attrName];
411
+ const [, propSetter] = this.#propSignals[attrName];
412
+ const attrValue = newValue === null ? null : String(newValue);
413
+ if (String(oldValue) === attrValue) return;
414
+ attrSetter(attrValue);
415
+ propSetter(newValue);
416
+ if (attrValue === null) this.removeAttribute(attrName);
417
+ else this.setAttribute(attrName, attrValue);
418
+ },
419
+ configurable: true,
420
+ enumerable: true
421
+ });
422
+ }
329
423
  for (const attr of this.attributes) {
330
424
  this.#attrSignals[attr.name] = createSignal(attr.value);
331
425
  }
@@ -348,8 +442,12 @@ var customElement = (render, options) => {
348
442
  }
349
443
  }
350
444
  attributeChangedCallback(name, oldValue, newValue) {
351
- const [, setValue] = this.#attrSignals[name];
352
- setValue(newValue);
445
+ const [, attrSetter] = this.#attrSignals[name] ?? [];
446
+ attrSetter == null ? void 0 : attrSetter(newValue);
447
+ const prop = this.#attributesAsPropertiesMap.get(name);
448
+ if (prop !== void 0) {
449
+ this[prop.prop] = newValue === null ? null : prop.coerce(newValue);
450
+ }
353
451
  for (const fn of this.#attributeChangedFns) {
354
452
  fn(name, oldValue, newValue);
355
453
  }
package/dist/index.d.cts CHANGED
@@ -1,3 +1,8 @@
1
+ declare global {
2
+ interface Element {
3
+ __findHost: () => Element;
4
+ }
5
+ }
1
6
  declare const html: (strings: TemplateStringsArray, ...values: unknown[]) => DocumentFragment;
2
7
  type Styles = CSSStyleSheet | HTMLStyleElement;
3
8
  declare const css: (strings: TemplateStringsArray, ...values: unknown[]) => Styles;
@@ -13,6 +18,11 @@ declare const createSignal: <T = undefined>(initVal?: T, options?: SignalOptions
13
18
  declare const derived: <T>(fn: () => T) => SignalGetter<T>;
14
19
  declare const createEffect: (fn: () => void) => void;
15
20
 
21
+ declare global {
22
+ interface DocumentFragment {
23
+ host: HTMLElement;
24
+ }
25
+ }
16
26
  type ElementResult = {
17
27
  define: (tagname: `${string}-${string}`) => ElementResult;
18
28
  register: (registry: Registry) => ElementResult;
@@ -21,7 +31,7 @@ type ElementResult = {
21
31
  type AttributeChangedCallback = (name: string, oldValue: string | null, newValue: string | null) => void;
22
32
  type RenderProps = {
23
33
  elementRef: HTMLElement;
24
- root: ShadowRoot;
34
+ root: ShadowRoot | HTMLElement;
25
35
  internals: ElementInternals;
26
36
  attributeChangedCallback: (fn: AttributeChangedCallback) => void;
27
37
  connectedCallback: (fn: () => void) => void;
@@ -32,12 +42,17 @@ type RenderProps = {
32
42
  formStateRestoreCallback: (fn: () => void) => void;
33
43
  customCallback: (fn: () => void) => `{{callback:${string}}}`;
34
44
  attrSignals: Record<string, Signal<string | null>>;
45
+ propSignals: Record<string, Signal<unknown>>;
35
46
  refs: Record<string, HTMLElement | null>;
36
47
  adoptStyleSheet: (stylesheet: Styles) => void;
37
48
  };
49
+ type Coerce<T = unknown> = (value: string) => T;
38
50
  type RenderOptions = {
39
51
  formAssociated: boolean;
40
52
  observedAttributes: string[];
53
+ attributesAsProperties: [string, Coerce][];
54
+ attachShadow: boolean;
55
+ shadowRootOptions: ShadowRootInit;
41
56
  };
42
57
  type RenderFunction = (props: RenderProps) => DocumentFragment;
43
58
  declare const customElement: (render: RenderFunction, options?: Partial<RenderOptions>) => ElementResult;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,8 @@
1
+ declare global {
2
+ interface Element {
3
+ __findHost: () => Element;
4
+ }
5
+ }
1
6
  declare const html: (strings: TemplateStringsArray, ...values: unknown[]) => DocumentFragment;
2
7
  type Styles = CSSStyleSheet | HTMLStyleElement;
3
8
  declare const css: (strings: TemplateStringsArray, ...values: unknown[]) => Styles;
@@ -13,6 +18,11 @@ declare const createSignal: <T = undefined>(initVal?: T, options?: SignalOptions
13
18
  declare const derived: <T>(fn: () => T) => SignalGetter<T>;
14
19
  declare const createEffect: (fn: () => void) => void;
15
20
 
21
+ declare global {
22
+ interface DocumentFragment {
23
+ host: HTMLElement;
24
+ }
25
+ }
16
26
  type ElementResult = {
17
27
  define: (tagname: `${string}-${string}`) => ElementResult;
18
28
  register: (registry: Registry) => ElementResult;
@@ -21,7 +31,7 @@ type ElementResult = {
21
31
  type AttributeChangedCallback = (name: string, oldValue: string | null, newValue: string | null) => void;
22
32
  type RenderProps = {
23
33
  elementRef: HTMLElement;
24
- root: ShadowRoot;
34
+ root: ShadowRoot | HTMLElement;
25
35
  internals: ElementInternals;
26
36
  attributeChangedCallback: (fn: AttributeChangedCallback) => void;
27
37
  connectedCallback: (fn: () => void) => void;
@@ -32,12 +42,17 @@ type RenderProps = {
32
42
  formStateRestoreCallback: (fn: () => void) => void;
33
43
  customCallback: (fn: () => void) => `{{callback:${string}}}`;
34
44
  attrSignals: Record<string, Signal<string | null>>;
45
+ propSignals: Record<string, Signal<unknown>>;
35
46
  refs: Record<string, HTMLElement | null>;
36
47
  adoptStyleSheet: (stylesheet: Styles) => void;
37
48
  };
49
+ type Coerce<T = unknown> = (value: string) => T;
38
50
  type RenderOptions = {
39
51
  formAssociated: boolean;
40
52
  observedAttributes: string[];
53
+ attributesAsProperties: [string, Coerce][];
54
+ attachShadow: boolean;
55
+ shadowRootOptions: ShadowRootInit;
41
56
  };
42
57
  type RenderFunction = (props: RenderProps) => DocumentFragment;
43
58
  declare const customElement: (render: RenderFunction, options?: Partial<RenderOptions>) => ElementResult;
package/dist/index.js CHANGED
@@ -151,6 +151,11 @@ var html = (strings, ...values) => {
151
151
  child.setAttribute(attr.name, newText);
152
152
  });
153
153
  } else if (callbackBindingRegex.test(attr.value)) {
154
+ const getRootNode = child.getRootNode.bind(child);
155
+ child.getRootNode = () => {
156
+ const rootNode = getRootNode();
157
+ return rootNode instanceof ShadowRoot ? rootNode : fragment;
158
+ };
154
159
  const uniqueKey = attr.value.replace(/\{\{callback:(.+)\}\}/, "$1");
155
160
  child.setAttribute(attr.name, `this.getRootNode().host.__customCallbackFns.get('${uniqueKey}')(event)`);
156
161
  }
@@ -203,12 +208,38 @@ var css = (strings, ...values) => {
203
208
  // src/custom-element.ts
204
209
  var DEFAULT_RENDER_OPTIONS = {
205
210
  formAssociated: false,
206
- observedAttributes: []
211
+ observedAttributes: [],
212
+ attributesAsProperties: [],
213
+ attachShadow: true,
214
+ shadowRootOptions: {
215
+ mode: "closed"
216
+ }
207
217
  };
218
+ var getPropName = (attrName) => attrName.replace(/^([A-Z]+)/, (_, letter) => letter.toLowerCase()).replace(/(-|_| )([a-zA-Z])/g, (_, letter) => letter.toUpperCase());
208
219
  var customElement = (render, options) => {
209
- const { formAssociated, observedAttributes } = { ...DEFAULT_RENDER_OPTIONS, ...options };
220
+ const {
221
+ formAssociated,
222
+ observedAttributes: _observedAttributes,
223
+ attributesAsProperties,
224
+ attachShadow,
225
+ shadowRootOptions: _shadowRootOptions
226
+ } = { ...DEFAULT_RENDER_OPTIONS, ...options };
227
+ const shadowRootOptions = { ...DEFAULT_RENDER_OPTIONS.shadowRootOptions, ..._shadowRootOptions };
228
+ const observedAttributesSet = new Set(_observedAttributes);
229
+ const attributesAsPropertiesMap = /* @__PURE__ */ new Map();
230
+ for (const [attrName, coerce] of attributesAsProperties) {
231
+ observedAttributesSet.add(attrName);
232
+ attributesAsPropertiesMap.set(attrName, {
233
+ prop: getPropName(attrName),
234
+ coerce,
235
+ value: null
236
+ });
237
+ }
238
+ const observedAttributes = Array.from(observedAttributesSet);
210
239
  class CustomElement extends HTMLElement {
240
+ #attributesAsPropertiesMap = new Map(attributesAsPropertiesMap);
211
241
  #attrSignals = {};
242
+ #propSignals = {};
212
243
  #attributeChangedFns = /* @__PURE__ */ new Set();
213
244
  #connectedFns = /* @__PURE__ */ new Set();
214
245
  #disconnectedFns = /* @__PURE__ */ new Set();
@@ -217,26 +248,28 @@ var customElement = (render, options) => {
217
248
  #formResetCallbackFns = /* @__PURE__ */ new Set();
218
249
  #formStateRestoreCallbackFns = /* @__PURE__ */ new Set();
219
250
  __customCallbackFns = /* @__PURE__ */ new Map();
220
- #shadowRoot = this.attachShadow({ mode: "closed" });
251
+ #shadowRoot = attachShadow ? this.attachShadow(shadowRootOptions) : null;
221
252
  #internals = this.attachInternals();
222
- #observer = observedAttributes.length > 0 ? null : new MutationObserver((mutations) => {
253
+ #observer = (options == null ? void 0 : options.observedAttributes) !== void 0 ? null : new MutationObserver((mutations) => {
223
254
  for (const mutation of mutations) {
224
255
  const attrName = mutation.attributeName;
225
256
  if (mutation.type !== "attributes" || attrName === null) continue;
226
- const [value, setValue] = this.#attrSignals[attrName];
227
- const _oldValue = value();
257
+ if (!(attrName in this.#attrSignals)) this.#attrSignals[attrName] = createSignal(null);
258
+ const [getter, setter] = this.#attrSignals[attrName];
259
+ const _oldValue = getter();
228
260
  const oldValue = _oldValue === null ? null : _oldValue;
229
261
  const newValue = this.getAttribute(attrName);
230
- setValue(newValue);
262
+ setter(newValue);
231
263
  for (const fn of this.#attributeChangedFns) {
232
264
  fn(attrName, oldValue, newValue);
233
265
  }
234
266
  }
235
267
  });
236
268
  #render() {
269
+ const root = this.#shadowRoot ?? this;
237
270
  const fragment = render({
238
271
  elementRef: this,
239
- root: this.#shadowRoot,
272
+ root,
240
273
  internals: this.#internals,
241
274
  attributeChangedCallback: (fn) => this.#attributeChangedFns.add(fn),
242
275
  connectedCallback: (fn) => this.#connectedFns.add(fn),
@@ -265,10 +298,28 @@ var customElement = (render, options) => {
265
298
  }
266
299
  }
267
300
  ),
301
+ propSignals: new Proxy(
302
+ {},
303
+ {
304
+ get: (_, prop) => {
305
+ if (!(prop in this.#propSignals)) this.#propSignals[prop] = createSignal(null);
306
+ const [getter, _setter] = this.#propSignals[prop];
307
+ const setter = (newValue) => {
308
+ this[prop] = newValue;
309
+ _setter(newValue);
310
+ };
311
+ return [getter, setter];
312
+ },
313
+ set: () => {
314
+ console.error("Signals must be assigned via setters.");
315
+ return false;
316
+ }
317
+ }
318
+ ),
268
319
  refs: new Proxy(
269
320
  {},
270
321
  {
271
- get: (_, prop) => this.#shadowRoot.querySelector(`[ref=${prop}]`),
322
+ get: (_, prop) => root.querySelector(`[ref=${prop}]`),
272
323
  set: () => {
273
324
  console.error("Refs are readonly and cannot be assigned.");
274
325
  return false;
@@ -276,16 +327,31 @@ var customElement = (render, options) => {
276
327
  }
277
328
  ),
278
329
  adoptStyleSheet: (stylesheet) => {
330
+ if (!attachShadow) {
331
+ console.warn(
332
+ "Styles are only encapsulated when using shadow DOM. The stylesheet will be applied to the global document instead."
333
+ );
334
+ }
279
335
  if (isCSSStyleSheet(stylesheet)) {
336
+ if (this.#shadowRoot === null) {
337
+ for (const rule of stylesheet.cssRules) {
338
+ if (rule instanceof CSSStyleRule && rule.selectorText.includes(":host")) {
339
+ console.error("Styles with :host are not supported when not using shadow DOM.");
340
+ }
341
+ }
342
+ document.adoptedStyleSheets.push(stylesheet);
343
+ return;
344
+ }
280
345
  this.#shadowRoot.adoptedStyleSheets.push(stylesheet);
281
346
  } else {
282
347
  requestAnimationFrame(() => {
283
- this.#shadowRoot.appendChild(stylesheet);
348
+ root.appendChild(stylesheet);
284
349
  });
285
350
  }
286
351
  }
287
352
  });
288
- setInnerHTML(this.#shadowRoot, fragment);
353
+ fragment.host = this;
354
+ setInnerHTML(root, fragment);
289
355
  }
290
356
  static get formAssociated() {
291
357
  return formAssociated;
@@ -295,6 +361,34 @@ var customElement = (render, options) => {
295
361
  }
296
362
  constructor() {
297
363
  super();
364
+ for (const [attrName, attr] of this.#attributesAsPropertiesMap) {
365
+ this.#attrSignals[attrName] = createSignal(null);
366
+ Object.defineProperty(this, attr.prop, {
367
+ get: () => {
368
+ if (!(attrName in this.#attrSignals)) this.#attrSignals[attrName] = createSignal(null);
369
+ const [getter] = this.#attrSignals[attrName];
370
+ const raw = getter();
371
+ const rawOnly = raw !== null && attr.value === null;
372
+ const value = rawOnly ? attr.coerce(raw) : attr.value;
373
+ return value === null ? null : value;
374
+ },
375
+ set: (newValue) => {
376
+ const oldValue = attr.value;
377
+ attr.value = newValue;
378
+ if (!(attrName in this.#attrSignals)) this.#attrSignals[attrName] = createSignal(null);
379
+ const [, attrSetter] = this.#attrSignals[attrName];
380
+ const [, propSetter] = this.#propSignals[attrName];
381
+ const attrValue = newValue === null ? null : String(newValue);
382
+ if (String(oldValue) === attrValue) return;
383
+ attrSetter(attrValue);
384
+ propSetter(newValue);
385
+ if (attrValue === null) this.removeAttribute(attrName);
386
+ else this.setAttribute(attrName, attrValue);
387
+ },
388
+ configurable: true,
389
+ enumerable: true
390
+ });
391
+ }
298
392
  for (const attr of this.attributes) {
299
393
  this.#attrSignals[attr.name] = createSignal(attr.value);
300
394
  }
@@ -317,8 +411,12 @@ var customElement = (render, options) => {
317
411
  }
318
412
  }
319
413
  attributeChangedCallback(name, oldValue, newValue) {
320
- const [, setValue] = this.#attrSignals[name];
321
- setValue(newValue);
414
+ const [, attrSetter] = this.#attrSignals[name] ?? [];
415
+ attrSetter == null ? void 0 : attrSetter(newValue);
416
+ const prop = this.#attributesAsPropertiesMap.get(name);
417
+ if (prop !== void 0) {
418
+ this[prop.prop] = newValue === null ? null : prop.coerce(newValue);
419
+ }
322
420
  for (const fn of this.#attributeChangedFns) {
323
421
  fn(name, oldValue, newValue);
324
422
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thunderous",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",