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 CHANGED
@@ -34,15 +34,10 @@ const myStyleSheet = css`
34
34
  }
35
35
  `;
36
36
 
37
- const MyElement = customElement((params) => {
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((params) => {
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((params) => {
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
- Instead of worrying about the manual `observedAttributes` approach, each element is observed with a `MutationObserver` watching all attributes. All changes trigger the `attributeChangedCallback` and you can access all attributes as signals. This makes it much less cumbersome to write reactive attributes.
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((params) => {
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((params) => {
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((params) => {
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.observe(this, { attributes: true });
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.disconnect();
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?: boolean;
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?: boolean;
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.observe(this, { attributes: true });
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.disconnect();
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thunderous",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",