mi-element 0.9.0 → 0.9.2
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/dist/element.js +1 -2
- package/docs/element.md +118 -32
- package/package.json +1 -1
- package/src/element.js +2 -1
package/dist/element.js
CHANGED
|
@@ -52,9 +52,8 @@ class MiElement extends HTMLElement {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
connectedCallback() {
|
|
55
|
-
this.#controllers.forEach(controller => controller.hostConnected?.());
|
|
56
55
|
const {shadowRootInit: shadowRootInit, useGlobalStyles: useGlobalStyles, template: template} = this.constructor;
|
|
57
|
-
this.renderRoot = shadowRootInit ? this.shadowRoot ?? this.attachShadow(shadowRootInit) : this,
|
|
56
|
+
this.#controllers.forEach(controller => controller.hostConnected?.()), this.renderRoot = shadowRootInit ? this.shadowRoot ?? this.attachShadow(shadowRootInit) : this,
|
|
58
57
|
this.addTemplate(template), useGlobalStyles && addGlobalStyles(this.renderRoot),
|
|
59
58
|
this.render(), this.requestUpdate();
|
|
60
59
|
}
|
package/docs/element.md
CHANGED
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- !toc (minlevel=2) -->
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
- [constructor()](#constructor)
|
|
6
|
+
- [connectedCallback()](#connectedcallback)
|
|
7
|
+
- [disconnectedCallback()](#disconnectedcallback)
|
|
8
|
+
- [attributeChangedCallback(name, oldValue, newValue)](#attributechangedcallbackname-oldvalue-newvalue)
|
|
9
|
+
- [Update Cycle](#update-cycle)
|
|
10
|
+
- [Form-Associated Elements](#form-associated-elements)
|
|
11
|
+
- [render()](#render)
|
|
12
|
+
- [update(changedAttributes)](#updatechangedattributes)
|
|
13
|
+
- [shouldUpdate(changedAttributes)](#shouldupdatechangedattributes)
|
|
14
|
+
- [on(eventName, listener, \[node\])](#oneventname-listener-node)
|
|
15
|
+
- [once(eventName, listener, \[node\])](#onceeventname-listener-node)
|
|
15
16
|
|
|
16
17
|
<!-- toc! -->
|
|
17
18
|
|
|
@@ -28,16 +29,15 @@ from the `static attributes` object. From there setters and getters for property
|
|
|
28
29
|
changes using `.[name] = newValue` instead of `setAttribute(name, newValue)` are
|
|
29
30
|
applied.
|
|
30
31
|
|
|
31
|
-
|
|
32
32
|
```js
|
|
33
33
|
class extends MiElement {
|
|
34
34
|
/**
|
|
35
|
-
* Declare observable attributes with this getter.
|
|
35
|
+
* Declare observable attributes with this getter.
|
|
36
36
|
* Use `true` to define boolean attributes!
|
|
37
|
-
* Do not use `static attribute = { text: false }` as components attributes
|
|
38
|
-
* will use a shallow copy only. With the getter we always get a real "deep"
|
|
37
|
+
* Do not use `static attribute = { text: false }` as components attributes
|
|
38
|
+
* will use a shallow copy only. With the getter we always get a real "deep"
|
|
39
39
|
* copy.
|
|
40
|
-
*
|
|
40
|
+
*
|
|
41
41
|
* Avoid using attributes which are HTMLElement properties e.g. `className`.
|
|
42
42
|
* camelCased attributes will be made observable using its kebab-cased name.
|
|
43
43
|
*/
|
|
@@ -76,7 +76,7 @@ Then the first `render()` is issued with a `requestUpdate()`
|
|
|
76
76
|
|
|
77
77
|
```js
|
|
78
78
|
class extends MiElement {
|
|
79
|
-
// { mode: 'open' } is the default shadow root option
|
|
79
|
+
// { mode: 'open' } is the default shadow root option
|
|
80
80
|
// use `null` for no shadow root or { mode: 'closed' } for closed mode
|
|
81
81
|
static shadowRootInit = { mode: 'open' }
|
|
82
82
|
|
|
@@ -126,11 +126,11 @@ prevent memory leaks.
|
|
|
126
126
|
|
|
127
127
|
See previous example.
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
129
|
+
> ℹ️ **No need to remove internal event listeners**
|
|
130
|
+
>
|
|
131
|
+
> You don't need to remove event listeners added on the component's own
|
|
132
|
+
> DOM. This includes those added in your template. Unlike external
|
|
133
|
+
> event listeners, these will be garbage collected with the component.
|
|
134
134
|
|
|
135
135
|
## attributeChangedCallback(name, oldValue, newValue)
|
|
136
136
|
|
|
@@ -206,6 +206,93 @@ class Counter extends MiElement {
|
|
|
206
206
|
}
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
+
## Form-Associated Elements
|
|
210
|
+
|
|
211
|
+
MiElement supports [form-associated custom elements][form-associated], allowing
|
|
212
|
+
your components to participate in HTML forms just like native form controls.
|
|
213
|
+
|
|
214
|
+
[form-associated]: https://web.dev/articles/more-capable-form-controls
|
|
215
|
+
|
|
216
|
+
### Declaring a Form-Associated Element
|
|
217
|
+
|
|
218
|
+
Set `static formAssociated = true` on your component to enable form association:
|
|
219
|
+
|
|
220
|
+
```js
|
|
221
|
+
import { define, MiElement } from 'mi-element'
|
|
222
|
+
|
|
223
|
+
class CustomInput extends MiElement {
|
|
224
|
+
#internals
|
|
225
|
+
|
|
226
|
+
static formAssociated = true
|
|
227
|
+
|
|
228
|
+
static get properties() {
|
|
229
|
+
return {
|
|
230
|
+
name: { type: String },
|
|
231
|
+
value: { type: String, initial: '' }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
static template = `<input type="text" />`
|
|
236
|
+
|
|
237
|
+
render() {
|
|
238
|
+
this.#internals = this.attachInternals()
|
|
239
|
+
// define the aria role
|
|
240
|
+
this.#internals.ariaRole = 'textbox'
|
|
241
|
+
// set the initial form value
|
|
242
|
+
this.#internals.setFormValue(this.value)
|
|
243
|
+
|
|
244
|
+
this.refs = this.refsBySelector({ input: 'input' })
|
|
245
|
+
this.refs.input.addEventListener('input', (ev) => {
|
|
246
|
+
this.value = ev.target.value
|
|
247
|
+
// !needs a `name` attribute on the custom element
|
|
248
|
+
this.#internals.setFormValue(this.value)
|
|
249
|
+
this.checkValidity(this.value)
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
checkValidity(newValue) {
|
|
254
|
+
if (newValue >= 2) {
|
|
255
|
+
this.#internals.setValidity({})
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
this.#internals.setValidity(
|
|
259
|
+
{ tooSort: true },
|
|
260
|
+
'value too short',
|
|
261
|
+
this.refs.input
|
|
262
|
+
)
|
|
263
|
+
this.#internals.reportValidity()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
formResetCallback() {
|
|
267
|
+
this.value = this.refs.input.value = ''
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
formStateRestoreCallback(state, reason) {
|
|
271
|
+
this.value = this.refs.input.value = state
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
define('custom-input', CustomInput)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Usage Example
|
|
279
|
+
|
|
280
|
+
```html
|
|
281
|
+
<form id="my-form">
|
|
282
|
+
<custom-input name="username" value="john"></custom-input>
|
|
283
|
+
<custom-input name="email" value="john@example.com"></custom-input>
|
|
284
|
+
<button type="submit">Submit</button>
|
|
285
|
+
</form>
|
|
286
|
+
|
|
287
|
+
<script>
|
|
288
|
+
const form = document.getElementById('my-form')
|
|
289
|
+
const formData = new FormData(form)
|
|
290
|
+
|
|
291
|
+
console.log(formData.get('username')) // 'john'
|
|
292
|
+
console.log(formData.get('email')) // 'john@example.com'
|
|
293
|
+
</script>
|
|
294
|
+
```
|
|
295
|
+
|
|
209
296
|
## render()
|
|
210
297
|
|
|
211
298
|
Initial rendering of the component. Try to render the component only once!
|
|
@@ -216,10 +303,10 @@ Within the `render()` method, bear in mind to:
|
|
|
216
303
|
- Avoid producing any side effects.
|
|
217
304
|
- Use only the component's properties as input.
|
|
218
305
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
306
|
+
> ⚠️ **XSS - Cross-Site Scripting**
|
|
307
|
+
>
|
|
308
|
+
> Using [`innerHTML`][innerHTML] to create the components DOM is susceptible to
|
|
309
|
+
> [XSS][XSS] attacks in case that user-supplied data contains valid HTML markup.
|
|
223
310
|
|
|
224
311
|
In all other cases you may consider the <code>html``</code> template literal or
|
|
225
312
|
`escHtml()` from the "mi-element" import, which escapes user-supplied data.
|
|
@@ -275,7 +362,7 @@ class Counter extends MiElement {
|
|
|
275
362
|
static template = `
|
|
276
363
|
<button id>Count</button>
|
|
277
364
|
<p>Counter value: <span>0</span></p>
|
|
278
|
-
|
|
365
|
+
``
|
|
279
366
|
|
|
280
367
|
render() {
|
|
281
368
|
// template is already rendered on `this.renderRoot`
|
|
@@ -323,9 +410,9 @@ import { MiElement, Signal } from 'mi-element'
|
|
|
323
410
|
|
|
324
411
|
class Counter extends MiElement {
|
|
325
412
|
static get properties() {
|
|
326
|
-
return {
|
|
327
|
-
value: { type: Number }
|
|
328
|
-
|
|
413
|
+
return {
|
|
414
|
+
value: { type: Number }
|
|
415
|
+
}
|
|
329
416
|
}
|
|
330
417
|
|
|
331
418
|
static template = `
|
|
@@ -339,7 +426,6 @@ class Counter extends MiElement {
|
|
|
339
426
|
this.value = 0
|
|
340
427
|
}
|
|
341
428
|
|
|
342
|
-
|
|
343
429
|
render() {
|
|
344
430
|
const refs = this.refsBySelector({
|
|
345
431
|
button: 'button',
|
|
@@ -351,7 +437,7 @@ class Counter extends MiElement {
|
|
|
351
437
|
})
|
|
352
438
|
|
|
353
439
|
Signal.effect(() => {
|
|
354
|
-
// an update only happens if `this.value` changes;
|
|
440
|
+
// an update only happens if `this.value` changes;
|
|
355
441
|
// other attribute changes are ignored.
|
|
356
442
|
refs.count.textContent = this.value
|
|
357
443
|
})
|
|
@@ -367,7 +453,7 @@ disconnects.
|
|
|
367
453
|
```js
|
|
368
454
|
class Router extends MiElement {
|
|
369
455
|
render() {
|
|
370
|
-
// add event listener 'hashchange' to `window` which is disposed as soon as
|
|
456
|
+
// add event listener 'hashchange' to `window` which is disposed as soon as
|
|
371
457
|
// the component unmounts
|
|
372
458
|
this.on('hashchange', this.update, window)
|
|
373
459
|
}
|
package/package.json
CHANGED
package/src/element.js
CHANGED
|
@@ -159,9 +159,10 @@ export class MiElement extends HTMLElement {
|
|
|
159
159
|
* @category lifecycle
|
|
160
160
|
*/
|
|
161
161
|
connectedCallback() {
|
|
162
|
-
this.#controllers.forEach((controller) => controller.hostConnected?.())
|
|
163
162
|
// @ts-expect-error
|
|
164
163
|
const { shadowRootInit, useGlobalStyles, template } = this.constructor
|
|
164
|
+
// connect all controllers
|
|
165
|
+
this.#controllers.forEach((controller) => controller.hostConnected?.())
|
|
165
166
|
this.renderRoot = shadowRootInit
|
|
166
167
|
? (this.shadowRoot ?? this.attachShadow(shadowRootInit))
|
|
167
168
|
: this
|