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 +92 -8
- package/dist/index.cjs +111 -13
- package/dist/index.d.cts +16 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +111 -13
- package/package.json +1 -1
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((
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
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
|
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
|
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 {
|
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(
|
282
|
+
#shadowRoot = attachShadow ? this.attachShadow(shadowRootOptions) : null;
|
252
283
|
#internals = this.attachInternals();
|
253
|
-
#observer = observedAttributes
|
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
|
-
|
258
|
-
const
|
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
|
-
|
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
|
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) =>
|
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
|
-
|
379
|
+
root.appendChild(stylesheet);
|
315
380
|
});
|
316
381
|
}
|
317
382
|
}
|
318
383
|
});
|
319
|
-
|
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 [,
|
352
|
-
|
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 {
|
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(
|
251
|
+
#shadowRoot = attachShadow ? this.attachShadow(shadowRootOptions) : null;
|
221
252
|
#internals = this.attachInternals();
|
222
|
-
#observer = observedAttributes
|
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
|
-
|
227
|
-
const
|
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
|
-
|
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
|
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) =>
|
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
|
-
|
348
|
+
root.appendChild(stylesheet);
|
284
349
|
});
|
285
350
|
}
|
286
351
|
}
|
287
352
|
});
|
288
|
-
|
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 [,
|
321
|
-
|
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
|
}
|