@manyducks.co/dolla 2.0.0-alpha.9 → 3.0.0
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 +132 -573
- package/dist/core/context.d.ts +23 -0
- package/dist/core/debug.d.ts +19 -0
- package/dist/core/index.d.ts +24 -0
- package/dist/core/markup/helpers.d.ts +34 -0
- package/dist/core/markup/html.d.ts +3 -0
- package/dist/core/markup/html.test.d.ts +1 -0
- package/dist/core/markup/nodes/dom.d.ts +14 -0
- package/dist/core/markup/nodes/dynamic.d.ts +16 -0
- package/dist/core/markup/nodes/element.d.ts +14 -0
- package/dist/core/markup/nodes/portal.d.ts +15 -0
- package/dist/core/markup/nodes/repeat.d.ts +21 -0
- package/dist/core/markup/nodes/view.d.ts +17 -0
- package/dist/core/markup/scheduler.d.ts +1 -0
- package/dist/core/markup/types.d.ts +62 -0
- package/dist/core/markup/utils.d.ts +22 -0
- package/dist/core/markup/utils.test.d.ts +1 -0
- package/dist/core/ref.d.ts +13 -0
- package/dist/core/root.d.ts +36 -0
- package/dist/core/signals.d.ts +70 -0
- package/dist/core/signals.test.d.ts +1 -0
- package/dist/core/symbols.d.ts +2 -0
- package/dist/core-BLkJ-xuh.js +242 -0
- package/dist/core-BLkJ-xuh.js.map +1 -0
- package/dist/http/index.d.ts +43 -0
- package/dist/http.js +90 -0
- package/dist/http.js.map +1 -0
- package/dist/index.js +4 -1428
- package/dist/jsx-dev-runtime.d.ts +4 -2
- package/dist/jsx-dev-runtime.js +12 -16
- package/dist/jsx-dev-runtime.js.map +1 -1
- package/dist/jsx-runtime.d.ts +5 -3
- package/dist/jsx-runtime.js +17 -18
- package/dist/jsx-runtime.js.map +1 -1
- package/dist/router/index.d.ts +4 -0
- package/dist/router/matcher.test.d.ts +1 -0
- package/dist/router/router.d.ts +23 -0
- package/dist/router/router.test.d.ts +1 -0
- package/dist/router/store.d.ts +12 -0
- package/dist/router/types.d.ts +152 -0
- package/dist/router/utils.d.ts +99 -0
- package/dist/router/utils.test.d.ts +1 -0
- package/dist/router.js +429 -0
- package/dist/router.js.map +1 -0
- package/dist/signals-CMJPGr_M.js +354 -0
- package/dist/signals-CMJPGr_M.js.map +1 -0
- package/dist/translate/index.d.ts +82 -0
- package/dist/translate.js +125 -0
- package/dist/translate.js.map +1 -0
- package/dist/types.d.ts +83 -29
- package/dist/utils.d.ts +46 -12
- package/dist/utils.test.d.ts +1 -0
- package/dist/view-cBN-hn_T.js +360 -0
- package/dist/view-cBN-hn_T.js.map +1 -0
- package/dist/virtual/index.d.ts +1 -0
- package/dist/virtual/list.d.ts +53 -0
- package/index.d.ts +2 -2
- package/package.json +34 -17
- package/build.js +0 -34
- package/dist/index.d.ts +0 -21
- package/dist/index.js.map +0 -1
- package/dist/markup.d.ts +0 -108
- package/dist/modules/dolla.d.ts +0 -111
- package/dist/modules/http.d.ts +0 -57
- package/dist/modules/i18n.d.ts +0 -59
- 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-9kwwjgWk.js +0 -1279
- package/dist/passthrough-9kwwjgWk.js.map +0 -1
- package/dist/routing.d.ts +0 -79
- package/dist/state.d.ts +0 -101
- package/dist/typeChecking.d.ts +0 -191
- package/dist/view.d.ts +0 -65
- package/dist/views/default-crash-view.d.ts +0 -18
- package/dist/views/passthrough.d.ts +0 -5
- package/notes/context-vars.md +0 -21
- package/notes/readme-scratch.md +0 -222
- package/notes/route-middleware.md +0 -42
- package/notes/scratch.md +0 -233
- package/notes/views.md +0 -195
- package/tests/state.test.js +0 -135
- package/vite.config.js +0 -28
- /package/dist/{routing.test.d.ts → core/context.test.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,646 +1,205 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 💲 @manyducks.co/dolla
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|

|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Dolla is a research framework for trying out ideas. The goal is to create a full-featured framework that, first and foremost, provides the best developer experience possible out of the box. Low resource usage and small code size are secondary objectives. It's more than a toy and less than a production-ready workhorse. It's a labor of love. Use at your own joy and peril.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
- 🚥 [**Signals**](./docs/reactivity.md) for pinpoint DOM updates.
|
|
9
|
+
- 📦 Reusable components in two types:
|
|
10
|
+
- 🖥️ **Views** for reusable UI elements.
|
|
11
|
+
- 💾 **Stores** for sharing state across many views.
|
|
12
|
+
- 🔀 A client-side [**router**](./src/router/README.md) with nested routes, auth guards, async data loading and more.
|
|
13
|
+
- 📍 A simple [**i18n system**](./src/translate/README.md). Put your translated strings in a JSON file and access them with the `t` function in your views.
|
|
14
|
+
- 🍳 The build step is optional. You can use a bundler (like Vite) and [write JSX](./docs/jsx.md), or skip the build step and use `html` tagged templates.
|
|
9
15
|
|
|
10
|
-
|
|
11
|
-
- 📦 Reusable components with [Views](#section-views).
|
|
12
|
-
- 🔀 Built in [routing]() with nested routes and middleware support (check login status, preload data, etc).
|
|
13
|
-
- 🐕 Built in [HTTP]() client with middleware support (set auth headers, etc).
|
|
14
|
-
- 📍 Built in [localization]() system (store translated strings in JSON files and call the `t` function to get them).
|
|
15
|
-
- 🍳 Build system optional. Write views in JSX or use `html` tagged template literals.
|
|
16
|
+
## Shut up and show me
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
## State
|
|
20
|
-
|
|
21
|
-
### Basic State API
|
|
22
|
-
|
|
23
|
-
```jsx
|
|
24
|
-
import { createState, toState, valueOf, derive } from "@manyducks.co/dolla";
|
|
25
|
-
|
|
26
|
-
const [$count, setCount] = createState(72);
|
|
27
|
-
|
|
28
|
-
// Get value
|
|
29
|
-
$count.get(): // 72
|
|
30
|
-
|
|
31
|
-
// Replace the stored value with something else
|
|
32
|
-
setCount(300);
|
|
33
|
-
$count.get(); // 300
|
|
34
|
-
|
|
35
|
-
// You can also pass a function that takes the current value and returns a new one
|
|
36
|
-
setCount((current) => current + 1);
|
|
37
|
-
$count.get(); // 301
|
|
38
|
-
|
|
39
|
-
// Watch for changes to the value
|
|
40
|
-
const unwatch = $count.watch((value) => {
|
|
41
|
-
// This function is called immediately with the current value, then again each time the value changes.
|
|
42
|
-
});
|
|
43
|
-
unwatch(); // Stop watching for changes
|
|
44
|
-
|
|
45
|
-
// Returns the value of a state. If the value is not a state it is returned as is.
|
|
46
|
-
const count = valueOf($count);
|
|
47
|
-
const bool = valueOf(true);
|
|
48
|
-
|
|
49
|
-
// Creates a state from a value. If the value is already a state it is returned as is.
|
|
50
|
-
const $bool = toState(true);
|
|
51
|
-
const $anotherCount = toState($count);
|
|
52
|
-
|
|
53
|
-
// Derive a new state from one or more other states. Whenever $count changes, $doubled will follow.
|
|
54
|
-
const $doubled = derive([$count], (count) => count * 2);
|
|
55
|
-
const $sum = derive([$count, $doubled], (count, doubled) => count + doubled);
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
States also come in a settable variety that includes the setter on the same object. Sometimes you want to pass around a two-way binding and this is what SettableState is for.
|
|
18
|
+
Here's an app that displays "Hello World" or "Goodbye World" and a button to toggle which message is displayed.
|
|
59
19
|
|
|
60
20
|
```jsx
|
|
61
|
-
import {
|
|
21
|
+
import { html, createAtom, createRoot, compose } from "@manyducks.co/dolla";
|
|
62
22
|
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
$$value.set("New Value");
|
|
23
|
+
function Hello() {
|
|
24
|
+
const [value, setValue] = createAtom(false);
|
|
66
25
|
|
|
67
|
-
|
|
68
|
-
const [$value, setValue] = fromSettableState($$value);
|
|
26
|
+
const word = compose(() => (value() ? "Hello" : "Goodbye"));
|
|
69
27
|
|
|
70
|
-
|
|
71
|
-
|
|
28
|
+
return html`
|
|
29
|
+
<p>${word} World</p>
|
|
30
|
+
<button onClick=${() => setValue((current) => !current)}>Toggle</button>
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
72
33
|
|
|
73
|
-
|
|
74
|
-
const $value = toState($$value);
|
|
34
|
+
createRoot(document.body).mount(Hello);
|
|
75
35
|
```
|
|
76
36
|
|
|
77
|
-
|
|
37
|
+
And here's a counter with a lot more going on, plus some comments to explain what's happening.
|
|
78
38
|
|
|
79
39
|
```jsx
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
```js
|
|
108
|
-
import Dolla, { createState, html } from "@manyducks.co/dolla";
|
|
109
|
-
|
|
110
|
-
function Counter(props, ctx) {
|
|
111
|
-
const [$count, setCount] = createState(0);
|
|
40
|
+
import { html, createAtom, createRoot, onMount, onCleanup, onEffect, showIf } from "@manyducks.co/dolla";
|
|
41
|
+
|
|
42
|
+
function Counter() {
|
|
43
|
+
// An atom is the basic building block of dynamic state.
|
|
44
|
+
// It consists of a getter function and a setter function, returned as a tuple:
|
|
45
|
+
const [count, setCount] = createAtom(0);
|
|
46
|
+
|
|
47
|
+
// Atoms can be composed to derive state from one or more other states.
|
|
48
|
+
// Composed states update automatically with the values of any atoms they call.
|
|
49
|
+
const isALot = compose(() => count() > 100);
|
|
50
|
+
|
|
51
|
+
// Composed states are lazy-computed if dependencies have changed.
|
|
52
|
+
isALot(); // computes the value; returns false
|
|
53
|
+
isALot(); // returns cached value (count has not changed)
|
|
54
|
+
isALot(); // returns cached value (count has not changed)
|
|
55
|
+
|
|
56
|
+
// Hooks can bind logic to the component lifecycle or store and access data on the context.
|
|
57
|
+
// They always take the Context object as a first argument by convention.
|
|
58
|
+
onMount(this, () => {
|
|
59
|
+
console.log("I'll be called when Counter is on the page");
|
|
60
|
+
|
|
61
|
+
// You can call hooks wherever and whenever you want as long as you have a Context object to pass.
|
|
62
|
+
onCleanup(this, () => {
|
|
63
|
+
console.log("I'll be called when Counter is no longer on the page");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
112
66
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
67
|
+
// Effects run side-effect code in response to state changes.
|
|
68
|
+
// Just like `compose`, the effect tracks getters called within and re-runs when values change.
|
|
69
|
+
onEffect(this, () => {
|
|
70
|
+
console.log("count has changed:", count());
|
|
71
|
+
});
|
|
116
72
|
|
|
73
|
+
// Getters can be dropped into the DOM where dynamic values are needed,
|
|
74
|
+
// either as children or as HTML attributes. DOM nodes will update in sync with state changes.
|
|
117
75
|
return html`
|
|
118
76
|
<div>
|
|
119
|
-
<p>
|
|
120
|
-
|
|
77
|
+
<p>Count: ${count}</p>
|
|
78
|
+
|
|
79
|
+
<button disabled=${isALot} onClick=${() => setCount((current) => current + 1)}>Increment</button>
|
|
80
|
+
|
|
81
|
+
${showIf(isALot, html`<p>That's a lot!</p>`)}
|
|
121
82
|
</div>
|
|
122
83
|
`;
|
|
84
|
+
// ^ You can use view helpers like `showIf`, `hideIf` and `forEach` for control flow in templates.
|
|
123
85
|
}
|
|
124
86
|
|
|
125
|
-
|
|
87
|
+
// A root will create and mount an instance of a view onto a DOM node.
|
|
88
|
+
createRoot(document.body).mount(Counter);
|
|
126
89
|
```
|
|
127
90
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
The biggest difference is that the Counter function runs only once when the component is mounted. All updates after that point are a direct result of `$count` being updated.
|
|
91
|
+
A few points to notice:
|
|
131
92
|
|
|
132
|
-
|
|
93
|
+
- The component function runs only once when the component is initialized (no re-renders).
|
|
94
|
+
- _All_ changes at runtime are a result of atoms being set.
|
|
95
|
+
- All DOM updates are synchronous with state changes. You can use [`batch`](./docs/reactivity.md) to process several changes as one.
|
|
96
|
+
- Getters are tracked in `compose` and `createEffect`/`onEffect` callbacks. You can use [`peek`](./docs/reactivity.md) to opt-out of tracking.
|
|
133
97
|
|
|
134
|
-
|
|
98
|
+
## Dependent data flow
|
|
135
99
|
|
|
136
|
-
|
|
100
|
+
Apps are built by composing atoms into ever more complex data structures, eventually attaching the fingers of the monstrosity to some switches and levers that can manipulate DOM nodes.
|
|
137
101
|
|
|
138
|
-
|
|
102
|
+
Imagine your state as a hamster. Your job is to create a mech suit around the hamster so when he twitches his paw his 10 ton fist takes a chunk out of a mountainside. This is no ordinary hamster; it's your app's state, and the mountainside is the DOM. And the mech suit is your code. Built out of Dolla parts. Yes.
|
|
139
103
|
|
|
140
|
-
|
|
104
|
+
Now that everything is clear, here's what that looks like in practice. Only a single number changes, but three values are displayed in sync with the original number. A few small state changes cause large changes visible to the user. It's easy to understand what data is important.
|
|
141
105
|
|
|
142
106
|
```tsx
|
|
143
|
-
import {
|
|
144
|
-
|
|
145
|
-
type HeadingProps = {
|
|
146
|
-
$text: State<string>;
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
function Heading(props: HeadingProps, c: Context) {
|
|
150
|
-
return html`<h1>${props.$text}</h1>`;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function Layout() {
|
|
154
|
-
const [$text, setText] = signal("HELLO THERE!");
|
|
155
|
-
|
|
156
|
-
return (
|
|
157
|
-
<section>
|
|
158
|
-
<Heading $text={$text}>
|
|
159
|
-
</section>
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
### Context
|
|
165
|
-
|
|
166
|
-
```tsx
|
|
167
|
-
import { type State, type Context, html } from "@manyducks.co/dolla";
|
|
168
|
-
|
|
169
|
-
type HeadingProps = {
|
|
170
|
-
$text: State<string>;
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
function Heading(props: HeadingProps, c: Context) {
|
|
174
|
-
// A full compliment of logging functions:
|
|
175
|
-
// Log levels that get printed can be set at the app level.
|
|
176
|
-
|
|
177
|
-
c.trace("What's going on? Let's find out.");
|
|
178
|
-
c.info("This is low priority info.");
|
|
179
|
-
c.log("This is normal priority info.");
|
|
180
|
-
c.warn("Hey! This could be serious.");
|
|
181
|
-
c.error("NOT GOOD! DEFINITELY NOT GOOD!!1");
|
|
107
|
+
import { html, createRoot, createAtom, compose } from "@manyducks.co/dolla";
|
|
182
108
|
|
|
183
|
-
|
|
184
|
-
|
|
109
|
+
function Converter() {
|
|
110
|
+
// Just one source value that changes.
|
|
111
|
+
const [celsius, setCelsius] = createAtom(0);
|
|
185
112
|
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
// c.info("Heading is going to be mounted. Good time to set things up.");
|
|
190
|
-
// });
|
|
191
|
-
|
|
192
|
-
c.onMount(() => {
|
|
193
|
-
c.info("Heading has just been mounted. Good time to access the DOM and finalize setup.");
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
// c.beforeUnmount(() => {
|
|
197
|
-
// c.info("Heading is going to be unmounted. Good time to begin teardown.");
|
|
198
|
-
// });
|
|
199
|
-
|
|
200
|
-
c.onUnmount(() => {
|
|
201
|
-
c.info("Heading has just been unmounted. Good time to finalize teardown.");
|
|
113
|
+
// Depends on `celsius`; updates when `celsius` updates.
|
|
114
|
+
const fahrenheit = compose(() => {
|
|
115
|
+
return (celsius() * 9) / 5 + 32;
|
|
202
116
|
});
|
|
203
117
|
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
118
|
+
// Depends on `fahrenheit`; updates when `fahrenheit` updates.
|
|
119
|
+
const description = compose(() => {
|
|
120
|
+
const f = fahrenheit();
|
|
121
|
+
if (f <= 32) return "Freezing ❄️";
|
|
122
|
+
if (f >= 90) return "Hot! 🔥";
|
|
123
|
+
return "Moderate 🌤️";
|
|
209
124
|
});
|
|
210
125
|
|
|
211
|
-
return html`<h1>${props.$text}</h1>`;
|
|
212
|
-
}
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
## Signals
|
|
216
|
-
|
|
217
|
-
Basics
|
|
218
|
-
|
|
219
|
-
```jsx
|
|
220
|
-
const [$count, setCount] = signal(0);
|
|
221
|
-
|
|
222
|
-
// Set the value directly.
|
|
223
|
-
setCount(1);
|
|
224
|
-
setCount(2);
|
|
225
|
-
|
|
226
|
-
// Transform the previous value into a new one.
|
|
227
|
-
setCount((current) => current + 1);
|
|
228
|
-
|
|
229
|
-
// This can be used to create easy helper functions:
|
|
230
|
-
function increment(amount = 1) {
|
|
231
|
-
setCount((current) => current + amount);
|
|
232
|
-
}
|
|
233
|
-
increment();
|
|
234
|
-
increment(5);
|
|
235
|
-
increment(-362);
|
|
236
|
-
|
|
237
|
-
// Get the current value
|
|
238
|
-
$count.get(); // -354
|
|
239
|
-
|
|
240
|
-
// Watch for new values. Don't forget to call stop() to clean up!
|
|
241
|
-
const stop = $count.watch((current) => {
|
|
242
|
-
console.log(`count is now ${current}`);
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
increment(); // "count is now -353"
|
|
246
|
-
increment(); // "count is now -352"
|
|
247
|
-
|
|
248
|
-
stop();
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
Derive
|
|
252
|
-
|
|
253
|
-
```jsx
|
|
254
|
-
import { signal, derive } from "@manyducks.co/dolla";
|
|
255
|
-
|
|
256
|
-
const [$names, setNames] = signal(["Morg", "Ton", "Bon"]);
|
|
257
|
-
const [$index, setIndex] = signal(0);
|
|
258
|
-
|
|
259
|
-
// Create a new signal that depends on two existing signals:
|
|
260
|
-
const $selected = derive([$names, $index], (names, index) => names[index]);
|
|
261
|
-
|
|
262
|
-
$selected.get(); // "Morg"
|
|
263
|
-
|
|
264
|
-
setIndex(2);
|
|
265
|
-
|
|
266
|
-
$selected.get(); // "Bon"
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
Proxy
|
|
270
|
-
|
|
271
|
-
```jsx
|
|
272
|
-
import { createState, createProxyState } from "@manyducks.co/dolla";
|
|
273
|
-
|
|
274
|
-
const [$names, setNames] = createState(["Morg", "Ton", "Bon"]);
|
|
275
|
-
const [$index, setIndex] = createState(0);
|
|
276
|
-
|
|
277
|
-
const [$selected, setSelected] = createProxyState([$names, $index], {
|
|
278
|
-
get(names, index) {
|
|
279
|
-
return names[index];
|
|
280
|
-
},
|
|
281
|
-
set(next, names, _) {
|
|
282
|
-
const index = names.indexOf(next);
|
|
283
|
-
if (index === -1) {
|
|
284
|
-
throw new Error("Name is not in the list!");
|
|
285
|
-
}
|
|
286
|
-
setIndex(index);
|
|
287
|
-
},
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
$selected.get(); // "Morg"
|
|
291
|
-
$index.get(); // 0
|
|
292
|
-
|
|
293
|
-
// Set selected directly by name through the proxy.
|
|
294
|
-
setSelected("Ton");
|
|
295
|
-
|
|
296
|
-
// Selected and the index have been updated to match.
|
|
297
|
-
$selected.get(); // "Ton"
|
|
298
|
-
$index.get(); // 1
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
## Views
|
|
302
|
-
|
|
303
|
-
Views are what most frameworks would call Components. Dolla calls them Views because they deal specifically with stuff the user sees, and because Dolla also has another type of component called Stores that share data between views. We will get into those later.
|
|
304
|
-
|
|
305
|
-
At its most basic, a view is a function that returns elements.
|
|
306
|
-
|
|
307
|
-
```jsx
|
|
308
|
-
function ExampleView() {
|
|
309
|
-
return <h1>Hello World!</h1>;
|
|
310
|
-
}
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
#### View Props
|
|
314
|
-
|
|
315
|
-
A view function takes a `props` object as its first argument. This object contains all properties passed to the view when it's invoked.
|
|
316
|
-
|
|
317
|
-
```js
|
|
318
|
-
import { html } from "@manyducks.co/dolla";
|
|
319
|
-
|
|
320
|
-
function ListView(props, ctx) {
|
|
321
126
|
return html`
|
|
322
|
-
<ul>
|
|
323
|
-
<${ListItemView} label="Squirrel" />
|
|
324
|
-
<${ListItemView} label="Chipmunk" />
|
|
325
|
-
<${ListItemView} label="Groundhog" />
|
|
326
|
-
</ul>
|
|
327
|
-
`;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function ListItemView(props, ctx) {
|
|
331
|
-
return html`<li>${props.label}</li>`;
|
|
332
|
-
}
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
```jsx
|
|
336
|
-
function ListView() {
|
|
337
|
-
return (
|
|
338
|
-
<ul>
|
|
339
|
-
<ListItemView label="Squirrel" />
|
|
340
|
-
<ListItemView label="Chipmunk" />
|
|
341
|
-
<ListItemView label="Groundhog" />
|
|
342
|
-
</ul>
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function ListItemView(props) {
|
|
347
|
-
return <li>{props.label}</li>;
|
|
348
|
-
}
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
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.
|
|
352
|
-
|
|
353
|
-
### View Helpers
|
|
354
|
-
|
|
355
|
-
#### `cond($condition, whenTruthy, whenFalsy)`
|
|
356
|
-
|
|
357
|
-
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.
|
|
358
|
-
|
|
359
|
-
```jsx
|
|
360
|
-
function ConditionalListView({ $show }) {
|
|
361
|
-
return (
|
|
362
127
|
<div>
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
)}
|
|
128
|
+
<input
|
|
129
|
+
type="number"
|
|
130
|
+
value=${celsius}
|
|
131
|
+
oninput=${(e) => {
|
|
132
|
+
// Set by user input.
|
|
133
|
+
setCelsius(e.target.valueAsNumber);
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
|
|
137
|
+
<p>Celsius: ${celsius}°C</p>
|
|
138
|
+
<p>Fahrenheit: ${fahrenheit}°F</p>
|
|
139
|
+
<p>Condition: ${description}</p>
|
|
376
140
|
</div>
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
#### `repeat($items, keyFn, renderFn)`
|
|
382
|
-
|
|
383
|
-
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.
|
|
384
|
-
|
|
385
|
-
```jsx
|
|
386
|
-
function RepeatedListView() {
|
|
387
|
-
const $items = Dolla.toState(["Squirrel", "Chipmunk", "Groundhog"]);
|
|
388
|
-
|
|
389
|
-
return (
|
|
390
|
-
<ul>
|
|
391
|
-
{repeat(
|
|
392
|
-
$items,
|
|
393
|
-
(item) => item, // Using the string itself as the key
|
|
394
|
-
($item, $index, ctx) => {
|
|
395
|
-
return <ListItemView label={$item} />;
|
|
396
|
-
},
|
|
397
|
-
)}
|
|
398
|
-
</ul>
|
|
399
|
-
);
|
|
141
|
+
`;
|
|
400
142
|
}
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
#### `portal(content, parentNode)`
|
|
404
|
-
|
|
405
|
-
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.
|
|
406
143
|
|
|
407
|
-
|
|
408
|
-
function PortalView() {
|
|
409
|
-
const content = (
|
|
410
|
-
<div class="modal">
|
|
411
|
-
<p>This is a modal.</p>
|
|
412
|
-
</div>
|
|
413
|
-
);
|
|
414
|
-
|
|
415
|
-
// Content will be appended to `document.body` while this view is connected.
|
|
416
|
-
return portal(document.body, content);
|
|
417
|
-
}
|
|
144
|
+
createRoot(document.body).mount(Converter);
|
|
418
145
|
```
|
|
419
146
|
|
|
420
|
-
###
|
|
421
|
-
|
|
422
|
-
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.
|
|
147
|
+
### Stores for shared state
|
|
423
148
|
|
|
424
|
-
|
|
149
|
+
Dolla comes with a really easy way to share state across views in the same subtree.
|
|
425
150
|
|
|
426
151
|
```jsx
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
152
|
+
// Define a Store function:
|
|
153
|
+
function CounterStore() {
|
|
154
|
+
const [value, setValue] = createAtom(0);
|
|
430
155
|
|
|
431
|
-
//
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
ctx.log("Standard messages");
|
|
435
|
-
ctx.warn("Something bad might be happening");
|
|
436
|
-
ctx.error("Uh oh!");
|
|
156
|
+
// We can define our own functions to control how the state gets changed.
|
|
157
|
+
const increment = () => setValue((current) => current + 1);
|
|
158
|
+
const decrement = () => setValue((current) => current - 1);
|
|
437
159
|
|
|
438
|
-
//
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return <h1>Hello World!</h1>;
|
|
160
|
+
// This object is accessible to other components that use this store.
|
|
161
|
+
return { value, increment, decrement };
|
|
442
162
|
}
|
|
443
163
|
```
|
|
444
164
|
|
|
445
|
-
|
|
165
|
+
You work with stores using the `addStore` and `getStore` hooks. When you add a store to a part of your app, any component inside can now use it.
|
|
446
166
|
|
|
447
|
-
|
|
448
|
-
function ExampleView(props, ctx) {
|
|
449
|
-
ctx.beforeConnect(() => {
|
|
450
|
-
// Do something before this view's DOM nodes are created.
|
|
451
|
-
});
|
|
167
|
+
> Like an umbrella; it provides shade (state) to those under it, but not next to or above it.
|
|
452
168
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
ctx.beforeDisconnect(() => {
|
|
458
|
-
// Do something before removing this view from the DOM.
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
ctx.onDisconnected(() => {
|
|
462
|
-
// Do some cleanup after this view is disconnected from the DOM.
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
return <h1>Hello World!</h1>;
|
|
466
|
-
}
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
#### Displaying Children
|
|
470
|
-
|
|
471
|
-
The context object has an `outlet` function that can be used to display children at a location of your choosing.
|
|
169
|
+
```jsx
|
|
170
|
+
function App() {
|
|
171
|
+
// Creates one instance of CounterStore and returns it for immediate use.
|
|
172
|
+
const counter = addStore(this, CounterStore);
|
|
472
173
|
|
|
473
|
-
```js
|
|
474
|
-
function LayoutView(props, ctx) {
|
|
475
174
|
return (
|
|
476
|
-
<div
|
|
477
|
-
|
|
478
|
-
<
|
|
175
|
+
<div>
|
|
176
|
+
{/* Child views inherit context and therefore access to stores above them in the view tree. */}
|
|
177
|
+
<CounterView />
|
|
178
|
+
<button onClick={counter.increment}>Increment</button>
|
|
479
179
|
</div>
|
|
480
180
|
);
|
|
481
181
|
}
|
|
482
182
|
|
|
483
|
-
function
|
|
484
|
-
//
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
<h1>Hello</h1>
|
|
488
|
-
<p>This is inside the box.</p>
|
|
489
|
-
</LayoutView>
|
|
490
|
-
);
|
|
491
|
-
}
|
|
492
|
-
```
|
|
493
|
-
|
|
494
|
-
#### Observing States
|
|
495
|
-
|
|
496
|
-
The `observe` function starts observing when the view is connected and stops when disconnected. This takes care of cleaning up observers so you don't have to worry about memory leaks.
|
|
497
|
-
|
|
498
|
-
```jsx
|
|
499
|
-
function ExampleView(props, ctx) {
|
|
500
|
-
const { $someValue } = ctx.getStore(SomeStore);
|
|
501
|
-
|
|
502
|
-
ctx.observe($someValue, (value) => {
|
|
503
|
-
ctx.log("someValue is now", value);
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
return <h1>Hello World!</h1>;
|
|
507
|
-
}
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
#### Routing
|
|
511
|
-
|
|
512
|
-
Dolla makes heavy use of client-side routing. You can define as many routes as you have views, and the URL
|
|
513
|
-
will determine which one the app shows at any given time. By building an app around routes, lots of things one expects
|
|
514
|
-
from a web app will just work; back and forward buttons, sharable URLs, bookmarks, etc.
|
|
515
|
-
|
|
516
|
-
Routes are matched by highest specificity regardless of the order they were registered.
|
|
517
|
-
This avoids some confusing situations that come up with order-based routers like that of `express`.
|
|
518
|
-
On the other hand, order-based routers can support regular expressions as patterns which Dolla's router cannot.
|
|
519
|
-
|
|
520
|
-
#### Route Patterns
|
|
521
|
-
|
|
522
|
-
Routes are defined with strings called patterns. A pattern defines the shape the URL path must match, with special
|
|
523
|
-
placeholders for variables that appear within the route. Values matched by those placeholders are parsed out and exposed
|
|
524
|
-
to your code (`router` store, `$params` readable). Below are some examples of patterns and how they work.
|
|
525
|
-
|
|
526
|
-
- Static: `/this/is/static` has no params and will match only when the route is exactly `/this/is/static`.
|
|
527
|
-
- Numeric params: `/users/{#id}/edit` has the named param `{#id}` which matches numbers only, such as `123` or `52`. The
|
|
528
|
-
resulting value will be parsed as a number.
|
|
529
|
-
- Generic params: `/users/{name}` has the named param `{name}` which matches anything in that position in the path. The
|
|
530
|
-
resulting value will be a string.
|
|
531
|
-
- Wildcard: `/users/*` will match anything beginning with `/users` and store everything after that in params
|
|
532
|
-
as `wildcard`. `*` is valid only at the end of a route.
|
|
533
|
-
|
|
534
|
-
Now, here are some route examples in the context of an app:
|
|
535
|
-
|
|
536
|
-
```js
|
|
537
|
-
import Dolla from "@manyducks.co/dolla";
|
|
538
|
-
import { PersonDetails, ThingIndex, ThingDetails, ThingEdit, ThingDelete } from "./views.js";
|
|
539
|
-
|
|
540
|
-
Dolla.router.setup({
|
|
541
|
-
routes: [
|
|
542
|
-
{ path: "/people/{name}", view: PersonDetails },
|
|
543
|
-
{
|
|
544
|
-
// A `null` component with subroutes acts as a namespace for those subroutes.
|
|
545
|
-
// Passing a view instead of `null` results in subroutes being rendered inside that view wherever `ctx.outlet()` is called.
|
|
546
|
-
path: "/things",
|
|
547
|
-
view: null,
|
|
548
|
-
routes: [
|
|
549
|
-
{ path: "/", view: ThingIndex }, // matches `/things`
|
|
550
|
-
{ path: "/{#id}", view: ThingDetails }, // matches `/things/{#id}`
|
|
551
|
-
{ path: "/{#id}/edit", view: ThingEdit }, // matches `/things/{#id}/edit`
|
|
552
|
-
{ path: "/{#id}/delete", view: ThingDelete }, // matches `/things/{#id}/delete`
|
|
553
|
-
],
|
|
554
|
-
},
|
|
555
|
-
],
|
|
556
|
-
});
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
As you may have inferred from the code above, when the URL matches a pattern the corresponding view is displayed. If we
|
|
560
|
-
visit `/people/john`, we will see the `PersonDetails` view and the params will be `{ name: "john" }`. Params can be
|
|
561
|
-
accessed anywhere through `Dolla.router`.
|
|
562
|
-
|
|
563
|
-
```js
|
|
564
|
-
function PersonDetails(props, ctx) {
|
|
565
|
-
// Info about the current route is exported as a set of Readables. Query params are also Writable through $$query:
|
|
566
|
-
const { $path, $pattern, $params, $query } = Dolla.router;
|
|
567
|
-
|
|
568
|
-
Dolla.router.back(); // Step back in the history to the previous route, if any.
|
|
569
|
-
Dolla.router.back(2); // Hit the back button twice.
|
|
570
|
-
|
|
571
|
-
Dolla.router.forward(); // Step forward in the history to the next route, if any.
|
|
572
|
-
Dolla.router.forward(4); // Hit the forward button 4 times.
|
|
573
|
-
|
|
574
|
-
Dolla.router.go("/things/152"); // Navigate to another path within the same app.
|
|
575
|
-
Dolla.router.go("https://www.example.com/another/site"); // Navigate to another domain entirely.
|
|
576
|
-
|
|
577
|
-
// Three ways to confirm with the user that they wish to navigate before actually doing it.
|
|
578
|
-
Dolla.router.go("/another/page", { prompt: true });
|
|
579
|
-
Dolla.router.go("/another/page", { prompt: "Are you sure you want to leave and go to /another/page?" });
|
|
580
|
-
Dolla.router.go("/another/page", { prompt: PromptView });
|
|
581
|
-
|
|
582
|
-
// Get the live value of `{name}` from the current path.
|
|
583
|
-
const $name = Dolla.derive([$params], (p) => p.name);
|
|
584
|
-
|
|
585
|
-
// Render it into a <p> tag. The name portion will update if the URL changes.
|
|
586
|
-
return <p>The person is: {$name}</p>;
|
|
183
|
+
function CounterView() {
|
|
184
|
+
// Returns the same instance of CounterStore.
|
|
185
|
+
const counter = getStore(this, CounterStore);
|
|
186
|
+
return <p>Current value: {counter.value}</p>;
|
|
587
187
|
}
|
|
588
188
|
```
|
|
589
189
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
```js
|
|
593
|
-
// Middleware!
|
|
594
|
-
Dolla.http.use((request, next) => {
|
|
595
|
-
// Add auth header for all requests going to the API.
|
|
596
|
-
if (request.url.pathname.startsWith("/api")) {
|
|
597
|
-
request.headers.set("authorization", `Bearer ${authToken}`);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
const response = await next();
|
|
190
|
+
### What are stores good for?
|
|
601
191
|
|
|
602
|
-
|
|
192
|
+
- Authentication state
|
|
193
|
+
- Caching data between router pages
|
|
194
|
+
- Avoiding prop drilling for local state
|
|
603
195
|
|
|
604
|
-
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
const exampleResponse = await Dolla.http.get("/api/example");
|
|
608
|
-
|
|
609
|
-
// Body is already parsed from JSON into an object.
|
|
610
|
-
exampleResponse.body.someValue;
|
|
611
|
-
```
|
|
196
|
+
## Extras
|
|
612
197
|
|
|
613
|
-
|
|
198
|
+
Now that you've seen how to wire up a Dolla app, here are a few things to try:
|
|
614
199
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
function Counter(props, ctx) {
|
|
619
|
-
const [$count, setCount] = Dolla.createState(0);
|
|
620
|
-
|
|
621
|
-
function increment() {
|
|
622
|
-
setCount((count) => count + 1);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
return html`
|
|
626
|
-
<div>
|
|
627
|
-
<p>Clicks: ${$count}</p>
|
|
628
|
-
<button onclick=${increment}>${t("buttonLabel")}</button>
|
|
629
|
-
</div>
|
|
630
|
-
`;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
Dolla.i18n.setup({
|
|
634
|
-
locale: "en",
|
|
635
|
-
translations: [
|
|
636
|
-
{ locale: "en", strings: { buttonLabel: "Click here to increment" } },
|
|
637
|
-
{ locale: "ja", strings: { buttonLabel: "ここに押して増加する" } },
|
|
638
|
-
],
|
|
639
|
-
});
|
|
640
|
-
|
|
641
|
-
Dolla.mount(document.body, Counter);
|
|
642
|
-
```
|
|
200
|
+
- Add a [router](./src/router/README.md) to create an SPA
|
|
201
|
+
- Add [language translations](./src/translate/README.md) to go international.
|
|
643
202
|
|
|
644
203
|
---
|
|
645
204
|
|
|
646
|
-
[🦆](https://www.manyducks.co)
|
|
205
|
+
[🦆 That's a lot of ducks.](https://www.manyducks.co)
|