@manyducks.co/dolla 2.0.0-alpha.3 → 2.0.0-alpha.31
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 -951
- package/dist/core/batch.d.ts +17 -0
- package/dist/core/context.d.ts +94 -0
- package/dist/core/dolla.d.ts +161 -0
- package/dist/core/markup.d.ts +91 -0
- package/dist/core/nodes/dom.d.ts +13 -0
- package/dist/core/nodes/html.d.ts +39 -0
- package/dist/core/nodes/observer.d.ts +30 -0
- package/dist/core/nodes/outlet.d.ts +19 -0
- package/dist/core/nodes/portal.d.ts +22 -0
- package/dist/core/nodes/repeat.d.ts +36 -0
- package/dist/core/nodes/view.d.ts +92 -0
- package/dist/core/ref.d.ts +29 -0
- package/dist/core/state.d.ts +126 -0
- package/dist/core/stats.d.ts +31 -0
- package/dist/core/store.d.ts +62 -0
- package/dist/core/symbols.d.ts +7 -0
- package/dist/index.d.ts +18 -11
- package/dist/index.js +1157 -1159
- 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-D1i09ddt.js +1563 -0
- package/dist/markup-D1i09ddt.js.map +1 -0
- package/dist/modules/http.d.ts +5 -5
- package/dist/modules/i18n.d.ts +129 -0
- package/dist/modules/router.d.ts +37 -48
- package/dist/typeChecking.d.ts +2 -2
- package/dist/types.d.ts +12 -13
- package/dist/utils.d.ts +14 -2
- package/dist/views/default-crash-view.d.ts +1 -1
- package/dist/views/passthrough.d.ts +2 -2
- package/docs/http.md +29 -0
- package/docs/i18n.md +38 -0
- package/docs/index.md +10 -0
- package/docs/router.md +77 -0
- package/docs/setup.md +31 -0
- package/docs/state.md +141 -0
- package/docs/stores.md +62 -0
- package/docs/views.md +308 -0
- package/index.d.ts +2 -2
- package/notes/TODO.md +6 -0
- package/notes/context-vars.md +21 -0
- package/notes/readme-scratch.md +222 -0
- package/notes/route-middleware.md +42 -0
- package/notes/scratch.md +195 -7
- package/notes/stores.md +73 -0
- package/package.json +12 -14
- package/tests/{signals.test.js → state.test.js} +6 -6
- package/vite.config.js +0 -10
- 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/nodes/cond.d.ts +0 -26
- package/dist/nodes/html.d.ts +0 -26
- 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-DrtCifRF.js +0 -1228
- package/dist/passthrough-DrtCifRF.js.map +0 -1
- package/dist/signals.d.ts +0 -101
- package/dist/view.d.ts +0 -50
- /package/dist/{routing.d.ts → modules/router.utils.d.ts} +0 -0
- /package/dist/{routing.test.d.ts → modules/router.utils.test.d.ts} +0 -0
package/docs/stores.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Stores
|
|
2
|
+
|
|
3
|
+
> TODO: Write about stores
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import Dolla, { createStore, createState, createView } from "@manyducks.co/dolla";
|
|
7
|
+
|
|
8
|
+
const CounterStore = createStore(function (initialValue: number) {
|
|
9
|
+
const [$count, setCount] = createState(initialValue);
|
|
10
|
+
|
|
11
|
+
// Respond to context events which bubble up from views.
|
|
12
|
+
this.on("counter:increment", () => {
|
|
13
|
+
setCount((count) => count + 1);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
this.on("counter:decrement", () => {
|
|
17
|
+
setCount((count) => count - 1);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
this.on("counter:reset", () => {
|
|
21
|
+
setCount(0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return $count;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Stores can be attached to the app itself.
|
|
28
|
+
// createStore returns a factory function that is called with the store's options to create a new instance.
|
|
29
|
+
Dolla.attachStore(CounterStore(0));
|
|
30
|
+
|
|
31
|
+
const CounterView = createView(function () {
|
|
32
|
+
// Store instances can also be attached at the view level to provide them to the current scope and those of child views.
|
|
33
|
+
// Views that are not children of this CounterView will not be able to access this particular instance of CounterStore.
|
|
34
|
+
this.attachStore(CounterStore(0));
|
|
35
|
+
|
|
36
|
+
// Store return values can be accessed with `useStore`.
|
|
37
|
+
// This method will traverse up the view tree to find the nearest attached instance.
|
|
38
|
+
// An error will be thrown if no instances are attached at or above this level in the tree.
|
|
39
|
+
const $count = this.useStore(CounterStore);
|
|
40
|
+
|
|
41
|
+
// The buttons increment the value inside the store by emitting events.
|
|
42
|
+
// Child views at any depth could also emit these events to update the store.
|
|
43
|
+
return (
|
|
44
|
+
<div>
|
|
45
|
+
<p>Clicks: {$count}</p>
|
|
46
|
+
<div>
|
|
47
|
+
<button onClick={() => this.emit("counter:decrement")}>-1</button>
|
|
48
|
+
<button onClick={() => this.emit("counter:reset")}>Reset</button>
|
|
49
|
+
<button onClick={() => this.emit("counter:increment")}>+1</button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
End.
|
|
59
|
+
|
|
60
|
+
- [🗂️ Docs](./index.md)
|
|
61
|
+
- [🏠 README](../README.md)
|
|
62
|
+
- [🦆 That's a lot of ducks.](https://www.manyducks.co)
|
package/docs/views.md
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# Views
|
|
2
|
+
|
|
3
|
+
Views are one of two component types in Dolla. We call them views because they deal specifically with presenting visible things to the user. The other type of component, [Stores](./stores.md), deal with data and events.
|
|
4
|
+
|
|
5
|
+
At its most basic, a view is a function that returns elements.
|
|
6
|
+
|
|
7
|
+
```jsx
|
|
8
|
+
const ExampleView = createView(function () {
|
|
9
|
+
return <h1>Hello World!</h1>;
|
|
10
|
+
});
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## View Props
|
|
14
|
+
|
|
15
|
+
A view function takes a `props` object as its first argument. This object contains all properties passed to the view when it's invoked.
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
const ListItemView = createView(function (props) {
|
|
19
|
+
return <li>{props.label}</li>;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const ListView = createView(function () {
|
|
23
|
+
return (
|
|
24
|
+
<ul>
|
|
25
|
+
<ListItemView label="Squirrel" />
|
|
26
|
+
<ListItemView label="Chipmunk" />
|
|
27
|
+
<ListItemView label="Groundhog" />
|
|
28
|
+
</ul>
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
As you may have guessed, you can pass States as props and slot them in in exactly the same way. This is important because Views do not re-render the way you might expect from other frameworks. Whatever you pass as props is what the View gets for its entire lifecycle.
|
|
34
|
+
|
|
35
|
+
## View Helpers
|
|
36
|
+
|
|
37
|
+
### `cond($condition, whenTruthy, whenFalsy)`
|
|
38
|
+
|
|
39
|
+
The `cond` helper does conditional rendering. When `$condition` is truthy, the second argument is rendered. When `$condition` is falsy the third argument is rendered. Either case can be left null or undefined if you don't want to render something for that condition.
|
|
40
|
+
|
|
41
|
+
```jsx
|
|
42
|
+
const ConditionalListView = createView(function (props) {
|
|
43
|
+
return (
|
|
44
|
+
<div>
|
|
45
|
+
{cond(
|
|
46
|
+
props.$show,
|
|
47
|
+
|
|
48
|
+
// Visible when truthy
|
|
49
|
+
<ul>
|
|
50
|
+
<ListItemView label="Squirrel" />
|
|
51
|
+
<ListItemView label="Chipmunk" />
|
|
52
|
+
<ListItemView label="Groundhog" />
|
|
53
|
+
</ul>,
|
|
54
|
+
|
|
55
|
+
// Visible when falsy
|
|
56
|
+
<span>List is hidden</span>,
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### `repeat($items, keyFn, renderFn)`
|
|
64
|
+
|
|
65
|
+
The `repeat` helper repeats a render function for each item in a list. The `keyFn` takes an item's value and returns a number, string or Symbol that uniquely identifies that list item. If `$items` changes or gets reordered, all rendered items with matching keys will be reused, those no longer in the list will be removed and those that didn't previously have a matching key are created.
|
|
66
|
+
|
|
67
|
+
```jsx
|
|
68
|
+
const RepeatedListView = createView(function () {
|
|
69
|
+
const [$items, setItems] = createState(["Squirrel", "Chipmunk", "Groundhog"]);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<ul>
|
|
73
|
+
{repeat(
|
|
74
|
+
$items,
|
|
75
|
+
(item, index) => item, // Using the string itself as the key
|
|
76
|
+
($item, $index, context) => {
|
|
77
|
+
return <ListItemView label={$item} />;
|
|
78
|
+
},
|
|
79
|
+
)}
|
|
80
|
+
</ul>
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `portal(content, parentNode)`
|
|
86
|
+
|
|
87
|
+
The `portal` helper displays DOM elements from a view as children of a parent element elsewhere in the document. Portals are typically used to display modals and other content that needs to appear at the top level of a document.
|
|
88
|
+
|
|
89
|
+
```jsx
|
|
90
|
+
const PortalView = createView(function () {
|
|
91
|
+
const content = (
|
|
92
|
+
<div class="modal">
|
|
93
|
+
<p>This is a modal.</p>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Content will be appended to `document.body` while this view is connected.
|
|
98
|
+
return portal(document.body, content);
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## View Context
|
|
103
|
+
|
|
104
|
+
A view function takes a context object as its second argument. The context provides a set of functions you can use to respond to lifecycle events, observe dynamic data, print debug messages and display child elements among other things.
|
|
105
|
+
|
|
106
|
+
The context can be accessed in one of two ways; as `this` when you pass a non-arrow function, or as the second parameter passed after the props object.
|
|
107
|
+
|
|
108
|
+
```jsx
|
|
109
|
+
// Option 1: Access through `this`
|
|
110
|
+
const ExampleView = createView(function (props) {
|
|
111
|
+
this.onMount(() => {
|
|
112
|
+
this.log("HELLO!");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return <h1>Hello World!</h1>;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Option 2: Access as second argument (for arrow functions)
|
|
119
|
+
const ExampleView = createView((props, ctx) => {
|
|
120
|
+
ctx.onMount(() => {
|
|
121
|
+
ctx.log("HELLO!");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return <h1>Hello World!</h1>;
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Which one you use is just an aesthetic preference, but I kind of like the classic `function` syntax with `this`.
|
|
129
|
+
|
|
130
|
+
### Printing Debug Messages
|
|
131
|
+
|
|
132
|
+
```jsx
|
|
133
|
+
const ExampleView = createView(function (props) {
|
|
134
|
+
// Set the name of this view's context. Console messages are prefixed with name.
|
|
135
|
+
this.setName("CustomName");
|
|
136
|
+
|
|
137
|
+
// Print messages to the console. These are suppressed by default in the app's "production" mode.
|
|
138
|
+
// You can also change which of these are printed and filter messages from certain contexts in the `createApp` options object.
|
|
139
|
+
this.info("Verbose debugging info that might be useful to know");
|
|
140
|
+
this.log("Standard messages");
|
|
141
|
+
this.warn("Something bad might be happening");
|
|
142
|
+
this.error("Uh oh!");
|
|
143
|
+
|
|
144
|
+
// If you encounter a bad enough situation, you can halt and disconnect the entire app.
|
|
145
|
+
this.crash(new Error("BOOM"));
|
|
146
|
+
|
|
147
|
+
return <h1>Hello World!</h1>;
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Lifecycle Events
|
|
152
|
+
|
|
153
|
+
```jsx
|
|
154
|
+
const ExampleView = createView(function (props) {
|
|
155
|
+
this.beforeMount(() => {
|
|
156
|
+
// Do something before this view's DOM nodes are created.
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
this.onMount(() => {
|
|
160
|
+
// Do something immediately after this view is connected to the DOM.
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
this.beforeUnmount(() => {
|
|
164
|
+
// Do something before removing this view from the DOM.
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
this.onUnmount(() => {
|
|
168
|
+
// Do some cleanup after this view is disconnected from the DOM.
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return <h1>Hello World!</h1>;
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Displaying Children
|
|
176
|
+
|
|
177
|
+
The context has an `outlet` function that can be used to display children at a location of your choosing.
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
const LayoutView = createView(function (props) {
|
|
181
|
+
return (
|
|
182
|
+
<div className="layout">
|
|
183
|
+
<div className="content">{this.outlet()}</div>
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const ExampleView = createView(function () {
|
|
189
|
+
// <h1> and <p> are displayed inside LayoutView's outlet.
|
|
190
|
+
return (
|
|
191
|
+
<LayoutView>
|
|
192
|
+
<h1>Hello</h1>
|
|
193
|
+
<p>This is inside the box.</p>
|
|
194
|
+
</LayoutView>
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Watching States
|
|
200
|
+
|
|
201
|
+
The `watch` function starts observing when the view is connected and stops when disconnected. This takes care of cleaning up watchers so you don't have to worry about memory leaks.
|
|
202
|
+
|
|
203
|
+
```jsx
|
|
204
|
+
const ExampleView = createView(function (props) {
|
|
205
|
+
const [$count, setCount] = createState(0);
|
|
206
|
+
|
|
207
|
+
// This callback will run when any states in the dependency array receive new values.
|
|
208
|
+
this.watch([$count], (count) => {
|
|
209
|
+
this.log("count is now", count);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ...
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Context Variables
|
|
217
|
+
|
|
218
|
+
> TODO: Write about context state (`.get` and `.set`)
|
|
219
|
+
|
|
220
|
+
### Context Events
|
|
221
|
+
|
|
222
|
+
Events can be emitted from views and [stores](./stores.md) using `this.emit(eventName, data)`. Context events will bubble up the view tree just like native browser events bubble up the DOM tree.
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
this.on("eventName", (event) => {
|
|
226
|
+
event.type; // "eventName"
|
|
227
|
+
event.detail; // the value that was passed when the event was emitted (or undefined if none)
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
this.once("eventName", (event) => {
|
|
231
|
+
// Receive only once and then stop listening.
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Remove a listener by reference.
|
|
235
|
+
// Listener must be the same exact function that was passed to `on` or `once`.
|
|
236
|
+
this.off("eventName", listener);
|
|
237
|
+
|
|
238
|
+
// Emit an event.
|
|
239
|
+
this.emit("eventName", { value: "This object will be exposed as event.detail" });
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Bubbling
|
|
243
|
+
|
|
244
|
+
Events bubble up through the view tree unless `stopPropagation` is called by a listener. In the following example we have a view listening for events that are emitted from a child of a child.
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
const ParentView = createView(function () {
|
|
248
|
+
// Listen for greetings that bubble up.
|
|
249
|
+
this.on("greeting", (event) => {
|
|
250
|
+
const { name, message } = event.detail;
|
|
251
|
+
|
|
252
|
+
this.log(`${name} says "${message}"!`);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
<div>
|
|
257
|
+
<ChildView />
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const ChildView = createView(function () {
|
|
263
|
+
this.on("greeting", (event) => {
|
|
264
|
+
// Let's perform some censorship.
|
|
265
|
+
// If propagation is stopped this event will not bubble any further and ParentView won't see it.
|
|
266
|
+
if (containsForbiddenKnowledge(event.message)) {
|
|
267
|
+
event.stopPropagation();
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<div>
|
|
273
|
+
<ChildOfChildView />
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const ChildOfChildView = createView(function () {
|
|
279
|
+
return (
|
|
280
|
+
<form
|
|
281
|
+
onSubmit={(e) => {
|
|
282
|
+
// This is a browser event handler.
|
|
283
|
+
// Browser events can't be listened for with `this.on`, but we can emit context events from here that can be.
|
|
284
|
+
e.preventDefault();
|
|
285
|
+
|
|
286
|
+
// Pluck the values from the form.
|
|
287
|
+
const name = e.currentTarget.name.value;
|
|
288
|
+
const message = e.currentTarget.message.value;
|
|
289
|
+
|
|
290
|
+
// Emit!
|
|
291
|
+
this.emit("greeting", { name, message });
|
|
292
|
+
}}
|
|
293
|
+
>
|
|
294
|
+
<input type="text" name="name" placeholder="Your Name" />
|
|
295
|
+
<input type="text" name="message" placeholder="Your Message" />
|
|
296
|
+
<button type="submit">Submit</button>
|
|
297
|
+
</form>
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
End.
|
|
305
|
+
|
|
306
|
+
- [🗂️ Docs](./index.md)
|
|
307
|
+
- [🏠 README](../README.md)
|
|
308
|
+
- [🦆 That's a lot of ducks.](https://www.manyducks.co)
|
package/index.d.ts
CHANGED
package/notes/TODO.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# TO DO LIST
|
|
2
|
+
|
|
3
|
+
- Combine/refactor very similar Group, Outlet and Observer nodes.
|
|
4
|
+
- Group is simplest and exists to mount an array of MarkupElements as one.
|
|
5
|
+
- Outlet is basically the same as Group but it expects a $children state with an array of MarkupElements.
|
|
6
|
+
- Observer is a generic catch-all that works with a set of states and a render function. The render function can return any kind of Renderable which is then converted into a MarkupElement, but very similar update logic outside of that. Observer uses Group internally.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Idea: Context Variables
|
|
2
|
+
|
|
3
|
+
In designing how Dolla's version of 'context' works, I've been going through a few different ideas. The simplest seems to be the ability to store _context variables_ that, once set, are accessible on the same context or any child context.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
function SomeView(props, ctx) {
|
|
7
|
+
ctx.set("key", 5);
|
|
8
|
+
|
|
9
|
+
// ... and in a child view do
|
|
10
|
+
ctx.get("key");
|
|
11
|
+
// which returns null if the value isn't present.
|
|
12
|
+
// It's like localStorage for the view tree.
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
They can be typed, but always with a possibility to return null.
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
const value = ctx.get<number>("key");
|
|
20
|
+
// value is number | null to force the programmer to check it.
|
|
21
|
+
```
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# README
|
|
2
|
+
|
|
3
|
+
> This note will eventually become the new README. Here I'm laying out my ideal framework API.
|
|
4
|
+
|
|
5
|
+
A basic component.
|
|
6
|
+
|
|
7
|
+
```jsx
|
|
8
|
+
import Dolla, { createState, derive } from "@manyducks.co/dolla";
|
|
9
|
+
|
|
10
|
+
function ExampleView(props, ctx) {
|
|
11
|
+
const [$count, setCount] = createState(5);
|
|
12
|
+
const $doubled = derive([$count], (n) => n * 2);
|
|
13
|
+
|
|
14
|
+
ctx.watch([$count], (count) => {
|
|
15
|
+
ctx.log("value of count is now %n", count);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return <p>{$count}</p>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
Dolla.mount(document.body, ExampleView);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
<details open>
|
|
25
|
+
<summary>
|
|
26
|
+
<h2>Signals API</h2>
|
|
27
|
+
</summary>
|
|
28
|
+
|
|
29
|
+
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.
|
|
30
|
+
|
|
31
|
+
```jsx
|
|
32
|
+
import { createState } from "@manyducks.co/dolla";
|
|
33
|
+
|
|
34
|
+
const [$count, setCount] = createState(256);
|
|
35
|
+
|
|
36
|
+
$count.get(); // 256; returns the current value
|
|
37
|
+
|
|
38
|
+
const stop = $count.watch((value) => {
|
|
39
|
+
// Runs once immediately, then again whenever the value changes.
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
setCount(512); // Update the value of $count. The new value is set and all watchers run synchronously.
|
|
43
|
+
|
|
44
|
+
stop(); // Stop watching for changes.
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
That is the basic signal API. Signals are all about composability. Here are some more advanced ways of working with them:
|
|
48
|
+
|
|
49
|
+
```jsx
|
|
50
|
+
import { createState, toState, valueOf, derive } from "@manyducks.co/dolla";
|
|
51
|
+
|
|
52
|
+
const [$count, setCount] = createState(72);
|
|
53
|
+
|
|
54
|
+
// Returns the value of the signal passed in. If the value is not a signal it is returned as is.
|
|
55
|
+
const count = valueOf($count);
|
|
56
|
+
const bool = valueOf(true);
|
|
57
|
+
|
|
58
|
+
// Creates a signal containing the value passed in. If the value is already a signal it is returned as is.
|
|
59
|
+
const $bool = toState(true);
|
|
60
|
+
const $anotherCount = toState($count);
|
|
61
|
+
|
|
62
|
+
// Derive a new signal from the value of another. Whenever $count changes, $doubled will follow.
|
|
63
|
+
const $doubled = derive([$count], (count) => count * 2);
|
|
64
|
+
|
|
65
|
+
// Derive a new signal from the values of several others. When any value in the list changes, $sum will be recomputed.
|
|
66
|
+
const $sum = derive([$count, $doubled], (count, doubled) => count + doubled);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The API if we call it State instead of Signal to distance from the Signal object in standardization process.
|
|
70
|
+
|
|
71
|
+
```jsx
|
|
72
|
+
import { createState, toState, valueOf, derive } from "@manyducks.co/dolla";
|
|
73
|
+
|
|
74
|
+
const [$count, setCount] = createState(72);
|
|
75
|
+
|
|
76
|
+
// Returns the value of the signal passed in. If the value is not a signal it is returned as is.
|
|
77
|
+
const count = valueOf($count);
|
|
78
|
+
const bool = valueOf(true);
|
|
79
|
+
|
|
80
|
+
// Creates a signal containing the value passed in. If the value is already a signal it is returned as is.
|
|
81
|
+
const $bool = toState(true);
|
|
82
|
+
const $anotherCount = toState($count);
|
|
83
|
+
|
|
84
|
+
// Derive a new signal from the value of another. Whenever $count changes, $doubled will follow.
|
|
85
|
+
const $doubled = derive([$count], (count) => count * 2);
|
|
86
|
+
|
|
87
|
+
// Derive a new signal from the values of several others. When any value in the list changes, $sum will be recomputed.
|
|
88
|
+
const $sum = derive([$count, $doubled], (count, doubled) => count + doubled);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
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.
|
|
92
|
+
|
|
93
|
+
```jsx
|
|
94
|
+
import { createSettableState, fromSettable, toSettable } from "@manyducks.co/dolla";
|
|
95
|
+
|
|
96
|
+
// Settable states have their setter included.
|
|
97
|
+
const $$value = createSettableState("Test");
|
|
98
|
+
$$value.set("New Value");
|
|
99
|
+
|
|
100
|
+
// They can also be split into a State and Setter
|
|
101
|
+
const [$value, setValue] = fromSettableState($$value);
|
|
102
|
+
|
|
103
|
+
// And a State and Setter can be combined into a SettableState.
|
|
104
|
+
const $$otherValue = toSettableState($value, setValue);
|
|
105
|
+
|
|
106
|
+
// Or discard the setter and make it read-only using the good old toState function:
|
|
107
|
+
const $value = toState($$value);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Alternative API
|
|
111
|
+
|
|
112
|
+
```jsx
|
|
113
|
+
import { State } from "@manyducks.co/dolla";
|
|
114
|
+
|
|
115
|
+
const [$count, setCount] = State(72);
|
|
116
|
+
|
|
117
|
+
const count = State.unwrap($count);
|
|
118
|
+
const bool = State.unwrap(true);
|
|
119
|
+
|
|
120
|
+
const $bool = State.wrap(true);
|
|
121
|
+
const $sameCount = State.wrap($count);
|
|
122
|
+
|
|
123
|
+
const $doubled = State.from([$count], (count) => count * 2);
|
|
124
|
+
|
|
125
|
+
const $sum = State.from([$count, $doubled], (count, doubled) => count + doubled);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Yet another
|
|
129
|
+
|
|
130
|
+
```jsx
|
|
131
|
+
import Dolla from "@manyducks.co/dolla";
|
|
132
|
+
|
|
133
|
+
const [$count, setCount] = Dolla.state(72);
|
|
134
|
+
|
|
135
|
+
const count = Dolla.get($count);
|
|
136
|
+
const bool = Dolla.get(true);
|
|
137
|
+
|
|
138
|
+
const $bool = Dolla.toState(true);
|
|
139
|
+
const $sameCount = Dolla.toState($count);
|
|
140
|
+
|
|
141
|
+
const $doubled = Dolla.computed([$count], (count) => count * 2);
|
|
142
|
+
const $sum = Dolla.computed([$count, $doubled], (count, doubled) => count + doubled);
|
|
143
|
+
|
|
144
|
+
// or
|
|
145
|
+
|
|
146
|
+
import { state, computed, get, toState } from "@manyducks.co/dolla";
|
|
147
|
+
|
|
148
|
+
const [$count, setCount] = state(72);
|
|
149
|
+
|
|
150
|
+
const count = get($count);
|
|
151
|
+
const bool = get(true);
|
|
152
|
+
|
|
153
|
+
const $bool = toState(true);
|
|
154
|
+
const $sameCount = toState($count);
|
|
155
|
+
|
|
156
|
+
const $doubled = computed([$count], (count) => count * 2);
|
|
157
|
+
const $sum = computed([$count, $doubled], (count, doubled) => count + doubled);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Settable signals:
|
|
161
|
+
|
|
162
|
+
```jsx
|
|
163
|
+
import { createSettableState, createSetter, toSettableSignal, fromSettableSignal } from "@manyducks.co/dolla";
|
|
164
|
+
|
|
165
|
+
// Create a SettableSignal, which is basically a signal and its setter combined into a single object.
|
|
166
|
+
const $$settable = createSettableState("Example");
|
|
167
|
+
|
|
168
|
+
// The basic API is identical...
|
|
169
|
+
$$settable.get();
|
|
170
|
+
const stop = $$settable.watch((value) => {
|
|
171
|
+
// ...
|
|
172
|
+
});
|
|
173
|
+
stop();
|
|
174
|
+
|
|
175
|
+
// ... except for the addition of a setter.
|
|
176
|
+
$$settable.set("Set me directly");
|
|
177
|
+
|
|
178
|
+
// When you already have a signal and a setter, they can be combined into one.
|
|
179
|
+
const $$count = toSettableSignal($count, setCount);
|
|
180
|
+
|
|
181
|
+
// This updates the original $signal value.
|
|
182
|
+
$$count.set(386);
|
|
183
|
+
|
|
184
|
+
// TODO: You can also split a SettableSignal into a signal and its setter.
|
|
185
|
+
const [$readable, setReadable] = fromSettableSignal($$settable);
|
|
186
|
+
|
|
187
|
+
// Create a custom setter. Calling this will cap the value to 100.
|
|
188
|
+
const setCountBounded = createSetter($count, (next, current) => {
|
|
189
|
+
return Math.min(100, next);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
setCountBounded((current) => {
|
|
193
|
+
return current + 1;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Or make a proxy $$doubled -- but would you actually want to proxy things like this?
|
|
197
|
+
const [$count, setCount] = createState(5);
|
|
198
|
+
const $doubled = derive([$count], (count) => count * 2);
|
|
199
|
+
const $$doubled = toSettableSignal(
|
|
200
|
+
$doubled,
|
|
201
|
+
createSetter($doubled, (next, current) => {
|
|
202
|
+
setCount(next * 2);
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
I'm not really sure we need all of this. On the chopping block:
|
|
208
|
+
|
|
209
|
+
- The entire concept of settable signals
|
|
210
|
+
- `createSettableState`
|
|
211
|
+
- `toSettableSignal`
|
|
212
|
+
- `fromSettableSignal`
|
|
213
|
+
- `createSetter`
|
|
214
|
+
|
|
215
|
+
This makes the entire API just four functions:
|
|
216
|
+
|
|
217
|
+
- `createState`
|
|
218
|
+
- `derive`
|
|
219
|
+
- `toState`
|
|
220
|
+
- `valueOf`
|
|
221
|
+
|
|
222
|
+
</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
|
+
```
|