mi-element 0.6.7 → 0.7.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
@@ -24,7 +24,7 @@ setting objects or functions either through `el.setAttribute(name, value)` or
24
24
  properties `el[name] = value`.
25
25
 
26
26
  Furthermore all observed attributes have a reactive behavior through the use of
27
- signals and effects (loosely) following the
27
+ signals and effects (loosely) following the
28
28
  [TC39 JavaScript Signals standard proposal][].
29
29
 
30
30
  # Usage
@@ -38,7 +38,8 @@ npm i mi-element
38
38
  ```js
39
39
  /** @file ./mi-counter.js */
40
40
 
41
- import { MiElement, define, refsById, Signal } from 'mi-element'
41
+ import { MiElement, define, Signal } from 'mi-element'
42
+ const { effect, createSignal } = Signal
42
43
 
43
44
  // define your Component
44
45
  class MiCounter extends MiElement {
@@ -46,25 +47,27 @@ class MiCounter extends MiElement {
46
47
  <style>
47
48
  :host { font-size: 1.25rem; }
48
49
  </style>
49
- <div id aria-label="Counter value">0</div>
50
- <button id="increment" aria-label="Increment counter"> + </button>
50
+ <div aria-label="Counter value">0</div>
51
+ <button aria-label="Increment counter"> + </button>
51
52
  `
52
53
 
53
- static get attributes() {
54
+ static get properties() {
54
55
  // declare reactive attribute(s)
55
- return { count: 0 }
56
+ return { count: { type: Number, initial: 0 } }
56
57
  }
57
58
 
59
+ static createSignal = createSignal
60
+
58
61
  // called by connectedCallback()
59
62
  render() {
60
63
  // gather refs from template (here by id)
61
- this.refs = refsById(this.renderRoot)
64
+ this.refs = this.refsBySelector({ increment: 'button', div: 'div' })
62
65
  // apply event listeners
63
66
  this.refs.increment.addEventListener('click', () => {
64
67
  // change observed and reactive attribute...
65
68
  this.count++
66
69
  })
67
- Signal.effect(() => {
70
+ effect(() => {
68
71
  // ...triggers update on every change of `this.count`
69
72
  this.refs.div.textContent = this.count
70
73
  })
@@ -114,7 +117,6 @@ MIT licensed
114
117
  [Web Components]: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks
115
118
  [TC39 JavaScript Signals standard proposal]: https://github.com/tc39/proposal-signals
116
119
  [krausest/js-framework-benchmark]: https://github.com/krausest/js-framework-benchmark
117
-
118
120
  [npm-badge]: https://badgen.net/npm/v/mi-element
119
121
  [npm]: https://www.npmjs.com/package/mi-element
120
122
  [types-badge]: https://badgen.net/npm/types/mi-element
package/dist/context.js CHANGED
@@ -17,6 +17,12 @@ class ContextProvider {
17
17
  get() {
18
18
  return this.state.get();
19
19
  }
20
+ set value(newValue) {
21
+ this.set(newValue);
22
+ }
23
+ get value() {
24
+ return this.get();
25
+ }
20
26
  onContextRequest=ev => {
21
27
  if (ev.context !== this.context) return;
22
28
  let unsubscribe;
package/dist/element.js CHANGED
@@ -1,86 +1,80 @@
1
- import { camelToKebabCase } from './case.js';
1
+ import { kebabToCamelCase, camelToKebabCase } from './case.js';
2
+
3
+ import { addGlobalStyles } from './styling.js';
4
+
5
+ import { refsBySelector } from './refs.js';
2
6
 
3
7
  import { createSignal } from 'mi-signal';
4
8
 
9
+ const nameMap = {
10
+ class: 'className',
11
+ for: 'htmlFor'
12
+ };
13
+
5
14
  class MiElement extends HTMLElement {
6
- #attr={};
7
- #attrLc=new Map;
8
- #types=new Map;
15
+ _props={};
16
+ #changedProps={};
9
17
  #disposers=new Set;
10
18
  #controllers=new Set;
11
- #changedAttr={};
12
- #dedupe=!1;
13
- static shadowRootOptions={
14
- mode: 'open'
15
- };
16
- static template;
17
- static get attributes() {
18
- return {};
19
+ #updateRequested=!1;
20
+ static get shadowRootInit() {
21
+ return {
22
+ mode: 'open'
23
+ };
19
24
  }
20
- static get properties() {
21
- return {};
25
+ static template;
26
+ static get properties() {}
27
+ static observedAttributes=[];
28
+ static styles='';
29
+ static get useGlobalStyles() {
30
+ return !1;
22
31
  }
32
+ static createSignal=createSignal;
23
33
  constructor() {
24
- super(), this.#observedAttributes(this.constructor.attributes), this.#observedProperties(this.constructor.properties);
25
- }
26
- #observe(name, initialValue) {
27
- this.#attr[name] = createSignal(initialValue), Object.defineProperty(this, name, {
28
- enumerable: !0,
29
- get() {
30
- return this.#attr[name].get();
31
- },
32
- set(newValue) {
33
- const oldValue = this.#attr[name].get();
34
- oldValue !== newValue && (this.#attr[name].set(newValue), this.#changedAttr[name] = oldValue,
35
- this.requestUpdate());
36
- }
37
- });
38
- }
39
- #observedAttributes(attributes = {}) {
40
- for (const [name, value] of Object.entries(attributes)) {
41
- const initial = initialValueType(value);
42
- this.#types.set(name, initial.type), this.#attrLc.set(name.toLowerCase(), name),
43
- this.#attrLc.set(camelToKebabCase(name), name), this.#observe(name, initial.value);
34
+ super();
35
+ const {createSignal: createSignal, properties: properties} = this.constructor;
36
+ for (const [name, {initial: initial}] of Object.entries(properties)) {
37
+ const descriptor = Object.getOwnPropertyDescriptor(this.constructor.prototype, name);
38
+ createSignal && (this._props[name] = createSignal()), Object.defineProperty(this, name, {
39
+ get() {
40
+ return descriptor?.get ? descriptor.get.call(this) : createSignal ? this._props[name].value : this._props[name];
41
+ },
42
+ set(value) {
43
+ const oldValue = this[name];
44
+ descriptor?.set ? descriptor.set.call(this, value) : createSignal ? this._props[name].value = value : this._props[name] = value,
45
+ oldValue !== this[name] && this.requestUpdate({
46
+ [name]: value
47
+ });
48
+ }
49
+ }), this[name] = initial;
44
50
  }
45
51
  }
46
- #observedProperties(properties = {}) {
47
- for (const [name, value] of Object.entries(properties)) this.#attrLc.has(name) || name in this.#attr || this.#observe(name, value);
48
- }
49
- #getName(name) {
50
- return this.#attrLc.get(name) || name;
51
- }
52
- #getType(name) {
53
- return this.#types.get(name);
54
- }
55
52
  connectedCallback() {
56
53
  this.#controllers.forEach(controller => controller.hostConnected?.());
57
- const {shadowRootOptions: shadowRootOptions, template: template} = this.constructor;
58
- this.renderRoot = shadowRootOptions ? this.shadowRoot ?? this.attachShadow(shadowRootOptions) : this,
59
- this.addTemplate(template), this.render(), this.requestUpdate();
54
+ const {shadowRootInit: shadowRootInit, useGlobalStyles: useGlobalStyles, template: template} = this.constructor;
55
+ this.renderRoot = shadowRootInit ? this.shadowRoot ?? this.attachShadow(shadowRootInit) : this,
56
+ this.addTemplate(template), useGlobalStyles && addGlobalStyles(this.renderRoot),
57
+ this.render(), this.requestUpdate();
60
58
  }
61
59
  disconnectedCallback() {
62
60
  this.#disposers.forEach(remover => remover()), this.#controllers.forEach(controller => controller.hostDisconnected?.());
63
61
  }
64
- attributeChangedCallback(name, oldValue, newValue) {
65
- const attr = this.#getName(name), type = this.#getType(attr);
66
- this.#changedAttr[attr] = this[attr], this[attr] = convertType(newValue, type),
67
- 'Boolean' === type && 'false' === newValue && this.removeAttribute(name), this.requestUpdate();
68
- }
69
- setAttribute(name, newValue) {
70
- const attr = this.#getName(name);
71
- if (!(attr in this.#attr)) return;
72
- const type = this.#getType(attr);
73
- 'Boolean' === type ? !0 === newValue || '' === newValue ? super.setAttribute(name, '') : super.removeAttribute(name) : [ 'String', 'Number' ].includes(type ?? '') || !0 === newValue ? super.setAttribute(name, newValue) : (this.#changedAttr[attr] = this[attr],
74
- this[attr] = newValue, this.requestUpdate());
75
- }
76
- shouldUpdate(_changedAttributes) {
77
- return !0;
78
- }
79
- requestUpdate() {
80
- !this.#dedupe && this.isConnected && (this.#dedupe = !0, requestAnimationFrame(() => {
81
- this.#dedupe = !1;
82
- const _changedAttributes = this.#changedAttr;
83
- this.#changedAttr = {}, this.shouldUpdate(_changedAttributes) && this.update(_changedAttributes);
62
+ attributeChangedCallback(name, _oldValue, newValue) {
63
+ const camelName = nameMap[name] ?? kebabToCamelCase(name), properties = this.constructor?.properties, {type: type} = properties?.[camelName] ?? {}, coercedValue = convertType(newValue, type);
64
+ if (name.startsWith('data-')) {
65
+ const datasetName = kebabToCamelCase(name.substring(5));
66
+ datasetName && (this.dataset[datasetName] = coercedValue);
67
+ }
68
+ this[camelName] = coercedValue;
69
+ }
70
+ requestUpdate(changedProps) {
71
+ this.#changedProps = {
72
+ ...this.#changedProps,
73
+ ...changedProps
74
+ }, !this.#updateRequested && this.renderRoot && (this.#updateRequested = !0, window.requestAnimationFrame(() => {
75
+ this.#updateRequested = !1;
76
+ const changedProps = this.#changedProps;
77
+ this.#changedProps = {}, this.update(changedProps);
84
78
  }));
85
79
  }
86
80
  addTemplate(template) {
@@ -90,7 +84,7 @@ class MiElement extends HTMLElement {
90
84
  }
91
85
  }
92
86
  render() {}
93
- update(_changedAttributes) {}
87
+ update(_changedProps) {}
94
88
  on(eventName, listener, node = this) {
95
89
  node.addEventListener(eventName, listener), this.#disposers.add(() => node.removeEventListener(eventName, listener));
96
90
  }
@@ -111,53 +105,42 @@ class MiElement extends HTMLElement {
111
105
  removeController(controller) {
112
106
  this.#controllers.delete(controller);
113
107
  }
108
+ refsBySelector(selectors) {
109
+ return refsBySelector(this.renderRoot, selectors);
110
+ }
114
111
  }
115
112
 
116
- const define = (name, element, options) => {
117
- element.observedAttributes = (element.observedAttributes || Object.keys(element.attributes || [])).map(attr => attr.toLowerCase()),
118
- renderTemplate(element), window.customElements.define(name, element, options);
113
+ const define = (tagName, elementClass, options) => {
114
+ if (customElements.get(tagName)) return;
115
+ const {usedCssPrefix: usedCssPrefix = "", cssPrefix: cssPrefix = "", styles: styles} = options || {};
116
+ if (elementClass.properties) {
117
+ const observedAttrs = [];
118
+ for (const [name, {attribute: attribute = !0}] of Object.entries(elementClass.properties)) attribute && observedAttrs.push(camelToKebabCase(name));
119
+ Object.defineProperty(elementClass, 'observedAttributes', {
120
+ get: () => observedAttrs
121
+ });
122
+ } else if (elementClass.observedAttributes) {
123
+ const properties = elementClass.observedAttributes.reduce((acc, attr) => (acc[kebabToCamelCase(attr)] = {},
124
+ acc), {});
125
+ Object.defineProperty(elementClass, 'properties', {
126
+ get: () => properties
127
+ });
128
+ }
129
+ elementClass.styles && (elementClass.styles = styles || (usedCssPrefix === cssPrefix ? elementClass.styles : elementClass.styles.replaceAll(`--${usedCssPrefix}-`, cssPrefix))),
130
+ renderTemplate(elementClass), window.customElements.define(tagName, elementClass);
119
131
  }, renderTemplate = element => {
120
132
  if (element.template instanceof HTMLTemplateElement) return;
121
133
  const el = document.createElement('template');
122
134
  el.innerHTML = element.template, element.template = el;
123
- }, initialValueType = value => {
124
- switch (value) {
125
- case Boolean:
126
- return {
127
- value: void 0,
128
- type: 'Boolean'
129
- };
130
-
131
- case Number:
132
- return {
133
- value: void 0,
134
- type: 'Number'
135
- };
136
-
137
- case String:
138
- return {
139
- value: void 0,
140
- type: 'String'
141
- };
142
-
143
- default:
144
- return {
145
- value: value,
146
- type: toString.call(value).slice(8, -1)
147
- };
148
- }
149
- }, convertType = (any, type) => {
150
- switch (type) {
151
- case 'Number':
152
- return (any => {
153
- const n = Number(any);
154
- return isNaN(n) ? any : n;
155
- })(any);
156
-
157
- case 'Boolean':
158
- return 'false' !== any && ('' === any || !!any);
159
- }
160
- return any;
161
- };
135
+ }, toJson = any => {
136
+ try {
137
+ return JSON.parse(any);
138
+ } catch {
139
+ return;
140
+ }
141
+ }, convertType = (value, type) => type === Boolean ? null !== value : type === Number ? (any => {
142
+ const n = Number(any);
143
+ return isNaN(n) ? 0 : n;
144
+ })(value) : type === Array ? toJson(value) ?? value.split(',').map(v => v.trim()) : type === Object ? toJson(value) : value;
162
145
 
163
146
  export { MiElement, convertType, define };
package/dist/escape.js CHANGED
@@ -3,11 +3,18 @@ class UnsafeHtml extends String {}
3
3
  const unsafeHtml = str => new UnsafeHtml(str), escMap = {
4
4
  '&': '&amp;',
5
5
  '<': '&lt;',
6
- '>': '&gt;',
7
- "'": '&#39;',
8
- '"': '&quot;'
9
- }, escHtml = string => string instanceof UnsafeHtml ? string : unsafeHtml(('' + string).replace(/&amp;/g, '&').replace(/[&<>'"]/g, tag => escMap[tag])), esc = (strings, ...values) => unsafeHtml(String.raw({
6
+ '>': '&gt;'
7
+ };
8
+
9
+ let esc = string => string.replace(/&amp;/g, '&').replace(/[&<>]/g, tag => escMap[tag]);
10
+
11
+ 'undefined' != typeof document && (esc = string => {
12
+ const div = document.createElement('div');
13
+ return div.textContent = string, div.innerHTML;
14
+ });
15
+
16
+ const escHtml = string => string instanceof UnsafeHtml ? string : unsafeHtml(esc('' + string)), html = (strings, ...values) => unsafeHtml(String.raw({
10
17
  raw: strings
11
18
  }, ...values.map(val => Array.isArray(val) ? val.map(escHtml).join('') : escHtml(val))));
12
19
 
13
- export { esc, escHtml, unsafeHtml };
20
+ export { escHtml, html, unsafeHtml };
package/dist/index.js CHANGED
@@ -2,12 +2,12 @@ export { ContextConsumer, ContextProvider, ContextRequestEvent } from './context
2
2
 
3
3
  export { MiElement, convertType, define } from './element.js';
4
4
 
5
- export { esc, escHtml, unsafeHtml } from './escape.js';
5
+ export { escHtml, html, unsafeHtml } from './escape.js';
6
6
 
7
- export { refsById, refsBySelector } from './refs.js';
8
-
9
- export { Computed, default as Signal, State, createSignal, effect } from 'mi-signal';
7
+ export { refsBySelector } from './refs.js';
10
8
 
11
9
  export { Store } from './store.js';
12
10
 
13
- export { addGlobalStyles, classMap, styleMap } from './styling.js';
11
+ export { addGlobalStyles, classNames, css, styleMap } from './styling.js';
12
+
13
+ export { default as Signal } from 'mi-signal';
package/dist/refs.js CHANGED
@@ -1,15 +1,7 @@
1
- import { kebabToCamelCase } from './case.js';
2
-
3
- function refsById(container) {
4
- const nodes = container.querySelectorAll?.('[id]') || [], found = {};
5
- for (const node of nodes) found[kebabToCamelCase(node.getAttribute('id') || node.nodeName.toLowerCase())] = node;
6
- return found;
7
- }
8
-
9
1
  function refsBySelector(container, selectors) {
10
2
  const found = {};
11
3
  for (const [name, selector] of Object.entries(selectors)) found[name] = container.querySelector?.(selector);
12
4
  return found;
13
5
  }
14
6
 
15
- export { refsById, refsBySelector };
7
+ export { refsBySelector };
package/dist/styling.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import { camelToKebabCase } from './case.js';
2
2
 
3
- const classMap = map => {
4
- const acc = [];
5
- for (const [name, value] of Object.entries(map ?? {})) value && acc.push(name);
6
- return acc.join(' ');
3
+ const classNames = (...args) => {
4
+ const classList = [];
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]) => {
7
+ value && classList.push(key);
8
+ }));
9
+ }), classList.join(' ');
7
10
  }, styleMap = (map, options) => {
8
11
  const {unit: unit = "px"} = options || {}, acc = [];
9
12
  for (const [name, value] of Object.entries(map ?? {})) {
@@ -23,4 +26,8 @@ function addGlobalStyles(renderRoot) {
23
26
  })), globalSheets));
24
27
  }
25
28
 
26
- export { addGlobalStyles, classMap, styleMap };
29
+ const css = (strings, ...values) => String.raw({
30
+ raw: strings
31
+ }, ...values);
32
+
33
+ export { addGlobalStyles, classNames, css, styleMap };
package/docs/context.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  <!-- !toc (minlevel=2) -->
4
4
 
5
- * [ContextProvider](#contextprovider)
6
- * [ContextConsumer](#contextconsumer)
7
- * [Connecting consumers to providers](#connecting-consumers-to-providers)
5
+ - [ContextProvider](#contextprovider)
6
+ - [ContextConsumer](#contextconsumer)
7
+ - [Connecting consumers to providers](#connecting-consumers-to-providers)
8
8
 
9
9
  <!-- toc! -->
10
10
 
@@ -17,29 +17,34 @@ Implements the [Context Protocol][].
17
17
  ## ContextProvider
18
18
 
19
19
  ```js
20
- import {
21
- define,
22
- MiElement,
23
- ContextProvider,
24
- ContextConsumer,
25
- } from 'mi-element'
20
+ import { define, MiElement, ContextProvider, ContextConsumer } from 'mi-element'
26
21
 
27
22
  define(
28
23
  'mi-context-provider',
29
24
  class extends MiElement {
30
- static get attributes() {
25
+ static get properties () {
31
26
  return {
32
- // define the context
33
- context: 'counter',
34
- value: 0
27
+ context: {},
28
+ value: { type: Number }
35
29
  }
36
30
  }
37
31
 
32
+ constructor() {
33
+ super()
34
+ // define the context and set initial value
35
+ // default context shall be unique amongst other context providers
36
+ this.context = 'my-context-provider'
37
+ this.value = 0
38
+ }
39
+
38
40
  render() {
39
- this.value = this.initialValue
40
41
  this.renderRoot.innerHTML = '<slot></slot>'
41
42
  this.provider = new ContextProvider(
42
- this, this.context, this._providerValue())
43
+ this,
44
+ this.context,
45
+ this._providerValue()
46
+ )
47
+ this.update()
43
48
  }
44
49
 
45
50
  update() {
@@ -48,14 +53,16 @@ define(
48
53
  }
49
54
 
50
55
  increment() {
51
- // value is observed value, requestUpdate() is called on any change
56
+ // value is observed value, requestUpdate() is called on every change
52
57
  this.value++
53
58
  }
54
59
 
55
60
  _providerValue() {
56
61
  // create a new object on every change and add all shared values and methods
57
62
  return {
58
- value: this.value, increment: this.increment }
63
+ value: this.value,
64
+ increment: () => this.increment()
65
+ }
59
66
  }
60
67
  }
61
68
  )
@@ -64,7 +71,7 @@ define(
64
71
  Works also with HTMLElement. In this case you must provide the necessary wiring.
65
72
 
66
73
  ```js
67
- customeElement.define('html-context-provider', extends class HTMLElement {
74
+ customElement.define('html-context-provider', extends class HTMLElement {
68
75
  connectedCallback() {
69
76
  this.provider = new ContextProvider(this, this.context, this)
70
77
  this.provider.hostConnected()
@@ -85,11 +92,10 @@ customeElement.define('html-context-provider', extends class HTMLElement {
85
92
  define(
86
93
  'mi-context-consumer',
87
94
  class extends MiElement {
88
- static get attributes() {
95
+ static get properties() {
89
96
  return {
90
- // define the context
91
- context: 'counter',
92
- subscribe: true
97
+ context: {},
98
+ subscribe: { type: Boolean }
93
99
  }
94
100
  }
95
101
 
@@ -97,14 +103,24 @@ define(
97
103
  <button id>Increment</button>
98
104
  <span id>0</span>`
99
105
 
106
+ constructor() {
107
+ super()
108
+ // define the context and set initial value
109
+ // default context shall be unique amongst other context providers
110
+ // must match the context-providers context!
111
+ this.context = 'my-context-provider'
112
+ this.subscribe = false
113
+ }
114
+
100
115
  render() {
101
116
  this.consumer = new ContextConsumer(this, this.context, {
102
- subscribe: !!this.subscribe
117
+ subscribe: this.subscribe
103
118
  })
104
119
  this.refs = refsById(this.renderRoot)
105
120
  this.refs.button.addEventListener('click', () => {
106
121
  this.consumer.value.increment()
107
122
  })
123
+ this.update()
108
124
  }
109
125
 
110
126
  update() {
@@ -120,14 +136,14 @@ define(
120
136
  <mi-context-provider context="outer">
121
137
  <mi-context-provider value="3">
122
138
  <div>
123
- <!-- does not subscribe to any changes (works only with MiElement)-->
124
- <mi-context-consumer subscribe="false">
139
+ <!-- does not subscribe to any changes -->
140
+ <mi-context-consumer>
125
141
  <!-- 3 -->
126
142
  </mi-context-consumer>
127
143
  </div>
128
144
  <div>
129
- <!-- connects to outer context provider -->
130
- <mi-context-consumer context="outer">
145
+ <!-- connects to outer context provider and subscribes to changes -->
146
+ <mi-context-consumer context="outer" subscribe>
131
147
  <!-- 0 -->
132
148
  </mi-context-consumer>
133
149
  </div>
@@ -55,6 +55,10 @@ define(
55
55
  this.controller = new ClockController(this)
56
56
  }
57
57
 
58
+ render() {
59
+ this.update()
60
+ }
61
+
58
62
  update() {
59
63
  // get value from controller
60
64
  const { value } = this.controller