@manyducks.co/dolla 2.0.0-alpha.42 → 2.0.0-alpha.43
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 +1 -1
- package/dist/core/signals.d.ts +1 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +258 -255
- package/dist/index.js.map +1 -1
- package/dist/jsx-dev-runtime.js +2 -2
- package/dist/jsx-runtime.js +2 -2
- package/dist/{markup-DQdkb3ri.js → markup-CSJbSI8_.js} +243 -232
- package/dist/markup-CSJbSI8_.js.map +1 -0
- package/dist/types.d.ts +12 -0
- package/notes/atomic.md +320 -145
- package/package.json +1 -1
- package/dist/markup-DQdkb3ri.js.map +0 -1
package/notes/atomic.md
CHANGED
|
@@ -1,209 +1,384 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ATOMIC
|
|
2
|
+
|
|
3
|
+
- New library; core is just signals, templates and views.
|
|
4
|
+
- Router released as companion library.
|
|
5
|
+
- Localize released as companion library.
|
|
6
|
+
- CSS components as new companion library.
|
|
7
|
+
|
|
8
|
+
Goals:
|
|
9
|
+
|
|
10
|
+
- Easy drop-in script tag to get started. Feasible to start with a CDN for prototyping and introduce build step later.
|
|
11
|
+
- Server side rendering support. `html` templates should create intermediate data structure that can be turned into DOM nodes or a string.
|
|
12
|
+
|
|
13
|
+
## Signals
|
|
2
14
|
|
|
3
15
|
```js
|
|
4
|
-
|
|
5
|
-
// Atoms are the basic building block of state.
|
|
6
|
-
const count = new Atom(5);
|
|
16
|
+
import { atom, memo, createScope, $effect } from "@manyducks.co/atomic";
|
|
7
17
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
count.update((value) => value + 1); // updates the value. You can use Immer here for complex objects.
|
|
18
|
+
// Atoms are hybrid getter/setter functions. Call without a value to get, call with a value to set.
|
|
19
|
+
const count = atom(0);
|
|
11
20
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
atom.value = produce(atom.value, callback);
|
|
15
|
-
}
|
|
16
|
-
update(count, (value) => value + 1);
|
|
21
|
+
// Basic computed properties are just functions. Taking advantage of the fact that functions called in functions will still be tracked.
|
|
22
|
+
const doubled = () => count() * 2;
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
});
|
|
24
|
+
// Use memo to make a memoized value for more expensive calculations.
|
|
25
|
+
const quadrupled = memo(() => doubled() * 2);
|
|
26
|
+
```
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
// The callback takes a getter function that will track that state as a dependency and return its current value.
|
|
25
|
-
// We recompute if any tracked dependency receives a new value.
|
|
26
|
-
const doubled = new Composed((get) => get(count) * 2);
|
|
28
|
+
### Scopes
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
> NOTE: Views and directives are called within a scope.
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
// Functions starting with $ can be called within a scope.
|
|
34
|
+
// Scopes will clean up all effects created within them when they are disconnected.
|
|
35
|
+
const scope = createScope(() => {
|
|
36
|
+
$effect(() => {
|
|
37
|
+
// Atoms and memos called within an $effect are automatically tracked.
|
|
38
|
+
console.log(`count is ${count()} (doubled: ${doubled()})`);
|
|
31
39
|
});
|
|
32
40
|
|
|
33
|
-
|
|
41
|
+
// Changing tracked values will trigger effects to run again.
|
|
42
|
+
count(1);
|
|
34
43
|
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
ctx.effect(() => {
|
|
38
|
-
if (get(print)) {
|
|
39
|
-
console.log(get(value));
|
|
40
|
-
}
|
|
41
|
-
});
|
|
44
|
+
// $effects are called immediately once, then again each time one or more dependencies change.
|
|
45
|
+
// Effects are settled in queueMicrotask(), effectively batching them.
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
});
|
|
47
|
+
count(2);
|
|
48
|
+
count(3);
|
|
49
|
+
count(4);
|
|
50
|
+
// Multiple synchronous calls in a row like this will only trigger the effect once.
|
|
51
|
+
});
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
// ----- Scope Functions ----- //
|
|
54
|
+
|
|
55
|
+
$effect(() => {
|
|
56
|
+
// Tracks dependencies. This function will run again when any of them change.
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
$connected(() => {
|
|
60
|
+
// Runs when scope is connected.
|
|
61
|
+
// In views and directives this happens in the next microtask after DOM nodes are attached.
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
$disconnected(() => {
|
|
65
|
+
// Runs when scope is disconnected.
|
|
66
|
+
// In views and directives this happens in the next microtask after DOM nodes are disconnected.
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ----- Scope API ----- //
|
|
70
|
+
|
|
71
|
+
const scope = createScope(() => {
|
|
72
|
+
/* ... */
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Connect starts all $effects and runs $connected callbacks.
|
|
76
|
+
scope.connect();
|
|
77
|
+
|
|
78
|
+
// Disconnect disposes all $effects and runs $disconnected callbacks.
|
|
79
|
+
scope.disconnect();
|
|
53
80
|
```
|
|
54
81
|
|
|
55
|
-
|
|
82
|
+
## Templates
|
|
56
83
|
|
|
57
84
|
```js
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
85
|
+
// Provide directives and views in a config object.
|
|
86
|
+
const template = html({
|
|
87
|
+
directives: { custom: customDirective },
|
|
88
|
+
views: { SomeView },
|
|
89
|
+
})`
|
|
90
|
+
<div>
|
|
91
|
+
<SomeView *custom=${x} prop=${value} />
|
|
92
|
+
</div>
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
// Or put the views and directives at the end?
|
|
96
|
+
const template = html`
|
|
97
|
+
<div>
|
|
98
|
+
<p>Counter: ${count}</p>
|
|
99
|
+
|
|
100
|
+
<!-- bind events with @name -->
|
|
101
|
+
<div>
|
|
102
|
+
<button @click=${increment}>+1</button>
|
|
103
|
+
<button @click=${decrement}>-1</button>
|
|
104
|
+
</div>
|
|
61
105
|
|
|
62
|
-
|
|
63
|
-
|
|
106
|
+
<!-- apply directives with *name -->
|
|
107
|
+
<div *ref=${refAtom} *if=${x} *unless=${x} *show=${x} *hide=${x} *classes=${x} *styles=${x} *custom=${whatever} />
|
|
64
108
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
109
|
+
<!-- bind properties with .name -->
|
|
110
|
+
<span .textContent=${x} />
|
|
111
|
+
|
|
112
|
+
<!-- two-way bind atoms with :value -->
|
|
113
|
+
<input :value=${valueAtom} />
|
|
114
|
+
|
|
115
|
+
<ul *if=${hasValues}>
|
|
116
|
+
<!-- render iterables from signals with list() -->
|
|
117
|
+
${list(values, (value, index) => {
|
|
118
|
+
// Render views into HTML templates with view()
|
|
119
|
+
return html`<li><SomeView item=${value} /></li>`.withViews({ SomeView });
|
|
120
|
+
})}
|
|
121
|
+
</ul>
|
|
122
|
+
</div>
|
|
123
|
+
`
|
|
124
|
+
.withDirectives({ custom: customDirective })
|
|
125
|
+
.withViews({ SomeView });
|
|
126
|
+
|
|
127
|
+
// Key
|
|
128
|
+
// @ for event listeners
|
|
129
|
+
// * for directives
|
|
130
|
+
// . for properties
|
|
131
|
+
// :value for two-way value binding
|
|
132
|
+
// no prefix for attributes
|
|
71
133
|
```
|
|
72
134
|
|
|
73
|
-
|
|
135
|
+
### Event modifiers
|
|
74
136
|
|
|
75
137
|
```js
|
|
76
|
-
|
|
138
|
+
html`<button
|
|
139
|
+
@click.stop.prevent.throttle[250]=${() => {
|
|
140
|
+
// stopPropagation & preventDefault already called
|
|
141
|
+
// Listener will be triggered a maximum of once every 250 milliseconds.
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
Click Me
|
|
145
|
+
</button>`;
|
|
146
|
+
```
|
|
77
147
|
|
|
78
|
-
|
|
79
|
-
const [$count, setCount] = createState(5);
|
|
80
|
-
setCount((count) => count + 1);
|
|
81
|
-
$count.get(); // 6
|
|
148
|
+
You can chain modifiers on event handlers. Inspired by [`Mizu.js`](https://mizu.sh/#event).
|
|
82
149
|
|
|
83
|
-
|
|
84
|
-
const count = atom(5);
|
|
85
|
-
count.value++;
|
|
86
|
-
count.value; // 6
|
|
150
|
+
#### `.prevent`
|
|
87
151
|
|
|
88
|
-
|
|
152
|
+
Calls event.preventDefault() when triggered.
|
|
89
153
|
|
|
90
|
-
|
|
91
|
-
const $doubled = derive([$count], (count) => count * 2);
|
|
92
|
-
const $quadrupled = derive([$doubled], (doubled) => doubled * 2);
|
|
154
|
+
#### `.stop`
|
|
93
155
|
|
|
94
|
-
|
|
95
|
-
const doubled = compose((get) => get(count) * 2);
|
|
96
|
-
const quadrupled = compose((get) => get(doubled) * 2);
|
|
156
|
+
Calls event.stopPropagation() when triggered.
|
|
97
157
|
|
|
98
|
-
|
|
158
|
+
#### `.once`
|
|
99
159
|
|
|
100
|
-
|
|
101
|
-
ctx.watch([$count, $quadrupled], (count, quadrupled) => {
|
|
102
|
-
if (count > 25) {
|
|
103
|
-
console.log($doubled.get()); // not tracked
|
|
160
|
+
Register listener with { once: true }. If present the listener is removed after being triggered once.
|
|
104
161
|
|
|
105
|
-
|
|
106
|
-
// changes to $quadrupled will trigger this callback to re-run, even if count is still <= 25
|
|
107
|
-
}
|
|
108
|
-
});
|
|
162
|
+
#### `.passive`
|
|
109
163
|
|
|
110
|
-
|
|
111
|
-
ctx.effect((get) => {
|
|
112
|
-
// count is tracked by reading it with 'get'
|
|
113
|
-
if (get(count) > 25) {
|
|
114
|
-
console.log(doubled.value); // not tracked
|
|
164
|
+
Register listener with { passive: true }.
|
|
115
165
|
|
|
116
|
-
|
|
117
|
-
// changes to 'quadrupled' will NOT trigger this callback to re-run unless 'count' is already >= 25
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
```
|
|
166
|
+
#### `.capture`
|
|
121
167
|
|
|
122
|
-
|
|
168
|
+
Register listener with { capture: true }.
|
|
123
169
|
|
|
124
|
-
|
|
170
|
+
#### `.self`
|
|
125
171
|
|
|
126
|
-
|
|
172
|
+
Trigger listener only if event.target is the element itself.
|
|
127
173
|
|
|
128
|
-
|
|
174
|
+
#### `.attach[element | window | document]`
|
|
129
175
|
|
|
130
|
-
|
|
131
|
-
// If count is reactive we get its current value. Otherwise we get it as is.
|
|
132
|
-
const value = unwrap(count);
|
|
176
|
+
> `@click.attach[document]=${...}`
|
|
133
177
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
178
|
+
Attach listener to a different target.
|
|
179
|
+
|
|
180
|
+
#### `.throttle[duration≈250ms]`
|
|
181
|
+
|
|
182
|
+
Prevent listener from being called more than once during the specified time frame. Duration value is in milliseconds.
|
|
183
|
+
|
|
184
|
+
#### `.debounce[duration≈250ms]`
|
|
185
|
+
|
|
186
|
+
Delay listener execution until the specified time frame has passed without any activity. Duration value is in milliseconds.
|
|
187
|
+
|
|
188
|
+
### Lists
|
|
137
189
|
|
|
138
190
|
```js
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return get(users)_?.find((u) => u.id === id);
|
|
191
|
+
list(values, (value, index) => {
|
|
192
|
+
return html`<li>${view(SomeComponent, value)}</li>`;
|
|
142
193
|
});
|
|
194
|
+
```
|
|
143
195
|
|
|
144
|
-
|
|
196
|
+
### Custom Directives
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
function myDirective(element, value, modifiers) {
|
|
200
|
+
// Directives are called inside a scope.
|
|
201
|
+
$disconnected(() => {
|
|
202
|
+
// Cleanup
|
|
203
|
+
});
|
|
204
|
+
}
|
|
145
205
|
```
|
|
146
206
|
|
|
207
|
+
## Full Example
|
|
208
|
+
|
|
147
209
|
```js
|
|
148
|
-
|
|
210
|
+
import { atom, memo, html, connect, $effect } from "@manyducks.co/atomic";
|
|
211
|
+
|
|
212
|
+
// Functions starting with $ can only be called in the body of a component function.
|
|
213
|
+
|
|
214
|
+
// IDEA: CSS components. Ref counted and added to head while used at least once on the page.
|
|
215
|
+
const button = css`
|
|
216
|
+
color: "red";
|
|
217
|
+
|
|
218
|
+
&:hover {
|
|
219
|
+
color: "blue";
|
|
220
|
+
}
|
|
221
|
+
`;
|
|
222
|
+
|
|
223
|
+
function Counter() {
|
|
224
|
+
const debug = logger("Component");
|
|
225
|
+
|
|
149
226
|
const count = atom(0);
|
|
150
227
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
<span>${count}</span>
|
|
228
|
+
// Simple computed value; computation runs each time function is called
|
|
229
|
+
const doubled = () => count() * 2;
|
|
154
230
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
231
|
+
// Memoized; computation only runs when one of its dependencies changes
|
|
232
|
+
const quadrupled = memo((previousValue) => doubled() * 2, { equals: deepEqual });
|
|
233
|
+
// memos pass their previous to their callback
|
|
234
|
+
// memos can have an equality function specified (as can atoms)
|
|
235
|
+
|
|
236
|
+
$effect(() => {
|
|
237
|
+
// Dependencies are tracked when getters are called in a tracked scope.
|
|
238
|
+
// Tracked scopes are the body of a `memo` or `effect` callback.
|
|
239
|
+
debug.log(`Count is: ${count()}`);
|
|
240
|
+
|
|
241
|
+
// untrack
|
|
242
|
+
const value = peek(count);
|
|
243
|
+
const doubled = peek(() => {
|
|
244
|
+
return count() * 2;
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
$connected(() => {
|
|
249
|
+
// Runs when component is connected.
|
|
250
|
+
});
|
|
160
251
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
// this still refers to view context
|
|
164
|
-
this.log("hello!");
|
|
252
|
+
$disconnected(() => {
|
|
253
|
+
// Runs when component is disconnected.
|
|
165
254
|
});
|
|
166
255
|
|
|
256
|
+
function increment() {
|
|
257
|
+
// Set new value
|
|
258
|
+
count(count() + 1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function decrement() {
|
|
262
|
+
count(count() - 1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const hasValues = () => values().length > 0;
|
|
266
|
+
|
|
167
267
|
return html`
|
|
168
268
|
<div>
|
|
169
|
-
${
|
|
170
|
-
|
|
269
|
+
<p>Counter: ${count}</p>
|
|
270
|
+
<div>
|
|
271
|
+
<button @click=${increment}>+1</button>
|
|
272
|
+
<button @click=${decrement}>-1</button>
|
|
273
|
+
</div>
|
|
171
274
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
275
|
+
<div *ref=${refAtom} *if=${x} *unless=${x} *show=${x} *hide=${x} *classes=${x} *styles=${x} *custom=${whatever} />
|
|
276
|
+
|
|
277
|
+
<!-- Property binding -->
|
|
278
|
+
<span .textContent=${x} />
|
|
279
|
+
|
|
280
|
+
<!-- Two way binding of atoms -->
|
|
281
|
+
<input :value=${valueAtom} />
|
|
282
|
+
|
|
283
|
+
<ul *if=${hasValues}>
|
|
284
|
+
${list(values, (value, index) => {
|
|
285
|
+
return html`<li>${view(SomeComponent, value)}</li>`;
|
|
286
|
+
})}
|
|
287
|
+
</ul>
|
|
176
288
|
</div>
|
|
177
|
-
|
|
178
|
-
}
|
|
289
|
+
`.withDirectives({ custom: customDirective });
|
|
290
|
+
}
|
|
179
291
|
|
|
180
|
-
|
|
181
|
-
const count = atom(0);
|
|
292
|
+
// In another file...
|
|
182
293
|
|
|
183
|
-
|
|
184
|
-
|
|
294
|
+
function refDirective(element, fn) {
|
|
295
|
+
fn(element);
|
|
296
|
+
$disconnected(() => {
|
|
297
|
+
fn(undefined);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
185
300
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
301
|
+
function ifDirective(element, condition) {
|
|
302
|
+
// directives run in microtask immediately after element is attached to parent, before next paint
|
|
303
|
+
|
|
304
|
+
const placeholder = document.createComment("");
|
|
305
|
+
|
|
306
|
+
// $functions work in directives; they hook into the lifecycle of the element
|
|
307
|
+
$effect(() => {
|
|
308
|
+
if (condition()) {
|
|
309
|
+
// show element
|
|
310
|
+
if (!element.parentNode && placeholder.parentNode) {
|
|
311
|
+
element.insertBefore(placeholder.parentNode);
|
|
312
|
+
placeholder.parentNode.removeChild(placeholder);
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
// hide element
|
|
316
|
+
if (element.parentNode && !placeholder.parentNode) {
|
|
317
|
+
placeholder.insertBefore(element.parentNode);
|
|
318
|
+
element.parentNode.removeChild(element);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
194
323
|
|
|
195
|
-
|
|
196
|
-
|
|
324
|
+
function unlessDirective(element, condition) {
|
|
325
|
+
return ifDirective(element, () => !condition());
|
|
326
|
+
}
|
|
197
327
|
|
|
198
|
-
|
|
328
|
+
function showDirective(element, condition) {
|
|
329
|
+
// Store the element's current value.
|
|
330
|
+
let value = element.style.display;
|
|
331
|
+
|
|
332
|
+
$effect(() => {
|
|
333
|
+
if (condition()) {
|
|
334
|
+
// Apply the stored value when truthy.
|
|
335
|
+
element.style.display = value;
|
|
336
|
+
} else {
|
|
337
|
+
// Store value and hide when falsy.
|
|
338
|
+
value = element.style.display;
|
|
339
|
+
element.style.display = "none !important";
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
199
343
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
344
|
+
function hideDirective(element, condition) {
|
|
345
|
+
return showDirective(element, () => !condition());
|
|
346
|
+
}
|
|
203
347
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
348
|
+
function classesDirective(element, classes) {
|
|
349
|
+
// TODO: Applies an object of class names and values, where the values may be signals or plain values.
|
|
350
|
+
// Truthy means "apply this class" while falsy means don't.
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function stylesDirective(element, styles) {
|
|
354
|
+
// TODO: Same idea as *classes but for styles.
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
connect(Component, document.body);
|
|
358
|
+
|
|
359
|
+
// Easy custom elements? Could be another library.
|
|
360
|
+
element("my-counter", function () {
|
|
361
|
+
// Runs just after connectedCallback. `this` is bound to the custom HTMLElement class.
|
|
362
|
+
const shadow = this.attachShadow({ mode: "closed" });
|
|
363
|
+
|
|
364
|
+
return html`<div></div>`;
|
|
208
365
|
});
|
|
366
|
+
|
|
367
|
+
// function css(strings, values) {
|
|
368
|
+
// return {
|
|
369
|
+
// type: "css",
|
|
370
|
+
|
|
371
|
+
// }
|
|
372
|
+
// }
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
interface TemplateNode {
|
|
377
|
+
mount(parent: Node, after?: Node): void;
|
|
378
|
+
unmount(skipDOM?: boolean): void;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
interface TemplateDirective {
|
|
382
|
+
(element: Element, value: unknown): Node | TemplateNode;
|
|
383
|
+
}
|
|
209
384
|
```
|