thunderous 0.1.0 → 0.2.1
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 +18 -45
- package/dist/index.cjs +36 -6
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +36 -6
- package/package.json +1 -1
package/README.md
CHANGED
@@ -34,15 +34,10 @@ const myStyleSheet = css`
|
|
34
34
|
}
|
35
35
|
`;
|
36
36
|
|
37
|
-
const MyElement = customElement((
|
38
|
-
const { customCallback, refs, adoptStyleSheet } = params;
|
39
|
-
|
37
|
+
const MyElement = customElement(({ customCallback, refs, adoptStyleSheet }) => {
|
40
38
|
const [count, setCount] = createSignal(0);
|
41
|
-
|
42
39
|
const increment = customCallback(() => setCount(count() + 1));
|
43
|
-
|
44
40
|
adoptStyleSheet(myStyleSheet);
|
45
|
-
|
46
41
|
return html`
|
47
42
|
<button onclick="${increment}">Increment</button>
|
48
43
|
<output>${count}</output>
|
@@ -70,7 +65,6 @@ const MyElement = customElement((params) => {
|
|
70
65
|
disconnectedCallback,
|
71
66
|
attributeChangedCallback,
|
72
67
|
} = params;
|
73
|
-
|
74
68
|
/* ... */
|
75
69
|
});
|
76
70
|
```
|
@@ -86,7 +80,6 @@ const MyElement = customElement((params) => {
|
|
86
80
|
formResetCallback,
|
87
81
|
formStateRestoreCallback,
|
88
82
|
} = params;
|
89
|
-
|
90
83
|
/* ... */
|
91
84
|
}, { formAssociated: true });
|
92
85
|
```
|
@@ -98,17 +91,10 @@ You can always define the internals the same as you usually would, and if for so
|
|
98
91
|
|
99
92
|
<!-- prettier-ignore-start -->
|
100
93
|
```ts
|
101
|
-
const MyElement = customElement((
|
102
|
-
const {
|
103
|
-
internals,
|
104
|
-
elementRef,
|
105
|
-
root,
|
106
|
-
} = params;
|
107
|
-
|
94
|
+
const MyElement = customElement(({ internals, elementRef, root }) => {
|
108
95
|
internals.ariaRequired = 'true';
|
109
96
|
const childLink = elementRef.querySelector('a[href]'); // light DOM
|
110
97
|
const innerLink = root.querySelector('a[href]'); // shadow DOM
|
111
|
-
|
112
98
|
/* ... */
|
113
99
|
}, { formAssociated: true });
|
114
100
|
```
|
@@ -133,11 +119,8 @@ const myStyleSheet = css`
|
|
133
119
|
}
|
134
120
|
`;
|
135
121
|
|
136
|
-
const MyElement = customElement((
|
137
|
-
const { adoptStyleSheet } = params;
|
138
|
-
|
122
|
+
const MyElement = customElement(({ adoptStyleSheet }) => {
|
139
123
|
adoptStyleSheet(myStyleSheet);
|
140
|
-
|
141
124
|
/* ... */
|
142
125
|
});
|
143
126
|
```
|
@@ -156,11 +139,8 @@ Creating signals should look pretty familiar to most modern developers.
|
|
156
139
|
import { createSignal } from 'thunderous';
|
157
140
|
|
158
141
|
const [count, setCount] = createSignal(0);
|
159
|
-
|
160
142
|
console.log(count()); // 0
|
161
|
-
|
162
143
|
setCount(1);
|
163
|
-
|
164
144
|
console.log(count()) // 1
|
165
145
|
```
|
166
146
|
<!-- prettier-ignore-end -->
|
@@ -175,9 +155,7 @@ import { createSignal, customElement, html } from 'thunderous';
|
|
175
155
|
|
176
156
|
const MyElement = customElement(() => {
|
177
157
|
const [count, setCount] = createSignal(0);
|
178
|
-
|
179
158
|
// presumably setCount() gets called
|
180
|
-
|
181
159
|
return html`<output>${count}</output>`;
|
182
160
|
});
|
183
161
|
```
|
@@ -191,22 +169,29 @@ By binding signals to templates, you allow fine-grained updates to be made direc
|
|
191
169
|
|
192
170
|
##### Attribute Signals
|
193
171
|
|
194
|
-
|
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.
|
195
173
|
|
196
174
|
<!-- prettier-ignore-start -->
|
197
175
|
```ts
|
198
|
-
const MyElement = customElement((
|
199
|
-
const { attrSignals } = params;
|
200
|
-
|
176
|
+
const MyElement = customElement(({ attrSignals }) => {
|
201
177
|
const [heading, setHeading] = attrSignals['my-heading'];
|
202
|
-
|
203
178
|
// setHeading() will also update the attribute in the DOM.
|
204
|
-
|
205
179
|
return html`<h2>${heading}</h2>`;
|
206
180
|
});
|
207
181
|
```
|
208
182
|
<!-- prettier-ignore-end -->
|
209
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
|
+
|
210
195
|
Usage:
|
211
196
|
|
212
197
|
```html
|
@@ -223,13 +208,9 @@ If you want to calculate a value based on another signal's value, you should use
|
|
223
208
|
import { derived, createSignal } from 'thunderous';
|
224
209
|
|
225
210
|
const [count, setCount] = createSignal(0);
|
226
|
-
|
227
211
|
const timesTen = derived(() => count() * 10);
|
228
|
-
|
229
212
|
console.log(timesTen()); // 0
|
230
|
-
|
231
213
|
setCount(10);
|
232
|
-
|
233
214
|
console.log(timesTen()); // 100
|
234
215
|
```
|
235
216
|
|
@@ -241,7 +222,6 @@ To run a callback each time a signal is changed, use the `createEffect()` functi
|
|
241
222
|
import { createEffect } from 'thunderous';
|
242
223
|
|
243
224
|
/* ... */
|
244
|
-
|
245
225
|
createEffect(() => {
|
246
226
|
console.log(count());
|
247
227
|
});
|
@@ -253,13 +233,10 @@ The refs property exists for convenience to avoid manually querying the DOM. Sin
|
|
253
233
|
|
254
234
|
<!-- prettier-ignore-start -->
|
255
235
|
```ts
|
256
|
-
const MyElement = customElement((
|
257
|
-
const { connectedCallback, refs } = params;
|
258
|
-
|
236
|
+
const MyElement = customElement(({ connectedCallback, refs }) => {
|
259
237
|
connectedCallback(() => {
|
260
238
|
console.log(refs.heading.textContent); // hello world
|
261
239
|
});
|
262
|
-
|
263
240
|
return html`<h2 ref="heading">hello world</h2>`;
|
264
241
|
});
|
265
242
|
```
|
@@ -271,13 +248,9 @@ While you could bind events in the `connectedCallback()` with `refs.button.addEv
|
|
271
248
|
|
272
249
|
<!-- prettier-ignore-start -->
|
273
250
|
```ts
|
274
|
-
const MyElement = customElement((
|
275
|
-
const { customCallback } = params;
|
276
|
-
|
251
|
+
const MyElement = customElement(({ customCallback }) => {
|
277
252
|
const [count, setCount] = createSignal(0);
|
278
|
-
|
279
253
|
const increment = customCallback(() => setCount(count() + 1));
|
280
|
-
|
281
254
|
return html`
|
282
255
|
<button onclick="${increment}">Increment</button>
|
283
256
|
<output>${count}</output>
|
package/dist/index.cjs
CHANGED
@@ -48,6 +48,8 @@ var setInnerHTML = (element, html2) => {
|
|
48
48
|
|
49
49
|
// src/signals.ts
|
50
50
|
var subscriber = null;
|
51
|
+
var updateQueue = /* @__PURE__ */ new Set();
|
52
|
+
var isBatchingUpdates = false;
|
51
53
|
var createSignal = (initVal) => {
|
52
54
|
const subscribers = /* @__PURE__ */ new Set();
|
53
55
|
let value = initVal;
|
@@ -58,9 +60,22 @@ var createSignal = (initVal) => {
|
|
58
60
|
return value;
|
59
61
|
};
|
60
62
|
const setter = (newValue) => {
|
63
|
+
const isObject = typeof newValue === "object" && newValue !== null;
|
64
|
+
if (!isObject && value === newValue) return;
|
61
65
|
value = newValue;
|
62
66
|
for (const fn of subscribers) {
|
63
|
-
fn
|
67
|
+
updateQueue.add(fn);
|
68
|
+
}
|
69
|
+
if (!isBatchingUpdates) {
|
70
|
+
isBatchingUpdates = true;
|
71
|
+
requestAnimationFrame(() => {
|
72
|
+
console.log("next animation frame");
|
73
|
+
for (const fn of updateQueue) {
|
74
|
+
fn();
|
75
|
+
}
|
76
|
+
updateQueue.clear();
|
77
|
+
isBatchingUpdates = false;
|
78
|
+
});
|
64
79
|
}
|
65
80
|
};
|
66
81
|
return [getter, setter];
|
@@ -80,10 +95,11 @@ var createEffect = (fn) => {
|
|
80
95
|
|
81
96
|
// src/custom-element.ts
|
82
97
|
var DEFAULT_RENDER_OPTIONS = {
|
83
|
-
formAssociated: false
|
98
|
+
formAssociated: false,
|
99
|
+
observedAttributes: []
|
84
100
|
};
|
85
101
|
var customElement = (render, options) => {
|
86
|
-
const { formAssociated } = { ...DEFAULT_RENDER_OPTIONS, ...options };
|
102
|
+
const { formAssociated, observedAttributes } = { ...DEFAULT_RENDER_OPTIONS, ...options };
|
87
103
|
class CustomElement extends HTMLElement {
|
88
104
|
#attrSignals = {};
|
89
105
|
#attributeChangedFns = /* @__PURE__ */ new Set();
|
@@ -96,7 +112,7 @@ var customElement = (render, options) => {
|
|
96
112
|
__customCallbackFns = /* @__PURE__ */ new Map();
|
97
113
|
#shadowRoot = this.attachShadow({ mode: "closed" });
|
98
114
|
#internals = this.attachInternals();
|
99
|
-
#observer = new MutationObserver((mutations) => {
|
115
|
+
#observer = observedAttributes.length > 0 ? null : new MutationObserver((mutations) => {
|
100
116
|
for (const mutation of mutations) {
|
101
117
|
const attrName = mutation.attributeName;
|
102
118
|
if (mutation.type !== "attributes" || attrName === null) continue;
|
@@ -161,6 +177,9 @@ var customElement = (render, options) => {
|
|
161
177
|
static get formAssociated() {
|
162
178
|
return formAssociated;
|
163
179
|
}
|
180
|
+
static get observedAttributes() {
|
181
|
+
return observedAttributes;
|
182
|
+
}
|
164
183
|
constructor() {
|
165
184
|
super();
|
166
185
|
for (const attr of this.attributes) {
|
@@ -169,17 +188,28 @@ var customElement = (render, options) => {
|
|
169
188
|
this.#render();
|
170
189
|
}
|
171
190
|
connectedCallback() {
|
172
|
-
this.#observer
|
191
|
+
if (this.#observer !== null) {
|
192
|
+
this.#observer.observe(this, { attributes: true });
|
193
|
+
}
|
173
194
|
for (const fn of this.#connectedFns) {
|
174
195
|
fn();
|
175
196
|
}
|
176
197
|
}
|
177
198
|
disconnectedCallback() {
|
178
|
-
this.#observer
|
199
|
+
if (this.#observer !== null) {
|
200
|
+
this.#observer.disconnect();
|
201
|
+
}
|
179
202
|
for (const fn of this.#disconnectedFns) {
|
180
203
|
fn();
|
181
204
|
}
|
182
205
|
}
|
206
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
207
|
+
const [, setValue] = this.#attrSignals[name];
|
208
|
+
setValue(newValue);
|
209
|
+
for (const fn of this.#attributeChangedFns) {
|
210
|
+
fn(name, oldValue, newValue);
|
211
|
+
}
|
212
|
+
}
|
183
213
|
adoptedCallback() {
|
184
214
|
for (const fn of this.#adoptedCallbackFns) {
|
185
215
|
fn();
|
package/dist/index.d.cts
CHANGED
@@ -28,10 +28,11 @@ type RenderProps = {
|
|
28
28
|
adoptStyleSheet: (stylesheet: CSSStyleSheet) => void;
|
29
29
|
};
|
30
30
|
type RenderOptions = {
|
31
|
-
formAssociated
|
31
|
+
formAssociated: boolean;
|
32
|
+
observedAttributes: string[];
|
32
33
|
};
|
33
34
|
type RenderFunction = (props: RenderProps) => DocumentFragment;
|
34
|
-
declare const customElement: (render: RenderFunction, options?: RenderOptions) => ElementResult;
|
35
|
+
declare const customElement: (render: RenderFunction, options?: Partial<RenderOptions>) => ElementResult;
|
35
36
|
type Registry = {
|
36
37
|
register: (tagName: string, element: CustomElementConstructor) => void;
|
37
38
|
getTagName: (element: CustomElementConstructor) => string | undefined;
|
package/dist/index.d.ts
CHANGED
@@ -28,10 +28,11 @@ type RenderProps = {
|
|
28
28
|
adoptStyleSheet: (stylesheet: CSSStyleSheet) => void;
|
29
29
|
};
|
30
30
|
type RenderOptions = {
|
31
|
-
formAssociated
|
31
|
+
formAssociated: boolean;
|
32
|
+
observedAttributes: string[];
|
32
33
|
};
|
33
34
|
type RenderFunction = (props: RenderProps) => DocumentFragment;
|
34
|
-
declare const customElement: (render: RenderFunction, options?: RenderOptions) => ElementResult;
|
35
|
+
declare const customElement: (render: RenderFunction, options?: Partial<RenderOptions>) => ElementResult;
|
35
36
|
type Registry = {
|
36
37
|
register: (tagName: string, element: CustomElementConstructor) => void;
|
37
38
|
getTagName: (element: CustomElementConstructor) => string | undefined;
|
package/dist/index.js
CHANGED
@@ -17,6 +17,8 @@ var setInnerHTML = (element, html2) => {
|
|
17
17
|
|
18
18
|
// src/signals.ts
|
19
19
|
var subscriber = null;
|
20
|
+
var updateQueue = /* @__PURE__ */ new Set();
|
21
|
+
var isBatchingUpdates = false;
|
20
22
|
var createSignal = (initVal) => {
|
21
23
|
const subscribers = /* @__PURE__ */ new Set();
|
22
24
|
let value = initVal;
|
@@ -27,9 +29,22 @@ var createSignal = (initVal) => {
|
|
27
29
|
return value;
|
28
30
|
};
|
29
31
|
const setter = (newValue) => {
|
32
|
+
const isObject = typeof newValue === "object" && newValue !== null;
|
33
|
+
if (!isObject && value === newValue) return;
|
30
34
|
value = newValue;
|
31
35
|
for (const fn of subscribers) {
|
32
|
-
fn
|
36
|
+
updateQueue.add(fn);
|
37
|
+
}
|
38
|
+
if (!isBatchingUpdates) {
|
39
|
+
isBatchingUpdates = true;
|
40
|
+
requestAnimationFrame(() => {
|
41
|
+
console.log("next animation frame");
|
42
|
+
for (const fn of updateQueue) {
|
43
|
+
fn();
|
44
|
+
}
|
45
|
+
updateQueue.clear();
|
46
|
+
isBatchingUpdates = false;
|
47
|
+
});
|
33
48
|
}
|
34
49
|
};
|
35
50
|
return [getter, setter];
|
@@ -49,10 +64,11 @@ var createEffect = (fn) => {
|
|
49
64
|
|
50
65
|
// src/custom-element.ts
|
51
66
|
var DEFAULT_RENDER_OPTIONS = {
|
52
|
-
formAssociated: false
|
67
|
+
formAssociated: false,
|
68
|
+
observedAttributes: []
|
53
69
|
};
|
54
70
|
var customElement = (render, options) => {
|
55
|
-
const { formAssociated } = { ...DEFAULT_RENDER_OPTIONS, ...options };
|
71
|
+
const { formAssociated, observedAttributes } = { ...DEFAULT_RENDER_OPTIONS, ...options };
|
56
72
|
class CustomElement extends HTMLElement {
|
57
73
|
#attrSignals = {};
|
58
74
|
#attributeChangedFns = /* @__PURE__ */ new Set();
|
@@ -65,7 +81,7 @@ var customElement = (render, options) => {
|
|
65
81
|
__customCallbackFns = /* @__PURE__ */ new Map();
|
66
82
|
#shadowRoot = this.attachShadow({ mode: "closed" });
|
67
83
|
#internals = this.attachInternals();
|
68
|
-
#observer = new MutationObserver((mutations) => {
|
84
|
+
#observer = observedAttributes.length > 0 ? null : new MutationObserver((mutations) => {
|
69
85
|
for (const mutation of mutations) {
|
70
86
|
const attrName = mutation.attributeName;
|
71
87
|
if (mutation.type !== "attributes" || attrName === null) continue;
|
@@ -130,6 +146,9 @@ var customElement = (render, options) => {
|
|
130
146
|
static get formAssociated() {
|
131
147
|
return formAssociated;
|
132
148
|
}
|
149
|
+
static get observedAttributes() {
|
150
|
+
return observedAttributes;
|
151
|
+
}
|
133
152
|
constructor() {
|
134
153
|
super();
|
135
154
|
for (const attr of this.attributes) {
|
@@ -138,17 +157,28 @@ var customElement = (render, options) => {
|
|
138
157
|
this.#render();
|
139
158
|
}
|
140
159
|
connectedCallback() {
|
141
|
-
this.#observer
|
160
|
+
if (this.#observer !== null) {
|
161
|
+
this.#observer.observe(this, { attributes: true });
|
162
|
+
}
|
142
163
|
for (const fn of this.#connectedFns) {
|
143
164
|
fn();
|
144
165
|
}
|
145
166
|
}
|
146
167
|
disconnectedCallback() {
|
147
|
-
this.#observer
|
168
|
+
if (this.#observer !== null) {
|
169
|
+
this.#observer.disconnect();
|
170
|
+
}
|
148
171
|
for (const fn of this.#disconnectedFns) {
|
149
172
|
fn();
|
150
173
|
}
|
151
174
|
}
|
175
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
176
|
+
const [, setValue] = this.#attrSignals[name];
|
177
|
+
setValue(newValue);
|
178
|
+
for (const fn of this.#attributeChangedFns) {
|
179
|
+
fn(name, oldValue, newValue);
|
180
|
+
}
|
181
|
+
}
|
152
182
|
adoptedCallback() {
|
153
183
|
for (const fn of this.#adoptedCallbackFns) {
|
154
184
|
fn();
|