@manyducks.co/dolla 2.0.0-alpha.6 → 2.0.0-alpha.61
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 +86 -591
- package/dist/core/context.d.ts +142 -0
- package/dist/core/env.d.ts +3 -0
- package/dist/core/index.d.ts +21 -0
- package/dist/core/logger.d.ts +42 -0
- package/dist/core/logger.test.d.ts +0 -0
- package/dist/core/markup.d.ts +104 -0
- package/dist/core/markup.test.d.ts +0 -0
- package/dist/core/mount.d.ts +15 -0
- package/dist/core/mount.test.d.ts +0 -0
- package/dist/core/nodes/_markup.d.ts +36 -0
- package/dist/core/nodes/dom.d.ts +13 -0
- package/dist/core/nodes/dynamic.d.ts +22 -0
- package/dist/core/nodes/element.d.ts +25 -0
- package/dist/core/nodes/portal.d.ts +18 -0
- package/dist/core/nodes/repeat.d.ts +27 -0
- package/dist/core/nodes/view.d.ts +25 -0
- package/dist/core/ref.d.ts +18 -0
- package/dist/core/ref.test.d.ts +1 -0
- package/dist/core/signals.d.ts +58 -0
- package/dist/core/signals.test.d.ts +1 -0
- package/dist/{views → core/views}/default-crash-view.d.ts +11 -4
- package/dist/core/views/fragment.d.ts +7 -0
- package/dist/fragment-BahD_BJA.js +7 -0
- package/dist/fragment-BahD_BJA.js.map +1 -0
- package/dist/hooks/index.d.ts +64 -0
- package/dist/hooks/index.test.d.ts +1 -0
- package/dist/hooks.js +69 -0
- package/dist/hooks.js.map +1 -0
- package/dist/{modules/http.d.ts → http/index.d.ts} +3 -5
- package/dist/http.js +163 -0
- package/dist/http.js.map +1 -0
- package/dist/i18n/index.d.ts +134 -0
- package/dist/i18n.js +318 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.js +98 -1388
- package/dist/index.js.map +1 -1
- package/dist/jsx-dev-runtime.d.ts +3 -2
- package/dist/jsx-dev-runtime.js +5 -12
- package/dist/jsx-dev-runtime.js.map +1 -1
- package/dist/jsx-runtime.d.ts +4 -3
- package/dist/jsx-runtime.js +9 -15
- package/dist/jsx-runtime.js.map +1 -1
- package/dist/logger-Bl496yfY.js +91 -0
- package/dist/logger-Bl496yfY.js.map +1 -0
- package/dist/markup-CX27GJ1M.js +1030 -0
- package/dist/markup-CX27GJ1M.js.map +1 -0
- package/dist/ref-BD79iqlg.js +15 -0
- package/dist/ref-BD79iqlg.js.map +1 -0
- package/dist/router/index.d.ts +2 -0
- package/dist/router/router.d.ts +160 -0
- package/dist/{routing.d.ts → router/router.utils.d.ts} +17 -3
- package/dist/router/router.utils.test.d.ts +1 -0
- package/dist/router-CjCkk4dA.js +543 -0
- package/dist/router-CjCkk4dA.js.map +1 -0
- package/dist/router.js +8 -0
- package/dist/router.js.map +1 -0
- package/dist/signals-gCwiIe5X.js +450 -0
- package/dist/signals-gCwiIe5X.js.map +1 -0
- package/dist/typeChecking-CbltMOUt.js +71 -0
- package/dist/typeChecking-CbltMOUt.js.map +1 -0
- package/dist/typeChecking.d.ts +2 -98
- package/dist/typeChecking.test.d.ts +1 -0
- package/dist/types.d.ts +98 -25
- package/dist/utils.d.ts +20 -3
- package/docs/hooks.md +211 -0
- package/docs/http.md +29 -0
- package/docs/i18n.md +43 -0
- package/docs/index.md +10 -0
- package/docs/markup.md +16 -0
- package/docs/mixins.md +32 -0
- package/docs/ref.md +93 -0
- package/docs/router.md +80 -0
- package/docs/setup.md +31 -0
- package/docs/signals.md +166 -0
- package/docs/state.md +141 -0
- package/docs/stores.md +62 -0
- package/docs/views.md +208 -0
- package/examples/webcomponent/index.html +14 -0
- package/examples/webcomponent/main.js +165 -0
- package/index.d.ts +2 -2
- package/notes/TODO.md +6 -0
- package/notes/atomic.md +452 -0
- package/notes/context-routes.md +61 -0
- package/notes/custom-nodes.md +17 -0
- package/notes/effection-idea.md +34 -0
- package/notes/elimination.md +33 -0
- package/notes/mixins.md +22 -0
- package/notes/molecule.md +35 -0
- package/notes/observable.md +180 -0
- package/notes/readme-scratch.md +45 -7
- package/notes/route-middleware.md +42 -0
- package/notes/scratch.md +353 -6
- package/notes/splitting.md +5 -0
- package/notes/stores.md +79 -0
- package/package.json +31 -12
- package/vite.config.js +6 -11
- package/build.js +0 -34
- package/dist/index.d.ts +0 -21
- package/dist/markup.d.ts +0 -100
- package/dist/modules/dolla.d.ts +0 -111
- package/dist/modules/language.d.ts +0 -41
- package/dist/modules/render.d.ts +0 -17
- package/dist/modules/router.d.ts +0 -152
- 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-CW8Ezjg-.js +0 -1244
- package/dist/passthrough-CW8Ezjg-.js.map +0 -1
- package/dist/state.d.ts +0 -101
- package/dist/view.d.ts +0 -50
- package/dist/views/passthrough.d.ts +0 -5
- package/tests/state.test.js +0 -135
- /package/dist/{routing.test.d.ts → core/context.test.d.ts} +0 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Observable
|
|
2
|
+
|
|
3
|
+
Signals have some downsides, like if you call them inside a function, and you then call that function inside a tracking context, it can cause the tracking context to re-run unexpectedly. You then have to defensively call things inside `untracked` to avoid tracking deeply nested signal getters. It's not unmanageable but it's extremely surprising and unintuitive when you first run into it. It's a new and unnecessary consideration that makes code feel less safe and predictable.
|
|
4
|
+
|
|
5
|
+
I'm considering using Observable (or my own extended version of it) as a basis for a state management system. Still built on top of `alien-signals` but with explicit tracking of signal values. It could look something like the following.
|
|
6
|
+
|
|
7
|
+
This would probably be best as a separate library, maybe called `@manyducks.co/atomic`.
|
|
8
|
+
|
|
9
|
+
Going back to the atom/compose terminology.
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
// Define an atom, the basic value holder object.
|
|
13
|
+
const count = atom(5);
|
|
14
|
+
|
|
15
|
+
// Atoms have a `value` field that is writable. This is not tracked by default.
|
|
16
|
+
count.value; // 5
|
|
17
|
+
count.value = 12;
|
|
18
|
+
count.value; // 12
|
|
19
|
+
|
|
20
|
+
// Implements the Observable interface.
|
|
21
|
+
const subscription = count.subscribe({
|
|
22
|
+
next: (value) => {
|
|
23
|
+
console.log("count is now", value);
|
|
24
|
+
},
|
|
25
|
+
error: (error) => {},
|
|
26
|
+
completed: () => {},
|
|
27
|
+
});
|
|
28
|
+
subscription.closed; // boolean
|
|
29
|
+
subscription.unsubscribe();
|
|
30
|
+
|
|
31
|
+
// Like `value` getter but tracks count in a signal tracking scope.
|
|
32
|
+
count.track(); // 12
|
|
33
|
+
|
|
34
|
+
// Can be closed which completes all subscribers and will throw an error if a new value is set.
|
|
35
|
+
count.close();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Atoms can be composed.
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
// You explicitly pass a dependencies array at the end, similar to React.
|
|
42
|
+
// Dependencies will be tracked and the compose function re-run any time they receive a new value.
|
|
43
|
+
const doubled = composed((prev) => count.value * 2, [count]);
|
|
44
|
+
|
|
45
|
+
// Read-only value.
|
|
46
|
+
doubled.value;
|
|
47
|
+
|
|
48
|
+
// Observable
|
|
49
|
+
const subscription = doubled.subscribe((value) => {
|
|
50
|
+
// ...
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Trackable
|
|
54
|
+
doubled.track();
|
|
55
|
+
|
|
56
|
+
// Completes subscriptions, untracks deps and prevents receiving any new values.
|
|
57
|
+
doubled.close();
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Effects work basically the same as `composed` but they return a cancel function instead of a value.
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
const cancel = effect(() => {
|
|
64
|
+
console.log(`count is now ${count.value}`);
|
|
65
|
+
}, [count]);
|
|
66
|
+
|
|
67
|
+
cancel();
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Other thoughts:
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
// You can name observables for debugging purposes. If one of them throws an error it can include the name.
|
|
74
|
+
const count = atom(5).named("count");
|
|
75
|
+
|
|
76
|
+
// Maybe even named effects.
|
|
77
|
+
const cancel = effect(() => {
|
|
78
|
+
console.log(`count is now ${count.value}`);
|
|
79
|
+
}, [count]).named("countReader");
|
|
80
|
+
|
|
81
|
+
// Promise-based await next? This will resolve when count.value is set.
|
|
82
|
+
// If the subscription errors it rejects with that error. If the subscription completes it rejects with an error to indicate that.
|
|
83
|
+
const nextCount = await count.nextValue();
|
|
84
|
+
|
|
85
|
+
// Filter and signal. Wait up to 5 seconds for next even value.
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
setTimeout(controller.abort, 5000);
|
|
88
|
+
const nextEven = await count.nextValue({
|
|
89
|
+
filter: (value) => value % 2 === 0,
|
|
90
|
+
signal: controller.signal,
|
|
91
|
+
// or timeout: 5000
|
|
92
|
+
});
|
|
93
|
+
// Resolves to null if aborted or timed out.
|
|
94
|
+
|
|
95
|
+
// Batching
|
|
96
|
+
|
|
97
|
+
const count1 = atom(5);
|
|
98
|
+
const count2 = atom(12);
|
|
99
|
+
|
|
100
|
+
effect(() => {
|
|
101
|
+
console.log(`total: ${count1.value + count2.value}`);
|
|
102
|
+
}, [count1, count2]);
|
|
103
|
+
|
|
104
|
+
// This causes the effect to run twice
|
|
105
|
+
count1.value = 50;
|
|
106
|
+
count2.value = 8;
|
|
107
|
+
|
|
108
|
+
// A batch suspends effects until it concludes; this runs the effect once
|
|
109
|
+
batch(() => {
|
|
110
|
+
count1.value = 50;
|
|
111
|
+
count2.value = 8;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Deep reactivity
|
|
115
|
+
const data = atom(
|
|
116
|
+
{
|
|
117
|
+
users: [
|
|
118
|
+
{ id: 1, name: "Tony" },
|
|
119
|
+
{ id: 2, name: "Morgan" },
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
{ deep: true },
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// These updates trigger effects and subscriptions.
|
|
126
|
+
// By default only setting `.value` directly will trigger notifications.
|
|
127
|
+
data.value.users[0].name = "Bon";
|
|
128
|
+
data.value.users.find((user) => user.id === 1).name = "Tony";
|
|
129
|
+
|
|
130
|
+
// Then in theory, if you referenced one of the values
|
|
131
|
+
const morgan = data.value.users.find((user) => user.id === 2);
|
|
132
|
+
|
|
133
|
+
// And passed that around and modified it that would also still be reactive to the original atom.
|
|
134
|
+
// I don't know if this is a good idea.
|
|
135
|
+
morgan.name = "AKLSJDAKSD";
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## What would Dolla look like with this?
|
|
139
|
+
|
|
140
|
+
```jsx
|
|
141
|
+
function CounterView() {
|
|
142
|
+
const count = atom(0);
|
|
143
|
+
|
|
144
|
+
const increment = () => count.value++;
|
|
145
|
+
const decrement = () => count.value--;
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div>
|
|
149
|
+
Counter: {count}
|
|
150
|
+
<button onClick={increment}>+1</button>
|
|
151
|
+
<button onClick={decrement}>-1</button>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function ExampleView(props, ctx) {
|
|
157
|
+
const name = atom("");
|
|
158
|
+
|
|
159
|
+
// Update local name whenever props.name changes
|
|
160
|
+
ctx.effect(() => {
|
|
161
|
+
name.value = props.name.value;
|
|
162
|
+
}, [props.name]);
|
|
163
|
+
|
|
164
|
+
// Update greeting whenever local name changes
|
|
165
|
+
const greeting = composed(() => `Hello, ${name.value}`, [name]);
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div>
|
|
169
|
+
<span>{greeting}</span>
|
|
170
|
+
<input value={name} onInput={(e) => (name.value = e.target.value)} />
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## TypeScript
|
|
177
|
+
|
|
178
|
+
- `Atom<T>` for the basic building block with a writable value.
|
|
179
|
+
- `Composed<T>` for a derived state based on other `Atom<T>` and `Composed<T>` values.
|
|
180
|
+
- `Atomic<T>` to encompass the basic API of both `Atom<T>` and `Composed<T>`.
|
package/notes/readme-scratch.md
CHANGED
|
@@ -1,24 +1,62 @@
|
|
|
1
1
|
# README
|
|
2
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
|
+
|
|
3
19
|
> This note will eventually become the new README. Here I'm laying out my ideal framework API.
|
|
4
20
|
|
|
5
21
|
A basic component.
|
|
6
22
|
|
|
7
23
|
```jsx
|
|
8
|
-
import
|
|
24
|
+
import { mount, state, derive, batch } from "@manyducks.co/dolla";
|
|
9
25
|
|
|
10
26
|
function ExampleView(props, ctx) {
|
|
11
|
-
|
|
12
|
-
const $doubled = derive([$count], (n) => n * 2);
|
|
27
|
+
// Signals: state, derive, effect and batch
|
|
13
28
|
|
|
14
|
-
|
|
15
|
-
|
|
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);
|
|
16
40
|
});
|
|
17
41
|
|
|
18
|
-
|
|
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>;
|
|
19
57
|
}
|
|
20
58
|
|
|
21
|
-
|
|
59
|
+
mount(ExampleView, document.body);
|
|
22
60
|
```
|
|
23
61
|
|
|
24
62
|
<details open>
|
|
@@ -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,352 @@
|
|
|
1
1
|
# Scratch Note
|
|
2
2
|
|
|
3
|
+
Library needs to be easier to render standalone elements. Idea to replace constructView and a lot of the store management weirdness with a `createContext` function and a `render` function that takes markup and a context.
|
|
4
|
+
|
|
5
|
+
The context is basically a refactor of the old ElementContext and serves the same purpose.
|
|
6
|
+
|
|
7
|
+
```jsx
|
|
8
|
+
import { m, render, createContext } from "@manyducks.co/dolla";
|
|
9
|
+
|
|
10
|
+
const context = createContext();
|
|
11
|
+
context.addStore(SomeStore);
|
|
12
|
+
|
|
13
|
+
function ExampleView(props, ctx) {
|
|
14
|
+
// Views now access the Context directly.
|
|
15
|
+
const store = ctx.getStore(SomeStore);
|
|
16
|
+
|
|
17
|
+
return <h1>Hello World</h1>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const element = render(ExampleView, context);
|
|
21
|
+
|
|
22
|
+
element.mount(document.body);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
Idea: Monomorphic app context. Replaces StoreContext, ViewContext, etc.
|
|
28
|
+
|
|
29
|
+
Routes are baked into the app once again, but
|
|
30
|
+
|
|
31
|
+
```jsx
|
|
32
|
+
import { createRoot } from "@manyducks.co/dolla";
|
|
33
|
+
import { example } from "./stores/example.js";
|
|
34
|
+
|
|
35
|
+
const root = createRoot();
|
|
36
|
+
|
|
37
|
+
root.use(example());
|
|
38
|
+
|
|
39
|
+
async function auth(_, state, redirect) {
|
|
40
|
+
// route context
|
|
41
|
+
// Routes run through each callback until one resolves to a renderable value.
|
|
42
|
+
// If redirect is called, the route is re-matched and no further callbacks are run for this route.
|
|
43
|
+
|
|
44
|
+
if (state.auth == null) {
|
|
45
|
+
redirect("/login");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
root.route("/users/*", auth, (C) => {
|
|
50
|
+
C.route("/{#id}/*", (C) => {
|
|
51
|
+
C.route("/", (C) => <UserDetailRoute userId={C.params.id} />);
|
|
52
|
+
C.route("*", "./");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
root.route("/users/*", auth, (route) => {
|
|
57
|
+
route("/{#id}/*", (route) => {
|
|
58
|
+
// TODO: It's possible to reference the wrong 'route'
|
|
59
|
+
// Track active context and throw error if the one you call belongs to the wrong context?
|
|
60
|
+
route("/", (_, state) => <UserDetailView userId={state.params.id} />);
|
|
61
|
+
route("*", "./");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
function ExampleView(props, ctx) {
|
|
66
|
+
// ctx.routes returns a special type of outlet that renders children based on
|
|
67
|
+
// the route segments that come after the ones at this ctx.
|
|
68
|
+
|
|
69
|
+
// The weakness of this idea is that routes can't be validated without initializing views.
|
|
70
|
+
return (
|
|
71
|
+
<div>
|
|
72
|
+
<Suspense fallback={<span>Loading...</span>}>
|
|
73
|
+
{ctx.routes((route) => {
|
|
74
|
+
route("/subroute", () => <OtherView />);
|
|
75
|
+
|
|
76
|
+
// Routes can be async.
|
|
77
|
+
route("/other", () => import("some-module"));
|
|
78
|
+
})}
|
|
79
|
+
</Suspense>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Also Suspense. This can be simply implemented with events.
|
|
84
|
+
ctx.emit("suspense:begin", uniqueId);
|
|
85
|
+
// Then when done:
|
|
86
|
+
ctx.emit("suspense:end", uniqueId);
|
|
87
|
+
|
|
88
|
+
// The nearest Suspense view will track ids which are in suspense and show fallback content in the meantime.
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function Suspense(props, ctx) {
|
|
92
|
+
const [$tracked, setTracked] = createState({});
|
|
93
|
+
|
|
94
|
+
ctx.on("suspense:begin", (e) => {
|
|
95
|
+
setTracked((tracked) => {
|
|
96
|
+
return {
|
|
97
|
+
...tracked,
|
|
98
|
+
[e.detail]: new Date(),
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
ctx.on("suspense:end", (e) => {
|
|
104
|
+
setTracked((tracked) => {
|
|
105
|
+
const updated = Object.assign({}, tracked);
|
|
106
|
+
delete updated[e.detail];
|
|
107
|
+
return updated;
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// TODO: Hide suspended view without unmounting it. This might take special logic.
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Can also pass markup directly if you don't need the context.
|
|
115
|
+
root.route("/", auth, <HomeRoute />);
|
|
116
|
+
|
|
117
|
+
// Static redirect.
|
|
118
|
+
root.route("*", "/");
|
|
119
|
+
|
|
120
|
+
// Programmatic redirect.
|
|
121
|
+
root.route("*", (C) => {
|
|
122
|
+
C.log("hit wildcard");
|
|
123
|
+
C.redirect("/");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
root.mount(document.body);
|
|
127
|
+
|
|
128
|
+
// generate an HTML string for server side rendering.
|
|
129
|
+
root.toString("/some/path");
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
class ClockStore extends Store {
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
constructor() {
|
|
139
|
+
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
class CounterStore extends Store {
|
|
144
|
+
// Could have better name. This will catch any
|
|
145
|
+
// this.emit('counter:increment') or this.emit('counter:decrement') calls
|
|
146
|
+
// and update the state according to these functions.
|
|
147
|
+
value = new Emittable('counter', 0, {
|
|
148
|
+
increment: state => state + 1,
|
|
149
|
+
decrement: state => state - 1
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
type CounterEvents = {
|
|
154
|
+
increment: [amount: number];
|
|
155
|
+
decrement: [amount: number];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
Bring the $ back and the name full circle.
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
import { $, $$ } from "@manyducks.co/dolla";
|
|
168
|
+
|
|
169
|
+
// Shorthand dolla sign
|
|
170
|
+
|
|
171
|
+
// An initial value (with optional options object) creates a state.
|
|
172
|
+
const [$count, setCount] = $(0);
|
|
173
|
+
// = createState(0)
|
|
174
|
+
|
|
175
|
+
// An array and a function derives a state.
|
|
176
|
+
const $doubled = $.map([$count], (count) => count * 2);
|
|
177
|
+
// = derive([$count], (count) => count * 2);
|
|
178
|
+
|
|
179
|
+
// A state returns the same state.
|
|
180
|
+
const $sameCount = $.from($count);
|
|
181
|
+
const $wrapped = $.from({ message: "This is a state with no setter." });
|
|
182
|
+
// = toState($count)
|
|
183
|
+
|
|
184
|
+
// Get value from a state. Values that are not states are returned directly.
|
|
185
|
+
const count = $.get($count);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
What about other operators like RxJS?
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
// These would be functionally equivalent.
|
|
192
|
+
const $doubled = $count.pipe($.map((count) => count * 2));
|
|
193
|
+
const $doubled = $.map([$count], (count) => count * 2);
|
|
194
|
+
|
|
195
|
+
// Chainable. Get doubled value, but only update if it's between 10 and 100.
|
|
196
|
+
const $boundedDouble = $count.pipe(
|
|
197
|
+
// Transforms the value
|
|
198
|
+
$.map((count) => count * 2),
|
|
199
|
+
|
|
200
|
+
// Receives the value when it changes without affecting the output.
|
|
201
|
+
// Only receives values while this state is actively being watched.
|
|
202
|
+
$.tap((count) => console.log(`doubled value is ${count}`))
|
|
203
|
+
|
|
204
|
+
// Value only changes if it's within the range.
|
|
205
|
+
$.filter((count) => count >= 10 && count <= 100),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Could have a top level pipe operator
|
|
209
|
+
const $boundedDouble = $.pipe(
|
|
210
|
+
[$count],
|
|
211
|
+
$.map((count) => count * 2),
|
|
212
|
+
$.tap((count) => console.log(`doubled value is ${count}`))
|
|
213
|
+
$.filter((count) => count >= 10 && count <= 100),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// Could also be chainable
|
|
217
|
+
const $boundedDouble = $count
|
|
218
|
+
.map((count) => count * 2)
|
|
219
|
+
.tap((count) => console.log(`doubled value is ${count}`))
|
|
220
|
+
.filter((count) => count >= 10 && count <= 100);
|
|
221
|
+
|
|
222
|
+
// I kind of like this more than the current derive. It's cleaner.
|
|
223
|
+
$count.map(c => c * 2);
|
|
224
|
+
$count.merge([$other], (c, o) => c * o);
|
|
225
|
+
|
|
226
|
+
// Another way to merge multiple.
|
|
227
|
+
$.merge([$count, $other], (c, o) => c * o);
|
|
228
|
+
|
|
229
|
+
// What if you want to add something in the middle?
|
|
230
|
+
|
|
231
|
+
const $example = $count
|
|
232
|
+
.map((count) => count * 2)
|
|
233
|
+
.tap((count) => console.log(`doubled value is ${count}`))
|
|
234
|
+
.merge([$other1, $other2], (count, other1, other2) => /* ... */)
|
|
235
|
+
.filter((value) => value >= 10 && value <= 100);
|
|
236
|
+
|
|
237
|
+
// Is this a good pattern?
|
|
238
|
+
$count
|
|
239
|
+
.merge([$other], (count, other) => count * other)
|
|
240
|
+
.merge([$another], (merged, another) => merged * another);
|
|
241
|
+
// I think it gets a little weird to follow.
|
|
242
|
+
|
|
243
|
+
// equivalent to
|
|
244
|
+
derive(
|
|
245
|
+
[
|
|
246
|
+
derive([$count, $other], (count, other) => count * other),
|
|
247
|
+
$another
|
|
248
|
+
],
|
|
249
|
+
(merged, another) => merged * another)
|
|
250
|
+
// Is this a pattern? Yeah, I guess I do that. Just never in line like that.
|
|
251
|
+
|
|
252
|
+
// Do we want to handle errors?
|
|
253
|
+
// I feel like errors usually happen in watchers though.
|
|
254
|
+
$boundedDouble.watch((value) => {
|
|
255
|
+
// Received a value.
|
|
256
|
+
}, (error) => {
|
|
257
|
+
// Something threw an error.
|
|
258
|
+
});
|
|
259
|
+
// Or like this.
|
|
260
|
+
$boundedDouble.watch({
|
|
261
|
+
change: (value) => {
|
|
262
|
+
// Received a value.
|
|
263
|
+
// This code is most likely to throw an error.
|
|
264
|
+
// Should errors here be passed to the error callback?
|
|
265
|
+
// What is the point if you can just try/catch?
|
|
266
|
+
|
|
267
|
+
// Although if you don't then Dolla could use this to catch
|
|
268
|
+
// and trace errors better than it does now.
|
|
269
|
+
},
|
|
270
|
+
error: (error) => {
|
|
271
|
+
// Something threw an error.
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Filter derives a new state where the value only updates if the function returns truthy.
|
|
276
|
+
const $evens = $count.pipe($.filter((count) => count % 1 === 0));
|
|
277
|
+
// This is equivalent to
|
|
278
|
+
const $events = $.map([$count], (count) => count, { equals: (a, b) => a % 1 === 0 });
|
|
279
|
+
|
|
280
|
+
function filter(...args) {
|
|
281
|
+
if (isArray(args[0]) && isFunction(args[1])) {
|
|
282
|
+
// Standalone signature. Returns a new derived state.
|
|
283
|
+
} else if (args.length === 1 && isFunction(args[1])) {
|
|
284
|
+
// Curried signature. Returns a function that takes an array of states
|
|
285
|
+
// and returns one with args[1] as the equality check.
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
And you can write your own operators that implement these two signatures.
|
|
291
|
+
|
|
292
|
+
```js
|
|
293
|
+
// Here's one I might want to include.
|
|
294
|
+
// Use this to prevent ever getting a null value.
|
|
295
|
+
compare((next, previous) => next ?? previous ?? "default");
|
|
296
|
+
|
|
297
|
+
function compare(...args) {}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
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.
|
|
303
|
+
|
|
304
|
+
```js
|
|
305
|
+
import { $ } from "@manyducks.co/dolla";
|
|
306
|
+
|
|
307
|
+
const [count, setCount] = $(0);
|
|
308
|
+
|
|
309
|
+
const doubled = $.computed(() => count() * 2);
|
|
310
|
+
|
|
311
|
+
$.effect(() => {
|
|
312
|
+
console.log(doubled());
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
$.batch(() => {
|
|
316
|
+
// Set multiple things but defer updates to after this function returns.
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Helpers on $; can plug into template as is.
|
|
320
|
+
$.if(
|
|
321
|
+
$.computed(() => count() > 5),
|
|
322
|
+
<span>Greater than 5!</span>,
|
|
323
|
+
<span>Not greater than 5...</span>,
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
const switched = $.switch(count, [[1, "one"], [2, "two"], [3, "three"]], "more...");
|
|
327
|
+
|
|
328
|
+
$.repeat()
|
|
329
|
+
|
|
330
|
+
// TODO: How feasible is this?
|
|
331
|
+
<Repeat each={}>
|
|
332
|
+
{(item, index) => {
|
|
333
|
+
|
|
334
|
+
}}
|
|
335
|
+
</Repeat>
|
|
336
|
+
|
|
337
|
+
<Show when={condition}>
|
|
338
|
+
Condition is true.
|
|
339
|
+
</Show>
|
|
340
|
+
|
|
341
|
+
// Get
|
|
342
|
+
count();
|
|
343
|
+
|
|
344
|
+
// Set
|
|
345
|
+
count(52);
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
3
350
|
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
351
|
|
|
5
352
|
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 +358,8 @@ Doing this would make it possible to access things inside the Dolla app from _ou
|
|
|
11
358
|
import Dolla from "@manyducks.co/dolla";
|
|
12
359
|
|
|
13
360
|
// Languages: add translation, set language and get localized string as a signal
|
|
14
|
-
Dolla.
|
|
15
|
-
initialLanguage: Dolla.
|
|
361
|
+
Dolla.i18n.setup({
|
|
362
|
+
initialLanguage: Dolla.i18n.detect({ fallback: "ja" }), // Detect user's language and fall back to passed value
|
|
16
363
|
languages: [
|
|
17
364
|
{ name: "ja", path: "/static/locales/ja.json" },
|
|
18
365
|
{
|
|
@@ -26,8 +373,8 @@ Dolla.language.setup({
|
|
|
26
373
|
]
|
|
27
374
|
});
|
|
28
375
|
|
|
29
|
-
Dolla.
|
|
30
|
-
Dolla.
|
|
376
|
+
Dolla.i18n.$locale
|
|
377
|
+
Dolla.i18n.t$()
|
|
31
378
|
|
|
32
379
|
// A single setup call to keep things contained (must happen before mount)
|
|
33
380
|
Dolla.router.setup({
|
|
@@ -75,10 +422,10 @@ debug.log("HELLO");
|
|
|
75
422
|
debug.warn("THIS IS A SCOPED LOGGER");
|
|
76
423
|
|
|
77
424
|
// Efficiently and safely read and mutate the DOM using Dolla's render batching
|
|
78
|
-
Dolla.
|
|
425
|
+
Dolla.batch.read(() => {
|
|
79
426
|
// Reference DOM nodes
|
|
80
427
|
});
|
|
81
|
-
Dolla.
|
|
428
|
+
Dolla.batch.write(() => {
|
|
82
429
|
// Mutate the DOM as part of Dolla's next batch
|
|
83
430
|
}, "some-key");
|
|
84
431
|
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Splitting
|
|
2
|
+
|
|
3
|
+
Thinking again of splitting this out into multiple libraries. Or at least having the base signals+markup be its own standalone thing that the rest of the framework is built on.
|
|
4
|
+
|
|
5
|
+
This implementation of signals + templates would be useful for web components.
|