thunderous 0.0.4 → 0.2.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 +69 -58
- package/dist/index.cjs +30 -5
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +30 -5
- package/package.json +1 -1
package/README.md
CHANGED
@@ -34,21 +34,12 @@ const myStyleSheet = css`
|
|
34
34
|
}
|
35
35
|
`;
|
36
36
|
|
37
|
-
const MyElement = customElement((
|
38
|
-
const { connectedCallback, refs, adoptStyleSheet } = params;
|
39
|
-
|
37
|
+
const MyElement = customElement(({ customCallback, refs, adoptStyleSheet }) => {
|
40
38
|
const [count, setCount] = createSignal(0);
|
41
|
-
|
42
|
-
connectedCallback(() => {
|
43
|
-
refs.increment.addEventListener('click', () => {
|
44
|
-
setCount(count() + 1);
|
45
|
-
});
|
46
|
-
});
|
47
|
-
|
39
|
+
const increment = customCallback(() => setCount(count() + 1));
|
48
40
|
adoptStyleSheet(myStyleSheet);
|
49
|
-
|
50
41
|
return html`
|
51
|
-
<button
|
42
|
+
<button onclick="${increment}">Increment</button>
|
52
43
|
<output>${count}</output>
|
53
44
|
`;
|
54
45
|
});
|
@@ -74,7 +65,6 @@ const MyElement = customElement((params) => {
|
|
74
65
|
disconnectedCallback,
|
75
66
|
attributeChangedCallback,
|
76
67
|
} = params;
|
77
|
-
|
78
68
|
/* ... */
|
79
69
|
});
|
80
70
|
```
|
@@ -90,7 +80,6 @@ const MyElement = customElement((params) => {
|
|
90
80
|
formResetCallback,
|
91
81
|
formStateRestoreCallback,
|
92
82
|
} = params;
|
93
|
-
|
94
83
|
/* ... */
|
95
84
|
}, { formAssociated: true });
|
96
85
|
```
|
@@ -102,17 +91,10 @@ You can always define the internals the same as you usually would, and if for so
|
|
102
91
|
|
103
92
|
<!-- prettier-ignore-start -->
|
104
93
|
```ts
|
105
|
-
const MyElement = customElement((
|
106
|
-
const {
|
107
|
-
internals,
|
108
|
-
elementRef,
|
109
|
-
root,
|
110
|
-
} = params;
|
111
|
-
|
94
|
+
const MyElement = customElement(({ internals, elementRef, root }) => {
|
112
95
|
internals.ariaRequired = 'true';
|
113
96
|
const childLink = elementRef.querySelector('a[href]'); // light DOM
|
114
97
|
const innerLink = root.querySelector('a[href]'); // shadow DOM
|
115
|
-
|
116
98
|
/* ... */
|
117
99
|
}, { formAssociated: true });
|
118
100
|
```
|
@@ -137,11 +119,8 @@ const myStyleSheet = css`
|
|
137
119
|
}
|
138
120
|
`;
|
139
121
|
|
140
|
-
const MyElement = customElement((
|
141
|
-
const { adoptStyleSheet } = params;
|
142
|
-
|
122
|
+
const MyElement = customElement(({ adoptStyleSheet }) => {
|
143
123
|
adoptStyleSheet(myStyleSheet);
|
144
|
-
|
145
124
|
/* ... */
|
146
125
|
});
|
147
126
|
```
|
@@ -157,19 +136,12 @@ Creating signals should look pretty familiar to most modern developers.
|
|
157
136
|
|
158
137
|
<!-- prettier-ignore-start -->
|
159
138
|
```ts
|
160
|
-
import { createSignal
|
139
|
+
import { createSignal } from 'thunderous';
|
161
140
|
|
162
|
-
const
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
setCount(1);
|
168
|
-
|
169
|
-
console.log(count()) // 1
|
170
|
-
|
171
|
-
/* ... */
|
172
|
-
});
|
141
|
+
const [count, setCount] = createSignal(0);
|
142
|
+
console.log(count()); // 0
|
143
|
+
setCount(1);
|
144
|
+
console.log(count()) // 1
|
173
145
|
```
|
174
146
|
<!-- prettier-ignore-end -->
|
175
147
|
|
@@ -183,9 +155,7 @@ import { createSignal, customElement, html } from 'thunderous';
|
|
183
155
|
|
184
156
|
const MyElement = customElement(() => {
|
185
157
|
const [count, setCount] = createSignal(0);
|
186
|
-
|
187
158
|
// presumably setCount() gets called
|
188
|
-
|
189
159
|
return html`<output>${count}</output>`;
|
190
160
|
});
|
191
161
|
```
|
@@ -199,22 +169,29 @@ By binding signals to templates, you allow fine-grained updates to be made direc
|
|
199
169
|
|
200
170
|
##### Attribute Signals
|
201
171
|
|
202
|
-
|
172
|
+
By default, each element is observed with a `MutationObserver` watching all attributes. Changes to _any_ attribute trigger the `attributeChangedCallback` and you can access all attributes as signals. This makes it much less cumbersome to write reactive attributes.
|
203
173
|
|
204
174
|
<!-- prettier-ignore-start -->
|
205
175
|
```ts
|
206
|
-
const MyElement = customElement((
|
207
|
-
const { attrSignals } = params;
|
208
|
-
|
176
|
+
const MyElement = customElement(({ attrSignals }) => {
|
209
177
|
const [heading, setHeading] = attrSignals['my-heading'];
|
210
|
-
|
211
178
|
// setHeading() will also update the attribute in the DOM.
|
212
|
-
|
213
179
|
return html`<h2>${heading}</h2>`;
|
214
180
|
});
|
215
181
|
```
|
216
182
|
<!-- prettier-ignore-end -->
|
217
183
|
|
184
|
+
However, the `MutationObserver` does impose a small performance tradeoff that may add up if you render a lot of elements. To better optimize for performance, you can pass `observedAttributes` to the options. Doing so will disable the `MutationObserver`, and only the observed attributes will trigger the `attributeChangedCallback`.
|
185
|
+
|
186
|
+
<!-- prettier-ignore-start -->
|
187
|
+
```ts
|
188
|
+
const MyElement = customElement(({ attrSignals }) => {
|
189
|
+
const [heading, setHeading] = attrSignals['my-heading'];
|
190
|
+
return html`<h2>${heading}</h2>`;
|
191
|
+
}, { observedAttributes: ['my-heading'] });
|
192
|
+
```
|
193
|
+
<!-- prettier-ignore-end -->
|
194
|
+
|
218
195
|
Usage:
|
219
196
|
|
220
197
|
```html
|
@@ -223,33 +200,67 @@ Usage:
|
|
223
200
|
|
224
201
|
> NOTICE: Since `attrSignals` is a `Proxy` object, _any_ property will return a signal and auto-bind it to the attribute it corresponds with.
|
225
202
|
|
226
|
-
|
203
|
+
##### Derived Signals
|
227
204
|
|
228
|
-
|
205
|
+
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.
|
229
206
|
|
230
|
-
<!-- prettier-ignore-start -->
|
231
207
|
```ts
|
232
|
-
|
233
|
-
const { connectedCallback, refs } = params;
|
208
|
+
import { derived, createSignal } from 'thunderous';
|
234
209
|
|
235
|
-
|
210
|
+
const [count, setCount] = createSignal(0);
|
211
|
+
const timesTen = derived(() => count() * 10);
|
212
|
+
console.log(timesTen()); // 0
|
213
|
+
setCount(10);
|
214
|
+
console.log(timesTen()); // 100
|
215
|
+
```
|
216
|
+
|
217
|
+
##### Effects
|
218
|
+
|
219
|
+
To run a callback each time a signal is changed, use the `createEffect()` function. Any signal used inside will trigger the callback when they're changed.
|
220
|
+
|
221
|
+
```ts
|
222
|
+
import { createEffect } from 'thunderous';
|
223
|
+
|
224
|
+
/* ... */
|
225
|
+
createEffect(() => {
|
226
|
+
console.log(count());
|
227
|
+
});
|
228
|
+
```
|
236
229
|
|
230
|
+
#### Refs
|
231
|
+
|
232
|
+
The refs property exists for convenience to avoid manually querying the DOM. Since the DOM is only available after rendering, refs will only work in and after the `connectedCallback` method.
|
233
|
+
|
234
|
+
<!-- prettier-ignore-start -->
|
235
|
+
```ts
|
236
|
+
const MyElement = customElement(({ connectedCallback, refs }) => {
|
237
237
|
connectedCallback(() => {
|
238
|
-
refs.
|
239
|
-
setCount(count() + 1);
|
240
|
-
});
|
238
|
+
console.log(refs.heading.textContent); // hello world
|
241
239
|
});
|
240
|
+
return html`<h2 ref="heading">hello world</h2>`;
|
241
|
+
});
|
242
|
+
```
|
243
|
+
<!-- prettier-ignore-end -->
|
242
244
|
|
245
|
+
#### Event Binding
|
246
|
+
|
247
|
+
While you could bind events in the `connectedCallback()` with `refs.button.addEventListener('click', handleClick)` for example, it may be more convenient to register a custom callback and bind it to the template.
|
248
|
+
|
249
|
+
<!-- prettier-ignore-start -->
|
250
|
+
```ts
|
251
|
+
const MyElement = customElement(({ customCallback }) => {
|
252
|
+
const [count, setCount] = createSignal(0);
|
253
|
+
const increment = customCallback(() => setCount(count() + 1));
|
243
254
|
return html`
|
244
|
-
<button
|
255
|
+
<button onclick="${increment}">Increment</button>
|
245
256
|
<output>${count}</output>
|
246
257
|
`;
|
247
258
|
});
|
248
|
-
|
249
|
-
MyElement.define('my-element');
|
250
259
|
```
|
251
260
|
<!-- prettier-ignore-end -->
|
252
261
|
|
262
|
+
> 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.
|
263
|
+
|
253
264
|
### Defining Custom Elements
|
254
265
|
|
255
266
|
The `customElement()` function allows you to author a web component, returning an `ElementResult` that has some helpful methods like `define()` and `eject()`.
|
package/dist/index.cjs
CHANGED
@@ -80,10 +80,11 @@ var createEffect = (fn) => {
|
|
80
80
|
|
81
81
|
// src/custom-element.ts
|
82
82
|
var DEFAULT_RENDER_OPTIONS = {
|
83
|
-
formAssociated: false
|
83
|
+
formAssociated: false,
|
84
|
+
observedAttributes: []
|
84
85
|
};
|
85
86
|
var customElement = (render, options) => {
|
86
|
-
const { formAssociated } = { ...DEFAULT_RENDER_OPTIONS, ...options };
|
87
|
+
const { formAssociated, observedAttributes } = { ...DEFAULT_RENDER_OPTIONS, ...options };
|
87
88
|
class CustomElement extends HTMLElement {
|
88
89
|
#attrSignals = {};
|
89
90
|
#attributeChangedFns = /* @__PURE__ */ new Set();
|
@@ -93,9 +94,10 @@ var customElement = (render, options) => {
|
|
93
94
|
#formDisabledCallbackFns = /* @__PURE__ */ new Set();
|
94
95
|
#formResetCallbackFns = /* @__PURE__ */ new Set();
|
95
96
|
#formStateRestoreCallbackFns = /* @__PURE__ */ new Set();
|
97
|
+
__customCallbackFns = /* @__PURE__ */ new Map();
|
96
98
|
#shadowRoot = this.attachShadow({ mode: "closed" });
|
97
99
|
#internals = this.attachInternals();
|
98
|
-
#observer = new MutationObserver((mutations) => {
|
100
|
+
#observer = observedAttributes.length > 0 ? null : new MutationObserver((mutations) => {
|
99
101
|
for (const mutation of mutations) {
|
100
102
|
const attrName = mutation.attributeName;
|
101
103
|
if (mutation.type !== "attributes" || attrName === null) continue;
|
@@ -121,6 +123,11 @@ var customElement = (render, options) => {
|
|
121
123
|
formDisabledCallback: (fn) => this.#formDisabledCallbackFns.add(fn),
|
122
124
|
formResetCallback: (fn) => this.#formResetCallbackFns.add(fn),
|
123
125
|
formStateRestoreCallback: (fn) => this.#formStateRestoreCallbackFns.add(fn),
|
126
|
+
customCallback: (fn) => {
|
127
|
+
const key = crypto.randomUUID();
|
128
|
+
this.__customCallbackFns.set(key, fn);
|
129
|
+
return `{{callback:${key}}}`;
|
130
|
+
},
|
124
131
|
attrSignals: new Proxy(
|
125
132
|
{},
|
126
133
|
{
|
@@ -155,6 +162,9 @@ var customElement = (render, options) => {
|
|
155
162
|
static get formAssociated() {
|
156
163
|
return formAssociated;
|
157
164
|
}
|
165
|
+
static get observedAttributes() {
|
166
|
+
return observedAttributes;
|
167
|
+
}
|
158
168
|
constructor() {
|
159
169
|
super();
|
160
170
|
for (const attr of this.attributes) {
|
@@ -163,17 +173,28 @@ var customElement = (render, options) => {
|
|
163
173
|
this.#render();
|
164
174
|
}
|
165
175
|
connectedCallback() {
|
166
|
-
this.#observer
|
176
|
+
if (this.#observer !== null) {
|
177
|
+
this.#observer.observe(this, { attributes: true });
|
178
|
+
}
|
167
179
|
for (const fn of this.#connectedFns) {
|
168
180
|
fn();
|
169
181
|
}
|
170
182
|
}
|
171
183
|
disconnectedCallback() {
|
172
|
-
this.#observer
|
184
|
+
if (this.#observer !== null) {
|
185
|
+
this.#observer.disconnect();
|
186
|
+
}
|
173
187
|
for (const fn of this.#disconnectedFns) {
|
174
188
|
fn();
|
175
189
|
}
|
176
190
|
}
|
191
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
192
|
+
const [, setValue] = this.#attrSignals[name];
|
193
|
+
setValue(newValue);
|
194
|
+
for (const fn of this.#attributeChangedFns) {
|
195
|
+
fn(name, oldValue, newValue);
|
196
|
+
}
|
197
|
+
}
|
177
198
|
adoptedCallback() {
|
178
199
|
for (const fn of this.#adoptedCallbackFns) {
|
179
200
|
fn();
|
@@ -245,6 +266,7 @@ var html = (strings, ...values) => {
|
|
245
266
|
innerHTML += string + String(value);
|
246
267
|
});
|
247
268
|
const fragment = parseFragment(innerHTML);
|
269
|
+
const callbackBindingRegex = /(\{\{callback:.+\}\})/;
|
248
270
|
const signalBindingRegex = /(\{\{signal:.+\}\})/;
|
249
271
|
const parseChildren = (element) => {
|
250
272
|
for (const child of element.childNodes) {
|
@@ -280,6 +302,9 @@ var html = (strings, ...values) => {
|
|
280
302
|
}
|
281
303
|
child.setAttribute(attr.name, newText);
|
282
304
|
});
|
305
|
+
} else if (callbackBindingRegex.test(attr.value)) {
|
306
|
+
const uniqueKey = attr.value.replace(/\{\{callback:(.+)\}\}/, "$1");
|
307
|
+
child.setAttribute(attr.name, `this.getRootNode().host.__customCallbackFns.get('${uniqueKey}')(event)`);
|
283
308
|
}
|
284
309
|
}
|
285
310
|
parseChildren(child);
|
package/dist/index.d.cts
CHANGED
@@ -22,15 +22,17 @@ type RenderProps = {
|
|
22
22
|
formDisabledCallback: (fn: () => void) => void;
|
23
23
|
formResetCallback: (fn: () => void) => void;
|
24
24
|
formStateRestoreCallback: (fn: () => void) => void;
|
25
|
+
customCallback: (fn: () => void) => `{{callback:${string}}}`;
|
25
26
|
attrSignals: Record<string, Signal<string | null>>;
|
26
27
|
refs: Record<string, HTMLElement | null>;
|
27
28
|
adoptStyleSheet: (stylesheet: CSSStyleSheet) => void;
|
28
29
|
};
|
29
30
|
type RenderOptions = {
|
30
|
-
formAssociated
|
31
|
+
formAssociated: boolean;
|
32
|
+
observedAttributes: string[];
|
31
33
|
};
|
32
34
|
type RenderFunction = (props: RenderProps) => DocumentFragment;
|
33
|
-
declare const customElement: (render: RenderFunction, options?: RenderOptions) => ElementResult;
|
35
|
+
declare const customElement: (render: RenderFunction, options?: Partial<RenderOptions>) => ElementResult;
|
34
36
|
type Registry = {
|
35
37
|
register: (tagName: string, element: CustomElementConstructor) => void;
|
36
38
|
getTagName: (element: CustomElementConstructor) => string | undefined;
|
package/dist/index.d.ts
CHANGED
@@ -22,15 +22,17 @@ type RenderProps = {
|
|
22
22
|
formDisabledCallback: (fn: () => void) => void;
|
23
23
|
formResetCallback: (fn: () => void) => void;
|
24
24
|
formStateRestoreCallback: (fn: () => void) => void;
|
25
|
+
customCallback: (fn: () => void) => `{{callback:${string}}}`;
|
25
26
|
attrSignals: Record<string, Signal<string | null>>;
|
26
27
|
refs: Record<string, HTMLElement | null>;
|
27
28
|
adoptStyleSheet: (stylesheet: CSSStyleSheet) => void;
|
28
29
|
};
|
29
30
|
type RenderOptions = {
|
30
|
-
formAssociated
|
31
|
+
formAssociated: boolean;
|
32
|
+
observedAttributes: string[];
|
31
33
|
};
|
32
34
|
type RenderFunction = (props: RenderProps) => DocumentFragment;
|
33
|
-
declare const customElement: (render: RenderFunction, options?: RenderOptions) => ElementResult;
|
35
|
+
declare const customElement: (render: RenderFunction, options?: Partial<RenderOptions>) => ElementResult;
|
34
36
|
type Registry = {
|
35
37
|
register: (tagName: string, element: CustomElementConstructor) => void;
|
36
38
|
getTagName: (element: CustomElementConstructor) => string | undefined;
|
package/dist/index.js
CHANGED
@@ -49,10 +49,11 @@ var createEffect = (fn) => {
|
|
49
49
|
|
50
50
|
// src/custom-element.ts
|
51
51
|
var DEFAULT_RENDER_OPTIONS = {
|
52
|
-
formAssociated: false
|
52
|
+
formAssociated: false,
|
53
|
+
observedAttributes: []
|
53
54
|
};
|
54
55
|
var customElement = (render, options) => {
|
55
|
-
const { formAssociated } = { ...DEFAULT_RENDER_OPTIONS, ...options };
|
56
|
+
const { formAssociated, observedAttributes } = { ...DEFAULT_RENDER_OPTIONS, ...options };
|
56
57
|
class CustomElement extends HTMLElement {
|
57
58
|
#attrSignals = {};
|
58
59
|
#attributeChangedFns = /* @__PURE__ */ new Set();
|
@@ -62,9 +63,10 @@ var customElement = (render, options) => {
|
|
62
63
|
#formDisabledCallbackFns = /* @__PURE__ */ new Set();
|
63
64
|
#formResetCallbackFns = /* @__PURE__ */ new Set();
|
64
65
|
#formStateRestoreCallbackFns = /* @__PURE__ */ new Set();
|
66
|
+
__customCallbackFns = /* @__PURE__ */ new Map();
|
65
67
|
#shadowRoot = this.attachShadow({ mode: "closed" });
|
66
68
|
#internals = this.attachInternals();
|
67
|
-
#observer = new MutationObserver((mutations) => {
|
69
|
+
#observer = observedAttributes.length > 0 ? null : new MutationObserver((mutations) => {
|
68
70
|
for (const mutation of mutations) {
|
69
71
|
const attrName = mutation.attributeName;
|
70
72
|
if (mutation.type !== "attributes" || attrName === null) continue;
|
@@ -90,6 +92,11 @@ var customElement = (render, options) => {
|
|
90
92
|
formDisabledCallback: (fn) => this.#formDisabledCallbackFns.add(fn),
|
91
93
|
formResetCallback: (fn) => this.#formResetCallbackFns.add(fn),
|
92
94
|
formStateRestoreCallback: (fn) => this.#formStateRestoreCallbackFns.add(fn),
|
95
|
+
customCallback: (fn) => {
|
96
|
+
const key = crypto.randomUUID();
|
97
|
+
this.__customCallbackFns.set(key, fn);
|
98
|
+
return `{{callback:${key}}}`;
|
99
|
+
},
|
93
100
|
attrSignals: new Proxy(
|
94
101
|
{},
|
95
102
|
{
|
@@ -124,6 +131,9 @@ var customElement = (render, options) => {
|
|
124
131
|
static get formAssociated() {
|
125
132
|
return formAssociated;
|
126
133
|
}
|
134
|
+
static get observedAttributes() {
|
135
|
+
return observedAttributes;
|
136
|
+
}
|
127
137
|
constructor() {
|
128
138
|
super();
|
129
139
|
for (const attr of this.attributes) {
|
@@ -132,17 +142,28 @@ var customElement = (render, options) => {
|
|
132
142
|
this.#render();
|
133
143
|
}
|
134
144
|
connectedCallback() {
|
135
|
-
this.#observer
|
145
|
+
if (this.#observer !== null) {
|
146
|
+
this.#observer.observe(this, { attributes: true });
|
147
|
+
}
|
136
148
|
for (const fn of this.#connectedFns) {
|
137
149
|
fn();
|
138
150
|
}
|
139
151
|
}
|
140
152
|
disconnectedCallback() {
|
141
|
-
this.#observer
|
153
|
+
if (this.#observer !== null) {
|
154
|
+
this.#observer.disconnect();
|
155
|
+
}
|
142
156
|
for (const fn of this.#disconnectedFns) {
|
143
157
|
fn();
|
144
158
|
}
|
145
159
|
}
|
160
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
161
|
+
const [, setValue] = this.#attrSignals[name];
|
162
|
+
setValue(newValue);
|
163
|
+
for (const fn of this.#attributeChangedFns) {
|
164
|
+
fn(name, oldValue, newValue);
|
165
|
+
}
|
166
|
+
}
|
146
167
|
adoptedCallback() {
|
147
168
|
for (const fn of this.#adoptedCallbackFns) {
|
148
169
|
fn();
|
@@ -214,6 +235,7 @@ var html = (strings, ...values) => {
|
|
214
235
|
innerHTML += string + String(value);
|
215
236
|
});
|
216
237
|
const fragment = parseFragment(innerHTML);
|
238
|
+
const callbackBindingRegex = /(\{\{callback:.+\}\})/;
|
217
239
|
const signalBindingRegex = /(\{\{signal:.+\}\})/;
|
218
240
|
const parseChildren = (element) => {
|
219
241
|
for (const child of element.childNodes) {
|
@@ -249,6 +271,9 @@ var html = (strings, ...values) => {
|
|
249
271
|
}
|
250
272
|
child.setAttribute(attr.name, newText);
|
251
273
|
});
|
274
|
+
} else if (callbackBindingRegex.test(attr.value)) {
|
275
|
+
const uniqueKey = attr.value.replace(/\{\{callback:(.+)\}\}/, "$1");
|
276
|
+
child.setAttribute(attr.name, `this.getRootNode().host.__customCallbackFns.get('${uniqueKey}')(event)`);
|
252
277
|
}
|
253
278
|
}
|
254
279
|
parseChildren(child);
|