mi-element 0.6.0 → 0.6.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/dist/case.js +1 -1
- package/dist/context.js +2 -2
- package/dist/element.js +12 -6
- package/dist/escape.js +1 -1
- package/dist/index.min.js +3 -3
- package/dist/index.min.js.map +1 -1
- package/dist/styling.js +3 -3
- package/docs/context.md +136 -0
- package/docs/controller.md +82 -0
- package/docs/element.md +394 -0
- package/docs/reactivity.md +41 -0
- package/docs/signal.md +192 -0
- package/docs/store.md +65 -0
- package/docs/styling.md +77 -0
- package/package.json +5 -4
- package/src/element.js +15 -14
- package/types/element.d.ts +12 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Controllers
|
|
2
|
+
|
|
3
|
+
A controller is meant to hook into the components update lifecycle and provide
|
|
4
|
+
an updated value to the host component.
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
/**
|
|
8
|
+
* @file './my-clock.js'
|
|
9
|
+
* Defines custom element my-clock with controller
|
|
10
|
+
*/
|
|
11
|
+
import { define, MiElement } from 'mi-element'
|
|
12
|
+
|
|
13
|
+
class ClockController {
|
|
14
|
+
/**
|
|
15
|
+
* @param {MiElement} host
|
|
16
|
+
*/
|
|
17
|
+
constructor(host) {
|
|
18
|
+
this.host = host
|
|
19
|
+
// Add controller to component.
|
|
20
|
+
this.host.addController(this)
|
|
21
|
+
// As soon as component is mounted `hostConnected()` is called.
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* called by the hosts connectedCallback()
|
|
25
|
+
*/
|
|
26
|
+
hostConnected() {
|
|
27
|
+
this._interval() // start timer
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* called by the hosts disconnectedCallback(); do cleanup herein.
|
|
31
|
+
*/
|
|
32
|
+
hostDisconnected() {
|
|
33
|
+
clearTimeout(this._timerId)
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* periodically update controller value
|
|
37
|
+
*/
|
|
38
|
+
_interval() {
|
|
39
|
+
this.value = new Date()
|
|
40
|
+
// on every change call requestUpdate()
|
|
41
|
+
this.host.requestUpdate()
|
|
42
|
+
|
|
43
|
+
this._timerId = setTimeout(() => {
|
|
44
|
+
this._interval()
|
|
45
|
+
}, 1000)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
define(
|
|
50
|
+
'my-clock',
|
|
51
|
+
class extends MiElement {
|
|
52
|
+
constructor() {
|
|
53
|
+
super()
|
|
54
|
+
// create controller and pass `this` as host
|
|
55
|
+
this.controller = new ClockController(this)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
update() {
|
|
59
|
+
// get value from controller
|
|
60
|
+
const { value } = this.controller
|
|
61
|
+
// apply some formatting
|
|
62
|
+
const formattedDateTime = new Intl.DateTimeFormat(navigator.language, {
|
|
63
|
+
dateStyle: 'short',
|
|
64
|
+
timeStyle: 'long'
|
|
65
|
+
}).format(value)
|
|
66
|
+
// update component
|
|
67
|
+
this.renderRoot.textContent = formattedDateTime
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<!-- @file ./index.html -->
|
|
75
|
+
<!doctype html>
|
|
76
|
+
<html>
|
|
77
|
+
<body>
|
|
78
|
+
<my-clock></my-clock>
|
|
79
|
+
<script type="module" src="./my-clock.js"></script>
|
|
80
|
+
</body>
|
|
81
|
+
</html>
|
|
82
|
+
```
|
package/docs/element.md
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
**Table of contents**
|
|
2
|
+
|
|
3
|
+
<!-- !toc (minlevel=2) -->
|
|
4
|
+
|
|
5
|
+
* [constructor()](#constructor)
|
|
6
|
+
* [connectedCallback()](#connectedcallback)
|
|
7
|
+
* [disconnectedCallback()](#disconnectedcallback)
|
|
8
|
+
* [attributeChangedCallback(name, oldValue, newValue)](#attributechangedcallbackname-oldvalue-newvalue)
|
|
9
|
+
* [Update Cycle](#update-cycle)
|
|
10
|
+
* [render()](#render)
|
|
11
|
+
* [update(changedAttributes)](#updatechangedattributes)
|
|
12
|
+
* [shouldUpdate(changedAttributes)](#shouldupdatechangedattributes)
|
|
13
|
+
* [on(eventName, listener, \[node\])](#oneventname-listener-node)
|
|
14
|
+
* [once(eventName, listener, \[node\])](#onceeventname-listener-node)
|
|
15
|
+
|
|
16
|
+
<!-- toc! -->
|
|
17
|
+
|
|
18
|
+
# Element Lifecycle
|
|
19
|
+
|
|
20
|
+
MiElement components use the [standard custom element lifecycle callbacks][].
|
|
21
|
+
|
|
22
|
+
[standard custom element lifecycle callbacks]: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks
|
|
23
|
+
|
|
24
|
+
## constructor()
|
|
25
|
+
|
|
26
|
+
Called whenever a new element is created. Default attributes are being applied
|
|
27
|
+
from the `static attributes` object. From there setters and getters for property
|
|
28
|
+
changes using `.[name] = newValue` instead of `setAttribute(name, newValue)` are
|
|
29
|
+
applied.
|
|
30
|
+
|
|
31
|
+
Direct properties can also made observable with `static properties` as long as
|
|
32
|
+
not yet being defined within attributes.
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
class extends MiElement {
|
|
36
|
+
/**
|
|
37
|
+
* Declare observable attributes and their default values with this getter.
|
|
38
|
+
* Do not use `static attribute = { text: 'Hi' }` as components attributes
|
|
39
|
+
* will use a shallow copy only. With the getter we always get a real "deep"
|
|
40
|
+
* copy.
|
|
41
|
+
*
|
|
42
|
+
* For yet to defined numbers, boolean or strings use `Number`, `Boolean`,
|
|
43
|
+
* `String`. Attributes are accessible via `.[name]` or `.getAttribute(name)`.
|
|
44
|
+
* Avoid using attributes which are HTMLElement properties e.g. `className`.
|
|
45
|
+
*/
|
|
46
|
+
static get attributes () {
|
|
47
|
+
return {
|
|
48
|
+
text: 'Hi',
|
|
49
|
+
// A yet to be defined boolean value
|
|
50
|
+
focus: Boolean
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Declare observable properties and their default values.
|
|
55
|
+
* Any changed value of a property, using `.[name] = nextValue` assignment,
|
|
56
|
+
* will cause a rerender.
|
|
57
|
+
* In case of objects or arrays consider changing the reference with the
|
|
58
|
+
* spread operator like `{...obj}` or `[...arr]` creating a new reference.
|
|
59
|
+
* If name is already declared in `attributes` it will be ignored.
|
|
60
|
+
*/
|
|
61
|
+
static get properties () {
|
|
62
|
+
return { prop: 0 }
|
|
63
|
+
}
|
|
64
|
+
constructor() {
|
|
65
|
+
super()
|
|
66
|
+
// optionally declare "non observable" and internal properties
|
|
67
|
+
this.foo = 'foo'
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## connectedCallback()
|
|
73
|
+
|
|
74
|
+
Invoked when a component is being added to the document's DOM.
|
|
75
|
+
|
|
76
|
+
Micro components create `this.renderRoot` (typically same as `this.shadowRoot`
|
|
77
|
+
for open components) using `this.attachShadow(shadowRootOptions)`. Shadow root
|
|
78
|
+
options are taken from the components `static shadowRootOptions = { mode: 'open'
|
|
79
|
+
}`. In advanced cases where no shadow root is desired, set `static
|
|
80
|
+
shadowRootOptions = null`
|
|
81
|
+
|
|
82
|
+
The most common use case is adding event listeners to external nodes in
|
|
83
|
+
`connectedCallback()`. Typically, anything done in `connectedCallback()` should
|
|
84
|
+
be undone when the element is disconnected, like removing all event listeners on
|
|
85
|
+
external nodes to prevent memory leaks.
|
|
86
|
+
|
|
87
|
+
Then the first `render()` is issued with a `requestUpdate()`
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
class extends MiElement {
|
|
91
|
+
// { mode: 'open' } is the default shadow root option
|
|
92
|
+
// use `null` for no shadow root or { mode: 'closed' } for closed mode
|
|
93
|
+
static shadowRootOptions = { mode: 'open' }
|
|
94
|
+
|
|
95
|
+
connectedCallback() {
|
|
96
|
+
super.connectedCallback() // don't forget to call the super method
|
|
97
|
+
window.addEventListener('keydown', this._handleKeyDown)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// create a event listener which is bound to this component
|
|
101
|
+
// note the `_listener () => {}` syntax (instead of `_listener () {}`)
|
|
102
|
+
_handleKeyDown = (ev) => {
|
|
103
|
+
// do sth. with the event
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
disconnectedCallback() {
|
|
107
|
+
super.disconnectedCallback()
|
|
108
|
+
window.removeEventListener('keydown', this._handleKeyDown)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
To simplify further use `this.on()` which automatically removes the event listener
|
|
114
|
+
on `window` when the component is unmounted with `disconnectedCallback()`.
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
class extends MiElement {
|
|
118
|
+
connectedCallback() {
|
|
119
|
+
super.connectedCallback()
|
|
120
|
+
this.on('keydown', this._handleKeyDown, window)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// create a event listener which is bound to this component
|
|
124
|
+
// note the `_listener () => {}` syntax (instead of `_listener () {}`)
|
|
125
|
+
_handleKeyDown = (ev) => {
|
|
126
|
+
// do sth. with the event
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## disconnectedCallback()
|
|
132
|
+
|
|
133
|
+
Invoked when a component is removed from the document's DOM.
|
|
134
|
+
|
|
135
|
+
Typically, anything done in `connectedCallback()` should be undone when the
|
|
136
|
+
element is disconnected, like removing all event listeners on external nodes to
|
|
137
|
+
prevent memory leaks.
|
|
138
|
+
|
|
139
|
+
See previous example.
|
|
140
|
+
|
|
141
|
+
!!! INFO No need to remove internal event listeners
|
|
142
|
+
|
|
143
|
+
You don't need to remove event listeners added on the component's own
|
|
144
|
+
DOM. This includes those added in your template. Unlike external
|
|
145
|
+
event listeners, these will be garbage collected with the component.
|
|
146
|
+
|
|
147
|
+
## attributeChangedCallback(name, oldValue, newValue)
|
|
148
|
+
|
|
149
|
+
Invoked when one of the element’s observedAttributes changes.
|
|
150
|
+
|
|
151
|
+
Usually no need to do something here. But if, don't forget to call
|
|
152
|
+
`super.attributeChangedCallback(name, oldValue, newValue)` within.
|
|
153
|
+
|
|
154
|
+
## Update Cycle
|
|
155
|
+
|
|
156
|
+
```mermaid
|
|
157
|
+
flowchart TD
|
|
158
|
+
constructoR("constructor()")
|
|
159
|
+
connectedCallback("connectedCallback()")
|
|
160
|
+
disconnectedCallback("disconnectedCallback()")
|
|
161
|
+
render("render()")
|
|
162
|
+
requestUpdate("requestUpdate()")
|
|
163
|
+
shouldUpdate("shouldUpdate(changedAttributes)")
|
|
164
|
+
update("update(changedAttributes)")
|
|
165
|
+
|
|
166
|
+
setAttribute("setAttribute(name, newVale)")
|
|
167
|
+
setProperty(".[name] = newValue")
|
|
168
|
+
|
|
169
|
+
START --> constructoR
|
|
170
|
+
constructoR -.->|"mount to DOM"| connectedCallback
|
|
171
|
+
connectedCallback --> render
|
|
172
|
+
render --> requestUpdate
|
|
173
|
+
requestUpdate -.->|async| shouldUpdate
|
|
174
|
+
shouldUpdate -->|true| update
|
|
175
|
+
|
|
176
|
+
setAttribute -->|"attributeChangedCallback"| requestUpdate
|
|
177
|
+
setProperty --> requestUpdate
|
|
178
|
+
|
|
179
|
+
update -.->|change| setAttribute
|
|
180
|
+
update -.->|change| setProperty
|
|
181
|
+
|
|
182
|
+
connectedCallback -.->|"unmount from DOM"| disconnectedCallback
|
|
183
|
+
disconnectedCallback --> END
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
A micro component usually implements `render()` and `update()`:
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
import { define, MiElement, refsBySelector } from 'mi-element'
|
|
190
|
+
|
|
191
|
+
class Counter extends MiElement {
|
|
192
|
+
static get attributes() {
|
|
193
|
+
return { value: 0 }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// define the innerHTML template for the component
|
|
197
|
+
static template = `
|
|
198
|
+
<button>Count</button>
|
|
199
|
+
<p>Counter value: <span>0</span></p>
|
|
200
|
+
`
|
|
201
|
+
|
|
202
|
+
render() {
|
|
203
|
+
// If `static template` is provided, it has already been rendered
|
|
204
|
+
// on `this.renderRoot`
|
|
205
|
+
|
|
206
|
+
// obtain references for events and update() with `refsBySelector`
|
|
207
|
+
this.refs = refsBySelector(this.renderRoot, {
|
|
208
|
+
button: 'button',
|
|
209
|
+
count: 'span'
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
// apply event listener on button
|
|
213
|
+
this.refs.button.addEventListener('click', () => {
|
|
214
|
+
// observed attribute will trigger `requestUpdate()` which then async
|
|
215
|
+
// calls `update()`
|
|
216
|
+
this.value++
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
update() {
|
|
221
|
+
this.refs.count.textContent = this.value
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## render()
|
|
227
|
+
|
|
228
|
+
Initial rendering of the component. Try to render the component only once!
|
|
229
|
+
If you need re-rendering by recrating the DOM do this outside of `render()`
|
|
230
|
+
|
|
231
|
+
Within the `render()` method, bear in mind to:
|
|
232
|
+
|
|
233
|
+
- Avoid changing the component's state.
|
|
234
|
+
- Avoid producing any side effects.
|
|
235
|
+
- Use only the component's attributes as input.
|
|
236
|
+
|
|
237
|
+
!!! WARNING XSS - Cross-Site Scripting
|
|
238
|
+
|
|
239
|
+
Using [`innerHTML`][innerHTML] to create the components DOM is susceptible to
|
|
240
|
+
[XSS][XSS] attacks in case that user-supplied data contains valid HTML markup.
|
|
241
|
+
|
|
242
|
+
In all other cases you may consider the <code>esc``</code> template literal or
|
|
243
|
+
`escHtml()` from the "mi-element" import, which escapes user-supplied data.
|
|
244
|
+
|
|
245
|
+
[innerHTML]: https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML
|
|
246
|
+
[XSS]: https://en.wikipedia.org/wiki/Cross-site_scripting
|
|
247
|
+
|
|
248
|
+
```js
|
|
249
|
+
import { define, MiElement } from 'mi-element'
|
|
250
|
+
|
|
251
|
+
// (1) get template directly from html ...
|
|
252
|
+
const template = document.querySelector('template#counter')
|
|
253
|
+
|
|
254
|
+
// (2) or define outside the component ...
|
|
255
|
+
const template = document.createElement('template')
|
|
256
|
+
template.innerHTML = `
|
|
257
|
+
<button>Count</button>
|
|
258
|
+
<p>Counter value: <span>0</span></p>
|
|
259
|
+
`
|
|
260
|
+
|
|
261
|
+
class Counter extends MiElement {
|
|
262
|
+
// (3) or as static string on the component (needs define from 'mi-element')
|
|
263
|
+
static template = `
|
|
264
|
+
<button id>Count</button>
|
|
265
|
+
<p>Counter value: <span id="count">0</span></p>
|
|
266
|
+
`
|
|
267
|
+
// ...
|
|
268
|
+
render() {
|
|
269
|
+
/*
|
|
270
|
+
// NEVER DO THIS, as this may cause XSS ///
|
|
271
|
+
this.renderRoot.innerHTML = `
|
|
272
|
+
<button>Count</button>
|
|
273
|
+
<p>Counter value: <span>${this.count}</span></p>`
|
|
274
|
+
*/
|
|
275
|
+
// always render a cloned template, which is safe
|
|
276
|
+
// NOT NEEDED with option (3) `static template = '...'`
|
|
277
|
+
this.addTemplate(template)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// always use define with (3)
|
|
282
|
+
define('mi-element-counter', Counter)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
To more easily obtain any references of interest use the `refs()` helper by
|
|
286
|
+
adding `id` attributes to the nodes where updates shall happen or event
|
|
287
|
+
listeners must be applied.
|
|
288
|
+
|
|
289
|
+
```js
|
|
290
|
+
import { MiElement, define, refsById } from 'mi-element'
|
|
291
|
+
|
|
292
|
+
class Counter extends MiElement {
|
|
293
|
+
static template = `
|
|
294
|
+
<button id>Count</button>
|
|
295
|
+
<p>Counter value: <span id="count">0</span></p>
|
|
296
|
+
`
|
|
297
|
+
|
|
298
|
+
render() {
|
|
299
|
+
// template is already rendered on `this.renderRoot`
|
|
300
|
+
|
|
301
|
+
// get refs though `refsById` of `refsBySelector`
|
|
302
|
+
this.refs = refsById(this.renderRoot)
|
|
303
|
+
// this.refs == {button: <button>, count: <span>}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
define('mi-element-counter', Counter)
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## update(changedAttributes)
|
|
311
|
+
|
|
312
|
+
Here all content or render updates on the component should happen. Avoid
|
|
313
|
+
re-rendering the full component and only apply partial changes on the
|
|
314
|
+
rendered elements as much as possible.
|
|
315
|
+
|
|
316
|
+
```js
|
|
317
|
+
class Counter extends MiElement {
|
|
318
|
+
// ...
|
|
319
|
+
update() {
|
|
320
|
+
this.refs.count.textContent = this.value
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
In order to allow judged decisions on the area where an update should take place
|
|
326
|
+
any changed attributes are passed.
|
|
327
|
+
|
|
328
|
+
To mitigate [XSS][] attacks prefer the use of `.textContent` and avoid
|
|
329
|
+
~~`.innerHTML`~~. For attribute changes use `.setAttribute(name, newValue)`.
|
|
330
|
+
Both `.textContent` and `setAttribute()` provide escaping for you.
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
For finer control on updates the use of signals is encouraged. With this there
|
|
334
|
+
is no need to add logic to `shouldUpdate()` or `update()`.
|
|
335
|
+
|
|
336
|
+
```js
|
|
337
|
+
import { MiElement, Signal } from 'mi-element'
|
|
338
|
+
|
|
339
|
+
class Counter extends MiElement {
|
|
340
|
+
static get attributes() {
|
|
341
|
+
return { value: 0 }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
static template = `
|
|
345
|
+
<button>Count</button>
|
|
346
|
+
<p>Counter value: <span>0</span></p>
|
|
347
|
+
`
|
|
348
|
+
|
|
349
|
+
render() {
|
|
350
|
+
const refs = refsBySelector(this.renderRoot, {
|
|
351
|
+
button: 'button',
|
|
352
|
+
count: 'span'
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
refs.button.addEventListener('click', () => {
|
|
356
|
+
this.value++
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
Signal.effect(() => {
|
|
360
|
+
// an update only happens if `this.value` changes;
|
|
361
|
+
// other attribute changes are ignored.
|
|
362
|
+
refs.count.textContent = this.value
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## shouldUpdate(changedAttributes)
|
|
369
|
+
|
|
370
|
+
Convenience method in order to be able to decide on the changed attributes,
|
|
371
|
+
whether `update()` should be called or not.
|
|
372
|
+
|
|
373
|
+
Return `true` if component should be updated.
|
|
374
|
+
|
|
375
|
+
## on(eventName, listener, \[node\])
|
|
376
|
+
|
|
377
|
+
Adds listener function for eventName. listener is removed before component
|
|
378
|
+
disconnects.
|
|
379
|
+
|
|
380
|
+
```js
|
|
381
|
+
class Router extends MiElement {
|
|
382
|
+
render() {
|
|
383
|
+
// add event listener 'hashchange' to `window` which is disposed as soon as
|
|
384
|
+
// the component unmounts
|
|
385
|
+
this.on('hashchange', this.update, window)
|
|
386
|
+
}
|
|
387
|
+
// ...
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
## once(eventName, listener, \[node\])
|
|
392
|
+
|
|
393
|
+
Adds one-time listener function for eventName. The next time eventName is
|
|
394
|
+
triggered, this listener is removed and then invoked.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Reactivity with mi-html
|
|
2
|
+
|
|
3
|
+
MiElement can be used with [mi-html][] for reactive updates.
|
|
4
|
+
|
|
5
|
+
To install use
|
|
6
|
+
|
|
7
|
+
pnpm install mi-html
|
|
8
|
+
|
|
9
|
+
MiElement provides signals for all its attributes defined in `static get
|
|
10
|
+
attributes() {}`.
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
import { define, MiElement } from 'mi-element'
|
|
14
|
+
import { html, render } from 'mi-html'
|
|
15
|
+
|
|
16
|
+
define(
|
|
17
|
+
'mi-counter',
|
|
18
|
+
class extends MiElement {
|
|
19
|
+
static get attributes() {
|
|
20
|
+
return {
|
|
21
|
+
count: 1 //<< this.count is already a signal
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
render() {
|
|
26
|
+
render(
|
|
27
|
+
this.renderRoot,
|
|
28
|
+
// use callback function!
|
|
29
|
+
() =>
|
|
30
|
+
html`<button @click=${() => this.count++}>
|
|
31
|
+
Clicked ${this.count} times
|
|
32
|
+
</button> `
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
See `./example/mi-html/index.html` for a running sample.
|
|
40
|
+
|
|
41
|
+
[mi-html]: https://github.com/commenthol/mi-element/blob/main/packages/mi-html/README.md
|
package/docs/signal.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
**Table of contents**
|
|
2
|
+
|
|
3
|
+
<!-- !toc (minlevel=2) -->
|
|
4
|
+
|
|
5
|
+
* [State, createSignal](#state-createsignal)
|
|
6
|
+
* [effect](#effect)
|
|
7
|
+
* [DONT'S](#donts)
|
|
8
|
+
* [Computed Signals](#computed-signals)
|
|
9
|
+
* [Signals in MiElement](#signals-in-mielement)
|
|
10
|
+
|
|
11
|
+
<!-- toc! -->
|
|
12
|
+
|
|
13
|
+
# Signal
|
|
14
|
+
|
|
15
|
+
Signal is the core for any reactive behavior of mi-element.
|
|
16
|
+
It loosely follows the [TC39 JavaScript Signals standard proposal][].
|
|
17
|
+
|
|
18
|
+
## State, createSignal
|
|
19
|
+
|
|
20
|
+
Reactive state and its subscribers is managed in this class. With
|
|
21
|
+
`.set(nextValue)` and `.get()` access to the state is achieved.
|
|
22
|
+
|
|
23
|
+
For convenience there is a `createSignal(initialValue<T>): State<T>` function to
|
|
24
|
+
create a signal.
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import { createSignal, State } from 'mi-element'
|
|
28
|
+
|
|
29
|
+
const signal = createSignal(1)
|
|
30
|
+
// same as
|
|
31
|
+
const signal = new State(1)
|
|
32
|
+
|
|
33
|
+
signal.get()
|
|
34
|
+
//> 1
|
|
35
|
+
signal.set(4)
|
|
36
|
+
signal.get()
|
|
37
|
+
//> 4
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For controlling the notifications to subscribers, the signal option `equals` for
|
|
41
|
+
a custom comparison function can be used, e.g. to trigger an effect on every
|
|
42
|
+
`.set(nextValue)`
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
// default is:
|
|
46
|
+
const equals = (value, nextValue) => value === nextValue
|
|
47
|
+
// changes to trigger change on every `.set()`
|
|
48
|
+
const equals = (value, nextValue) => true
|
|
49
|
+
const signal = createSignal(initialValue, { equals })
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## effect
|
|
53
|
+
|
|
54
|
+
Reactivity is achieved by subscribing to a signals State using an effect
|
|
55
|
+
callback function. Such callback function is called for registration to the
|
|
56
|
+
signals state as well as to update on any change through
|
|
57
|
+
`signal.set(nextValue)`. Within that callback the `signal.get()` must be called
|
|
58
|
+
_synchronously_!
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
import { createSignal, effect } from 'mi-element'
|
|
62
|
+
|
|
63
|
+
const signal = createSignal(1)
|
|
64
|
+
|
|
65
|
+
const callback = () => console.log('value is %s', signal.get())
|
|
66
|
+
// `callback` is executed with assigning to the effect!
|
|
67
|
+
const unsubscribe = effect(callback)
|
|
68
|
+
//> "value is 1"
|
|
69
|
+
signal.set(4)
|
|
70
|
+
//> "value is 4"
|
|
71
|
+
|
|
72
|
+
// Signal.effect returns a function to unsubscribe `callback` from the signal
|
|
73
|
+
unsubscribe()
|
|
74
|
+
signal.set(5)
|
|
75
|
+
// gives no console.log output
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For asynchronous usage, request the value from the signal first. Otherwise no
|
|
79
|
+
subscription to the signal will take place.
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
const signal = createSignal(1)
|
|
83
|
+
|
|
84
|
+
const callback = async () => {
|
|
85
|
+
// synchronously get the value
|
|
86
|
+
const value = signal.get()
|
|
87
|
+
const p = Promise.withResolvers()
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
console.log('value is %s', )
|
|
90
|
+
p.resolve()
|
|
91
|
+
}, 100)
|
|
92
|
+
}.catch(() => {})
|
|
93
|
+
// callback is executed with assigning to the effect!
|
|
94
|
+
effect(callback)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### DONT'S
|
|
98
|
+
|
|
99
|
+
Effects are executed synchronously for a better debugging experience. But be
|
|
100
|
+
warned to never set the signal in the an effects callback!
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
const signal = createSignal(0)
|
|
104
|
+
|
|
105
|
+
// DON'T DO THIS
|
|
106
|
+
effect(() => {
|
|
107
|
+
const value = signal.get()
|
|
108
|
+
signal.set(value++) //< meeeeh
|
|
109
|
+
})
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The signal value getter triggers the registration of the callback through the
|
|
113
|
+
effect. So don't hide a signals getter inside conditionals!
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
const signal = createSignal(0)
|
|
117
|
+
const rand = Math.random()
|
|
118
|
+
|
|
119
|
+
// DON'T DO THIS
|
|
120
|
+
effect(() => {
|
|
121
|
+
if (rand < 0.5) {
|
|
122
|
+
console.log(signal.get()) //< meeeeh
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// DO THIS
|
|
127
|
+
effect(() => {
|
|
128
|
+
const value = signal.get() //< much better
|
|
129
|
+
if (rand < 0.5) {
|
|
130
|
+
console.log(value)
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Computed Signals
|
|
136
|
+
|
|
137
|
+
Computed signals from more than one signal can be obtained from `Computed`.
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
const firstName = createSignal('Joe')
|
|
141
|
+
const lastName = createSignal('Doe')
|
|
142
|
+
// define computed signal
|
|
143
|
+
const name = new Computed(() => `${firstName.get()} ${lastName.get()}`)
|
|
144
|
+
const events = []
|
|
145
|
+
// apply effect
|
|
146
|
+
effect(() => console.log(name.get()))
|
|
147
|
+
//> 'Joe Doe'
|
|
148
|
+
firstName.set('Alice')
|
|
149
|
+
//> 'Alice Doe'
|
|
150
|
+
lastName.set('Wonderland')
|
|
151
|
+
//> 'Alice Wonderland'
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Signals in MiElement
|
|
155
|
+
|
|
156
|
+
MiElement attributes are backed by signals. To subscribe to reactive changes a
|
|
157
|
+
`Signal.effect` callback can be used on all observed attributes.
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
import { effect, define, MiElement, refByIds } from 'mi-element'
|
|
161
|
+
|
|
162
|
+
define(
|
|
163
|
+
'mi-counter',
|
|
164
|
+
class extends MiElement {
|
|
165
|
+
static template = `
|
|
166
|
+
<button id> + </button>
|
|
167
|
+
<div id></div>
|
|
168
|
+
`
|
|
169
|
+
|
|
170
|
+
static get attributes() {
|
|
171
|
+
return {
|
|
172
|
+
// define reactive attribute
|
|
173
|
+
count: 0
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
render() {
|
|
178
|
+
this.refs = refsById(this.renderRoot)
|
|
179
|
+
this.refs.button.addEventListener('click', () => {
|
|
180
|
+
// change observed and reactive attribute...
|
|
181
|
+
this.count++
|
|
182
|
+
})
|
|
183
|
+
effect(() => {
|
|
184
|
+
// ...triggers update on every change
|
|
185
|
+
this.refs.div.textContent = `${this.count} clicks counted`
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
[TC39 JavaScript Signals standard proposal]: https://github.com/tc39/proposal-signals
|