thunderous 0.3.0 → 0.3.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 +3 -340
- package/package.json +3 -3
package/README.md
CHANGED
@@ -8,6 +8,7 @@ Each component renders only once, then binds signals to DOM nodes for direct upd
|
|
8
8
|
|
9
9
|
- [Installation](#installation)
|
10
10
|
- [Usage](#usage)
|
11
|
+
- [Documentation](#documentation)
|
11
12
|
- [Development](#development)
|
12
13
|
- [License](#license)
|
13
14
|
|
@@ -48,347 +49,9 @@ MyElement.define('my-element');
|
|
48
49
|
```
|
49
50
|
<!-- prettier-ignore-end -->
|
50
51
|
|
51
|
-
|
52
|
+
## Documentation
|
52
53
|
|
53
|
-
|
54
|
-
|
55
|
-
#### Lifecycle Methods
|
56
|
-
|
57
|
-
Any lifecycle method you may need can be accessed from the params of your render function. The only difference is that these are callback registrations, so the same callback you would normally write is just passed in instead.
|
58
|
-
|
59
|
-
<!-- prettier-ignore-start -->
|
60
|
-
```ts
|
61
|
-
const MyElement = customElement((params) => {
|
62
|
-
const {
|
63
|
-
adoptedCallback,
|
64
|
-
connectedCallback,
|
65
|
-
disconnectedCallback,
|
66
|
-
attributeChangedCallback,
|
67
|
-
} = params;
|
68
|
-
/* ... */
|
69
|
-
});
|
70
|
-
```
|
71
|
-
<!-- prettier-ignore-end -->
|
72
|
-
|
73
|
-
If you need to support forms, pass an options object as the second argument to `customElement`.
|
74
|
-
|
75
|
-
<!-- prettier-ignore-start -->
|
76
|
-
```ts
|
77
|
-
const MyElement = customElement((params) => {
|
78
|
-
const {
|
79
|
-
formDisabledCallback,
|
80
|
-
formResetCallback,
|
81
|
-
formStateRestoreCallback,
|
82
|
-
} = params;
|
83
|
-
/* ... */
|
84
|
-
}, { formAssociated: true });
|
85
|
-
```
|
86
|
-
<!-- prettier-ignore-end -->
|
87
|
-
|
88
|
-
#### Roots and Element Internals
|
89
|
-
|
90
|
-
You can always define the internals the same as you usually would, and if for some reason you need access to either the element itself or the shadow root, you can do so as illustrated below.
|
91
|
-
|
92
|
-
<!-- prettier-ignore-start -->
|
93
|
-
```ts
|
94
|
-
const MyElement = customElement((params) => {
|
95
|
-
const {
|
96
|
-
internals,
|
97
|
-
elementRef,
|
98
|
-
root,
|
99
|
-
connectedCallback,
|
100
|
-
} = params;
|
101
|
-
|
102
|
-
internals.setFormValue('hello world');
|
103
|
-
connectedCallback(() => {
|
104
|
-
const childLink = elementRef.querySelector('a[href]'); // light DOM
|
105
|
-
const innerLink = root.querySelector('a[href]'); // shadow DOM
|
106
|
-
/* ... */
|
107
|
-
});
|
108
|
-
/* ... */
|
109
|
-
}, { formAssociated: true });
|
110
|
-
```
|
111
|
-
<!-- prettier-ignore-end -->
|
112
|
-
|
113
|
-
If you need to pass certain options to the `attachShadow()` call, you can do so by passing `shadowRootOptions`.
|
114
|
-
|
115
|
-
<!-- prettier-ignore-start -->
|
116
|
-
```ts
|
117
|
-
const MyElement = customElement(() => {
|
118
|
-
/* ... */
|
119
|
-
}, { shadowRootOptions: { delegatesFocus: true } });
|
120
|
-
```
|
121
|
-
<!-- prettier-ignore-end -->
|
122
|
-
|
123
|
-
To opt out of using shadow DOM at all, pass `false` to `attachShadow`. This will change `root` above to reference the element itself, and stylesheets will apply to the global document rather than being encapsulated within the component.
|
124
|
-
|
125
|
-
<!-- prettier-ignore-start -->
|
126
|
-
```ts
|
127
|
-
const MyElement = customElement(() => {
|
128
|
-
/* ... */
|
129
|
-
}, { attachShadow: false });
|
130
|
-
```
|
131
|
-
<!-- prettier-ignore-end -->
|
132
|
-
|
133
|
-
#### Adopted Style Sheets
|
134
|
-
|
135
|
-
This one diverges from native slightly, since the native approach is a bit manual. For convenience, you can use the `adoptStyleSheet()` function.
|
136
|
-
|
137
|
-
> If you prefer the manual approach, `root.adoptedStyleSheets = []`, you can always do that with the `root` property above, or the global `document`.
|
138
|
-
|
139
|
-
The `css` tagged template function will construct a `CSSStyleSheet` object that can be adopted by documents and shadow roots.
|
140
|
-
|
141
|
-
> When `attachShadow` is set to `false`, `adoptStyleSheet()` will use the global document to adopt the styles.
|
142
|
-
|
143
|
-
<!-- prettier-ignore-start -->
|
144
|
-
```ts
|
145
|
-
import { customElement, css } from 'thunderous';
|
146
|
-
|
147
|
-
const myStyleSheet = css`
|
148
|
-
:host {
|
149
|
-
display: block;
|
150
|
-
font-family: sans-serif;
|
151
|
-
}
|
152
|
-
`;
|
153
|
-
|
154
|
-
const MyElement = customElement(({ adoptStyleSheet }) => {
|
155
|
-
adoptStyleSheet(myStyleSheet);
|
156
|
-
/* ... */
|
157
|
-
});
|
158
|
-
```
|
159
|
-
<!-- prettier-ignore-end -->
|
160
|
-
|
161
|
-
### Non-Native extras
|
162
|
-
|
163
|
-
In addition to the native features, there are a few features that supercharge your web components. Most notably, signals.
|
164
|
-
|
165
|
-
#### Signals
|
166
|
-
|
167
|
-
Creating signals should look pretty familiar to most modern developers.
|
168
|
-
|
169
|
-
<!-- prettier-ignore-start -->
|
170
|
-
```ts
|
171
|
-
import { createSignal } from 'thunderous';
|
172
|
-
|
173
|
-
const [count, setCount] = createSignal(0);
|
174
|
-
console.log(count()); // 0
|
175
|
-
setCount(1);
|
176
|
-
console.log(count()) // 1
|
177
|
-
```
|
178
|
-
<!-- prettier-ignore-end -->
|
179
|
-
|
180
|
-
##### Binding Signals to Templates
|
181
|
-
|
182
|
-
To bind signals to a template, use the provided `html` tagged template function to pass them in.
|
183
|
-
|
184
|
-
<!-- prettier-ignore-start -->
|
185
|
-
```ts
|
186
|
-
import { createSignal, customElement, html } from 'thunderous';
|
187
|
-
|
188
|
-
const MyElement = customElement(() => {
|
189
|
-
const [count, setCount] = createSignal(0);
|
190
|
-
// presumably setCount() gets called
|
191
|
-
return html`<output>${count}</output>`;
|
192
|
-
});
|
193
|
-
```
|
194
|
-
<!-- prettier-ignore-end -->
|
195
|
-
|
196
|
-
> NOTICE: we are not running the signal's getter above. This is intentional, as we delegate that to the template to run for proper binding.
|
197
|
-
|
198
|
-
By binding signals to templates, you allow fine-grained updates to be made directly to DOM nodes every time the signal changes, without requiring any diffing or re-rendering.
|
199
|
-
|
200
|
-
> This also works for `css`, but bear in mind that passing signals to a shared `CSSStyleSheet` may not have the effect you expect. Sharing Style Sheets across many component instances is best for performance, but signals will update every instance of each component with that approach. The suggested alternative is to write static CSS and toggle classes in the HTML instead.
|
201
|
-
|
202
|
-
##### Attribute Signals
|
203
|
-
|
204
|
-
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.
|
205
|
-
|
206
|
-
<!-- prettier-ignore-start -->
|
207
|
-
```ts
|
208
|
-
const MyElement = customElement(({ attrSignals }) => {
|
209
|
-
const [heading, setHeading] = attrSignals['my-heading'];
|
210
|
-
// setHeading() will also update the attribute in the HTML.
|
211
|
-
return html`<h2>${heading}</h2>`;
|
212
|
-
});
|
213
|
-
```
|
214
|
-
<!-- prettier-ignore-end -->
|
215
|
-
|
216
|
-
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`.
|
217
|
-
|
218
|
-
<!-- prettier-ignore-start -->
|
219
|
-
```ts
|
220
|
-
const MyElement = customElement(({ attrSignals }) => {
|
221
|
-
const [heading, setHeading] = attrSignals['my-heading'];
|
222
|
-
return html`<h2>${heading}</h2>`;
|
223
|
-
}, { observedAttributes: ['my-heading'] });
|
224
|
-
```
|
225
|
-
<!-- prettier-ignore-end -->
|
226
|
-
|
227
|
-
Usage:
|
228
|
-
|
229
|
-
```html
|
230
|
-
<my-element my-heading="My Element's Title"></my-element>
|
231
|
-
```
|
232
|
-
|
233
|
-
> NOTICE: Since `attrSignals` is a `Proxy` object, _any_ property will return a signal and auto-bind it to the attribute it corresponds with.
|
234
|
-
|
235
|
-
##### Property Signals
|
236
|
-
|
237
|
-
In addition to attributes, there are also properties. Though often conflated, there is an important distinction: _attributes_ are strings defined in HTML, and _properties_ can be any type of data, strictly in JavaScript and completely invisible to HTML.
|
238
|
-
|
239
|
-
Modern templating solutions often allow developers to assign properties via HTML attribute _syntax_, even though they are not actually attributes. While this distinction may seem trivial for those working with modern frameworks, it becomes much more relevant when defining custom elements that may be used in plain HTML.
|
240
|
-
|
241
|
-
Thunderous supports properties for cases where strings are not sufficient. These are also reflected as signals _within_ the component, but the _consumer_ of the component will not directly interact with this signal. `myElement.count = 1` will update the internal signal.
|
242
|
-
|
243
|
-
<!-- prettier-ignore-start -->
|
244
|
-
```ts
|
245
|
-
const MyElement = customElement(({ propSignals }) => {
|
246
|
-
const [count, setCount] = propSignals['prop'];
|
247
|
-
// setCount() will also update the property in the DOM
|
248
|
-
return html`<output>${count}</output>`;
|
249
|
-
});
|
250
|
-
```
|
251
|
-
<!-- prettier-ignore-end -->
|
252
|
-
|
253
|
-
There is also a way to sync attributes with properties, though it should be used deliberately, with caution. Since it requires some coercion to occur, it may introduce some performance overhead. It's best not to use this to parse JSON strings, for example.
|
254
|
-
|
255
|
-
To use this feature, pass `attributesAsProperties` in the options. It accepts an array of `[propertyName, coerceFn]` pairs. For primitive types, you can use their constructors for coercion, like `['count', Number]`.
|
256
|
-
|
257
|
-
<!-- prettier-ignore-start -->
|
258
|
-
```ts
|
259
|
-
const MyElement = customElement(({ propSignals }) => {
|
260
|
-
const [count, setCount] = propSignals['prop'];
|
261
|
-
// setCount() will also update the property in the DOM
|
262
|
-
return html`<output>${count}</output>`;
|
263
|
-
}, { attributesAsProperties: [['count', Number]] });
|
264
|
-
```
|
265
|
-
<!-- prettier-ignore-end -->
|
266
|
-
|
267
|
-
With the above snippet, `count` may be controlled by setting the attribute, like so:
|
268
|
-
|
269
|
-
```html
|
270
|
-
<my-element count="1"></my-element>
|
271
|
-
```
|
272
|
-
|
273
|
-
...and the attribute will reflect changes made to the property as well:
|
274
|
-
|
275
|
-
```ts
|
276
|
-
const myElement = document.querySelector('my-element');
|
277
|
-
myElement.count = 1;
|
278
|
-
```
|
279
|
-
|
280
|
-
In both cases, the `count()` signal will be updated.
|
281
|
-
|
282
|
-
##### Derived Signals
|
283
|
-
|
284
|
-
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.
|
285
|
-
|
286
|
-
```ts
|
287
|
-
import { derived, createSignal } from 'thunderous';
|
288
|
-
|
289
|
-
const [count, setCount] = createSignal(0);
|
290
|
-
const timesTen = derived(() => count() * 10);
|
291
|
-
console.log(timesTen()); // 0
|
292
|
-
setCount(10);
|
293
|
-
console.log(timesTen()); // 100
|
294
|
-
```
|
295
|
-
|
296
|
-
##### Effects
|
297
|
-
|
298
|
-
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.
|
299
|
-
|
300
|
-
```ts
|
301
|
-
import { createEffect } from 'thunderous';
|
302
|
-
|
303
|
-
/* ... */
|
304
|
-
createEffect(() => {
|
305
|
-
console.log(count());
|
306
|
-
});
|
307
|
-
```
|
308
|
-
|
309
|
-
##### Debugging Signals
|
310
|
-
|
311
|
-
If you're having a tough time tracing an issue, you can provide the `debugMode` option to any signal to log potentially helpful information to the console. For differentiating values, you can also provide an optional `label` property to easily associate logs with their respective sources. These options can be passed to signals themselves, or to specific calls to setters and getters.
|
312
|
-
|
313
|
-
```ts
|
314
|
-
const [count, setCount] = createSignal(0, {
|
315
|
-
debugMode: true,
|
316
|
-
label: 'count signal',
|
317
|
-
});
|
318
|
-
|
319
|
-
setCount(1, {
|
320
|
-
debugMode: true,
|
321
|
-
label: 'start count',
|
322
|
-
});
|
323
|
-
|
324
|
-
const newCount = count({
|
325
|
-
debugMode: true,
|
326
|
-
label: 'new count',
|
327
|
-
});
|
328
|
-
```
|
329
|
-
|
330
|
-
#### Refs
|
331
|
-
|
332
|
-
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.
|
333
|
-
|
334
|
-
<!-- prettier-ignore-start -->
|
335
|
-
```ts
|
336
|
-
const MyElement = customElement(({ connectedCallback, refs }) => {
|
337
|
-
connectedCallback(() => {
|
338
|
-
console.log(refs.heading.textContent); // hello world
|
339
|
-
});
|
340
|
-
return html`<h2 ref="heading">hello world</h2>`;
|
341
|
-
});
|
342
|
-
```
|
343
|
-
<!-- prettier-ignore-end -->
|
344
|
-
|
345
|
-
#### Event Binding
|
346
|
-
|
347
|
-
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.
|
348
|
-
|
349
|
-
<!-- prettier-ignore-start -->
|
350
|
-
```ts
|
351
|
-
const MyElement = customElement(({ customCallback }) => {
|
352
|
-
const [count, setCount] = createSignal(0);
|
353
|
-
const increment = customCallback(() => setCount(count() + 1));
|
354
|
-
return html`
|
355
|
-
<button onclick="${increment}">Increment</button>
|
356
|
-
<output>${count}</output>
|
357
|
-
`;
|
358
|
-
});
|
359
|
-
```
|
360
|
-
|
361
|
-
> NOTICE: This uses the native HTML inline event-binding syntax. There is no special syntax for `on` attributes, because it simply renders the reference to the host element.
|
362
|
-
>
|
363
|
-
> You will see something like this when you inspect the element in the browser:
|
364
|
-
> ```html
|
365
|
-
> <my-element onclick="this.getRootNode().host.__customCallbackFns.get('d7121610-e89d-4629-ab00-d4e22644f16b')(event)">
|
366
|
-
> ```
|
367
|
-
<!-- prettier-ignore-end -->
|
368
|
-
|
369
|
-
### Defining Custom Elements
|
370
|
-
|
371
|
-
The `customElement()` function allows you to author a web component, returning an `ElementResult` that has some helpful methods like `define()` and `eject()`.
|
372
|
-
|
373
|
-
- `ElementResult.define()` is a little safer than `customElements.define()` because it first checks if the component was already defined, without throwing an error. It will, however, log a warning. There's no need to pass the class since it already has that context.
|
374
|
-
|
375
|
-
```ts
|
376
|
-
const MyElement = customElement(() => html`<slot></slot>`);
|
377
|
-
|
378
|
-
MyElement.define('my-element');
|
379
|
-
```
|
380
|
-
|
381
|
-
- `ElementResult.eject()` is useful in case you need to access the underlying class for some reason; perhaps you want to extend it and/or set static properties.
|
382
|
-
|
383
|
-
```ts
|
384
|
-
const MyElementClass = MyElement.eject();
|
385
|
-
|
386
|
-
class MyOtherElement extends MyElementClass {
|
387
|
-
/* ... */
|
388
|
-
}
|
389
|
-
```
|
390
|
-
|
391
|
-
These may also be chained together, like `MyElement.define('my-element').eject()`.
|
54
|
+
Please consult the [documentation](https://thunderous.dev) to learn how to build web components with Thunderous.
|
392
55
|
|
393
56
|
## Contributing
|
394
57
|
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "thunderous",
|
3
|
-
"version": "0.3.
|
3
|
+
"version": "0.3.1",
|
4
4
|
"description": "",
|
5
5
|
"type": "module",
|
6
6
|
"main": "./dist/index.cjs",
|
@@ -43,7 +43,7 @@
|
|
43
43
|
"eslint-plugin-promise": "^7.1.0",
|
44
44
|
"express": "^4.17.1",
|
45
45
|
"prettier": "^3.3.3",
|
46
|
-
"
|
47
|
-
"
|
46
|
+
"tsup": "^8.3.0",
|
47
|
+
"typescript": "^5.6.3"
|
48
48
|
}
|
49
49
|
}
|