-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
60
-
61
-
62
-
63
-
64
-
65
-
66
-
67
-
68
-
69
-
70
-
71
-
72
-
73
-
74
-
75
-
76
-
77
-
78
-
79
-
80
-
81
-
82
-
83
-
84
-
85
-
86
-
87
-
88
-
89
-
90
-
91
-
92
-
93
-
94
-
95
-
96
-
97
-
98
-
99
-
100
-
101
-
102
-
103
-
104
-
105
-
106
-
107
-
108
-
109
-
110
-
111
-
112
-
113
-
114
-
115
-
116
-
117
-
118
-
119
-
120
-
121
-
122
-
123
-
124
-
125
-
126
-
127
-
128
-
129
-
130
-
131
-
132
-
133
-
134
-
135
-
136
-
137
-
138
-
139
-
140
-
141
-
142
-
143
-
144
-
145
-
146
-
147
-
148
-
149
-
150
-
151
-
152
-
153
-
154
-
155
-
156
-
157
-
158
-
159
-
160
-
161
-
162
-
163
-
164
-
165
-
166
-
167
-
168
-
169
-
170
-
171
-
172
-
173
-
174
-
175
-
176
-
177
-
178
-
179
-
180
-
181
-
182
-
183
-
184
-
185
-
186
-
187
-
188
-
189
-
190
-
191
-
192
-
193
-
194
-
195
-
196
-
197
-
198
-
199
-
200
-
201
-
202
-
203
-
204
-
205
-
206
-
207
-
208
-
209
-
210
-
211
-
212
-
213
-
214
-
215
-
216
-
217
-
218
-
219
-
220
-
221
-
222
-
223
-
224
-
225
-
226
-
227
-
228
-
229
-
230
-
231
-
232
-
233
-
234
-
235
-
236
-
237
-
238
-
239
-
240
-
241
-
242
-
243
-
244
-
245
-
246
-
247
-
248
-
249
-
250
-
251
-
252
-
253
-
254
-
255
-
256
-
257
-
258
-
259
-
260
-
261
-
262
-
263
-
264
-
265
-
266
-
267
-
268
-
269
-
270
-
271
-
272
-
273
-
274
-
275
-
276
-
277
-
278
-
279
-
280
-
281
-
282
-
283
-
284
-
285
-
286
-
287
-
288
-
289
-
290
-
291
-
292
-
293
-
294
-
295
-
296
-
297
-
298
-
299
-
300
-
301
-
302
-
303
-
304
-
305
-
306
-
307
-
308
-
309
-
310
-
311
-
312
-
313
-
314
-
315
-
316
-
317
-
318
-
319
-
320
-
321
-
322
-
323
-
324
-
325
-
326
-
327
-
328
-
329
-
330
-
331
-
332
-
333
-
334
-
335
-
336
-
337
-
338
-
339
-
340
-
341
-
342
-
343
-
344
-
345
-
346
-
347
-
348
-
349
-
350
-
351
-
352
-
353
-
354
-
355
-
356
-
357
-
358
-
359
-
360
-
361
-
362
-
363
-
364
-
365
-
366
-
367
-
368
-
369
-
370
-
371
-
372
-
373
-
374
-
375
-
376
-
377
-
378
-
379
-
380
-
381
-
382
-
383
-
384
-
385
-
386
-
387
-
388
-
389
-
390
-
391
-
392
-
393
-
394
-
395
-
396
-
397
-
398
-
399
-
400
-
401
-
402
-
403
-
404
-
405
-
406
-
407
-
408
-
409
-
410
-
411
-
412
-
413
-
414
-
415
-
416
-
417
-
418
-
419
-
420
-
421
-
422
-
423
-
424
-
425
-
426
-
427
-
428
-
429
-
430
-
431
-
432
-
433
-
434
-
435
-
436
-
437
-
438
# Usage
## Key concepts
ef.js provides two fundamental concept for state management: _Reactive Value_ and _Computation_.
_Reactive Value_ is an object that holds a value and notifies its changes to dependant _Computations_.
You can think this as a Subject in Observer Pattern, `EventEmitter` in Node.js, or `EventTarget` in DOM.
_Computation_ is an object that holds a callback function and reruns it everytime dependent _Reactive Values_ has changed.
In other words, _Computation_ subscribes to _Reactive Values_' changes.
You can think this as an Observer in Observer Pattern or an event listener in Node.js and DOM.
## Work with Signals
### Create a Reactive Value
You can create a _Reactive Value_ via `signal` and `derived` function.
Difference between the two is you can directly change `signal`'s containing value while `derived` uses a value returned by its callback as its containing value.
`derived` is also a _Computation_ and hence the `derived` marks every _Reactive Values_ accessed in its callback as its dependency.
```ts
import { derived, signal } from "@pocka/ef";
// The starting dollar sign ($) in variable names is
// just a convention in my source code.
// You can freely omit and use regular variable name.
const $age = signal(18);
// The value is a result of the callback function.
const $isLegalToDrink = derived(() => $age.get() >= 20);
$age.get(); // 18
$isLegalToDrink.get(); // false
// This updates $age's value, and $age notifies its change
// to $isLegalToDrink, which reruns the callback function.
$age.set(20); // 20
$age.get(); // 18
$isLegalToDrink.get(); // true
```
You can also use `Signal` and `Derived` class directly.
Both `signal` and `derived` are thin wrapper around corresponding class constructor.
```ts
import { Derived, Signal } from "@pocka/ef";
const $age = new Signal(18);
const $isLegalToDrink = new Derived(() => $age.get() >= 20);
// Returned object is no different
const $foo = signal(0);
const $bar = new Signal(0);
$foo instanceof Signal; // true
$bar instanceof Signal; // true
```
### Retrieving value from a Reactive Value
_Reactive Value_ has three way to access its containing value.
- `.get(computation?)`
- `.value`
- `.once()`
`.get(computation?)` returns the containing value and associates the _Reactive Value_ to a surrounding _Computation_ if exists.
When the optional first parameter is specified, the _Reactive Value_ associates itself to the specified _Computation_ instead of a surrounding one.
`.value` getter is an alias for `.get()`.
`.once()` just returns the containing value and does nothing else.
This is suitable for retrieving a value outside of _Computations_ or isolated callback functions.
```ts
import { signal } from "@pocka/ef";
const $count = signal(0);
$count.get(); // 0
$count.value; // 0
$count.once(); // 0
```
### Perform action on changes
To perform something that has side effects everytime _Reactive Values_ changed, use `effect(callback)`.
```ts
import { signal, effect } from "@pocka/ef";
const $count = signal(0);
effect(() => {
console.log($count.get());
});
// logs 0
$count.set(1);
// logs 1
```
If you don't want a _Reactive Value_ to be a dependency of the _Computation_, retrieve a value using `.once()`.
```ts
import { signal, effect } from "@pocka/ef";
const $count = signal(0);
effect(() => {
console.log($count.once());
});
// logs 0
$count.set(1);
// nothing happens
```
If the `callback` returns a function, it'll be invoked before every rerun and disposal.
```ts
import { signal, effect } from "@pocka/ef";
const $count = signal(0);
const log = effect(() => {
console.log($count.get());
return () => {
console.log("cleanup");
};
});
// logs 0
$count.set(1);
// logs "cleanup"
// logs 1
log.dispose();
// logs "cleanup"
```
### Explicitly passing dependant Computation
Normally, when `.get()` is called on a _Reactive Value_, ef.js can find a correct _Computation_ to associate.
However, as ef.js relies on a global variable due to language and tooling limitations, this mechanism does not work in callback function.
This restriction also applies to async function (`Promise`), which is essentially a set of callbacks.
```ts
import { signal, effect } from "@pocka/ef";
const $count = signal(0);
effect(function effectCallback() {
const id = setTimeout(() => {
// At this point, the execution of `effectCallback` is already completed.
// Therefore, ef.js can't figure out which Computation to associate to.
console.log($count.get());
}, 100);
return () => {
clearTimeout(id);
};
});
setTimeout(() => {
$count.set(1);
}, 200);
// wait 100ms
// logs 0
// wait 100ms
// -- script terminates --
```
As a workaround, a callback function of a _Computation_ pass the _Computation_ itself as the first argument and you can explicitly pass it to `get()` method.
```ts
import { signal, effect } from "@pocka/ef";
const $count = signal(0);
effect((ctx) => {
const id = setTimeout(() => {
console.log($count.get(ctx));
}, 100);
return () => {
clearTimeout(id);
};
});
setTimeout(() => {
$count.set(1);
}, 200);
// wait 100ms
// logs 0
// wait 200ms
// logs 1
// -- script terminates --
```
However, most of the time, you should avoid this due to increase of complexity.
Eager binding of the value is recommended.
```ts
import { signal, effect } from "@pocka/ef";
const $count = signal(0);
effect(() => {
const count = $count.get();
const id = setTimeout(() => {
console.log(count);
}, 100);
return () => {
clearTimeout(id);
};
});
setTimeout(() => {
$count.set(1);
}, 200);
// wait 100ms
// logs 0
// wait 200ms
// logs 1
```
### Resource disposal
`Signal`, `Derived` and `Effect` has `dispose` method, which stops reactivity and make them collectable.
```ts
import { signal, effect } from "@pocka/ef";
const $count = signal(0);
const log = effect(() => {
console.log($count.get());
});
// logs 0
$count.set(1);
// logs 1
log.dispose();
$count.set(2);
// nothing happens
```
### Async helper
Due to async (Promise) is frequently used in JavaScript ecosystem and Web API, ef.js provides a helper function named `asyncDerived`.
```ts
import { signal, effect, asyncDerived } from "@pocka/ef";
const $name = signal("Alice");
const $profile = asyncDerived(async function (ctx, abortSignal) {
const resp = await fetch(`/my-api/profiles/${$name.get(ctx)}`, {
signal: abortSignal,
});
if (resp.status !== 200) {
throw new Error(`Unexpected API status: ${resp.status}`);
}
return resp.json();
});
effect(() => {
if (!$profile.value.isSettled) {
console.log("fetching...");
return;
}
if ($profile.value.isRejected) {
console.error("failed to fetch", $profile.value.error);
return;
}
// `.data` is a resolved data of `resp.json()`
console.log($profile.value.data);
});
```
## Working with DOM
### Element creation
The most important function in ef.js to work with DOM is `el`.
It creates and returns an `HTMLElement` with specified tag name.
```ts
import { el } from "@pocka/ef";
const div = el("div");
// div is HTMLDivElement
```
You can also use `svg` for SVG elements and `mathml` for MathML elements.
```ts
import { svg, mathml } from "@pocka/ef";
const path = svg("path");
// path is SVGPathElement
const mn = mathml("mn");
// mn is MathMLElement
```
### Element setup
Second parameter of `el` (also `svg` and `mathml`) is an array of element setup function.
Element setup function is a function takes an element in the first argument and do something to/with the element.
```ts
import { el } from "@pocka/ef";
el("input", [
(el) => {
// el is a current element (HTMLInputElement)
el.setAttribute("type", "checkbox");
el.checked = true;
el.addEventListener("input", (ev) => {
console.log(ev.currentTarget.checked);
});
},
]);
```
ef.js provides three core and two extra helper functions for this task.
Those helper functions are higer-ordered functions (return element setup function).
```ts
import { attr, el, on, prop } from "@pocka/ef";
el("input", [
attr("type", "checkbox"),
prop("checked", true),
on("input", (ev) => {
console.log(ev.currentTarget.checked);
}),
]);
```
These helper functions accept _Reactive Value_ and update the only necessary parts on changes.
```ts
import { attr, derived, el, on, prop, signal } from "@pocka/ef";
const $disabled = signal(false);
const $name = signal("Alice");
function noop() {}
el("button", [
attr(
"title",
derived(() => `Invite ${$name.get()}`),
),
prop("disabled", $disabled),
on(
"click",
derived(() => {
if ($disabled.get()) {
return noop;
}
return () => {
console.log($name.once());
};
}),
),
]);
$name.set("Bob");
// Update "title" attribute
$disabled.set(true);
// Update "disabled" property
// Remove "click" event listener
// Add "click" event listener
```
### Element children
Third parameter of `el` (`svg` and `mathml`) function is children.
You can pass `Node` (e.g. `Element`, `Text`), string, `null` and `undefined`.
String value is converted to `Text` (text node) and `null` and `undefined` are skipped.
_Reactive Value_ containing these value is also accepted.
```ts
import { el, signal } from "@pocka/ef";
const $span = signal(el("span"));
const $name = signal("Alice");
el(
"div",
[],
[document.createElement("i"), el("p"), $span, "Foo", $name, null, undefined],
);
```
### `DocumentFragment`
`fragment` function creates `DocumentFragment` with reactivity.
Available child type is same as `el`.
```ts
import { el, fragment, signal } from "@pocka/ef";
const $name = signal("Alice");
const profile = fragment(["Name is ", $name]);
el("p", [], [profile]);
```