mi-element 0.9.1 → 0.9.3

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,13 +52,9 @@ class MiElement extends HTMLElement {
52
52
  }
53
53
  }
54
54
  connectedCallback() {
55
- const {shadowRootInit: shadowRootInit, useGlobalStyles: useGlobalStyles, template: template, formAssociated: formAssociated} = this.constructor;
56
- if (this.#controllers.forEach(controller => controller.hostConnected?.()), this.renderRoot = shadowRootInit ? this.shadowRoot ?? this.attachShadow(shadowRootInit) : this,
55
+ const {shadowRootInit: shadowRootInit, useGlobalStyles: useGlobalStyles, template: template} = this.constructor;
56
+ this.#controllers.forEach(controller => controller.hostConnected?.()), this.renderRoot = shadowRootInit ? this.shadowRoot ?? this.attachShadow(shadowRootInit) : this,
57
57
  this.addTemplate(template), useGlobalStyles && addGlobalStyles(this.renderRoot),
58
- formAssociated && this.handleFormdata) {
59
- const internals = this.attachInternals();
60
- internals.form && this.on('formdata', ev => this.handleFormdata(ev), internals.form);
61
- }
62
58
  this.render(), this.requestUpdate();
63
59
  }
64
60
  disconnectedCallback() {
package/dist/html.js CHANGED
@@ -2,21 +2,26 @@ import { toJson } from './utils.js';
2
2
 
3
3
  const globalRenderCache = new class {
4
4
  cnt=0;
5
- map=new Map;
6
- cache=new WeakMap;
5
+ cache=new Map;
6
+ last=0;
7
+ get size() {
8
+ return this.cache.size;
9
+ }
7
10
  _inc() {
8
11
  return this.cnt = 268435455 & ++this.cnt, this.cnt;
9
12
  }
10
13
  clear() {
11
- this.cnt = 0, this.map.clear();
14
+ this.cnt = 0, this.cache.clear();
12
15
  }
13
16
  set(value) {
14
- const key = '__rc:' + this._inc().toString(36), ref = {};
15
- return this.map.set(key, ref), this.cache.set(ref, value), key;
17
+ const now = Date.now();
18
+ this.last < now && this.cache.clear(), this.last = now + 5e3;
19
+ const key = '__rc:' + this._inc().toString(36);
20
+ return this.cache.set(key, value), key;
16
21
  }
17
22
  get(key) {
18
- const ref = this.map.get(key);
19
- return this.map.delete(key), this.cache.get(ref);
23
+ const value = this.cache.get(key);
24
+ return this.cache.delete(key), value;
20
25
  }
21
26
  };
22
27
 
package/dist/index.js CHANGED
@@ -8,6 +8,6 @@ export { refsBySelector } from './refs.js';
8
8
 
9
9
  export { Store } from './store.js';
10
10
 
11
- export { addGlobalStyles, classNames, css, styleMap } from './styling.js';
11
+ export { addGlobalStyles, classNames, css, escCss, styleMap, unsafeCss } from './styling.js';
12
12
 
13
13
  export { default as Signal } from 'mi-signal';
package/dist/styling.js CHANGED
@@ -3,7 +3,7 @@ import { camelToKebabCase } from './case.js';
3
3
  const classNames = (...args) => {
4
4
  const classList = [];
5
5
  return args.forEach(arg => {
6
- arg && ('string' == typeof arg ? classList.push(arg) : Array.isArray(arg) ? classList.push(classNames(...arg)) : 'object' == typeof arg && Object.entries(arg).forEach(([key, value]) => {
6
+ arg && ('string' == typeof arg ? classList.push(arg) : 'object' == typeof arg && Object.entries(arg).forEach(([key, value]) => {
7
7
  value && classList.push(key);
8
8
  }));
9
9
  }), classList.join(' ');
@@ -26,8 +26,14 @@ function addGlobalStyles(renderRoot) {
26
26
  })), globalSheets));
27
27
  }
28
28
 
29
- const css = (strings, ...values) => String.raw({
29
+ class UnsafeCss extends String {}
30
+
31
+ const unsafeCss = str => new UnsafeCss(str), escMap = {
32
+ '&': '\\26 ',
33
+ '<': '\\3c ',
34
+ '>': '\\3e '
35
+ }, escCss = string => string instanceof UnsafeCss ? string : unsafeCss((string => string.replace(/[&<>]/g, tag => escMap[tag]))('' + string)), css = (strings, ...values) => String.raw({
30
36
  raw: strings
31
- }, ...values);
37
+ }, ...values.map(escCss));
32
38
 
33
- export { addGlobalStyles, classNames, css, styleMap };
39
+ export { addGlobalStyles, classNames, css, escCss, styleMap, unsafeCss };
package/docs/element.md CHANGED
@@ -2,17 +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
- * [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)
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)
16
16
 
17
17
  <!-- toc! -->
18
18
 
@@ -29,16 +29,15 @@ from the `static attributes` object. From there setters and getters for property
29
29
  changes using `.[name] = newValue` instead of `setAttribute(name, newValue)` are
30
30
  applied.
31
31
 
32
-
33
32
  ```js
34
33
  class extends MiElement {
35
34
  /**
36
- * Declare observable attributes with this getter.
35
+ * Declare observable attributes with this getter.
37
36
  * Use `true` to define boolean attributes!
38
- * Do not use `static attribute = { text: false }` as components attributes
39
- * 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"
40
39
  * copy.
41
- *
40
+ *
42
41
  * Avoid using attributes which are HTMLElement properties e.g. `className`.
43
42
  * camelCased attributes will be made observable using its kebab-cased name.
44
43
  */
@@ -77,7 +76,7 @@ Then the first `render()` is issued with a `requestUpdate()`
77
76
 
78
77
  ```js
79
78
  class extends MiElement {
80
- // { mode: 'open' } is the default shadow root option
79
+ // { mode: 'open' } is the default shadow root option
81
80
  // use `null` for no shadow root or { mode: 'closed' } for closed mode
82
81
  static shadowRootInit = { mode: 'open' }
83
82
 
@@ -127,11 +126,11 @@ prevent memory leaks.
127
126
 
128
127
  See previous example.
129
128
 
130
- !!! INFO No need to remove internal event listeners
131
-
132
- You don't need to remove event listeners added on the component's own
133
- DOM. This includes those added in your template. Unlike external
134
- 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.
135
134
 
136
135
  ## attributeChangedCallback(name, oldValue, newValue)
137
136
 
@@ -212,16 +211,18 @@ class Counter extends MiElement {
212
211
  MiElement supports [form-associated custom elements][form-associated], allowing
213
212
  your components to participate in HTML forms just like native form controls.
214
213
 
215
- [form-associated]: https://web.dev/articles/form-associated-custom-elements
214
+ [form-associated]: https://web.dev/articles/more-capable-form-controls
216
215
 
217
216
  ### Declaring a Form-Associated Element
218
217
 
219
218
  Set `static formAssociated = true` on your component to enable form association:
220
219
 
221
220
  ```js
222
- import { define, MiElement, html } from 'mi-element'
221
+ import { define, MiElement } from 'mi-element'
223
222
 
224
223
  class CustomInput extends MiElement {
224
+ #internals
225
+
225
226
  static formAssociated = true
226
227
 
227
228
  static get properties() {
@@ -231,41 +232,48 @@ class CustomInput extends MiElement {
231
232
  }
232
233
  }
233
234
 
234
- static template = html`<input type="text" />`
235
-
235
+ static template = `<input type="text" />`
236
+
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
+
237
244
  this.refs = this.refsBySelector({ input: 'input' })
238
245
  this.refs.input.addEventListener('input', (ev) => {
239
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)
240
250
  })
241
251
  }
242
- }
243
252
 
244
- define('custom-input', CustomInput)
245
- ```
246
-
247
- ### Handling Form Data
248
-
249
- Implement the `handleFormdata(ev)` method to submit your component's data with
250
- the form:
251
-
252
- ```js
253
- class CustomInput extends MiElement {
254
- static formAssociated = true
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
+ }
255
265
 
256
- // ...other code...
266
+ formResetCallback() {
267
+ this.value = this.refs.input.value = ''
268
+ }
257
269
 
258
- handleFormdata(ev) {
259
- // Only include data if the component has a name attribute
260
- if (this.name) {
261
- ev.formData.append(this.name, this.refs.input.value)
262
- }
270
+ formStateRestoreCallback(state, reason) {
271
+ this.value = this.refs.input.value = state
263
272
  }
264
273
  }
265
- ```
266
274
 
267
- The `handleFormdata` method is automatically called when the form is submitted or
268
- when `FormData` is created from the form.
275
+ define('custom-input', CustomInput)
276
+ ```
269
277
 
270
278
  ### Usage Example
271
279
 
@@ -279,13 +287,12 @@ when `FormData` is created from the form.
279
287
  <script>
280
288
  const form = document.getElementById('my-form')
281
289
  const formData = new FormData(form)
282
-
290
+
283
291
  console.log(formData.get('username')) // 'john'
284
- console.log(formData.get('email')) // 'john@example.com'
292
+ console.log(formData.get('email')) // 'john@example.com'
285
293
  </script>
286
294
  ```
287
295
 
288
-
289
296
  ## render()
290
297
 
291
298
  Initial rendering of the component. Try to render the component only once!
@@ -296,10 +303,10 @@ Within the `render()` method, bear in mind to:
296
303
  - Avoid producing any side effects.
297
304
  - Use only the component's properties as input.
298
305
 
299
- !!! WARNING XSS - Cross-Site Scripting
300
-
301
- Using [`innerHTML`][innerHTML] to create the components DOM is susceptible to
302
- [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.
303
310
 
304
311
  In all other cases you may consider the <code>html``</code> template literal or
305
312
  `escHtml()` from the "mi-element" import, which escapes user-supplied data.
@@ -403,9 +410,9 @@ import { MiElement, Signal } from 'mi-element'
403
410
 
404
411
  class Counter extends MiElement {
405
412
  static get properties() {
406
- return {
407
- value: { type: Number }
408
- }
413
+ return {
414
+ value: { type: Number }
415
+ }
409
416
  }
410
417
 
411
418
  static template = `
@@ -419,7 +426,6 @@ class Counter extends MiElement {
419
426
  this.value = 0
420
427
  }
421
428
 
422
-
423
429
  render() {
424
430
  const refs = this.refsBySelector({
425
431
  button: 'button',
@@ -431,7 +437,7 @@ class Counter extends MiElement {
431
437
  })
432
438
 
433
439
  Signal.effect(() => {
434
- // an update only happens if `this.value` changes;
440
+ // an update only happens if `this.value` changes;
435
441
  // other attribute changes are ignored.
436
442
  refs.count.textContent = this.value
437
443
  })
@@ -447,7 +453,7 @@ disconnects.
447
453
  ```js
448
454
  class Router extends MiElement {
449
455
  render() {
450
- // 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
451
457
  // the component unmounts
452
458
  this.on('hashchange', this.update, window)
453
459
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mi-element",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
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
@@ -160,8 +160,7 @@ export class MiElement extends HTMLElement {
160
160
  */
161
161
  connectedCallback() {
162
162
  // @ts-expect-error
163
- const { shadowRootInit, useGlobalStyles, template, formAssociated } =
164
- this.constructor
163
+ const { shadowRootInit, useGlobalStyles, template } = this.constructor
165
164
  // connect all controllers
166
165
  this.#controllers.forEach((controller) => controller.hostConnected?.())
167
166
  this.renderRoot = shadowRootInit
@@ -171,31 +170,6 @@ export class MiElement extends HTMLElement {
171
170
  if (useGlobalStyles) {
172
171
  addGlobalStyles(this.renderRoot)
173
172
  }
174
- /**
175
- * handle formdata event if `handleFormdata` method is defined on the component.
176
- * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/formdata_event
177
- * ```js
178
- * class MyElement extends MiElement {
179
- * static formAssociated = true // required to receive formdata event
180
- * handleFormdata(ev) {
181
- * const { name, value } = this.refs.input
182
- * ev.formData.append(name, value)
183
- * }
184
- * render() {
185
- * this.renderRoot.innerHTML = html`<input name="${this.name}" value="${this.value}">`
186
- * this.refs = { input: this.renderRoot.querySelector('input') }
187
- * }
188
- * }
189
- * ```
190
- */
191
- // @ts-expect-error
192
- if (formAssociated && this.handleFormdata) {
193
- const internals = this.attachInternals()
194
- if (internals.form) {
195
- // @ts-expect-error
196
- this.on('formdata', (ev) => this.handleFormdata(ev), internals.form)
197
- }
198
- }
199
173
  this.render() // initial render
200
174
  this.requestUpdate() // request initial update
201
175
  }
package/src/html.js CHANGED
@@ -5,8 +5,12 @@ import { toJson } from './utils.js'
5
5
  */
6
6
  class RenderCache {
7
7
  cnt = 0
8
- map = new Map()
9
- cache = new WeakMap()
8
+ cache = new Map()
9
+ last = 0
10
+
11
+ get size() {
12
+ return this.cache.size
13
+ }
10
14
 
11
15
  _inc() {
12
16
  this.cnt = ++this.cnt & 0xfffffff
@@ -15,21 +19,24 @@ class RenderCache {
15
19
 
16
20
  clear() {
17
21
  this.cnt = 0
18
- this.map.clear()
22
+ this.cache.clear()
19
23
  }
20
24
 
21
25
  set(value) {
26
+ const now = Date.now()
27
+ if (this.last < now) {
28
+ this.cache.clear()
29
+ }
30
+ this.last = now + 5e3
22
31
  const key = '__rc:' + this._inc().toString(36)
23
- const ref = {}
24
- this.map.set(key, ref)
25
- this.cache.set(ref, value)
32
+ this.cache.set(key, value)
26
33
  return key
27
34
  }
28
35
 
29
36
  get(key) {
30
- const ref = this.map.get(key)
31
- this.map.delete(key)
32
- return this.cache.get(ref)
37
+ const value = this.cache.get(key)
38
+ this.cache.delete(key)
39
+ return value
33
40
  }
34
41
  }
35
42
 
package/src/index.js CHANGED
@@ -10,11 +10,18 @@ export {
10
10
  * @typedef {import('./element.js').HostController} HostController
11
11
  */
12
12
  export { MiElement, convertType, define } from './element.js'
13
- export { unsafeHtml, html, escHtml, render, renderAttrs } from './html.js'
13
+ export { html, unsafeHtml, escHtml, render, renderAttrs } from './html.js'
14
14
  export { refsBySelector } from './refs.js'
15
15
  /**
16
16
  * @typedef {import('./store.js').Action} Action
17
17
  */
18
18
  export { Store } from './store.js'
19
- export { classNames, styleMap, addGlobalStyles, css } from './styling.js'
19
+ export {
20
+ classNames,
21
+ styleMap,
22
+ addGlobalStyles,
23
+ css,
24
+ unsafeCss,
25
+ escCss
26
+ } from './styling.js'
20
27
  export { default as Signal } from 'mi-signal'
package/src/styling.js CHANGED
@@ -11,8 +11,6 @@ export const classNames = (...args) => {
11
11
  if (!arg) return
12
12
  if (typeof arg === 'string') {
13
13
  classList.push(arg)
14
- } else if (Array.isArray(arg)) {
15
- classList.push(classNames(...arg))
16
14
  } else if (typeof arg === 'object') {
17
15
  Object.entries(arg).forEach(([key, value]) => {
18
16
  if (value) {
@@ -77,9 +75,41 @@ export function addGlobalStyles(renderRoot) {
77
75
  renderRoot.adoptedStyleSheets.push(...getGlobalStyleSheets())
78
76
  }
79
77
 
78
+ /**
79
+ * A helper class to avoid double escaping of HTML strings
80
+ */
81
+ class UnsafeCss extends String {}
82
+
83
+ /**
84
+ * tag a string as css for not to be escaped
85
+ * @param {string} str
86
+ * @returns {string}
87
+ */
88
+ // @ts-expect-error
89
+ export const unsafeCss = (str) => new UnsafeCss(str)
90
+
91
+ const escMap = {
92
+ '&': '\\26 ',
93
+ '<': '\\3c ',
94
+ '>': '\\3e '
95
+ }
96
+
97
+ const esc = (string) => string.replace(/[&<>]/g, (tag) => escMap[tag])
98
+
99
+ /**
100
+ * @see https://mathiasbynens.be/notes/css-escapes
101
+ * Escape a value interpolated into a css tagged template literal.
102
+ * Prevents injection of closing style tags or unexpected CSS constructs.
103
+ * @param {*} string
104
+ * @returns {string}
105
+ */
106
+ export const escCss = (string) =>
107
+ // @ts-expect-error
108
+ string instanceof UnsafeCss ? string : unsafeCss(esc('' + string))
109
+
80
110
  /**
81
111
  * Helper literal to show css styles in JS e.g. with
82
112
  * https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html
83
113
  */
84
114
  export const css = (strings, ...values) =>
85
- String.raw({ raw: strings }, ...values)
115
+ String.raw({ raw: strings }, ...values.map(escCss))
package/types/html.d.ts CHANGED
@@ -40,8 +40,9 @@ declare class UnsafeHtml extends String {
40
40
  */
41
41
  declare class RenderCache {
42
42
  cnt: number;
43
- map: Map<any, any>;
44
- cache: WeakMap<WeakKey, any>;
43
+ cache: Map<any, any>;
44
+ last: number;
45
+ get size(): number;
45
46
  _inc(): number;
46
47
  clear(): void;
47
48
  set(value: any): string;
package/types/index.d.ts CHANGED
@@ -6,5 +6,5 @@ export type HostController = import("./element.js").HostController;
6
6
  export type Action = import("./store.js").Action;
7
7
  export { ContextConsumer, ContextProvider, ContextRequestEvent } from "./context.js";
8
8
  export { MiElement, convertType, define } from "./element.js";
9
- export { unsafeHtml, html, escHtml, render, renderAttrs } from "./html.js";
10
- export { classNames, styleMap, addGlobalStyles, css } from "./styling.js";
9
+ export { html, unsafeHtml, escHtml, render, renderAttrs } from "./html.js";
10
+ export { classNames, styleMap, addGlobalStyles, css, unsafeCss, escCss } from "./styling.js";
@@ -15,4 +15,6 @@ export function styleMap(map: {
15
15
  }, options?: {
16
16
  unit?: string | undefined;
17
17
  }): string;
18
+ export function unsafeCss(str: string): string;
19
+ export function escCss(string: any): string;
18
20
  export function css(strings: any, ...values: any[]): string;