@manyducks.co/dolla 2.0.0-alpha.6 → 2.0.0-alpha.60

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.
Files changed (104) hide show
  1. package/README.md +86 -591
  2. package/dist/core/context.d.ts +144 -0
  3. package/dist/core/env.d.ts +3 -0
  4. package/dist/core/index.d.ts +22 -0
  5. package/dist/core/logger.d.ts +42 -0
  6. package/dist/core/markup.d.ts +125 -0
  7. package/dist/core/mount.d.ts +15 -0
  8. package/dist/core/nodes/dom.d.ts +14 -0
  9. package/dist/core/nodes/dynamic.d.ts +29 -0
  10. package/dist/core/nodes/html.d.ts +24 -0
  11. package/dist/core/nodes/portal.d.ts +19 -0
  12. package/dist/core/nodes/repeat.d.ts +25 -0
  13. package/dist/core/nodes/view.d.ts +24 -0
  14. package/dist/core/ref.d.ts +18 -0
  15. package/dist/core/signals.d.ts +43 -0
  16. package/dist/core/symbols.d.ts +2 -0
  17. package/dist/{views → core/views}/default-crash-view.d.ts +6 -2
  18. package/dist/core/views/fragment.d.ts +7 -0
  19. package/dist/fragment-BahD_BJA.js +7 -0
  20. package/dist/fragment-BahD_BJA.js.map +1 -0
  21. package/dist/{modules/http.d.ts → http/index.d.ts} +3 -5
  22. package/dist/http.js +163 -0
  23. package/dist/http.js.map +1 -0
  24. package/dist/i18n/index.d.ts +134 -0
  25. package/dist/i18n.js +318 -0
  26. package/dist/i18n.js.map +1 -0
  27. package/dist/index.js +105 -1385
  28. package/dist/index.js.map +1 -1
  29. package/dist/jsx-dev-runtime.d.ts +3 -2
  30. package/dist/jsx-dev-runtime.js +5 -12
  31. package/dist/jsx-dev-runtime.js.map +1 -1
  32. package/dist/jsx-runtime.d.ts +4 -3
  33. package/dist/jsx-runtime.js +9 -15
  34. package/dist/jsx-runtime.js.map +1 -1
  35. package/dist/logger-MPwl-Xqu.js +524 -0
  36. package/dist/logger-MPwl-Xqu.js.map +1 -0
  37. package/dist/markup-BGlfQYQk.js +996 -0
  38. package/dist/markup-BGlfQYQk.js.map +1 -0
  39. package/dist/router/index.d.ts +2 -0
  40. package/dist/router/router.d.ts +149 -0
  41. package/dist/{routing.d.ts → router/router.utils.d.ts} +17 -3
  42. package/dist/router/router.utils.test.d.ts +1 -0
  43. package/dist/router-BpuJQ6OA.js +516 -0
  44. package/dist/router-BpuJQ6OA.js.map +1 -0
  45. package/dist/router.js +8 -0
  46. package/dist/router.js.map +1 -0
  47. package/dist/typeChecking-CbltMOUt.js +71 -0
  48. package/dist/typeChecking-CbltMOUt.js.map +1 -0
  49. package/dist/typeChecking.d.ts +2 -98
  50. package/dist/typeChecking.test.d.ts +1 -0
  51. package/dist/types.d.ts +98 -25
  52. package/dist/utils.d.ts +20 -3
  53. package/docs/http.md +29 -0
  54. package/docs/i18n.md +43 -0
  55. package/docs/index.md +10 -0
  56. package/docs/markup.md +16 -0
  57. package/docs/mixins.md +32 -0
  58. package/docs/ref.md +93 -0
  59. package/docs/router.md +80 -0
  60. package/docs/setup.md +31 -0
  61. package/docs/signals.md +166 -0
  62. package/docs/state.md +141 -0
  63. package/docs/stores.md +62 -0
  64. package/docs/views.md +208 -0
  65. package/examples/webcomponent/index.html +14 -0
  66. package/examples/webcomponent/main.js +165 -0
  67. package/index.d.ts +2 -2
  68. package/notes/TODO.md +6 -0
  69. package/notes/atomic.md +452 -0
  70. package/notes/context-routes.md +61 -0
  71. package/notes/custom-nodes.md +17 -0
  72. package/notes/effection-idea.md +34 -0
  73. package/notes/elimination.md +33 -0
  74. package/notes/mixins.md +22 -0
  75. package/notes/molecule.md +35 -0
  76. package/notes/observable.md +180 -0
  77. package/notes/readme-scratch.md +45 -7
  78. package/notes/route-middleware.md +42 -0
  79. package/notes/scratch.md +353 -6
  80. package/notes/splitting.md +5 -0
  81. package/notes/stores.md +79 -0
  82. package/package.json +27 -12
  83. package/vite.config.js +5 -11
  84. package/build.js +0 -34
  85. package/dist/index.d.ts +0 -21
  86. package/dist/markup.d.ts +0 -100
  87. package/dist/modules/dolla.d.ts +0 -111
  88. package/dist/modules/language.d.ts +0 -41
  89. package/dist/modules/render.d.ts +0 -17
  90. package/dist/modules/router.d.ts +0 -152
  91. package/dist/nodes/cond.d.ts +0 -26
  92. package/dist/nodes/html.d.ts +0 -31
  93. package/dist/nodes/observer.d.ts +0 -29
  94. package/dist/nodes/outlet.d.ts +0 -22
  95. package/dist/nodes/portal.d.ts +0 -19
  96. package/dist/nodes/repeat.d.ts +0 -34
  97. package/dist/nodes/text.d.ts +0 -19
  98. package/dist/passthrough-CW8Ezjg-.js +0 -1244
  99. package/dist/passthrough-CW8Ezjg-.js.map +0 -1
  100. package/dist/state.d.ts +0 -101
  101. package/dist/view.d.ts +0 -50
  102. package/dist/views/passthrough.d.ts +0 -5
  103. package/tests/state.test.js +0 -135
  104. /package/dist/{routing.test.d.ts → core/signals.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>`.
@@ -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 Dolla, { createState, derive } from "@manyducks.co/dolla";
24
+ import { mount, state, derive, batch } from "@manyducks.co/dolla";
9
25
 
10
26
  function ExampleView(props, ctx) {
11
- const [$count, setCount] = createState(5);
12
- const $doubled = derive([$count], (n) => n * 2);
27
+ // Signals: state, derive, effect and batch
13
28
 
14
- ctx.watch([$count], (count) => {
15
- ctx.log("value of count is now %n", count);
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
- return <p>{$count}</p>;
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
- Dolla.mount(document.body, ExampleView);
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.language.setup({
15
- initialLanguage: Dolla.language.detect({ fallback: "ja" }), // Detect user's language and fall back to passed value
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.language.$current
30
- Dolla.language.t$()
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.render.read(() => {
425
+ Dolla.batch.read(() => {
79
426
  // Reference DOM nodes
80
427
  });
81
- Dolla.render.update(() => {
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.