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 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
- * [constructor()](#constructor)
6
- * [connectedCallback()](#connectedcallback)
7
- * [disconnectedCallback()](#disconnectedcallback)
8
- * [attributeChangedCallback(name, oldValue, newValue)](#attributechangedcallbackname-oldvalue-newvalue)
9
- * [Update Cycle](#update-cycle)
10
- * [render()](#render)
11
- * [update(changedAttributes)](#updatechangedattributes)
12
- * [shouldUpdate(changedAttributes)](#shouldupdatechangedattributes)
13
- * [on(eventName, listener, \[node\])](#oneventname-listener-node)
14
- * [once(eventName, listener, \[node\])](#onceeventname-listener-node)
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
- !!! INFO 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.
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
- !!! WARNING XSS - Cross-Site Scripting
220
-
221
- Using [`innerHTML`][innerHTML] to create the components DOM is susceptible to
222
- [XSS][XSS] attacks in case that user-supplied data contains valid HTML markup.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mi-element",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Build lightweight reactive micro web-components",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/commenthol/mi-element/tree/main/packages/mi-element#readme",
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