mi-element 0.2.0 → 0.3.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 +31 -16
- package/dist/signal.js +21 -30
- package/package.json +2 -1
- package/src/signal.js +58 -44
- package/types/signal.d.ts +29 -16
package/README.md
CHANGED
|
@@ -4,13 +4,24 @@
|
|
|
4
4
|
|
|
5
5
|
Only weights 2.3kB minified and gzipped.
|
|
6
6
|
|
|
7
|
-
mi-element provides
|
|
7
|
+
mi-element provides features to build web applications through
|
|
8
8
|
[Web Components][] like:
|
|
9
9
|
|
|
10
|
+
- type coercion with setAttribute
|
|
10
11
|
- controllers to hook into the components lifecycle
|
|
11
12
|
- ContextProvider, ContextConsumer for data provisioning from outside of a
|
|
12
13
|
component
|
|
13
14
|
- Store for managing shared state across components
|
|
15
|
+
- Signal for reactive behavior
|
|
16
|
+
|
|
17
|
+
The motivation to build this module comes from the confusions around attributes
|
|
18
|
+
and properties. "mi-element" solves this by providing the same results when
|
|
19
|
+
setting objects or functions either through `el.setAttribute(name, value)` or
|
|
20
|
+
properties `el[name] = value`.
|
|
21
|
+
|
|
22
|
+
Furthermore all observed attributes have a reactive behavior through the use of
|
|
23
|
+
signals and effects. It implements signals (loosely) following the
|
|
24
|
+
[TC39 JavaScript Signals standard proposal][].
|
|
14
25
|
|
|
15
26
|
# Usage
|
|
16
27
|
|
|
@@ -23,7 +34,7 @@ npm i mi-element
|
|
|
23
34
|
```js
|
|
24
35
|
/** @file ./mi-counter.js */
|
|
25
36
|
|
|
26
|
-
import { MiElement, define, refsById } from 'mi-element'
|
|
37
|
+
import { MiElement, define, refsById, Signal } from 'mi-element'
|
|
27
38
|
|
|
28
39
|
// define your Component
|
|
29
40
|
class MiCounter extends MiElement {
|
|
@@ -31,13 +42,12 @@ class MiCounter extends MiElement {
|
|
|
31
42
|
<style>
|
|
32
43
|
:host { font-size: 1.25rem; }
|
|
33
44
|
</style>
|
|
34
|
-
<
|
|
35
|
-
<span id aria-label="Counter value">0</span>
|
|
45
|
+
<div id aria-label="Counter value">0</div>
|
|
36
46
|
<button id="increment" aria-label="Increment counter"> + </button>
|
|
37
47
|
`
|
|
38
48
|
|
|
39
|
-
// declare reactive attributes
|
|
40
49
|
static get attributes() {
|
|
50
|
+
// declare reactive attribute(s)
|
|
41
51
|
return { count: 0 }
|
|
42
52
|
}
|
|
43
53
|
|
|
@@ -46,13 +56,14 @@ class MiCounter extends MiElement {
|
|
|
46
56
|
// gather refs from template (here by id)
|
|
47
57
|
this.refs = refsById(this.renderRoot)
|
|
48
58
|
// apply event listeners
|
|
49
|
-
this.refs.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
this.refs.increment.addEventListener('click', () => {
|
|
60
|
+
// change observed and reactive attribute...
|
|
61
|
+
this.count++
|
|
62
|
+
})
|
|
63
|
+
Signal.effect(() => {
|
|
64
|
+
// ...triggers update on every change of `this.count`
|
|
65
|
+
this.refs.div.textContent = this.count
|
|
66
|
+
})
|
|
56
67
|
}
|
|
57
68
|
}
|
|
58
69
|
|
|
@@ -60,7 +71,7 @@ class MiCounter extends MiElement {
|
|
|
60
71
|
define('mi-counter', MiCounter)
|
|
61
72
|
```
|
|
62
73
|
|
|
63
|
-
Now use in your HTML
|
|
74
|
+
Now use your now component in your HTML
|
|
64
75
|
|
|
65
76
|
```html
|
|
66
77
|
<body>
|
|
@@ -77,18 +88,22 @@ In `./example` you'll find a working sample of a Todo App. Check it out with
|
|
|
77
88
|
|
|
78
89
|
- [lifecycle][docs-lifecycle] mi-element's lifecycle
|
|
79
90
|
- [controller][docs-controller] adding controllers to mi-element to hook into the lifecycle
|
|
80
|
-
- [
|
|
91
|
+
- [signal][docs-signal] Signals and effect for reactive behavior
|
|
81
92
|
- [store][docs-store] Manage shared state in an application
|
|
93
|
+
- [context][docs-context] Implementation of the [Context Protocol][].
|
|
82
94
|
- [styling][docs-styling] Styling directives for "class" and "style"
|
|
83
95
|
|
|
84
96
|
# License
|
|
85
97
|
|
|
86
98
|
MIT licensed
|
|
87
99
|
|
|
88
|
-
[Context Protocol]: https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md
|
|
89
|
-
[Web Components]: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks
|
|
90
100
|
[docs-lifecycle]: https://github.com/commenthol/mi-element/tree/main/packages/mi-element/docs/lifecycle.md
|
|
91
101
|
[docs-controller]: https://github.com/commenthol/mi-element/tree/main/packages/mi-element/docs/controller.md
|
|
92
102
|
[docs-context]: https://github.com/commenthol/mi-element/tree/main/packages/mi-element/docs/context.md
|
|
103
|
+
[docs-signal]: https://github.com/commenthol/mi-element/tree/main/packages/mi-element/docs/signal.md
|
|
93
104
|
[docs-store]: https://github.com/commenthol/mi-element/tree/main/packages/mi-element/docs/store.md
|
|
94
105
|
[docs-styling]: https://github.com/commenthol/mi-element/tree/main/packages/mi-element/docs/styling.md
|
|
106
|
+
[Context Protocol]: https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md
|
|
107
|
+
[Web Components]: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks
|
|
108
|
+
[TC39 JavaScript Signals standard proposal]: https://github.com/tc39/proposal-signals
|
|
109
|
+
[krausest/js-framework-benchmark]: https://github.com/krausest/js-framework-benchmark
|
package/dist/signal.js
CHANGED
|
@@ -1,54 +1,45 @@
|
|
|
1
1
|
const context = [];
|
|
2
2
|
|
|
3
|
-
class State {
|
|
4
|
-
|
|
3
|
+
class State extends EventTarget {
|
|
4
|
+
#value;
|
|
5
|
+
#equals;
|
|
5
6
|
constructor(value, options) {
|
|
7
|
+
super();
|
|
6
8
|
const {equals: equals} = options || {};
|
|
7
|
-
this
|
|
9
|
+
this.#value = value, this.#equals = equals ?? ((value, nextValue) => value === nextValue);
|
|
8
10
|
}
|
|
9
11
|
get() {
|
|
10
12
|
const running = context[context.length - 1];
|
|
11
|
-
return running &&
|
|
12
|
-
this.value;
|
|
13
|
+
return running && running.add(this), this.#value;
|
|
13
14
|
}
|
|
14
15
|
set(nextValue) {
|
|
15
|
-
|
|
16
|
-
this.value = nextValue;
|
|
17
|
-
for (const running of [ ...this.subscribers ]) running.execute();
|
|
18
|
-
}
|
|
16
|
+
this.#equals(this.#value, nextValue) || (this.#value = nextValue, this.dispatchEvent(new CustomEvent('signal')));
|
|
19
17
|
}
|
|
20
18
|
}
|
|
21
19
|
|
|
22
|
-
const createSignal =
|
|
23
|
-
|
|
24
|
-
function cleanup(running) {
|
|
25
|
-
for (const dep of running.dependencies) dep.delete(running);
|
|
26
|
-
running.dependencies.clear();
|
|
27
|
-
}
|
|
20
|
+
const createSignal = (initialValue, options) => initialValue instanceof State ? initialValue : new State(initialValue, options);
|
|
28
21
|
|
|
29
22
|
function effect(cb) {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
};
|
|
41
|
-
return execute(), () => {
|
|
42
|
-
cleanup(running);
|
|
23
|
+
const running = new Set;
|
|
24
|
+
context.push(running);
|
|
25
|
+
try {
|
|
26
|
+
cb();
|
|
27
|
+
} finally {
|
|
28
|
+
context.pop();
|
|
29
|
+
}
|
|
30
|
+
for (const dep of running) dep.addEventListener('signal', cb);
|
|
31
|
+
return () => {
|
|
32
|
+
for (const dep of running) dep.removeEventListener('signal', cb);
|
|
43
33
|
};
|
|
44
34
|
}
|
|
45
35
|
|
|
46
36
|
class Computed {
|
|
37
|
+
#state;
|
|
47
38
|
constructor(cb) {
|
|
48
|
-
this
|
|
39
|
+
this.#state = new State, effect((() => this.#state.set(cb())));
|
|
49
40
|
}
|
|
50
41
|
get() {
|
|
51
|
-
return this
|
|
42
|
+
return this.#state.get();
|
|
52
43
|
}
|
|
53
44
|
}
|
|
54
45
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mi-element",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
"scripts": {
|
|
96
96
|
"all": "npm-run-all pretty lint test build types",
|
|
97
97
|
"build": "rimraf dist && rollup -c && gzip -k dist/index.min.js && ls -al dist && rimraf dist/index.min.*",
|
|
98
|
+
"docs": "cd docs; for f in *.md; do markedpp -s -i $f; done",
|
|
98
99
|
"example": "vite --open /example/",
|
|
99
100
|
"lint": "eslint",
|
|
100
101
|
"pretty": "prettier -w **/*.js",
|
package/src/signal.js
CHANGED
|
@@ -1,27 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tries to follow proposal JavaScript Signals standard proposal which is in
|
|
3
|
+
* stage 1
|
|
4
|
+
* @see https://github.com/tc39/proposal-signals
|
|
5
|
+
* @credits https://github.com/jsebrech/tiny-signals
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
// global context for nested reactivity
|
|
2
9
|
const context = []
|
|
3
10
|
|
|
4
11
|
/**
|
|
5
12
|
* @template T
|
|
6
|
-
* @typedef {
|
|
13
|
+
* @typedef {(value?: T|null, nextValue?: T|null) => boolean} EqualsFn
|
|
14
|
+
* Custom comparison function between old and new value
|
|
15
|
+
* default `(value, nextValue) => value === nextValue`
|
|
7
16
|
*/
|
|
8
17
|
|
|
9
18
|
/**
|
|
10
|
-
* tries to follow proposal (a bit)
|
|
11
|
-
* @see https://github.com/tc39/proposal-signals
|
|
12
19
|
* @template T
|
|
20
|
+
* @typedef {{equals: EqualsFn<T>}} SignalOptions
|
|
13
21
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* read- write signal
|
|
25
|
+
* @template T
|
|
26
|
+
*/
|
|
27
|
+
export class State extends EventTarget {
|
|
28
|
+
#value
|
|
29
|
+
#equals
|
|
16
30
|
|
|
17
31
|
/**
|
|
18
32
|
* @param {T|null} [value]
|
|
19
33
|
* @param {SignalOptions<T>} [options]
|
|
20
34
|
*/
|
|
21
35
|
constructor(value, options) {
|
|
36
|
+
super()
|
|
22
37
|
const { equals } = options || {}
|
|
23
|
-
this
|
|
24
|
-
this
|
|
38
|
+
this.#value = value
|
|
39
|
+
this.#equals = equals ?? ((value, nextValue) => value === nextValue)
|
|
25
40
|
}
|
|
26
41
|
|
|
27
42
|
/**
|
|
@@ -30,73 +45,72 @@ export class State {
|
|
|
30
45
|
get() {
|
|
31
46
|
const running = context[context.length - 1]
|
|
32
47
|
if (running) {
|
|
33
|
-
|
|
34
|
-
running.dependencies.add(this.subscribers)
|
|
48
|
+
running.add(this)
|
|
35
49
|
}
|
|
36
|
-
return this
|
|
50
|
+
return this.#value
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
/**
|
|
40
54
|
* @param {T|null|undefined} nextValue
|
|
41
55
|
*/
|
|
42
56
|
set(nextValue) {
|
|
43
|
-
if (this
|
|
57
|
+
if (this.#equals(this.#value, nextValue)) {
|
|
44
58
|
return
|
|
45
59
|
}
|
|
46
|
-
this
|
|
47
|
-
|
|
48
|
-
// run the effect()
|
|
49
|
-
running.execute()
|
|
50
|
-
}
|
|
60
|
+
this.#value = nextValue
|
|
61
|
+
this.dispatchEvent(new CustomEvent('signal'))
|
|
51
62
|
}
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
/**
|
|
55
66
|
* @template T
|
|
56
|
-
* @param {T}
|
|
67
|
+
* @param {T} initialValue
|
|
68
|
+
* @param {SignalOptions<T>} [options]
|
|
57
69
|
* @returns {State<T>}
|
|
58
70
|
*/
|
|
59
|
-
export const createSignal = (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// delete all dependent subscribers from state
|
|
64
|
-
for (const dep of running.dependencies) {
|
|
65
|
-
dep.delete(running)
|
|
66
|
-
}
|
|
67
|
-
running.dependencies.clear()
|
|
68
|
-
}
|
|
71
|
+
export const createSignal = (initialValue, options) =>
|
|
72
|
+
initialValue instanceof State
|
|
73
|
+
? initialValue
|
|
74
|
+
: new State(initialValue, options)
|
|
69
75
|
|
|
70
76
|
/**
|
|
71
|
-
*
|
|
77
|
+
* effect subscribes to state at first run only. Do not hide a signal.get()
|
|
78
|
+
* inside conditionals!
|
|
79
|
+
* @param {() => void|Promise<void>} cb
|
|
72
80
|
*/
|
|
73
81
|
export function effect(cb) {
|
|
74
|
-
const
|
|
75
|
-
cleanup(running)
|
|
76
|
-
context.push(running)
|
|
77
|
-
try {
|
|
78
|
-
cb()
|
|
79
|
-
} finally {
|
|
80
|
-
context.pop()
|
|
81
|
-
}
|
|
82
|
-
}
|
|
82
|
+
const running = new Set()
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
context.push(running)
|
|
85
|
+
try {
|
|
86
|
+
cb()
|
|
87
|
+
} finally {
|
|
88
|
+
context.pop()
|
|
89
|
+
}
|
|
90
|
+
for (const dep of running) {
|
|
91
|
+
dep.addEventListener('signal', cb)
|
|
92
|
+
}
|
|
86
93
|
|
|
87
94
|
return () => {
|
|
88
95
|
// unsubscribe from all dependencies
|
|
89
|
-
|
|
96
|
+
for (const dep of running) {
|
|
97
|
+
dep.removeEventListener('signal', cb)
|
|
98
|
+
}
|
|
90
99
|
}
|
|
91
100
|
}
|
|
92
101
|
|
|
102
|
+
/**
|
|
103
|
+
* @template T
|
|
104
|
+
*/
|
|
93
105
|
export class Computed {
|
|
106
|
+
#state
|
|
107
|
+
|
|
94
108
|
/**
|
|
95
|
-
* @param {() =>
|
|
109
|
+
* @param {() => T} cb
|
|
96
110
|
*/
|
|
97
111
|
constructor(cb) {
|
|
98
|
-
this
|
|
99
|
-
effect(() => this
|
|
112
|
+
this.#state = new State()
|
|
113
|
+
effect(() => this.#state.set(cb()))
|
|
100
114
|
}
|
|
101
115
|
|
|
102
116
|
/**
|
|
@@ -104,7 +118,7 @@ export class Computed {
|
|
|
104
118
|
* @returns {T}
|
|
105
119
|
*/
|
|
106
120
|
get() {
|
|
107
|
-
return this
|
|
121
|
+
return this.#state.get()
|
|
108
122
|
}
|
|
109
123
|
}
|
|
110
124
|
|
package/types/signal.d.ts
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* effect subscribes to state at first run only. Do not hide a signal.get()
|
|
3
|
+
* inside conditionals!
|
|
4
|
+
* @param {() => void|Promise<void>} cb
|
|
3
5
|
*/
|
|
4
|
-
export function effect(cb: () => void): () => void;
|
|
6
|
+
export function effect(cb: () => void | Promise<void>): () => void;
|
|
5
7
|
/**
|
|
6
8
|
* @template T
|
|
7
|
-
* @typedef {
|
|
9
|
+
* @typedef {(value?: T|null, nextValue?: T|null) => boolean} EqualsFn
|
|
10
|
+
* Custom comparison function between old and new value
|
|
11
|
+
* default `(value, nextValue) => value === nextValue`
|
|
8
12
|
*/
|
|
9
13
|
/**
|
|
10
|
-
* tries to follow proposal (a bit)
|
|
11
|
-
* @see https://github.com/tc39/proposal-signals
|
|
12
14
|
* @template T
|
|
15
|
+
* @typedef {{equals: EqualsFn<T>}} SignalOptions
|
|
13
16
|
*/
|
|
14
|
-
|
|
17
|
+
/**
|
|
18
|
+
* read- write signal
|
|
19
|
+
* @template T
|
|
20
|
+
*/
|
|
21
|
+
export class State<T> extends EventTarget {
|
|
15
22
|
/**
|
|
16
23
|
* @param {T|null} [value]
|
|
17
24
|
* @param {SignalOptions<T>} [options]
|
|
18
25
|
*/
|
|
19
26
|
constructor(value?: T | null | undefined, options?: SignalOptions<T> | undefined);
|
|
20
|
-
subscribers: Set<any>;
|
|
21
|
-
value: T | null | undefined;
|
|
22
|
-
equals: (value?: T | null | undefined, nextValue?: T | null | undefined) => boolean;
|
|
23
27
|
/**
|
|
24
28
|
* @returns {T|null|undefined}
|
|
25
29
|
*/
|
|
@@ -28,19 +32,23 @@ export class State<T> {
|
|
|
28
32
|
* @param {T|null|undefined} nextValue
|
|
29
33
|
*/
|
|
30
34
|
set(nextValue: T | null | undefined): void;
|
|
35
|
+
#private;
|
|
31
36
|
}
|
|
32
|
-
export function createSignal<T>(
|
|
33
|
-
|
|
37
|
+
export function createSignal<T>(initialValue: T, options?: SignalOptions<T> | undefined): State<T>;
|
|
38
|
+
/**
|
|
39
|
+
* @template T
|
|
40
|
+
*/
|
|
41
|
+
export class Computed<T> {
|
|
34
42
|
/**
|
|
35
|
-
* @param {() =>
|
|
43
|
+
* @param {() => T} cb
|
|
36
44
|
*/
|
|
37
|
-
constructor(cb: () =>
|
|
38
|
-
state: State<any>;
|
|
45
|
+
constructor(cb: () => T);
|
|
39
46
|
/**
|
|
40
47
|
* @template T
|
|
41
48
|
* @returns {T}
|
|
42
49
|
*/
|
|
43
|
-
get<
|
|
50
|
+
get<T_1>(): T_1;
|
|
51
|
+
#private;
|
|
44
52
|
}
|
|
45
53
|
declare namespace _default {
|
|
46
54
|
export { State };
|
|
@@ -49,6 +57,11 @@ declare namespace _default {
|
|
|
49
57
|
export { Computed };
|
|
50
58
|
}
|
|
51
59
|
export default _default;
|
|
60
|
+
/**
|
|
61
|
+
* Custom comparison function between old and new value
|
|
62
|
+
* default `(value, nextValue) => value === nextValue`
|
|
63
|
+
*/
|
|
64
|
+
export type EqualsFn<T> = (value?: T | null, nextValue?: T | null) => boolean;
|
|
52
65
|
export type SignalOptions<T> = {
|
|
53
|
-
equals:
|
|
66
|
+
equals: EqualsFn<T>;
|
|
54
67
|
};
|