@tmbr/component 1.0.0 → 1.1.1
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 -13
- package/bind.js +4 -4
- package/index.js +51 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,6 +62,24 @@ class Counter extends Component {
|
|
|
62
62
|
new Counter('#counter');
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
## State
|
|
66
|
+
|
|
67
|
+
State is deeply reactive so mutations to nested objects and arrays trigger a render.
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
class Form extends Component {
|
|
71
|
+
static state = {
|
|
72
|
+
fields: {name: '', email: ''},
|
|
73
|
+
errors: {}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```html
|
|
79
|
+
<input type="text" :model="fields.name" />
|
|
80
|
+
<span :text="errors.name"></span>
|
|
81
|
+
```
|
|
82
|
+
|
|
65
83
|
## Directives
|
|
66
84
|
|
|
67
85
|
Directives are expressions evaluated with the current state.
|
|
@@ -136,16 +154,16 @@ this.dom.items // [li, li]
|
|
|
136
154
|
|
|
137
155
|
## Instance API
|
|
138
156
|
|
|
139
|
-
| Property or Method
|
|
140
|
-
|
|
|
141
|
-
|
|
|
142
|
-
|
|
|
143
|
-
|
|
|
144
|
-
|
|
|
145
|
-
|
|
|
146
|
-
|
|
|
147
|
-
|
|
|
148
|
-
|
|
|
149
|
-
|
|
|
150
|
-
|
|
|
151
|
-
|
|
|
157
|
+
| Property or Method | Description |
|
|
158
|
+
| --------------------------------- | --------------------------------------------------- |
|
|
159
|
+
| `el` | root element |
|
|
160
|
+
| `dom` | child elements collected from `ref` attributes |
|
|
161
|
+
| `props` | parsed from `data-props` attribute |
|
|
162
|
+
| `state` | reactive proxy where changes trigger a rerender |
|
|
163
|
+
| `findOne(selector)` | scoped `querySelector` |
|
|
164
|
+
| `findAll(selector)` | scoped `querySelectorAll` |
|
|
165
|
+
| `init()` | called after constructor — override in subclass |
|
|
166
|
+
| `update(state)` | called after each render — override in subclass |
|
|
167
|
+
| `on(event, target, fn)` | delegate event listener, cleaned up on `.destroy()` |
|
|
168
|
+
| `dispatch(type, detail, options)` | dispatches a `CustomEvent` from `.el` |
|
|
169
|
+
| `destroy()` | removes all listeners and directives |
|
package/bind.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cx, isFunction } from '@tmbr/utils';
|
|
1
|
+
import { cx, dot, isFunction } from '@tmbr/utils';
|
|
2
2
|
|
|
3
3
|
const BOOLEANS = new Set([
|
|
4
4
|
'allowfullscreen',
|
|
@@ -60,7 +60,7 @@ export function bindDirective(component, node, attr, expression) {
|
|
|
60
60
|
|
|
61
61
|
node.addEventListener(type, event => {
|
|
62
62
|
const value = isCheckbox ? event.target.checked : event.target.value;
|
|
63
|
-
component.state
|
|
63
|
+
dot(component.state, expression, isNumber ? Number(value) : value);
|
|
64
64
|
}, {signal: component.controller.signal});
|
|
65
65
|
|
|
66
66
|
} else if (attr === 'text') {
|
|
@@ -70,8 +70,8 @@ export function bindDirective(component, node, attr, expression) {
|
|
|
70
70
|
} else if (attr === 'value') {
|
|
71
71
|
apply = value => node.value = value;
|
|
72
72
|
} else if (attr === 'show') {
|
|
73
|
-
const
|
|
74
|
-
apply = value => node.style.display = value ?
|
|
73
|
+
const initial = node.style.display || '';
|
|
74
|
+
apply = value => node.style.display = value ? initial : 'none';
|
|
75
75
|
} else if (attr === 'class') {
|
|
76
76
|
const initial = node.className;
|
|
77
77
|
apply = value => { node.className = initial; cx(node, value); };
|
package/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { on, findOne, findAll, isDefined, isString, toJSON, traverse } from '@tmbr/utils';
|
|
1
|
+
import { on, findOne, findAll, isArray, isDefined, isObject, isString, toJSON, traverse } from '@tmbr/utils';
|
|
2
2
|
import { bindDirective, bindEvent } from './bind.js';
|
|
3
3
|
|
|
4
|
+
let cache;
|
|
4
5
|
let queue;
|
|
5
6
|
let scheduled = false;
|
|
6
7
|
|
|
7
8
|
function enqueue(component) {
|
|
8
9
|
queue ??= new Set();
|
|
9
10
|
queue.add(component);
|
|
10
|
-
|
|
11
11
|
if (scheduled) return;
|
|
12
12
|
queueMicrotask(flush);
|
|
13
13
|
scheduled = true;
|
|
@@ -20,18 +20,25 @@ function flush() {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function render(component) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
23
|
+
cache ??= new WeakMap();
|
|
24
|
+
let context = cache.get(component);
|
|
25
|
+
|
|
26
|
+
if (!context) {
|
|
27
|
+
const proto = Object.getPrototypeOf(component);
|
|
28
|
+
|
|
29
|
+
context = proto !== Component.prototype ? new Proxy(component.state, {
|
|
30
|
+
has(target, key) {
|
|
31
|
+
const own = Object.getOwnPropertyDescriptor(proto, key);
|
|
32
|
+
return own?.get ? true : key in target;
|
|
33
|
+
},
|
|
34
|
+
get(target, key, receiver) {
|
|
35
|
+
const own = Object.getOwnPropertyDescriptor(proto, key);
|
|
36
|
+
return own?.get ? own.get.call(component) : Reflect.get(target, key, receiver);
|
|
37
|
+
}
|
|
38
|
+
}) : component.state;
|
|
39
|
+
|
|
40
|
+
cache.set(component, context);
|
|
41
|
+
}
|
|
35
42
|
|
|
36
43
|
for (const apply of component.directives) apply(context);
|
|
37
44
|
component.update?.(context);
|
|
@@ -80,6 +87,30 @@ export default class Component {
|
|
|
80
87
|
|
|
81
88
|
#state() {
|
|
82
89
|
|
|
90
|
+
const state = this.el.dataset.state
|
|
91
|
+
? toJSON(this.el.dataset.state)
|
|
92
|
+
: structuredClone(this.constructor.state);
|
|
93
|
+
|
|
94
|
+
const instance = this;
|
|
95
|
+
const cache = new WeakMap();
|
|
96
|
+
|
|
97
|
+
const reactive = (obj) => {
|
|
98
|
+
if (cache.has(obj)) return cache.get(obj);
|
|
99
|
+
const proxy = new Proxy(obj, {
|
|
100
|
+
get(target, key, receiver) {
|
|
101
|
+
const value = Reflect.get(target, key, receiver);
|
|
102
|
+
return isArray(value) || isObject(value) ? reactive(value) : value;
|
|
103
|
+
},
|
|
104
|
+
set(target, key, value) {
|
|
105
|
+
target[key] = value;
|
|
106
|
+
enqueue(instance);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
cache.set(obj, proxy);
|
|
111
|
+
return proxy;
|
|
112
|
+
};
|
|
113
|
+
|
|
83
114
|
traverse(this.el, child => {
|
|
84
115
|
for (const {name, value} of [...child.attributes]) {
|
|
85
116
|
if (name.startsWith(':')) {
|
|
@@ -92,18 +123,8 @@ export default class Component {
|
|
|
92
123
|
}
|
|
93
124
|
});
|
|
94
125
|
|
|
95
|
-
const state = this.el.dataset.state
|
|
96
|
-
? toJSON(this.el.dataset.state)
|
|
97
|
-
: structuredClone(this.constructor.state);
|
|
98
|
-
|
|
99
|
-
const set = (target, key, value) => {
|
|
100
|
-
target[key] = value;
|
|
101
|
-
enqueue(this);
|
|
102
|
-
return true;
|
|
103
|
-
};
|
|
104
|
-
|
|
105
126
|
enqueue(this);
|
|
106
|
-
return
|
|
127
|
+
return reactive(state);
|
|
107
128
|
}
|
|
108
129
|
|
|
109
130
|
findOne(s) {
|
|
@@ -116,8 +137,8 @@ export default class Component {
|
|
|
116
137
|
|
|
117
138
|
on(event, target, fn) {
|
|
118
139
|
const off = on(event, target, fn, this.el);
|
|
119
|
-
this.
|
|
120
|
-
this.
|
|
140
|
+
this.offs ??= [];
|
|
141
|
+
this.offs.push(off);
|
|
121
142
|
return off;
|
|
122
143
|
}
|
|
123
144
|
|
|
@@ -127,9 +148,10 @@ export default class Component {
|
|
|
127
148
|
}
|
|
128
149
|
|
|
129
150
|
destroy() {
|
|
130
|
-
this.on.destroy?.forEach(off => off());
|
|
131
151
|
this.controller?.abort();
|
|
132
152
|
this.directives.length = 0;
|
|
133
|
-
|
|
153
|
+
this.offs?.forEach(off => off());
|
|
154
|
+
cache?.delete(this);
|
|
155
|
+
queue?.delete(this);
|
|
134
156
|
}
|
|
135
157
|
}
|