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 CHANGED
@@ -34,21 +34,12 @@ const myStyleSheet = css`
34
34
  }
35
35
  `;
36
36
 
37
- const MyElement = customElement((params) => {
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 ref="increment">Increment</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((params) => {
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((params) => {
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, customElement } from 'thunderous';
139
+ import { createSignal } from 'thunderous';
161
140
 
162
- const MyElement = customElement(() => {
163
- const [count, setCount] = createSignal(0);
164
-
165
- console.log(count()); // 0
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
- 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.
203
173
 
204
174
  <!-- prettier-ignore-start -->
205
175
  ```ts
206
- const MyElement = customElement((params) => {
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
- #### Refs
203
+ ##### Derived Signals
227
204
 
228
- Finally, 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. This is the best place for event binding to occur.
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
- const MyElement = customElement((params) => {
233
- const { connectedCallback, refs } = params;
208
+ import { derived, createSignal } from 'thunderous';
234
209
 
235
- const [count, setCount] = createSignal(0);
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.increment.addEventListener('click', () => {
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 ref="increment">Increment</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.observe(this, { attributes: true });
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.disconnect();
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?: boolean;
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?: boolean;
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.observe(this, { attributes: true });
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.disconnect();
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thunderous",
3
- "version": "0.0.4",
3
+ "version": "0.2.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",