@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.
Files changed (90) hide show
  1. package/README.md +132 -573
  2. package/dist/core/context.d.ts +23 -0
  3. package/dist/core/debug.d.ts +19 -0
  4. package/dist/core/index.d.ts +24 -0
  5. package/dist/core/markup/helpers.d.ts +34 -0
  6. package/dist/core/markup/html.d.ts +3 -0
  7. package/dist/core/markup/html.test.d.ts +1 -0
  8. package/dist/core/markup/nodes/dom.d.ts +14 -0
  9. package/dist/core/markup/nodes/dynamic.d.ts +16 -0
  10. package/dist/core/markup/nodes/element.d.ts +14 -0
  11. package/dist/core/markup/nodes/portal.d.ts +15 -0
  12. package/dist/core/markup/nodes/repeat.d.ts +21 -0
  13. package/dist/core/markup/nodes/view.d.ts +17 -0
  14. package/dist/core/markup/scheduler.d.ts +1 -0
  15. package/dist/core/markup/types.d.ts +62 -0
  16. package/dist/core/markup/utils.d.ts +22 -0
  17. package/dist/core/markup/utils.test.d.ts +1 -0
  18. package/dist/core/ref.d.ts +13 -0
  19. package/dist/core/root.d.ts +36 -0
  20. package/dist/core/signals.d.ts +70 -0
  21. package/dist/core/signals.test.d.ts +1 -0
  22. package/dist/core/symbols.d.ts +2 -0
  23. package/dist/core-BLkJ-xuh.js +242 -0
  24. package/dist/core-BLkJ-xuh.js.map +1 -0
  25. package/dist/http/index.d.ts +43 -0
  26. package/dist/http.js +90 -0
  27. package/dist/http.js.map +1 -0
  28. package/dist/index.js +4 -1428
  29. package/dist/jsx-dev-runtime.d.ts +4 -2
  30. package/dist/jsx-dev-runtime.js +12 -16
  31. package/dist/jsx-dev-runtime.js.map +1 -1
  32. package/dist/jsx-runtime.d.ts +5 -3
  33. package/dist/jsx-runtime.js +17 -18
  34. package/dist/jsx-runtime.js.map +1 -1
  35. package/dist/router/index.d.ts +4 -0
  36. package/dist/router/matcher.test.d.ts +1 -0
  37. package/dist/router/router.d.ts +23 -0
  38. package/dist/router/router.test.d.ts +1 -0
  39. package/dist/router/store.d.ts +12 -0
  40. package/dist/router/types.d.ts +152 -0
  41. package/dist/router/utils.d.ts +99 -0
  42. package/dist/router/utils.test.d.ts +1 -0
  43. package/dist/router.js +429 -0
  44. package/dist/router.js.map +1 -0
  45. package/dist/signals-CMJPGr_M.js +354 -0
  46. package/dist/signals-CMJPGr_M.js.map +1 -0
  47. package/dist/translate/index.d.ts +82 -0
  48. package/dist/translate.js +125 -0
  49. package/dist/translate.js.map +1 -0
  50. package/dist/types.d.ts +83 -29
  51. package/dist/utils.d.ts +46 -12
  52. package/dist/utils.test.d.ts +1 -0
  53. package/dist/view-cBN-hn_T.js +360 -0
  54. package/dist/view-cBN-hn_T.js.map +1 -0
  55. package/dist/virtual/index.d.ts +1 -0
  56. package/dist/virtual/list.d.ts +53 -0
  57. package/index.d.ts +2 -2
  58. package/package.json +34 -17
  59. package/build.js +0 -34
  60. package/dist/index.d.ts +0 -21
  61. package/dist/index.js.map +0 -1
  62. package/dist/markup.d.ts +0 -108
  63. package/dist/modules/dolla.d.ts +0 -111
  64. package/dist/modules/http.d.ts +0 -57
  65. package/dist/modules/i18n.d.ts +0 -59
  66. package/dist/modules/render.d.ts +0 -17
  67. package/dist/modules/router.d.ts +0 -152
  68. package/dist/nodes/cond.d.ts +0 -26
  69. package/dist/nodes/html.d.ts +0 -31
  70. package/dist/nodes/observer.d.ts +0 -29
  71. package/dist/nodes/outlet.d.ts +0 -22
  72. package/dist/nodes/portal.d.ts +0 -19
  73. package/dist/nodes/repeat.d.ts +0 -34
  74. package/dist/nodes/text.d.ts +0 -19
  75. package/dist/passthrough-9kwwjgWk.js +0 -1279
  76. package/dist/passthrough-9kwwjgWk.js.map +0 -1
  77. package/dist/routing.d.ts +0 -79
  78. package/dist/state.d.ts +0 -101
  79. package/dist/typeChecking.d.ts +0 -191
  80. package/dist/view.d.ts +0 -65
  81. package/dist/views/default-crash-view.d.ts +0 -18
  82. package/dist/views/passthrough.d.ts +0 -5
  83. package/notes/context-vars.md +0 -21
  84. package/notes/readme-scratch.md +0 -222
  85. package/notes/route-middleware.md +0 -42
  86. package/notes/scratch.md +0 -233
  87. package/notes/views.md +0 -195
  88. package/tests/state.test.js +0 -135
  89. package/vite.config.js +0 -28
  90. /package/dist/{routing.test.d.ts → core/context.test.d.ts} +0 -0
package/README.md CHANGED
@@ -1,646 +1,205 @@
1
- # 🖥 @manyducks.co/dolla
1
+ # 💲 @manyducks.co/dolla
2
2
 
3
3
  ![bundle size](https://img.shields.io/bundlephobia/min/@manyducks.co/dolla)
4
4
  ![bundle size](https://img.shields.io/bundlephobia/minzip/@manyducks.co/dolla)
5
5
 
6
- > WARNING: This package is in active development. It may contain serious bugs and docs may be outdated or inaccurate. Use at your own risk.
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
- Dolla is a batteries-included JavaScript frontend framework covering the needs of moderate-to-complex single page apps:
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
- - Reactive DOM updates with [State](). Inspired by Signals, but with more explicit tracking.
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
- Let's first get into some examples.
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 { createSettableState, fromSettable, toSettable } from "@manyducks.co/dolla";
21
+ import { html, createAtom, createRoot, compose } from "@manyducks.co/dolla";
62
22
 
63
- // Settable states have their setter included.
64
- const $$value = createSettableState("Test");
65
- $$value.set("New Value");
23
+ function Hello() {
24
+ const [value, setValue] = createAtom(false);
66
25
 
67
- // They can also be split into a State and Setter
68
- const [$value, setValue] = fromSettableState($$value);
26
+ const word = compose(() => (value() ? "Hello" : "Goodbye"));
69
27
 
70
- // And a State and Setter can be combined into a SettableState.
71
- const $$otherValue = toSettableState($value, setValue);
28
+ return html`
29
+ <p>${word} World</p>
30
+ <button onClick=${() => setValue((current) => !current)}>Toggle</button>
31
+ `;
32
+ }
72
33
 
73
- // Or discard the setter and make it read-only using the good old toState function:
74
- const $value = toState($$value);
34
+ createRoot(document.body).mount(Hello);
75
35
  ```
76
36
 
77
- You can also do weird proxy things like this:
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
- // Create an original place for the state to live
81
- const [$value, setValue] = createState(5);
82
-
83
- // Derive a state that doubles the value
84
- const $doubled = derive([$value], (value) => value * 2);
85
-
86
- // Create a setter that takes the doubled value and sets the original $value accordingly.
87
- const setDoubled = createSetter($doubled, (next, current) => {
88
- setValue(next / 2);
89
- });
90
-
91
- // Bundle the derived state and setter into a SettableState to pass around.
92
- const $$doubled = toSettableState($doubled, setDoubled);
93
-
94
- // Setting the doubled state...
95
- $$doubled.set(100);
96
-
97
- // ... will be reflected everywhere.
98
- $$doubled.get(); // 100
99
- $doubled.get(); // 100
100
- $value.get(); // 50
101
- ```
102
-
103
- ## Views [id="section-views"]
104
-
105
- A basic view:
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
- function increment() {
114
- setCount((count) => count + 1);
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>Clicks: ${$count}</p>
120
- <button onclick=${increment}>+1</button>
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
- Dolla.mount(document.body, Counter);
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
- If you've ever used React before (and chances are you have if you're interested in obscure frameworks like this one) this should look very familiar to you.
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
- ## Advanced Componentry
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
- Component functions take two arguments; props and a `Context` object. Props are passed from parent components to child components, and `Context` is provided by the app.
98
+ ## Dependent data flow
135
99
 
136
- > The following examples are shown in TypeScript for clarity. Feel free to omit the type annotations in your own code if you prefer vanilla JS.
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
- ### Props
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
- Props are values passed down from parent components. These can be static values, signals, callbacks and anything else the child component needs to do its job.
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 { type State, type Context, html } from "@manyducks.co/dolla";
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
- // And sometimes things are just too borked to press on:
184
- c.crash(new Error("STOP THE PRESSES! BURN IT ALL DOWN!!!"));
109
+ function Converter() {
110
+ // Just one source value that changes.
111
+ const [celsius, setCelsius] = createAtom(0);
185
112
 
186
- // The four lifecycle hooks:
187
-
188
- // c.beforeMount(() => {
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
- // States can be watched by the component context.
205
- // Watchers created this way are cleaned up automatically when the component unmounts.
206
-
207
- c.watch(props.$text, (value) => {
208
- c.warn(`text has changed to: ${value}`);
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
- {cond(
364
- $show,
365
-
366
- // Visible when truthy
367
- <ul>
368
- <ListItemView label="Squirrel" />
369
- <ListItemView label="Chipmunk" />
370
- <ListItemView label="Groundhog" />
371
- </ul>,
372
-
373
- // Visible when falsy
374
- <span>List is hidden</span>,
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
- ```jsx
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
- ### View Context
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
- #### Printing Debug Messages
149
+ Dolla comes with a really easy way to share state across views in the same subtree.
425
150
 
426
151
  ```jsx
427
- function ExampleView(props, ctx) {
428
- // Set the name of this view's context. Console messages are prefixed with name.
429
- ctx.name = "CustomName";
152
+ // Define a Store function:
153
+ function CounterStore() {
154
+ const [value, setValue] = createAtom(0);
430
155
 
431
- // Print messages to the console. These are suppressed by default in the app's "production" mode.
432
- // You can also change which of these are printed and filter messages from certain contexts in the `createApp` options object.
433
- ctx.info("Verbose debugging info that might be useful to know");
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
- // If you encounter a bad enough situation, you can halt and disconnect the entire app.
439
- ctx.crash(new Error("BOOM"));
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
- #### Lifecycle Events
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
- ```jsx
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
- ctx.onConnected(() => {
454
- // Do something immediately after this view is connected to the DOM.
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 className="layout">
477
- <OtherView />
478
- <div className="content">{ctx.outlet()}</div>
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 ExampleView() {
484
- // <h1> and <p> are displayed inside LayoutView's outlet.
485
- return (
486
- <LayoutView>
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
- ## HTTP Client
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
- // Could do something with the response here.
192
+ - Authentication state
193
+ - Caching data between router pages
194
+ - Avoiding prop drilling for local state
603
195
 
604
- return response;
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
- ## Localization
198
+ Now that you've seen how to wire up a Dolla app, here are a few things to try:
614
199
 
615
- ```js
616
- import Dolla, { html, t } from "@manyducks.co/dolla";
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)