@manyducks.co/dolla 2.0.0-alpha.4 → 2.0.0-alpha.41
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 -964
- package/dist/core/context.d.ts +53 -0
- package/dist/{modules → core}/dolla.d.ts +43 -26
- package/dist/core/markup.d.ts +90 -0
- package/dist/core/nodes/dom.d.ts +13 -0
- package/dist/core/nodes/dynamic.d.ts +28 -0
- package/dist/core/nodes/html.d.ts +33 -0
- package/dist/core/nodes/list.d.ts +28 -0
- package/dist/core/nodes/outlet.d.ts +19 -0
- package/dist/core/nodes/portal.d.ts +22 -0
- package/dist/core/nodes/view.d.ts +78 -0
- package/dist/core/ref.d.ts +28 -0
- package/dist/core/signals.d.ts +127 -0
- package/dist/core/store.d.ts +52 -0
- package/dist/core/symbols.d.ts +4 -0
- package/dist/{views → core/views}/passthrough.d.ts +1 -1
- package/dist/{modules/http.d.ts → http/index.d.ts} +3 -5
- package/dist/index.d.ts +14 -11
- package/dist/index.js +986 -1216
- package/dist/index.js.map +1 -1
- package/dist/jsx-dev-runtime.d.ts +2 -2
- package/dist/jsx-dev-runtime.js +2 -2
- package/dist/jsx-dev-runtime.js.map +1 -1
- package/dist/jsx-runtime.d.ts +3 -3
- package/dist/jsx-runtime.js +2 -2
- package/dist/jsx-runtime.js.map +1 -1
- package/dist/markup-DkQI155j.js +1447 -0
- package/dist/markup-DkQI155j.js.map +1 -0
- package/dist/{modules/router.d.ts → router/index.d.ts} +37 -48
- package/dist/router/router.utils.test.d.ts +1 -0
- package/dist/translate/index.d.ts +133 -0
- package/dist/typeChecking.d.ts +2 -98
- package/dist/typeChecking.test.d.ts +1 -0
- package/dist/types.d.ts +12 -14
- package/dist/utils.d.ts +18 -3
- package/docs/http.md +29 -0
- package/docs/i18n.md +38 -0
- package/docs/index.md +10 -0
- package/docs/router.md +80 -0
- package/docs/setup.md +31 -0
- package/docs/signals.md +149 -0
- package/docs/state.md +141 -0
- package/docs/stores.md +62 -0
- package/docs/views.md +208 -0
- package/index.d.ts +2 -2
- package/notes/TODO.md +6 -0
- package/notes/atomic.md +209 -0
- package/notes/context-routes.md +56 -0
- package/notes/elimination.md +33 -0
- package/notes/readme-scratch.md +260 -0
- package/notes/route-middleware.md +42 -0
- package/notes/scratch.md +330 -7
- package/notes/stores.md +53 -0
- package/package.json +14 -10
- package/vite.config.js +5 -10
- package/build.js +0 -34
- package/dist/markup.d.ts +0 -100
- package/dist/modules/language.d.ts +0 -41
- package/dist/modules/render.d.ts +0 -17
- package/dist/nodes/cond.d.ts +0 -26
- package/dist/nodes/html.d.ts +0 -31
- package/dist/nodes/observer.d.ts +0 -29
- package/dist/nodes/outlet.d.ts +0 -22
- package/dist/nodes/portal.d.ts +0 -19
- package/dist/nodes/repeat.d.ts +0 -34
- package/dist/nodes/text.d.ts +0 -19
- package/dist/passthrough-BSLd3foL.js +0 -1245
- package/dist/passthrough-BSLd3foL.js.map +0 -1
- package/dist/signals.d.ts +0 -101
- package/dist/view.d.ts +0 -50
- package/tests/signals.test.js +0 -135
- /package/dist/{routing.test.d.ts → core/signals.test.d.ts} +0 -0
- /package/dist/{views → core/views}/default-crash-view.d.ts +0 -0
- /package/dist/{routing.d.ts → router/router.utils.d.ts} +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# What can we remove?
|
|
2
|
+
|
|
3
|
+
I still want to get this library with all its features down below 15kb. It's currently at 17.8kb. That said, I don't want to strip out things that are actually useful. The mission of this library is to be batteries-included, which implies some extra weight.
|
|
4
|
+
|
|
5
|
+
What can be removed without compromising the basics?
|
|
6
|
+
|
|
7
|
+
## Events?
|
|
8
|
+
|
|
9
|
+
> 1st ELIMINATED. We're at 16.77kb now.
|
|
10
|
+
|
|
11
|
+
If we have the ability to get contexts and call methods on them, isn't that just a better version of events?
|
|
12
|
+
|
|
13
|
+
## Logger / Built-in Crash View
|
|
14
|
+
|
|
15
|
+
> REDUCED! Removed simple-color-hash in favor of custom OKLCH hash and we're down to 15.9kb.
|
|
16
|
+
|
|
17
|
+
Logger could be a different package. Crashes could be handled by a crash handler you attach.
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
Dolla.onCrash((error) => {
|
|
21
|
+
// Do what you will with this error.
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## HTTP Client?
|
|
26
|
+
|
|
27
|
+
Do we really need this? It's kind of a nice wrapper but the only thing I use the middleware for is to add auth headers for API calls, and it's trivial to write a function around `fetch` that does that. No need for the complexity of middleware.
|
|
28
|
+
|
|
29
|
+
Not really. Fetch is hella basic. I just tried to write my own trivial wrapper around fetch and it took a little too much thought. I don't want to do that for every project. Adding middleware to authenticate feels trivial with `http`.
|
|
30
|
+
|
|
31
|
+
## Markup?
|
|
32
|
+
|
|
33
|
+
Can JSX and `html` return DOM nodes directly? Not really, because views need to be mounted and unmounted like DOM nodes but they don't have the same API. Although I could define a minimal DOM-compatible API so that DOM nodes could directly work.
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# README
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
import { mount, atom, html } from "@manyducks.co/atomic";
|
|
5
|
+
|
|
6
|
+
function Home() {
|
|
7
|
+
return html` <h1>This is the home page!</h1> `;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// mount to DOM element
|
|
11
|
+
mount(Home, document.body);
|
|
12
|
+
|
|
13
|
+
// render to string
|
|
14
|
+
const string = await render(Home, "/the/path/here");
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
> This note will eventually become the new README. Here I'm laying out my ideal framework API.
|
|
20
|
+
|
|
21
|
+
A basic component.
|
|
22
|
+
|
|
23
|
+
```jsx
|
|
24
|
+
import { mount, state, derive, batch } from "@manyducks.co/dolla";
|
|
25
|
+
|
|
26
|
+
function ExampleView(props, ctx) {
|
|
27
|
+
// Signals: state, derive, effect and batch
|
|
28
|
+
|
|
29
|
+
const count = state(5);
|
|
30
|
+
|
|
31
|
+
const doubled = derive(() => count.value * 2);
|
|
32
|
+
|
|
33
|
+
batch(() => {
|
|
34
|
+
// Perform multiple updates in one go and commit at the end.
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// If effect is called in the body of a view function it will be cleaned up automatically with the view.
|
|
38
|
+
ctx.effect(() => {
|
|
39
|
+
console.log(nested.value);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Emit and listen for context events.
|
|
43
|
+
ctx.on("event", (e, ...args) => {
|
|
44
|
+
e.cancel();
|
|
45
|
+
});
|
|
46
|
+
ctx.emit("event", ...args);
|
|
47
|
+
|
|
48
|
+
// Get and set context values.
|
|
49
|
+
ctx.set("context value", 5);
|
|
50
|
+
ctx.get("context value");
|
|
51
|
+
|
|
52
|
+
// Provide and use a store.
|
|
53
|
+
const store = ctx.provide(someStore); // provide creates a new instance attached to this view and returns it.
|
|
54
|
+
const store = ctx.use(someStore);
|
|
55
|
+
|
|
56
|
+
return <p>{count}</p>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
mount(ExampleView, document.body);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
<details open>
|
|
63
|
+
<summary>
|
|
64
|
+
<h2>Signals API</h2>
|
|
65
|
+
</summary>
|
|
66
|
+
|
|
67
|
+
The signals API. Dolla's signals use explicit tracking, meaning any function where signal values are tracked take an array of the signals you want to track. This way you know exactly what depends on what at a glance without any kind of hidden tracking logic behind the scenes. You are free to `.get()` the value of a signal without worrying about untracking it first.
|
|
68
|
+
|
|
69
|
+
```jsx
|
|
70
|
+
import { createState } from "@manyducks.co/dolla";
|
|
71
|
+
|
|
72
|
+
const [$count, setCount] = createState(256);
|
|
73
|
+
|
|
74
|
+
$count.get(); // 256; returns the current value
|
|
75
|
+
|
|
76
|
+
const stop = $count.watch((value) => {
|
|
77
|
+
// Runs once immediately, then again whenever the value changes.
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
setCount(512); // Update the value of $count. The new value is set and all watchers run synchronously.
|
|
81
|
+
|
|
82
|
+
stop(); // Stop watching for changes.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
That is the basic signal API. Signals are all about composability. Here are some more advanced ways of working with them:
|
|
86
|
+
|
|
87
|
+
```jsx
|
|
88
|
+
import { createState, toState, valueOf, derive } from "@manyducks.co/dolla";
|
|
89
|
+
|
|
90
|
+
const [$count, setCount] = createState(72);
|
|
91
|
+
|
|
92
|
+
// Returns the value of the signal passed in. If the value is not a signal it is returned as is.
|
|
93
|
+
const count = valueOf($count);
|
|
94
|
+
const bool = valueOf(true);
|
|
95
|
+
|
|
96
|
+
// Creates a signal containing the value passed in. If the value is already a signal it is returned as is.
|
|
97
|
+
const $bool = toState(true);
|
|
98
|
+
const $anotherCount = toState($count);
|
|
99
|
+
|
|
100
|
+
// Derive a new signal from the value of another. Whenever $count changes, $doubled will follow.
|
|
101
|
+
const $doubled = derive([$count], (count) => count * 2);
|
|
102
|
+
|
|
103
|
+
// Derive a new signal from the values of several others. When any value in the list changes, $sum will be recomputed.
|
|
104
|
+
const $sum = derive([$count, $doubled], (count, doubled) => count + doubled);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The API if we call it State instead of Signal to distance from the Signal object in standardization process.
|
|
108
|
+
|
|
109
|
+
```jsx
|
|
110
|
+
import { createState, toState, valueOf, derive } from "@manyducks.co/dolla";
|
|
111
|
+
|
|
112
|
+
const [$count, setCount] = createState(72);
|
|
113
|
+
|
|
114
|
+
// Returns the value of the signal passed in. If the value is not a signal it is returned as is.
|
|
115
|
+
const count = valueOf($count);
|
|
116
|
+
const bool = valueOf(true);
|
|
117
|
+
|
|
118
|
+
// Creates a signal containing the value passed in. If the value is already a signal it is returned as is.
|
|
119
|
+
const $bool = toState(true);
|
|
120
|
+
const $anotherCount = toState($count);
|
|
121
|
+
|
|
122
|
+
// Derive a new signal from the value of another. Whenever $count changes, $doubled will follow.
|
|
123
|
+
const $doubled = derive([$count], (count) => count * 2);
|
|
124
|
+
|
|
125
|
+
// Derive a new signal from the values of several others. When any value in the list changes, $sum will be recomputed.
|
|
126
|
+
const $sum = derive([$count, $doubled], (count, doubled) => count + doubled);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
States also come in a settable variety, with the setter included on the same object. Sometimes you want to pass around a two-way binding and this is what SettableState is for.
|
|
130
|
+
|
|
131
|
+
```jsx
|
|
132
|
+
import { createSettableState, fromSettable, toSettable } from "@manyducks.co/dolla";
|
|
133
|
+
|
|
134
|
+
// Settable states have their setter included.
|
|
135
|
+
const $$value = createSettableState("Test");
|
|
136
|
+
$$value.set("New Value");
|
|
137
|
+
|
|
138
|
+
// They can also be split into a State and Setter
|
|
139
|
+
const [$value, setValue] = fromSettableState($$value);
|
|
140
|
+
|
|
141
|
+
// And a State and Setter can be combined into a SettableState.
|
|
142
|
+
const $$otherValue = toSettableState($value, setValue);
|
|
143
|
+
|
|
144
|
+
// Or discard the setter and make it read-only using the good old toState function:
|
|
145
|
+
const $value = toState($$value);
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Alternative API
|
|
149
|
+
|
|
150
|
+
```jsx
|
|
151
|
+
import { State } from "@manyducks.co/dolla";
|
|
152
|
+
|
|
153
|
+
const [$count, setCount] = State(72);
|
|
154
|
+
|
|
155
|
+
const count = State.unwrap($count);
|
|
156
|
+
const bool = State.unwrap(true);
|
|
157
|
+
|
|
158
|
+
const $bool = State.wrap(true);
|
|
159
|
+
const $sameCount = State.wrap($count);
|
|
160
|
+
|
|
161
|
+
const $doubled = State.from([$count], (count) => count * 2);
|
|
162
|
+
|
|
163
|
+
const $sum = State.from([$count, $doubled], (count, doubled) => count + doubled);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Yet another
|
|
167
|
+
|
|
168
|
+
```jsx
|
|
169
|
+
import Dolla from "@manyducks.co/dolla";
|
|
170
|
+
|
|
171
|
+
const [$count, setCount] = Dolla.state(72);
|
|
172
|
+
|
|
173
|
+
const count = Dolla.get($count);
|
|
174
|
+
const bool = Dolla.get(true);
|
|
175
|
+
|
|
176
|
+
const $bool = Dolla.toState(true);
|
|
177
|
+
const $sameCount = Dolla.toState($count);
|
|
178
|
+
|
|
179
|
+
const $doubled = Dolla.computed([$count], (count) => count * 2);
|
|
180
|
+
const $sum = Dolla.computed([$count, $doubled], (count, doubled) => count + doubled);
|
|
181
|
+
|
|
182
|
+
// or
|
|
183
|
+
|
|
184
|
+
import { state, computed, get, toState } from "@manyducks.co/dolla";
|
|
185
|
+
|
|
186
|
+
const [$count, setCount] = state(72);
|
|
187
|
+
|
|
188
|
+
const count = get($count);
|
|
189
|
+
const bool = get(true);
|
|
190
|
+
|
|
191
|
+
const $bool = toState(true);
|
|
192
|
+
const $sameCount = toState($count);
|
|
193
|
+
|
|
194
|
+
const $doubled = computed([$count], (count) => count * 2);
|
|
195
|
+
const $sum = computed([$count, $doubled], (count, doubled) => count + doubled);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Settable signals:
|
|
199
|
+
|
|
200
|
+
```jsx
|
|
201
|
+
import { createSettableState, createSetter, toSettableSignal, fromSettableSignal } from "@manyducks.co/dolla";
|
|
202
|
+
|
|
203
|
+
// Create a SettableSignal, which is basically a signal and its setter combined into a single object.
|
|
204
|
+
const $$settable = createSettableState("Example");
|
|
205
|
+
|
|
206
|
+
// The basic API is identical...
|
|
207
|
+
$$settable.get();
|
|
208
|
+
const stop = $$settable.watch((value) => {
|
|
209
|
+
// ...
|
|
210
|
+
});
|
|
211
|
+
stop();
|
|
212
|
+
|
|
213
|
+
// ... except for the addition of a setter.
|
|
214
|
+
$$settable.set("Set me directly");
|
|
215
|
+
|
|
216
|
+
// When you already have a signal and a setter, they can be combined into one.
|
|
217
|
+
const $$count = toSettableSignal($count, setCount);
|
|
218
|
+
|
|
219
|
+
// This updates the original $signal value.
|
|
220
|
+
$$count.set(386);
|
|
221
|
+
|
|
222
|
+
// TODO: You can also split a SettableSignal into a signal and its setter.
|
|
223
|
+
const [$readable, setReadable] = fromSettableSignal($$settable);
|
|
224
|
+
|
|
225
|
+
// Create a custom setter. Calling this will cap the value to 100.
|
|
226
|
+
const setCountBounded = createSetter($count, (next, current) => {
|
|
227
|
+
return Math.min(100, next);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
setCountBounded((current) => {
|
|
231
|
+
return current + 1;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Or make a proxy $$doubled -- but would you actually want to proxy things like this?
|
|
235
|
+
const [$count, setCount] = createState(5);
|
|
236
|
+
const $doubled = derive([$count], (count) => count * 2);
|
|
237
|
+
const $$doubled = toSettableSignal(
|
|
238
|
+
$doubled,
|
|
239
|
+
createSetter($doubled, (next, current) => {
|
|
240
|
+
setCount(next * 2);
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
I'm not really sure we need all of this. On the chopping block:
|
|
246
|
+
|
|
247
|
+
- The entire concept of settable signals
|
|
248
|
+
- `createSettableState`
|
|
249
|
+
- `toSettableSignal`
|
|
250
|
+
- `fromSettableSignal`
|
|
251
|
+
- `createSetter`
|
|
252
|
+
|
|
253
|
+
This makes the entire API just four functions:
|
|
254
|
+
|
|
255
|
+
- `createState`
|
|
256
|
+
- `derive`
|
|
257
|
+
- `toState`
|
|
258
|
+
- `valueOf`
|
|
259
|
+
|
|
260
|
+
</details>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Router Middleware
|
|
2
|
+
|
|
3
|
+
Allow handling route guards, preloading, etc with per-route middleware. When a route is matched, all middleware from higher layers are run again.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
Dolla.router.setup({
|
|
7
|
+
middleware: [/* does it make sense to have global middleware? */]
|
|
8
|
+
routes: [
|
|
9
|
+
{ path: "/login", middleware: [auth] },
|
|
10
|
+
{ path: "/", middleware: [auth], routes: [{ path: "/example", view: ExampleView }] }
|
|
11
|
+
]
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
async function auth(ctx) {
|
|
15
|
+
// This check can be implemented however it needs to be for the app.
|
|
16
|
+
const authed = await isAuthorized();
|
|
17
|
+
|
|
18
|
+
if (ctx.path === "/login") {
|
|
19
|
+
if (authed) {
|
|
20
|
+
ctx.redirect("/");
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
if (!authed) {
|
|
24
|
+
ctx.redirect("/login");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// If no redirect has happened and nothing has been returned then we're clear to proceed.
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// A middleware can also return Markup to stay on the URL but show something different.
|
|
31
|
+
async function randomVisitor(ctx) {
|
|
32
|
+
if (Math.random() > 0.99) {
|
|
33
|
+
return <LuckyVisitorView />
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Or preload async data and set a context variable before navigating.
|
|
38
|
+
async function preload(ctx) {
|
|
39
|
+
const data = await fetchData();
|
|
40
|
+
ctx.set("data", data);
|
|
41
|
+
}
|
|
42
|
+
```
|
package/notes/scratch.md
CHANGED
|
@@ -1,5 +1,328 @@
|
|
|
1
1
|
# Scratch Note
|
|
2
2
|
|
|
3
|
+
Idea: Monomorphic app context. Replaces StoreContext, ViewContext, etc.
|
|
4
|
+
|
|
5
|
+
Routes are baked into the app once again, but
|
|
6
|
+
|
|
7
|
+
```jsx
|
|
8
|
+
import { createRoot } from "@manyducks.co/dolla";
|
|
9
|
+
import { example } from "./stores/example.js";
|
|
10
|
+
|
|
11
|
+
const root = createRoot();
|
|
12
|
+
|
|
13
|
+
root.use(example());
|
|
14
|
+
|
|
15
|
+
async function auth(_, state, redirect) {
|
|
16
|
+
// route context
|
|
17
|
+
// Routes run through each callback until one resolves to a renderable value.
|
|
18
|
+
// If redirect is called, the route is re-matched and no further callbacks are run for this route.
|
|
19
|
+
|
|
20
|
+
if (state.auth == null) {
|
|
21
|
+
redirect("/login");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
root.route("/users/*", auth, (C) => {
|
|
26
|
+
C.route("/{#id}/*", (C) => {
|
|
27
|
+
C.route("/", (C) => <UserDetailRoute userId={C.params.id} />);
|
|
28
|
+
C.route("*", "./");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
root.route("/users/*", auth, (route) => {
|
|
33
|
+
route("/{#id}/*", (route) => {
|
|
34
|
+
// TODO: It's possible to reference the wrong 'route'
|
|
35
|
+
// Track active context and throw error if the one you call belongs to the wrong context?
|
|
36
|
+
route("/", (_, state) => <UserDetailView userId={state.params.id} />);
|
|
37
|
+
route("*", "./");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
function ExampleView(props, ctx) {
|
|
42
|
+
// ctx.routes returns a special type of outlet that renders children based on
|
|
43
|
+
// the route segments that come after the ones at this ctx.
|
|
44
|
+
|
|
45
|
+
// The weakness of this idea is that routes can't be validated without initializing views.
|
|
46
|
+
return (
|
|
47
|
+
<div>
|
|
48
|
+
<Suspense fallback={<span>Loading...</span>}>
|
|
49
|
+
{ctx.routes((route) => {
|
|
50
|
+
route("/subroute", () => <OtherView />);
|
|
51
|
+
|
|
52
|
+
// Routes can be async.
|
|
53
|
+
route("/other", () => import("some-module"));
|
|
54
|
+
})}
|
|
55
|
+
</Suspense>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Also Suspense. This can be simply implemented with events.
|
|
60
|
+
ctx.emit("suspense:begin", uniqueId);
|
|
61
|
+
// Then when done:
|
|
62
|
+
ctx.emit("suspense:end", uniqueId);
|
|
63
|
+
|
|
64
|
+
// The nearest Suspense view will track ids which are in suspense and show fallback content in the meantime.
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function Suspense(props, ctx) {
|
|
68
|
+
const [$tracked, setTracked] = createState({});
|
|
69
|
+
|
|
70
|
+
ctx.on("suspense:begin", (e) => {
|
|
71
|
+
setTracked((tracked) => {
|
|
72
|
+
return {
|
|
73
|
+
...tracked,
|
|
74
|
+
[e.detail]: new Date(),
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
ctx.on("suspense:end", (e) => {
|
|
80
|
+
setTracked((tracked) => {
|
|
81
|
+
const updated = Object.assign({}, tracked);
|
|
82
|
+
delete updated[e.detail];
|
|
83
|
+
return updated;
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// TODO: Hide suspended view without unmounting it. This might take special logic.
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Can also pass markup directly if you don't need the context.
|
|
91
|
+
root.route("/", auth, <HomeRoute />);
|
|
92
|
+
|
|
93
|
+
// Static redirect.
|
|
94
|
+
root.route("*", "/");
|
|
95
|
+
|
|
96
|
+
// Programmatic redirect.
|
|
97
|
+
root.route("*", (C) => {
|
|
98
|
+
C.log("hit wildcard");
|
|
99
|
+
C.redirect("/");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
root.mount(document.body);
|
|
103
|
+
|
|
104
|
+
// generate an HTML string for server side rendering.
|
|
105
|
+
root.toString("/some/path");
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
class ClockStore extends Store {
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
constructor() {
|
|
115
|
+
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
class CounterStore extends Store {
|
|
120
|
+
// Could have better name. This will catch any
|
|
121
|
+
// this.emit('counter:increment') or this.emit('counter:decrement') calls
|
|
122
|
+
// and update the state according to these functions.
|
|
123
|
+
value = new Emittable('counter', 0, {
|
|
124
|
+
increment: state => state + 1,
|
|
125
|
+
decrement: state => state - 1
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
type CounterEvents = {
|
|
130
|
+
increment: [amount: number];
|
|
131
|
+
decrement: [amount: number];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
Bring the $ back and the name full circle.
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
import { $, $$ } from "@manyducks.co/dolla";
|
|
144
|
+
|
|
145
|
+
// Shorthand dolla sign
|
|
146
|
+
|
|
147
|
+
// An initial value (with optional options object) creates a state.
|
|
148
|
+
const [$count, setCount] = $(0);
|
|
149
|
+
// = createState(0)
|
|
150
|
+
|
|
151
|
+
// An array and a function derives a state.
|
|
152
|
+
const $doubled = $.map([$count], (count) => count * 2);
|
|
153
|
+
// = derive([$count], (count) => count * 2);
|
|
154
|
+
|
|
155
|
+
// A state returns the same state.
|
|
156
|
+
const $sameCount = $.from($count);
|
|
157
|
+
const $wrapped = $.from({ message: "This is a state with no setter." });
|
|
158
|
+
// = toState($count)
|
|
159
|
+
|
|
160
|
+
// Get value from a state. Values that are not states are returned directly.
|
|
161
|
+
const count = $.get($count);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
What about other operators like RxJS?
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
// These would be functionally equivalent.
|
|
168
|
+
const $doubled = $count.pipe($.map((count) => count * 2));
|
|
169
|
+
const $doubled = $.map([$count], (count) => count * 2);
|
|
170
|
+
|
|
171
|
+
// Chainable. Get doubled value, but only update if it's between 10 and 100.
|
|
172
|
+
const $boundedDouble = $count.pipe(
|
|
173
|
+
// Transforms the value
|
|
174
|
+
$.map((count) => count * 2),
|
|
175
|
+
|
|
176
|
+
// Receives the value when it changes without affecting the output.
|
|
177
|
+
// Only receives values while this state is actively being watched.
|
|
178
|
+
$.tap((count) => console.log(`doubled value is ${count}`))
|
|
179
|
+
|
|
180
|
+
// Value only changes if it's within the range.
|
|
181
|
+
$.filter((count) => count >= 10 && count <= 100),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// Could have a top level pipe operator
|
|
185
|
+
const $boundedDouble = $.pipe(
|
|
186
|
+
[$count],
|
|
187
|
+
$.map((count) => count * 2),
|
|
188
|
+
$.tap((count) => console.log(`doubled value is ${count}`))
|
|
189
|
+
$.filter((count) => count >= 10 && count <= 100),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Could also be chainable
|
|
193
|
+
const $boundedDouble = $count
|
|
194
|
+
.map((count) => count * 2)
|
|
195
|
+
.tap((count) => console.log(`doubled value is ${count}`))
|
|
196
|
+
.filter((count) => count >= 10 && count <= 100);
|
|
197
|
+
|
|
198
|
+
// I kind of like this more than the current derive. It's cleaner.
|
|
199
|
+
$count.map(c => c * 2);
|
|
200
|
+
$count.merge([$other], (c, o) => c * o);
|
|
201
|
+
|
|
202
|
+
// Another way to merge multiple.
|
|
203
|
+
$.merge([$count, $other], (c, o) => c * o);
|
|
204
|
+
|
|
205
|
+
// What if you want to add something in the middle?
|
|
206
|
+
|
|
207
|
+
const $example = $count
|
|
208
|
+
.map((count) => count * 2)
|
|
209
|
+
.tap((count) => console.log(`doubled value is ${count}`))
|
|
210
|
+
.merge([$other1, $other2], (count, other1, other2) => /* ... */)
|
|
211
|
+
.filter((value) => value >= 10 && value <= 100);
|
|
212
|
+
|
|
213
|
+
// Is this a good pattern?
|
|
214
|
+
$count
|
|
215
|
+
.merge([$other], (count, other) => count * other)
|
|
216
|
+
.merge([$another], (merged, another) => merged * another);
|
|
217
|
+
// I think it gets a little weird to follow.
|
|
218
|
+
|
|
219
|
+
// equivalent to
|
|
220
|
+
derive(
|
|
221
|
+
[
|
|
222
|
+
derive([$count, $other], (count, other) => count * other),
|
|
223
|
+
$another
|
|
224
|
+
],
|
|
225
|
+
(merged, another) => merged * another)
|
|
226
|
+
// Is this a pattern? Yeah, I guess I do that. Just never in line like that.
|
|
227
|
+
|
|
228
|
+
// Do we want to handle errors?
|
|
229
|
+
// I feel like errors usually happen in watchers though.
|
|
230
|
+
$boundedDouble.watch((value) => {
|
|
231
|
+
// Received a value.
|
|
232
|
+
}, (error) => {
|
|
233
|
+
// Something threw an error.
|
|
234
|
+
});
|
|
235
|
+
// Or like this.
|
|
236
|
+
$boundedDouble.watch({
|
|
237
|
+
change: (value) => {
|
|
238
|
+
// Received a value.
|
|
239
|
+
// This code is most likely to throw an error.
|
|
240
|
+
// Should errors here be passed to the error callback?
|
|
241
|
+
// What is the point if you can just try/catch?
|
|
242
|
+
|
|
243
|
+
// Although if you don't then Dolla could use this to catch
|
|
244
|
+
// and trace errors better than it does now.
|
|
245
|
+
},
|
|
246
|
+
error: (error) => {
|
|
247
|
+
// Something threw an error.
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Filter derives a new state where the value only updates if the function returns truthy.
|
|
252
|
+
const $evens = $count.pipe($.filter((count) => count % 1 === 0));
|
|
253
|
+
// This is equivalent to
|
|
254
|
+
const $events = $.map([$count], (count) => count, { equals: (a, b) => a % 1 === 0 });
|
|
255
|
+
|
|
256
|
+
function filter(...args) {
|
|
257
|
+
if (isArray(args[0]) && isFunction(args[1])) {
|
|
258
|
+
// Standalone signature. Returns a new derived state.
|
|
259
|
+
} else if (args.length === 1 && isFunction(args[1])) {
|
|
260
|
+
// Curried signature. Returns a function that takes an array of states
|
|
261
|
+
// and returns one with args[1] as the equality check.
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
And you can write your own operators that implement these two signatures.
|
|
267
|
+
|
|
268
|
+
```js
|
|
269
|
+
// Here's one I might want to include.
|
|
270
|
+
// Use this to prevent ever getting a null value.
|
|
271
|
+
compare((next, previous) => next ?? previous ?? "default");
|
|
272
|
+
|
|
273
|
+
function compare(...args) {}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
I've been looking into other libraries that don't make you track your dependencies specifically. I think this is weird and unhinged to be honest. Calling functions with side effects that magically re-run things when the value changes is a truly weird and unexpected lifecycle. At least if you're explicitly tracking dependencies you know exactly what depends on what at a glance. Getting the computer to figure it out for you doesn't seem smart.
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
import { $ } from "@manyducks.co/dolla";
|
|
282
|
+
|
|
283
|
+
const [count, setCount] = $(0);
|
|
284
|
+
|
|
285
|
+
const doubled = $.computed(() => count() * 2);
|
|
286
|
+
|
|
287
|
+
$.effect(() => {
|
|
288
|
+
console.log(doubled());
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
$.batch(() => {
|
|
292
|
+
// Set multiple things but defer updates to after this function returns.
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Helpers on $; can plug into template as is.
|
|
296
|
+
$.if(
|
|
297
|
+
$.computed(() => count() > 5),
|
|
298
|
+
<span>Greater than 5!</span>,
|
|
299
|
+
<span>Not greater than 5...</span>,
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
const switched = $.switch(count, [[1, "one"], [2, "two"], [3, "three"]], "more...");
|
|
303
|
+
|
|
304
|
+
$.repeat()
|
|
305
|
+
|
|
306
|
+
// TODO: How feasible is this?
|
|
307
|
+
<Repeat each={}>
|
|
308
|
+
{(item, index) => {
|
|
309
|
+
|
|
310
|
+
}}
|
|
311
|
+
</Repeat>
|
|
312
|
+
|
|
313
|
+
<Show when={condition}>
|
|
314
|
+
Condition is true.
|
|
315
|
+
</Show>
|
|
316
|
+
|
|
317
|
+
// Get
|
|
318
|
+
count();
|
|
319
|
+
|
|
320
|
+
// Set
|
|
321
|
+
count(52);
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
3
326
|
What if Dolla was just a global object that you don't instantiate. I have never personally run into a use case for having more than one app on a page at once. In all my projects, the page and the app are synonymous.
|
|
4
327
|
|
|
5
328
|
Doing this would make it possible to access things inside the Dolla app from _outside_ code such as Quill blots. Effectively all code that has access to your Dolla import is _inside_ the app.
|
|
@@ -11,8 +334,8 @@ Doing this would make it possible to access things inside the Dolla app from _ou
|
|
|
11
334
|
import Dolla from "@manyducks.co/dolla";
|
|
12
335
|
|
|
13
336
|
// Languages: add translation, set language and get localized string as a signal
|
|
14
|
-
Dolla.
|
|
15
|
-
initialLanguage: Dolla.
|
|
337
|
+
Dolla.i18n.setup({
|
|
338
|
+
initialLanguage: Dolla.i18n.detect({ fallback: "ja" }), // Detect user's language and fall back to passed value
|
|
16
339
|
languages: [
|
|
17
340
|
{ name: "ja", path: "/static/locales/ja.json" },
|
|
18
341
|
{
|
|
@@ -26,8 +349,8 @@ Dolla.language.setup({
|
|
|
26
349
|
]
|
|
27
350
|
});
|
|
28
351
|
|
|
29
|
-
Dolla.
|
|
30
|
-
Dolla.
|
|
352
|
+
Dolla.i18n.$locale
|
|
353
|
+
Dolla.i18n.t$()
|
|
31
354
|
|
|
32
355
|
// A single setup call to keep things contained (must happen before mount)
|
|
33
356
|
Dolla.router.setup({
|
|
@@ -75,10 +398,10 @@ debug.log("HELLO");
|
|
|
75
398
|
debug.warn("THIS IS A SCOPED LOGGER");
|
|
76
399
|
|
|
77
400
|
// Efficiently and safely read and mutate the DOM using Dolla's render batching
|
|
78
|
-
Dolla.
|
|
401
|
+
Dolla.batch.read(() => {
|
|
79
402
|
// Reference DOM nodes
|
|
80
403
|
});
|
|
81
|
-
Dolla.
|
|
404
|
+
Dolla.batch.write(() => {
|
|
82
405
|
// Mutate the DOM as part of Dolla's next batch
|
|
83
406
|
}, "some-key");
|
|
84
407
|
|
|
@@ -93,7 +416,7 @@ function SomeView (props: SomeViewProps, ctx: Dolla.ViewContext) {
|
|
|
93
416
|
const debug = Dolla.createLogger("SomeView");
|
|
94
417
|
|
|
95
418
|
// returns a signal and a setter function
|
|
96
|
-
const [$someValue, setSomeValue] = Dolla.
|
|
419
|
+
const [$someValue, setSomeValue] = Dolla.createState(4);
|
|
97
420
|
|
|
98
421
|
// Router is now a part of the Dolla object
|
|
99
422
|
Dolla.router.$path;
|