mi-element 0.1.0 → 0.2.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 +4 -0
- package/dist/case.js +3 -0
- package/dist/context.js +11 -12
- package/dist/element.js +26 -19
- package/dist/index.js +4 -2
- package/dist/refs.js +3 -3
- package/dist/signal.js +54 -16
- package/dist/store.js +6 -24
- package/dist/styling.js +17 -0
- package/package.json +13 -3
- package/src/case.js +15 -0
- package/src/context.js +19 -17
- package/src/element.js +39 -31
- package/src/index.js +6 -3
- package/src/refs.js +2 -8
- package/src/signal.js +84 -49
- package/src/store.js +27 -48
- package/src/styling.js +33 -0
- package/types/case.d.ts +2 -0
- package/types/context.d.ts +56 -58
- package/types/element.d.ts +101 -123
- package/types/escape.d.ts +3 -3
- package/types/index.d.ts +15 -14
- package/types/refs.d.ts +4 -6
- package/types/signal.d.ts +51 -33
- package/types/store.d.ts +53 -35
- package/types/styling.d.ts +8 -0
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
> a lightweight alternative to write web components
|
|
4
4
|
|
|
5
|
+
Only weights 2.3kB minified and gzipped.
|
|
6
|
+
|
|
5
7
|
mi-element provides further features to build web applications through
|
|
6
8
|
[Web Components][] like:
|
|
7
9
|
|
|
@@ -77,6 +79,7 @@ In `./example` you'll find a working sample of a Todo App. Check it out with
|
|
|
77
79
|
- [controller][docs-controller] adding controllers to mi-element to hook into the lifecycle
|
|
78
80
|
- [context][docs-context] Implementation of the [Context Protocol][].
|
|
79
81
|
- [store][docs-store] Manage shared state in an application
|
|
82
|
+
- [styling][docs-styling] Styling directives for "class" and "style"
|
|
80
83
|
|
|
81
84
|
# License
|
|
82
85
|
|
|
@@ -88,3 +91,4 @@ MIT licensed
|
|
|
88
91
|
[docs-controller]: https://github.com/commenthol/mi-element/tree/main/packages/mi-element/docs/controller.md
|
|
89
92
|
[docs-context]: https://github.com/commenthol/mi-element/tree/main/packages/mi-element/docs/context.md
|
|
90
93
|
[docs-store]: https://github.com/commenthol/mi-element/tree/main/packages/mi-element/docs/store.md
|
|
94
|
+
[docs-styling]: https://github.com/commenthol/mi-element/tree/main/packages/mi-element/docs/styling.md
|
package/dist/case.js
ADDED
package/dist/context.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createSignal, effect } from './signal.js';
|
|
2
2
|
|
|
3
3
|
class ContextProvider {
|
|
4
4
|
constructor(host, context, initialValue) {
|
|
5
|
-
this.host = host, this.context = context, this.state =
|
|
5
|
+
this.host = host, this.context = context, this.state = createSignal(initialValue),
|
|
6
6
|
this.host.addController?.(this);
|
|
7
7
|
}
|
|
8
8
|
hostConnected() {
|
|
@@ -11,20 +11,19 @@ class ContextProvider {
|
|
|
11
11
|
hostDisconnected() {
|
|
12
12
|
this.host.removeEventListener("context-request", this.onContextRequest);
|
|
13
13
|
}
|
|
14
|
-
set
|
|
15
|
-
this.state.
|
|
14
|
+
set(newValue) {
|
|
15
|
+
this.state.set(newValue);
|
|
16
16
|
}
|
|
17
|
-
get
|
|
18
|
-
return this.state.
|
|
19
|
-
}
|
|
20
|
-
notify() {
|
|
21
|
-
this.state.notify();
|
|
17
|
+
get() {
|
|
18
|
+
return this.state.get();
|
|
22
19
|
}
|
|
23
20
|
onContextRequest=ev => {
|
|
24
21
|
if (ev.context !== this.context) return;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
let unsubscribe;
|
|
23
|
+
ev.stopPropagation(), ev.subscribe && (unsubscribe = effect((() => {
|
|
24
|
+
const value = this.get();
|
|
25
|
+
unsubscribe && ev.callback(value, unsubscribe);
|
|
26
|
+
}))), ev.callback(this.get(), unsubscribe);
|
|
28
27
|
};
|
|
29
28
|
}
|
|
30
29
|
|
package/dist/element.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { camelToKebabCase } from './case.js';
|
|
2
|
+
|
|
3
|
+
import { createSignal } from './signal.js';
|
|
4
|
+
|
|
1
5
|
class MiElement extends HTMLElement {
|
|
2
6
|
#attr={};
|
|
3
7
|
#attrLc=new Map;
|
|
@@ -9,19 +13,19 @@ class MiElement extends HTMLElement {
|
|
|
9
13
|
mode: 'open'
|
|
10
14
|
};
|
|
11
15
|
constructor() {
|
|
12
|
-
super(), this.#
|
|
13
|
-
...this.constructor.attributes
|
|
14
|
-
}, this.#observedAttributes();
|
|
16
|
+
super(), this.#observedAttributes(this.constructor.attributes);
|
|
15
17
|
}
|
|
16
|
-
#observedAttributes() {
|
|
17
|
-
for (const [name,
|
|
18
|
-
this.#attrLc.set(name.toLowerCase(), name),
|
|
18
|
+
#observedAttributes(attributes = {}) {
|
|
19
|
+
for (const [name, value] of Object.entries(attributes)) this.#types.set(name, initialType(value)),
|
|
20
|
+
this.#attrLc.set(name.toLowerCase(), name), this.#attrLc.set(camelToKebabCase(name), name),
|
|
21
|
+
this.#attr[name] = createSignal(value), Object.defineProperty(this, name, {
|
|
19
22
|
enumerable: !0,
|
|
20
23
|
get() {
|
|
21
|
-
return this.#attr[name];
|
|
24
|
+
return this.#attr[name].get();
|
|
22
25
|
},
|
|
23
26
|
set(newValue) {
|
|
24
|
-
|
|
27
|
+
const oldValue = this.#attr[name].get();
|
|
28
|
+
oldValue !== newValue && (this.#attr[name].set(newValue), this.#changedAttr[name] = oldValue,
|
|
25
29
|
this.requestUpdate());
|
|
26
30
|
}
|
|
27
31
|
});
|
|
@@ -41,16 +45,20 @@ class MiElement extends HTMLElement {
|
|
|
41
45
|
disconnectedCallback() {
|
|
42
46
|
this.#disposers.forEach((remover => remover())), this.#controllers.forEach((controller => controller.hostDisconnected?.()));
|
|
43
47
|
}
|
|
44
|
-
attributeChangedCallback(name,
|
|
45
|
-
const attr = this.#getName(name), type = this.#getType(attr)
|
|
46
|
-
this.#
|
|
47
|
-
this.requestUpdate();
|
|
48
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
49
|
+
const attr = this.#getName(name), type = this.#getType(attr);
|
|
50
|
+
this.#changedAttr[attr] = this[attr], this[attr] = convertType(newValue, type),
|
|
51
|
+
'Boolean' === type && 'false' === newValue && this.removeAttribute(name), this.requestUpdate();
|
|
48
52
|
}
|
|
49
53
|
setAttribute(name, newValue) {
|
|
50
54
|
const attr = this.#getName(name);
|
|
51
55
|
if (!(attr in this.#attr)) return;
|
|
52
56
|
const type = this.#getType(attr);
|
|
53
|
-
|
|
57
|
+
'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],
|
|
58
|
+
this[attr] = newValue, this.requestUpdate());
|
|
59
|
+
}
|
|
60
|
+
shouldUpdate(_changedAttributes) {
|
|
61
|
+
return !0;
|
|
54
62
|
}
|
|
55
63
|
requestUpdate() {
|
|
56
64
|
this.isConnected && requestAnimationFrame((() => {
|
|
@@ -61,9 +69,6 @@ class MiElement extends HTMLElement {
|
|
|
61
69
|
template instanceof HTMLTemplateElement && this.renderRoot.appendChild(template.content.cloneNode(!0));
|
|
62
70
|
}
|
|
63
71
|
render() {}
|
|
64
|
-
shouldUpdate(_changedAttributes) {
|
|
65
|
-
return !0;
|
|
66
|
-
}
|
|
67
72
|
update(_changedAttributes) {}
|
|
68
73
|
on(eventName, listener, node = this) {
|
|
69
74
|
node.addEventListener(eventName, listener), this.#disposers.add((() => node.removeEventListener(eventName, listener)));
|
|
@@ -73,9 +78,11 @@ class MiElement extends HTMLElement {
|
|
|
73
78
|
once: !0
|
|
74
79
|
});
|
|
75
80
|
}
|
|
76
|
-
dispose(
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
dispose(...listeners) {
|
|
82
|
+
for (const listener of listeners) {
|
|
83
|
+
if ('function' != typeof listener) throw new TypeError('listener must be a function');
|
|
84
|
+
this.#disposers.add(listener);
|
|
85
|
+
}
|
|
79
86
|
}
|
|
80
87
|
addController(controller) {
|
|
81
88
|
this.#controllers.add(controller), this.isConnected && controller.hostConnected?.();
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,8 @@ export { esc, escAttr, escHtml } from './escape.js';
|
|
|
6
6
|
|
|
7
7
|
export { refsById, refsBySelector } from './refs.js';
|
|
8
8
|
|
|
9
|
-
export {
|
|
9
|
+
export { default as Signal } from './signal.js';
|
|
10
10
|
|
|
11
|
-
export { Store
|
|
11
|
+
export { Store } from './store.js';
|
|
12
|
+
|
|
13
|
+
export { classMap, styleMap } from './styling.js';
|
package/dist/refs.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
+
import { kebabToCamelCase } from './case.js';
|
|
2
|
+
|
|
1
3
|
function refsById(container) {
|
|
2
4
|
const nodes = container.querySelectorAll?.('[id]') || [], found = {};
|
|
3
5
|
for (const node of nodes) found[kebabToCamelCase(node.getAttribute('id') || node.nodeName.toLowerCase())] = node;
|
|
4
6
|
return found;
|
|
5
7
|
}
|
|
6
8
|
|
|
7
|
-
const kebabToCamelCase = (str = "") => str.toLowerCase().replace(/[-_]\w/g, (m => m[1].toUpperCase()));
|
|
8
|
-
|
|
9
9
|
function refsBySelector(container, selectors) {
|
|
10
10
|
const found = {};
|
|
11
11
|
for (const [name, selector] of Object.entries(selectors)) found[name] = container.querySelector?.(selector);
|
|
12
12
|
return found;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export {
|
|
15
|
+
export { refsById, refsBySelector };
|
package/dist/signal.js
CHANGED
|
@@ -1,24 +1,62 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
const context = [];
|
|
2
|
+
|
|
3
|
+
class State {
|
|
4
|
+
subscribers=new Set;
|
|
5
|
+
constructor(value, options) {
|
|
6
|
+
const {equals: equals} = options || {};
|
|
7
|
+
this.value = value, this.equals = equals ?? ((value, nextValue) => value === nextValue);
|
|
5
8
|
}
|
|
6
|
-
get
|
|
7
|
-
|
|
9
|
+
get() {
|
|
10
|
+
const running = context[context.length - 1];
|
|
11
|
+
return running && (this.subscribers.add(running), running.dependencies.add(this.subscribers)),
|
|
12
|
+
this.value;
|
|
8
13
|
}
|
|
9
|
-
set
|
|
10
|
-
this.
|
|
14
|
+
set(nextValue) {
|
|
15
|
+
if (!this.equals(this.value, nextValue)) {
|
|
16
|
+
this.value = nextValue;
|
|
17
|
+
for (const running of [ ...this.subscribers ]) running.execute();
|
|
18
|
+
}
|
|
11
19
|
}
|
|
12
|
-
|
|
13
|
-
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const createSignal = value => value instanceof State ? value : new State(value);
|
|
23
|
+
|
|
24
|
+
function cleanup(running) {
|
|
25
|
+
for (const dep of running.dependencies) dep.delete(running);
|
|
26
|
+
running.dependencies.clear();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function effect(cb) {
|
|
30
|
+
const execute = () => {
|
|
31
|
+
cleanup(running), context.push(running);
|
|
32
|
+
try {
|
|
33
|
+
cb();
|
|
34
|
+
} finally {
|
|
35
|
+
context.pop();
|
|
36
|
+
}
|
|
37
|
+
}, running = {
|
|
38
|
+
execute: execute,
|
|
39
|
+
dependencies: new Set
|
|
40
|
+
};
|
|
41
|
+
return execute(), () => {
|
|
42
|
+
cleanup(running);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class Computed {
|
|
47
|
+
constructor(cb) {
|
|
48
|
+
this.state = new State, effect((() => this.state.set(cb)));
|
|
14
49
|
}
|
|
15
|
-
|
|
16
|
-
return this.
|
|
17
|
-
this._subscribers.delete(callback);
|
|
18
|
-
};
|
|
50
|
+
get() {
|
|
51
|
+
return this.state.get();
|
|
19
52
|
}
|
|
20
53
|
}
|
|
21
54
|
|
|
22
|
-
|
|
55
|
+
var signal = {
|
|
56
|
+
State: State,
|
|
57
|
+
createSignal: createSignal,
|
|
58
|
+
effect: effect,
|
|
59
|
+
Computed: Computed
|
|
60
|
+
};
|
|
23
61
|
|
|
24
|
-
export {
|
|
62
|
+
export { Computed, State, createSignal, signal as default, effect };
|
package/dist/store.js
CHANGED
|
@@ -1,28 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { State } from './signal.js';
|
|
2
2
|
|
|
3
|
-
class Store extends
|
|
4
|
-
constructor(actions, initialValue) {
|
|
5
|
-
super(initialValue);
|
|
6
|
-
for (const [action, dispatcher] of Object.entries(actions)) this[action] = data =>
|
|
7
|
-
this.value = dispatcher(data)(this.value);
|
|
8
|
-
};
|
|
3
|
+
class Store extends State {
|
|
4
|
+
constructor(actions, initialValue, options) {
|
|
5
|
+
super(initialValue, options);
|
|
6
|
+
for (const [action, dispatcher] of Object.entries(actions)) this[action] = data => this.set(dispatcher(data)(this.get()));
|
|
9
7
|
}
|
|
10
8
|
}
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
if (propOrSignal instanceof Signal || isSignalLike(propOrSignal)) return void element.dispose(store.subscribe((value => {
|
|
14
|
-
propOrSignal.value = value;
|
|
15
|
-
})));
|
|
16
|
-
const keys = Array.isArray(propOrSignal) ? propOrSignal : propOrSignal.split('.').filter(Boolean), last = keys.pop();
|
|
17
|
-
if (!last) throw TypeError('need prop');
|
|
18
|
-
let tmp = element;
|
|
19
|
-
for (const key of keys) {
|
|
20
|
-
if ('object' != typeof tmp[key]) throw new TypeError(`object expected for property "${key}"`);
|
|
21
|
-
tmp = tmp[key];
|
|
22
|
-
}
|
|
23
|
-
element.dispose(store.subscribe((value => {
|
|
24
|
-
tmp[last] = value, element.requestUpdate();
|
|
25
|
-
})));
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export { Store, subscribeToStore };
|
|
10
|
+
export { Store };
|
package/dist/styling.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { camelToKebabCase } from './case.js';
|
|
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(' ');
|
|
7
|
+
}, styleMap = (map, options) => {
|
|
8
|
+
const {unit: unit = "px"} = options || {}, acc = [];
|
|
9
|
+
for (const [name, value] of Object.entries(map ?? {})) {
|
|
10
|
+
if (null == value) continue;
|
|
11
|
+
const _unit = Number.isFinite(value) ? unit : '';
|
|
12
|
+
acc.push(`${camelToKebabCase(name)}:${value}${_unit}`);
|
|
13
|
+
}
|
|
14
|
+
return acc.join(';');
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export { classMap, styleMap };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mi-element",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
"development": "./src/element.js",
|
|
30
30
|
"types": "./types/element.d.ts"
|
|
31
31
|
},
|
|
32
|
+
"./case": {
|
|
33
|
+
"default": "./dist/case.js",
|
|
34
|
+
"development": "./src/case.js",
|
|
35
|
+
"types": "./types/case.d.ts"
|
|
36
|
+
},
|
|
32
37
|
"./context": {
|
|
33
38
|
"default": "./dist/context.js",
|
|
34
39
|
"development": "./src/context.js",
|
|
@@ -54,6 +59,11 @@
|
|
|
54
59
|
"development": "./src/store.js",
|
|
55
60
|
"types": "./types/store.d.ts"
|
|
56
61
|
},
|
|
62
|
+
"./styling": {
|
|
63
|
+
"default": "./dist/styling.js",
|
|
64
|
+
"development": "./src/styling.js",
|
|
65
|
+
"types": "./types/styling.d.ts"
|
|
66
|
+
},
|
|
57
67
|
"./package.json": {
|
|
58
68
|
"default": "./package.json"
|
|
59
69
|
}
|
|
@@ -83,8 +93,8 @@
|
|
|
83
93
|
"vitest": "^2.0.5"
|
|
84
94
|
},
|
|
85
95
|
"scripts": {
|
|
86
|
-
"all": "npm-run-all lint test build",
|
|
87
|
-
"build": "rimraf dist && rollup -c && gzip dist/index.min.js && ls -al dist && rimraf dist/index.min.*",
|
|
96
|
+
"all": "npm-run-all pretty lint test build types",
|
|
97
|
+
"build": "rimraf dist && rollup -c && gzip -k dist/index.min.js && ls -al dist && rimraf dist/index.min.*",
|
|
88
98
|
"example": "vite --open /example/",
|
|
89
99
|
"lint": "eslint",
|
|
90
100
|
"pretty": "prettier -w **/*.js",
|
package/src/case.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* convert lowerCamelCase to kebab-case
|
|
3
|
+
* @param {string} str
|
|
4
|
+
* @returns {string}
|
|
5
|
+
*/
|
|
6
|
+
export const camelToKebabCase = (str = '') =>
|
|
7
|
+
str.replace(/([A-Z])/g, (_, m) => `-${m.toLowerCase()}`)
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* convert kebab-case to lowerCamelCase
|
|
11
|
+
* @param {string} str
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export const kebabToCamelCase = (str = '') =>
|
|
15
|
+
str.toLowerCase().replace(/[-_]\w/g, (m) => m[1].toUpperCase())
|
package/src/context.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @see https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { createSignal,
|
|
5
|
+
import { createSignal, effect } from './signal.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @typedef {import('./element.js').HostController} HostController
|
|
@@ -25,9 +25,7 @@ export class ContextProvider {
|
|
|
25
25
|
constructor(host, context, initialValue) {
|
|
26
26
|
this.host = host
|
|
27
27
|
this.context = context
|
|
28
|
-
this.state =
|
|
29
|
-
? initialValue
|
|
30
|
-
: createSignal(initialValue)
|
|
28
|
+
this.state = createSignal(initialValue)
|
|
31
29
|
// @ts-expect-error
|
|
32
30
|
this.host.addController?.(this)
|
|
33
31
|
}
|
|
@@ -45,24 +43,20 @@ export class ContextProvider {
|
|
|
45
43
|
/**
|
|
46
44
|
* @param {any} newValue
|
|
47
45
|
*/
|
|
48
|
-
set
|
|
49
|
-
this.state.
|
|
46
|
+
set(newValue) {
|
|
47
|
+
this.state.set(newValue)
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
/**
|
|
53
51
|
* @returns {any}
|
|
54
52
|
*/
|
|
55
|
-
get
|
|
56
|
-
return this.state.
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
notify() {
|
|
60
|
-
this.state.notify()
|
|
53
|
+
get() {
|
|
54
|
+
return this.state.get()
|
|
61
55
|
}
|
|
62
56
|
|
|
63
57
|
/**
|
|
64
58
|
* @private
|
|
65
|
-
* @param {ContextRequestEvent} ev
|
|
59
|
+
* @param {ContextRequestEvent} ev
|
|
66
60
|
*/
|
|
67
61
|
onContextRequest = (ev) => {
|
|
68
62
|
if (ev.context !== this.context) {
|
|
@@ -70,10 +64,17 @@ export class ContextProvider {
|
|
|
70
64
|
return
|
|
71
65
|
}
|
|
72
66
|
ev.stopPropagation()
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
console.debug('provider.onContextRequest', this.state)
|
|
68
|
+
let unsubscribe
|
|
69
|
+
if (ev.subscribe) {
|
|
70
|
+
unsubscribe = effect(() => {
|
|
71
|
+
// needed to subscribe to signal
|
|
72
|
+
const value = this.get()
|
|
73
|
+
// don't call callback the first time where unsubscribe is not defined
|
|
74
|
+
if (unsubscribe) ev.callback(value, unsubscribe)
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
ev.callback(this.get(), unsubscribe)
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
|
|
@@ -140,6 +141,7 @@ export class ContextConsumer {
|
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
_callback(value, unsubscribe) {
|
|
144
|
+
console.debug('consumer.callback', { value, unsubscribe })
|
|
143
145
|
if (unsubscribe) {
|
|
144
146
|
if (!this.subscribe) {
|
|
145
147
|
// unsubscribe as we didn't ask for subscription
|
package/src/element.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { camelToKebabCase } from './case.js'
|
|
2
|
+
import { createSignal } from './signal.js'
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* @typedef {object} HostController controller
|
|
3
6
|
* @property {() => void} hostConnected is called when host element is added to
|
|
@@ -57,28 +60,29 @@ export class MiElement extends HTMLElement {
|
|
|
57
60
|
constructor() {
|
|
58
61
|
super()
|
|
59
62
|
// @ts-expect-error
|
|
60
|
-
this.#
|
|
61
|
-
this.#observedAttributes()
|
|
63
|
+
this.#observedAttributes(this.constructor.attributes)
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
/**
|
|
65
67
|
* requests update on component when property changes
|
|
66
68
|
*/
|
|
67
|
-
#observedAttributes() {
|
|
68
|
-
for (const [name,
|
|
69
|
-
this.#types.set(name, initialType(
|
|
69
|
+
#observedAttributes(attributes = {}) {
|
|
70
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
71
|
+
this.#types.set(name, initialType(value))
|
|
70
72
|
this.#attrLc.set(name.toLowerCase(), name)
|
|
73
|
+
this.#attrLc.set(camelToKebabCase(name), name)
|
|
74
|
+
this.#attr[name] = createSignal(value)
|
|
71
75
|
Object.defineProperty(this, name, {
|
|
72
76
|
enumerable: true,
|
|
73
77
|
get() {
|
|
74
|
-
return this.#attr[name]
|
|
78
|
+
return this.#attr[name].get()
|
|
75
79
|
},
|
|
76
80
|
set(newValue) {
|
|
77
81
|
console.debug('%s.%s =', this.nodeName, name, newValue)
|
|
78
|
-
const oldValue = this.#attr[name]
|
|
82
|
+
const oldValue = this.#attr[name].get()
|
|
79
83
|
if (oldValue === newValue) return
|
|
80
|
-
this.#attr[name]
|
|
81
|
-
this.#changedAttr[name] =
|
|
84
|
+
this.#attr[name].set(newValue)
|
|
85
|
+
this.#changedAttr[name] = oldValue
|
|
82
86
|
this.requestUpdate()
|
|
83
87
|
}
|
|
84
88
|
})
|
|
@@ -129,14 +133,14 @@ export class MiElement extends HTMLElement {
|
|
|
129
133
|
|
|
130
134
|
/**
|
|
131
135
|
* @param {string} name change attribute
|
|
132
|
-
* @param {any}
|
|
136
|
+
* @param {any} oldValue
|
|
133
137
|
* @param {any} newValue new value
|
|
134
138
|
*/
|
|
135
|
-
attributeChangedCallback(name,
|
|
139
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
136
140
|
const attr = this.#getName(name)
|
|
137
141
|
const type = this.#getType(attr)
|
|
138
|
-
|
|
139
|
-
this
|
|
142
|
+
this.#changedAttr[attr] = this[attr]
|
|
143
|
+
this[attr] = convertType(newValue, type)
|
|
140
144
|
// correct initial setting of `trueish="false"` otherwise there's no chance
|
|
141
145
|
// to overwrite a trueish value. The case `falsish="true"` is covered.
|
|
142
146
|
if (type === 'Boolean' && newValue === 'false') {
|
|
@@ -146,7 +150,7 @@ export class MiElement extends HTMLElement {
|
|
|
146
150
|
'%s.attributeChangedCallback("%s",',
|
|
147
151
|
this.nodeName,
|
|
148
152
|
name,
|
|
149
|
-
|
|
153
|
+
oldValue,
|
|
150
154
|
newValue
|
|
151
155
|
)
|
|
152
156
|
this.requestUpdate()
|
|
@@ -165,7 +169,6 @@ export class MiElement extends HTMLElement {
|
|
|
165
169
|
return
|
|
166
170
|
}
|
|
167
171
|
const type = this.#getType(attr)
|
|
168
|
-
this.#attr[attr] = this.#changedAttr[attr] = newValue
|
|
169
172
|
console.debug('%s.setAttribute("%s",', this.nodeName, name, newValue)
|
|
170
173
|
|
|
171
174
|
// only set string values in these cases
|
|
@@ -178,10 +181,21 @@ export class MiElement extends HTMLElement {
|
|
|
178
181
|
} else if (['String', 'Number'].includes(type) || newValue === true) {
|
|
179
182
|
super.setAttribute(name, newValue)
|
|
180
183
|
} else {
|
|
184
|
+
this.#changedAttr[attr] = this[attr]
|
|
185
|
+
this[attr] = newValue
|
|
181
186
|
this.requestUpdate()
|
|
182
187
|
}
|
|
183
188
|
}
|
|
184
189
|
|
|
190
|
+
/**
|
|
191
|
+
* controls if component shall be updated
|
|
192
|
+
* @param {Record<string,any>} [_changedAttributes] previous values of changed attributes
|
|
193
|
+
* @returns {boolean}
|
|
194
|
+
*/
|
|
195
|
+
shouldUpdate(_changedAttributes) {
|
|
196
|
+
return true
|
|
197
|
+
}
|
|
198
|
+
|
|
185
199
|
/**
|
|
186
200
|
* request rendering
|
|
187
201
|
*/
|
|
@@ -198,7 +212,7 @@ export class MiElement extends HTMLElement {
|
|
|
198
212
|
|
|
199
213
|
/**
|
|
200
214
|
* adds a template to renderRoot
|
|
201
|
-
* @param {HTMLTemplateElement}
|
|
215
|
+
* @param {HTMLTemplateElement} template
|
|
202
216
|
*/
|
|
203
217
|
addTemplate(template) {
|
|
204
218
|
if (!(template instanceof HTMLTemplateElement)) {
|
|
@@ -213,18 +227,10 @@ export class MiElement extends HTMLElement {
|
|
|
213
227
|
*/
|
|
214
228
|
render() {}
|
|
215
229
|
|
|
216
|
-
/**
|
|
217
|
-
* controls if component shall be updated
|
|
218
|
-
* @param {Record<string,any>} _changedAttributes changed attributes
|
|
219
|
-
* @returns {boolean}
|
|
220
|
-
*/
|
|
221
|
-
shouldUpdate(_changedAttributes) {
|
|
222
|
-
return true
|
|
223
|
-
}
|
|
224
|
-
|
|
225
230
|
/**
|
|
226
231
|
* called every time the components needs a render update
|
|
227
|
-
* @param {Record<string,any>} _changedAttributes changed
|
|
232
|
+
* @param {Record<string,any>} [_changedAttributes] previous values of changed
|
|
233
|
+
* attributes
|
|
228
234
|
*/
|
|
229
235
|
update(_changedAttributes) {}
|
|
230
236
|
|
|
@@ -253,13 +259,15 @@ export class MiElement extends HTMLElement {
|
|
|
253
259
|
|
|
254
260
|
/**
|
|
255
261
|
* Unsubscribe a listener function for disposal on disconnectedCallback()
|
|
256
|
-
* @param {function}
|
|
262
|
+
* @param {...function} listeners
|
|
257
263
|
*/
|
|
258
|
-
dispose(
|
|
259
|
-
|
|
260
|
-
|
|
264
|
+
dispose(...listeners) {
|
|
265
|
+
for (const listener of listeners) {
|
|
266
|
+
if (typeof listener !== 'function') {
|
|
267
|
+
throw new TypeError('listener must be a function')
|
|
268
|
+
}
|
|
269
|
+
this.#disposers.add(listener)
|
|
261
270
|
}
|
|
262
|
-
this.#disposers.add(listener)
|
|
263
271
|
}
|
|
264
272
|
|
|
265
273
|
/**
|
package/src/index.js
CHANGED
|
@@ -13,10 +13,13 @@ export { MiElement, convertType, define } from './element.js'
|
|
|
13
13
|
export { esc, escAttr, escHtml } from './escape.js'
|
|
14
14
|
export { refsById, refsBySelector } from './refs.js'
|
|
15
15
|
/**
|
|
16
|
-
* @
|
|
16
|
+
* @template T
|
|
17
|
+
* @typedef {import('./signal.js').SignalOptions<T>} SignalOptions<T>
|
|
17
18
|
*/
|
|
18
|
-
|
|
19
|
+
import Signal from './signal.js'
|
|
20
|
+
export { Signal }
|
|
19
21
|
/**
|
|
20
22
|
* @typedef {import('./store.js').Action} Action
|
|
21
23
|
*/
|
|
22
|
-
export { Store
|
|
24
|
+
export { Store } from './store.js'
|
|
25
|
+
export { classMap, styleMap } from './styling.js'
|
package/src/refs.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { kebabToCamelCase } from './case.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Helper function to find `id` attributes in `container`s node tree.
|
|
3
5
|
* id names are camelCased, e.g. 'list-container' becomes 'listContainer'
|
|
@@ -20,14 +22,6 @@ export function refsById(container) {
|
|
|
20
22
|
return found
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
/**
|
|
24
|
-
* convert kebab-case to lowerCamelCase
|
|
25
|
-
* @param {string} str
|
|
26
|
-
* @returns {string}
|
|
27
|
-
*/
|
|
28
|
-
export const kebabToCamelCase = (str = '') =>
|
|
29
|
-
str.toLowerCase().replace(/[-_]\w/g, (m) => m[1].toUpperCase())
|
|
30
|
-
|
|
31
25
|
/**
|
|
32
26
|
* Helper function to gather references by a map of selectors
|
|
33
27
|
* @param {Element} container root element
|