thunderous 0.1.0 → 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 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
@@ -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();
@@ -96,7 +97,7 @@ var customElement = (render, options) => {
96
97
  __customCallbackFns = /* @__PURE__ */ new Map();
97
98
  #shadowRoot = this.attachShadow({ mode: "closed" });
98
99
  #internals = this.attachInternals();
99
- #observer = new MutationObserver((mutations) => {
100
+ #observer = observedAttributes.length > 0 ? null : new MutationObserver((mutations) => {
100
101
  for (const mutation of mutations) {
101
102
  const attrName = mutation.attributeName;
102
103
  if (mutation.type !== "attributes" || attrName === null) continue;
@@ -161,6 +162,9 @@ var customElement = (render, options) => {
161
162
  static get formAssociated() {
162
163
  return formAssociated;
163
164
  }
165
+ static get observedAttributes() {
166
+ return observedAttributes;
167
+ }
164
168
  constructor() {
165
169
  super();
166
170
  for (const attr of this.attributes) {
@@ -169,17 +173,28 @@ var customElement = (render, options) => {
169
173
  this.#render();
170
174
  }
171
175
  connectedCallback() {
172
- this.#observer.observe(this, { attributes: true });
176
+ if (this.#observer !== null) {
177
+ this.#observer.observe(this, { attributes: true });
178
+ }
173
179
  for (const fn of this.#connectedFns) {
174
180
  fn();
175
181
  }
176
182
  }
177
183
  disconnectedCallback() {
178
- this.#observer.disconnect();
184
+ if (this.#observer !== null) {
185
+ this.#observer.disconnect();
186
+ }
179
187
  for (const fn of this.#disconnectedFns) {
180
188
  fn();
181
189
  }
182
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
+ }
183
198
  adoptedCallback() {
184
199
  for (const fn of this.#adoptedCallbackFns) {
185
200
  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
@@ -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();
@@ -65,7 +66,7 @@ var customElement = (render, options) => {
65
66
  __customCallbackFns = /* @__PURE__ */ new Map();
66
67
  #shadowRoot = this.attachShadow({ mode: "closed" });
67
68
  #internals = this.attachInternals();
68
- #observer = new MutationObserver((mutations) => {
69
+ #observer = observedAttributes.length > 0 ? null : new MutationObserver((mutations) => {
69
70
  for (const mutation of mutations) {
70
71
  const attrName = mutation.attributeName;
71
72
  if (mutation.type !== "attributes" || attrName === null) continue;
@@ -130,6 +131,9 @@ var customElement = (render, options) => {
130
131
  static get formAssociated() {
131
132
  return formAssociated;
132
133
  }
134
+ static get observedAttributes() {
135
+ return observedAttributes;
136
+ }
133
137
  constructor() {
134
138
  super();
135
139
  for (const attr of this.attributes) {
@@ -138,17 +142,28 @@ var customElement = (render, options) => {
138
142
  this.#render();
139
143
  }
140
144
  connectedCallback() {
141
- this.#observer.observe(this, { attributes: true });
145
+ if (this.#observer !== null) {
146
+ this.#observer.observe(this, { attributes: true });
147
+ }
142
148
  for (const fn of this.#connectedFns) {
143
149
  fn();
144
150
  }
145
151
  }
146
152
  disconnectedCallback() {
147
- this.#observer.disconnect();
153
+ if (this.#observer !== null) {
154
+ this.#observer.disconnect();
155
+ }
148
156
  for (const fn of this.#disconnectedFns) {
149
157
  fn();
150
158
  }
151
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
+ }
152
167
  adoptedCallback() {
153
168
  for (const fn of this.#adoptedCallbackFns) {
154
169
  fn();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thunderous",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",