@manyducks.co/dolla 2.0.0-alpha.1 → 2.0.0-alpha.10
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 +196 -473
- package/dist/index.d.ts +10 -33
- package/dist/index.js +792 -675
- package/dist/index.js.map +1 -1
- package/dist/jsx-dev-runtime.d.ts +1 -1
- package/dist/jsx-dev-runtime.js +2 -2
- package/dist/jsx-dev-runtime.js.map +1 -1
- package/dist/jsx-runtime.d.ts +1 -1
- package/dist/jsx-runtime.js +2 -2
- package/dist/jsx-runtime.js.map +1 -1
- package/dist/markup.d.ts +37 -23
- package/dist/modules/dolla.d.ts +41 -17
- package/dist/modules/i18n.d.ts +83 -0
- package/dist/modules/router.d.ts +9 -10
- package/dist/nodes/cond.d.ts +9 -10
- package/dist/nodes/html.d.ts +14 -10
- package/dist/nodes/observer.d.ts +9 -10
- package/dist/nodes/outlet.d.ts +10 -11
- package/dist/nodes/portal.d.ts +6 -7
- package/dist/nodes/repeat.d.ts +15 -16
- package/dist/nodes/text.d.ts +8 -9
- package/dist/passthrough-9kwwjgWk.js +1279 -0
- package/dist/passthrough-9kwwjgWk.js.map +1 -0
- package/dist/state.d.ts +101 -0
- package/dist/types.d.ts +12 -12
- package/dist/view.d.ts +28 -7
- package/dist/views/default-crash-view.d.ts +18 -0
- package/dist/views/passthrough.d.ts +5 -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 +42 -5
- package/package.json +8 -12
- package/tests/{signals.test.js → state.test.js} +6 -6
- package/vite.config.js +1 -0
- package/dist/fragment-DHJiX0-a.js +0 -1241
- package/dist/fragment-DHJiX0-a.js.map +0 -1
- package/dist/modules/language.d.ts +0 -41
- package/dist/signals.d.ts +0 -101
- package/dist/views/default-crash-page.d.ts +0 -8
- package/dist/views/default-view.d.ts +0 -2
- package/dist/views/fragment.d.ts +0 -2
package/README.md
CHANGED
|
@@ -3,81 +3,123 @@
|
|
|
3
3
|

|
|
4
4
|

|
|
5
5
|
|
|
6
|
-
> WARNING: This package is
|
|
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.
|
|
7
7
|
|
|
8
|
-
Dolla is a batteries-included JavaScript frontend framework covering the needs of complex
|
|
8
|
+
Dolla is a batteries-included JavaScript frontend framework covering the needs of moderate-to-complex single page apps:
|
|
9
9
|
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
-
|
|
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
16
|
|
|
17
17
|
Let's first get into some examples.
|
|
18
18
|
|
|
19
|
-
##
|
|
19
|
+
## State
|
|
20
20
|
|
|
21
|
-
###
|
|
21
|
+
### Basic State API
|
|
22
22
|
|
|
23
23
|
```jsx
|
|
24
|
-
import {
|
|
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);
|
|
25
48
|
|
|
26
|
-
//
|
|
27
|
-
const
|
|
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);
|
|
28
52
|
|
|
29
|
-
// Derive a new state from one or more states.
|
|
30
|
-
const $doubled = derive([
|
|
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);
|
|
31
56
|
```
|
|
32
57
|
|
|
33
|
-
|
|
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.
|
|
34
59
|
|
|
35
60
|
```jsx
|
|
36
|
-
import {
|
|
37
|
-
|
|
38
|
-
const [$count, setCount] = createSignal(0);
|
|
61
|
+
import { createSettableState, fromSettable, toSettable } from "@manyducks.co/dolla";
|
|
39
62
|
|
|
40
|
-
//
|
|
41
|
-
|
|
63
|
+
// Settable states have their setter included.
|
|
64
|
+
const $$value = createSettableState("Test");
|
|
65
|
+
$$value.set("New Value");
|
|
42
66
|
|
|
43
|
-
//
|
|
44
|
-
const
|
|
45
|
-
const decrement = () => setCount((current) => current - 1);
|
|
67
|
+
// They can also be split into a State and Setter
|
|
68
|
+
const [$value, setValue] = fromSettableState($$value);
|
|
46
69
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
decrement(); // $count = 2
|
|
70
|
+
// And a State and Setter can be combined into a SettableState.
|
|
71
|
+
const $$otherValue = toSettableState($value, setValue);
|
|
50
72
|
|
|
51
|
-
|
|
73
|
+
// Or discard the setter and make it read-only using the good old toState function:
|
|
74
|
+
const $value = toState($$value);
|
|
52
75
|
```
|
|
53
76
|
|
|
54
|
-
|
|
77
|
+
You can also do weird proxy things like this:
|
|
55
78
|
|
|
56
79
|
```jsx
|
|
57
|
-
|
|
80
|
+
// Create an original place for the state to live
|
|
81
|
+
const [$value, setValue] = createState(5);
|
|
58
82
|
|
|
59
|
-
|
|
60
|
-
const $doubled = derive([$
|
|
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
|
|
61
101
|
```
|
|
62
102
|
|
|
63
|
-
##
|
|
103
|
+
## Views [id="section-views"]
|
|
64
104
|
|
|
65
|
-
|
|
66
|
-
|
|
105
|
+
A basic view:
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
import Dolla, { createState, html } from "@manyducks.co/dolla";
|
|
67
109
|
|
|
68
|
-
function Counter(props,
|
|
69
|
-
const [$count, setCount] =
|
|
110
|
+
function Counter(props, ctx) {
|
|
111
|
+
const [$count, setCount] = createState(0);
|
|
70
112
|
|
|
71
113
|
function increment() {
|
|
72
114
|
setCount((count) => count + 1);
|
|
73
115
|
}
|
|
74
116
|
|
|
75
|
-
return
|
|
117
|
+
return html`
|
|
76
118
|
<div>
|
|
77
|
-
<p>Clicks: {$count}</p>
|
|
78
|
-
<button
|
|
119
|
+
<p>Clicks: ${$count}</p>
|
|
120
|
+
<button onclick=${increment}>+1</button>
|
|
79
121
|
</div>
|
|
80
|
-
|
|
122
|
+
`;
|
|
81
123
|
}
|
|
82
124
|
|
|
83
125
|
Dolla.mount(document.body, Counter);
|
|
@@ -85,9 +127,7 @@ Dolla.mount(document.body, Counter);
|
|
|
85
127
|
|
|
86
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.
|
|
87
129
|
|
|
88
|
-
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
|
|
89
|
-
|
|
90
|
-
You'll notice that signals are typically named with a `$` at the beginning to indicate that they contain special values that may change over time.
|
|
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
131
|
|
|
92
132
|
## Advanced Componentry
|
|
93
133
|
|
|
@@ -100,10 +140,10 @@ Component functions take two arguments; props and a `Context` object. Props are
|
|
|
100
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.
|
|
101
141
|
|
|
102
142
|
```tsx
|
|
103
|
-
import { type
|
|
143
|
+
import { type State, type Context, html } from "@manyducks.co/dolla";
|
|
104
144
|
|
|
105
145
|
type HeadingProps = {
|
|
106
|
-
$text:
|
|
146
|
+
$text: State<string>;
|
|
107
147
|
};
|
|
108
148
|
|
|
109
149
|
function Heading(props: HeadingProps, c: Context) {
|
|
@@ -124,10 +164,10 @@ function Layout() {
|
|
|
124
164
|
### Context
|
|
125
165
|
|
|
126
166
|
```tsx
|
|
127
|
-
import { type
|
|
167
|
+
import { type State, type Context, html } from "@manyducks.co/dolla";
|
|
128
168
|
|
|
129
169
|
type HeadingProps = {
|
|
130
|
-
$text:
|
|
170
|
+
$text: State<string>;
|
|
131
171
|
};
|
|
132
172
|
|
|
133
173
|
function Heading(props: HeadingProps, c: Context) {
|
|
@@ -161,7 +201,7 @@ function Heading(props: HeadingProps, c: Context) {
|
|
|
161
201
|
c.info("Heading has just been unmounted. Good time to finalize teardown.");
|
|
162
202
|
});
|
|
163
203
|
|
|
164
|
-
//
|
|
204
|
+
// States can be watched by the component context.
|
|
165
205
|
// Watchers created this way are cleaned up automatically when the component unmounts.
|
|
166
206
|
|
|
167
207
|
c.watch(props.$text, (value) => {
|
|
@@ -229,17 +269,17 @@ $selected.get(); // "Bon"
|
|
|
229
269
|
Proxy
|
|
230
270
|
|
|
231
271
|
```jsx
|
|
232
|
-
import {
|
|
272
|
+
import { createState, createProxyState } from "@manyducks.co/dolla";
|
|
233
273
|
|
|
234
|
-
const [$names, setNames] =
|
|
235
|
-
const [$index, setIndex] =
|
|
274
|
+
const [$names, setNames] = createState(["Morg", "Ton", "Bon"]);
|
|
275
|
+
const [$index, setIndex] = createState(0);
|
|
236
276
|
|
|
237
|
-
const [$selected, setSelected] =
|
|
277
|
+
const [$selected, setSelected] = createProxyState([$names, $index], {
|
|
238
278
|
get(names, index) {
|
|
239
279
|
return names[index];
|
|
240
280
|
},
|
|
241
|
-
set(next) {
|
|
242
|
-
const index =
|
|
281
|
+
set(next, names, _) {
|
|
282
|
+
const index = names.indexOf(next);
|
|
243
283
|
if (index === -1) {
|
|
244
284
|
throw new Error("Name is not in the list!");
|
|
245
285
|
}
|
|
@@ -258,137 +298,6 @@ $selected.get(); // "Ton"
|
|
|
258
298
|
$index.get(); // 1
|
|
259
299
|
```
|
|
260
300
|
|
|
261
|
-
##
|
|
262
|
-
|
|
263
|
-
States come in two varieties, each with a constructor function and a TypeScript type to match. These are:
|
|
264
|
-
|
|
265
|
-
- `Readable<T>`, which has only a `.get()` method that returns the current value.
|
|
266
|
-
- `Writable<T>`, which extends `Readable<T>` and adds a couple methods:
|
|
267
|
-
- `.set(value: T)` to replace the stored value.
|
|
268
|
-
- `.update(callback: (current: T) => T)` which takes a function that receives the current value and returns a new one.
|
|
269
|
-
|
|
270
|
-
The constructor functions are `$` for `Readable` and `$$` for `Writable`. By convention, the names of each are prefixed with `$` or `$$` to indicate its type, making the data flow a lot easier to understand at a glance.
|
|
271
|
-
|
|
272
|
-
```js
|
|
273
|
-
import { signal } from "@manyducks.co/dolla";
|
|
274
|
-
|
|
275
|
-
// By convention, Writable names are prefixed with two dollar signs and Readable with one.
|
|
276
|
-
const [$number, setNumber] = signal(5);
|
|
277
|
-
|
|
278
|
-
// Returns the current value held by the Writable.
|
|
279
|
-
$number.get();
|
|
280
|
-
// Stores a new value to the Writable.
|
|
281
|
-
setNumber(12);
|
|
282
|
-
// Uses a callback to update the value. Takes the current value and returns the next.
|
|
283
|
-
setNumber((current) => current + 1);
|
|
284
|
-
|
|
285
|
-
// Derive a new state from an existing one.
|
|
286
|
-
const $doubled = derive([$number], (value) => value * 2);
|
|
287
|
-
$doubled.get(); // 26 ($number is 13)
|
|
288
|
-
|
|
289
|
-
// Derive one new state from the latest values of many other states.
|
|
290
|
-
const $many = derive([$number, $doubled], (num, doubled) => num + doubled);
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
Now how do we use it? For a real example, a simple greeter app. The user types their name into a text input and that value is reflected in a heading above the input. For this we will use the `writable` function to create a state container. That container can be slotted into our JSX as a text node or DOM property. Any changes to the value will now be reflected in the DOM.
|
|
294
|
-
|
|
295
|
-
```jsx
|
|
296
|
-
import { signal } from "@manyducks.co/dolla";
|
|
297
|
-
|
|
298
|
-
function Greeter() {
|
|
299
|
-
const [$name, setName] = signal("Valued Customer");
|
|
300
|
-
|
|
301
|
-
return (
|
|
302
|
-
<section>
|
|
303
|
-
<header>
|
|
304
|
-
<h1>Hello, {$name}!</h1>
|
|
305
|
-
</header>
|
|
306
|
-
|
|
307
|
-
<input
|
|
308
|
-
value={$name}
|
|
309
|
-
onChange={(e) => {
|
|
310
|
-
setName(e.target.value);
|
|
311
|
-
}}
|
|
312
|
-
/>
|
|
313
|
-
</section>
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
### Computed
|
|
319
|
-
|
|
320
|
-
Computed states take one or more Readables or Writables and produce a new value _computed_ from those.
|
|
321
|
-
|
|
322
|
-
```js
|
|
323
|
-
import { $, $$ } from "@manyducks.co/dolla";
|
|
324
|
-
|
|
325
|
-
const $$count = $$(100);
|
|
326
|
-
|
|
327
|
-
const $double = $($$count, (value) => value * 2);
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
In that example, `$$double` will always have a value derived from that of `$$count`.
|
|
331
|
-
|
|
332
|
-
Let's look at a more typical example where we're basically joining two pieces of data; a list of users and the ID of the selected user.
|
|
333
|
-
|
|
334
|
-
```js
|
|
335
|
-
import { $, $$ } from "@manyducks.co/dolla";
|
|
336
|
-
|
|
337
|
-
// Let's assume this list of users was fetched from an API somewhere.
|
|
338
|
-
const $$people = $$([
|
|
339
|
-
{
|
|
340
|
-
id: 1,
|
|
341
|
-
name: "Borb",
|
|
342
|
-
},
|
|
343
|
-
{
|
|
344
|
-
id: 2,
|
|
345
|
-
name: "Bex",
|
|
346
|
-
},
|
|
347
|
-
{
|
|
348
|
-
id: 3,
|
|
349
|
-
name: "Bleeblop",
|
|
350
|
-
},
|
|
351
|
-
]);
|
|
352
|
-
|
|
353
|
-
// Let's assume this ID was chosen from an input where the above users were displayed.
|
|
354
|
-
const $$selectedId = $$(2);
|
|
355
|
-
|
|
356
|
-
// Now we get the object of the person who is selected.
|
|
357
|
-
const $selectedPerson = $($$people, $$selectedId, (people, selectedId) => {
|
|
358
|
-
return people.find((person) => person.id === selectedId);
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
// Now we get a Readable of just that person's name. Say we're going to display it on the page somewhere.
|
|
362
|
-
const $personName = $($selectedPerson, (person) => person.name);
|
|
363
|
-
|
|
364
|
-
console.log($personName.get()); // "Bex"
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
Notice that the structure above composes a data pipeline; if any of the data changes, so do the computed values, but the relationship between the data remains the same. Now that we've defined these relationships, `$selectedPerson` is always the person pointed to by `$$selectedId`. `$personName` is always the name of `$selectedPerson`, etc.
|
|
368
|
-
|
|
369
|
-
### Unwrap
|
|
370
|
-
|
|
371
|
-
The `unwrap` function returns the current value of a Readable or Writable, or if passed a non-Readable value returns that exact value. This function is used to guarantee you have a plain value when you may be dealing with either a container or a plain value.
|
|
372
|
-
|
|
373
|
-
```js
|
|
374
|
-
import { $, $$, unwrap } from "@manyducks.co/dolla";
|
|
375
|
-
|
|
376
|
-
const $$number = $$(5);
|
|
377
|
-
|
|
378
|
-
unwrap($$number); // 5
|
|
379
|
-
unwrap($(5)); // 5
|
|
380
|
-
unwrap(5); // 5
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
### Advanced Use Cases
|
|
384
|
-
|
|
385
|
-
<details>
|
|
386
|
-
<summary><code>observe</code> and <code>proxy</code></summary>
|
|
387
|
-
|
|
388
|
-
> TO DO
|
|
389
|
-
|
|
390
|
-
</details>
|
|
391
|
-
|
|
392
301
|
## Views
|
|
393
302
|
|
|
394
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.
|
|
@@ -405,6 +314,24 @@ function ExampleView() {
|
|
|
405
314
|
|
|
406
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.
|
|
407
316
|
|
|
317
|
+
```js
|
|
318
|
+
import { html } from "@manyducks.co/dolla";
|
|
319
|
+
|
|
320
|
+
function ListView(props, ctx) {
|
|
321
|
+
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
|
+
|
|
408
335
|
```jsx
|
|
409
336
|
function ListView() {
|
|
410
337
|
return (
|
|
@@ -457,7 +384,7 @@ The `repeat` helper repeats a render function for each item in a list. The `keyF
|
|
|
457
384
|
|
|
458
385
|
```jsx
|
|
459
386
|
function RepeatedListView() {
|
|
460
|
-
const $items =
|
|
387
|
+
const $items = Dolla.toState(["Squirrel", "Chipmunk", "Groundhog"]);
|
|
461
388
|
|
|
462
389
|
return (
|
|
463
390
|
<ul>
|
|
@@ -486,7 +413,7 @@ function PortalView() {
|
|
|
486
413
|
);
|
|
487
414
|
|
|
488
415
|
// Content will be appended to `document.body` while this view is connected.
|
|
489
|
-
return portal(
|
|
416
|
+
return portal(document.body, content);
|
|
490
417
|
}
|
|
491
418
|
```
|
|
492
419
|
|
|
@@ -564,18 +491,6 @@ function ExampleView() {
|
|
|
564
491
|
}
|
|
565
492
|
```
|
|
566
493
|
|
|
567
|
-
#### Using Stores
|
|
568
|
-
|
|
569
|
-
```jsx
|
|
570
|
-
import { UserStore } from "../stores/UserStore.js";
|
|
571
|
-
|
|
572
|
-
function ExampleView(props, ctx) {
|
|
573
|
-
const { $name } = ctx.getStore(UserStore);
|
|
574
|
-
|
|
575
|
-
return <h1>Hello {$name}!</h1>;
|
|
576
|
-
}
|
|
577
|
-
```
|
|
578
|
-
|
|
579
494
|
#### Observing States
|
|
580
495
|
|
|
581
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.
|
|
@@ -592,239 +507,6 @@ function ExampleView(props, ctx) {
|
|
|
592
507
|
}
|
|
593
508
|
```
|
|
594
509
|
|
|
595
|
-
#### Example: Counter View
|
|
596
|
-
|
|
597
|
-
Putting it all together, we have a view that maintains a counter. The user sees the current count displayed, and below it three buttons; one to increment by 1, one to decrement by 1, and one to reset the value to 0.
|
|
598
|
-
|
|
599
|
-
```jsx
|
|
600
|
-
import { $$ } from "@manyducks.co/dolla";
|
|
601
|
-
|
|
602
|
-
function CounterView(props, ctx) {
|
|
603
|
-
const $$count = $$(0);
|
|
604
|
-
|
|
605
|
-
function increment() {
|
|
606
|
-
$$count.update((n) => n + 1);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
function decrement() {
|
|
610
|
-
$$count.update((n) => n - 1);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
function reset() {
|
|
614
|
-
$$count.set(0);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
return (
|
|
618
|
-
<div>
|
|
619
|
-
<p>The count is {$$count}</p>
|
|
620
|
-
<div>
|
|
621
|
-
<button onClick={increment}>+1</button>
|
|
622
|
-
<button onClick={decrement}>-1</button>
|
|
623
|
-
<button onClick={reset}>Reset</button>
|
|
624
|
-
</div>
|
|
625
|
-
</div>
|
|
626
|
-
);
|
|
627
|
-
}
|
|
628
|
-
```
|
|
629
|
-
|
|
630
|
-
## Stores
|
|
631
|
-
|
|
632
|
-
A store is a function that returns a plain JavaScript object. If this store is registered on the app, a single instance of the store is shared across all views and stores in the app. If the store is registered using a `StoreScope`, a single instance of the store is shared amongst all child elements of that `StoreScope`.
|
|
633
|
-
|
|
634
|
-
Stores are accessed with the `getStore` function available on the context object in views and other stores.
|
|
635
|
-
|
|
636
|
-
Stores are helpful for managing persistent state that needs to be accessed in many places.
|
|
637
|
-
|
|
638
|
-
```js
|
|
639
|
-
import { App } from "@manyducks.co/dolla";
|
|
640
|
-
|
|
641
|
-
const app = App({
|
|
642
|
-
view: LayoutView,
|
|
643
|
-
stores: [MessageStore],
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
// We define a store that just exports a message.
|
|
647
|
-
function MessageStore() {
|
|
648
|
-
return {
|
|
649
|
-
message: "Hello from the message store!",
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// All instances of MessageView will share just one instance of MessageStore.
|
|
654
|
-
function MessageView(props, ctx) {
|
|
655
|
-
const store = ctx.getStore(MessageStore);
|
|
656
|
-
|
|
657
|
-
return <p>{store.message}</p>;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// And a layout view with five MessageViews inside.
|
|
661
|
-
function LayoutView() {
|
|
662
|
-
return (
|
|
663
|
-
<div>
|
|
664
|
-
<h1>Title</h1>
|
|
665
|
-
<MessageView />
|
|
666
|
-
<MessageView />
|
|
667
|
-
<MessageView />
|
|
668
|
-
<MessageView />
|
|
669
|
-
<MessageView />
|
|
670
|
-
</div>
|
|
671
|
-
);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// Connect the app.
|
|
675
|
-
app.connect("#app");
|
|
676
|
-
```
|
|
677
|
-
|
|
678
|
-
The output:
|
|
679
|
-
|
|
680
|
-
```html
|
|
681
|
-
<div id="app">
|
|
682
|
-
<div>
|
|
683
|
-
<h1>Title</h1>
|
|
684
|
-
<p>Hello from the message store!</p>
|
|
685
|
-
<p>Hello from the message store!</p>
|
|
686
|
-
<p>Hello from the message store!</p>
|
|
687
|
-
<p>Hello from the message store!</p>
|
|
688
|
-
<p>Hello from the message store!</p>
|
|
689
|
-
</div>
|
|
690
|
-
</div>
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
### StoreScope
|
|
694
|
-
|
|
695
|
-
Stores relevant to only a part of the view tree can be scoped using a `StoreScope`.
|
|
696
|
-
|
|
697
|
-
```jsx
|
|
698
|
-
function ExampleStore() {
|
|
699
|
-
return { value: 5 };
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
function ExampleView(props, ctx) {
|
|
703
|
-
const store = ctx.getStore(ExampleStore);
|
|
704
|
-
|
|
705
|
-
return <div>{store.value}</div>;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
function LayoutView() {
|
|
709
|
-
return (
|
|
710
|
-
<StoreScope stores={[ExampleStore]}>
|
|
711
|
-
<ExampleView />
|
|
712
|
-
</StoreScope>
|
|
713
|
-
);
|
|
714
|
-
}
|
|
715
|
-
```
|
|
716
|
-
|
|
717
|
-
## Apps and Routing
|
|
718
|
-
|
|
719
|
-
```jsx
|
|
720
|
-
import { App } from "@manyducks.co/dolla";
|
|
721
|
-
|
|
722
|
-
const app = App({
|
|
723
|
-
// Debug options control what gets printed from messages logged through view and store contexts.
|
|
724
|
-
debug: {
|
|
725
|
-
// A comma-separated list of filters. '*' means allow everything and '-dolla/*' means suppress messages with labels beginning with 'dolla/'.
|
|
726
|
-
filter: "*,-dolla/*",
|
|
727
|
-
|
|
728
|
-
// Never print ctx.info() messages
|
|
729
|
-
info: false,
|
|
730
|
-
|
|
731
|
-
// Only print ctx.log() and ctx.warn() messages in development mode
|
|
732
|
-
log: "development",
|
|
733
|
-
warn: "development",
|
|
734
|
-
|
|
735
|
-
// Always print ctx.error() messages
|
|
736
|
-
error: true,
|
|
737
|
-
},
|
|
738
|
-
|
|
739
|
-
mode: "development", // or "production" (enables additional debug features and logging in "development")
|
|
740
|
-
|
|
741
|
-
view: (_, ctx) => {
|
|
742
|
-
// Define a custom root view. By default this just renders any routes like so:
|
|
743
|
-
return ctx.outlet();
|
|
744
|
-
},
|
|
745
|
-
});
|
|
746
|
-
```
|
|
747
|
-
|
|
748
|
-
#### Routes and Outlets
|
|
749
|
-
|
|
750
|
-
The main view (defined with the app's `main` method) is the top-level view that will always be displayed while the app is connected.
|
|
751
|
-
|
|
752
|
-
```jsx
|
|
753
|
-
// Here is an app with a hypothetical main view with a layout and navigation:
|
|
754
|
-
const app = App({
|
|
755
|
-
view: (_, ctx) => {
|
|
756
|
-
return (
|
|
757
|
-
<div class="todo-layout">
|
|
758
|
-
<nav>
|
|
759
|
-
<ul>
|
|
760
|
-
<li>
|
|
761
|
-
<a href="/tasks">Tasks</a>
|
|
762
|
-
</li>
|
|
763
|
-
<li>
|
|
764
|
-
<a href="/completed">Completed</a>
|
|
765
|
-
</li>
|
|
766
|
-
</ul>
|
|
767
|
-
</nav>
|
|
768
|
-
{/*
|
|
769
|
-
* An outlet is where children of a view are shown.
|
|
770
|
-
* Because this is a main view, children in this case
|
|
771
|
-
* are the views that correspond to matched routes.
|
|
772
|
-
*/}
|
|
773
|
-
{ctx.outlet()}
|
|
774
|
-
</div>
|
|
775
|
-
);
|
|
776
|
-
},
|
|
777
|
-
|
|
778
|
-
stores: [
|
|
779
|
-
{
|
|
780
|
-
store: RouterStore,
|
|
781
|
-
options: {
|
|
782
|
-
hash: true, // Use hash-based routing (default false)
|
|
783
|
-
|
|
784
|
-
// Here are a couple of routes to be rendered into our layout:
|
|
785
|
-
routes: [
|
|
786
|
-
{ path: "/tasks", view: TasksView },
|
|
787
|
-
{ path: "/completed", view: CompletedView },
|
|
788
|
-
],
|
|
789
|
-
},
|
|
790
|
-
},
|
|
791
|
-
],
|
|
792
|
-
});
|
|
793
|
-
```
|
|
794
|
-
|
|
795
|
-
Routes can also be nested. Just like the main view and its routes, subroutes will be displayed in the outlet of their parent view.
|
|
796
|
-
|
|
797
|
-
```jsx
|
|
798
|
-
const app = App({
|
|
799
|
-
stores: [
|
|
800
|
-
{
|
|
801
|
-
store: RouterStore,
|
|
802
|
-
options: {
|
|
803
|
-
routes: [
|
|
804
|
-
{
|
|
805
|
-
path: "/tasks",
|
|
806
|
-
view: TasksView,
|
|
807
|
-
routes: [
|
|
808
|
-
{ path: "/", view: TaskListView },
|
|
809
|
-
|
|
810
|
-
// In routes, `{value}` is a dynamic value that matches anything,
|
|
811
|
-
// and `{#value}` is a dynamic value that matches a number.
|
|
812
|
-
{ path: "/{#id}", view: TaskDetailsView },
|
|
813
|
-
{ path: "/{#id}/edit", view: TaskEditView },
|
|
814
|
-
|
|
815
|
-
// If the route is any other than the ones defined above, redirect to the list.
|
|
816
|
-
// Redirects support './' and '../' style relative paths.
|
|
817
|
-
{ path: "*", redirect: "./" },
|
|
818
|
-
],
|
|
819
|
-
},
|
|
820
|
-
{ path: "/completed", view: CompletedView },
|
|
821
|
-
],
|
|
822
|
-
},
|
|
823
|
-
},
|
|
824
|
-
],
|
|
825
|
-
});
|
|
826
|
-
```
|
|
827
|
-
|
|
828
510
|
#### Routing
|
|
829
511
|
|
|
830
512
|
Dolla makes heavy use of client-side routing. You can define as many routes as you have views, and the URL
|
|
@@ -852,30 +534,23 @@ to your code (`router` store, `$params` readable). Below are some examples of pa
|
|
|
852
534
|
Now, here are some route examples in the context of an app:
|
|
853
535
|
|
|
854
536
|
```js
|
|
855
|
-
import
|
|
856
|
-
import { PersonDetails, ThingIndex, ThingDetails, ThingEdit, ThingDelete } from "./
|
|
537
|
+
import Dolla from "@manyducks.co/dolla";
|
|
538
|
+
import { PersonDetails, ThingIndex, ThingDetails, ThingEdit, ThingDelete } from "./views.js";
|
|
857
539
|
|
|
858
|
-
|
|
859
|
-
|
|
540
|
+
Dolla.router.setup({
|
|
541
|
+
routes: [
|
|
542
|
+
{ path: "/people/{name}", view: PersonDetails },
|
|
860
543
|
{
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
{ path: "/", view: ThingIndex }, // matches `/things`
|
|
872
|
-
{ path: "/{#id}", view: ThingDetails }, // matches `/things/{#id}`
|
|
873
|
-
{ path: "/{#id}/edit", view: ThingEdit }, // matches `/things/{#id}/edit`
|
|
874
|
-
{ path: "/{#id}/delete", view: ThingDelete }, // matches `/things/{#id}/delete`
|
|
875
|
-
],
|
|
876
|
-
},
|
|
877
|
-
],
|
|
878
|
-
},
|
|
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
|
+
],
|
|
879
554
|
},
|
|
880
555
|
],
|
|
881
556
|
});
|
|
@@ -883,41 +558,89 @@ const app = App({
|
|
|
883
558
|
|
|
884
559
|
As you may have inferred from the code above, when the URL matches a pattern the corresponding view is displayed. If we
|
|
885
560
|
visit `/people/john`, we will see the `PersonDetails` view and the params will be `{ name: "john" }`. Params can be
|
|
886
|
-
accessed
|
|
561
|
+
accessed anywhere through `Dolla.router`.
|
|
887
562
|
|
|
888
563
|
```js
|
|
889
564
|
function PersonDetails(props, ctx) {
|
|
890
|
-
// `router` store allows you to work with the router from inside the app.
|
|
891
|
-
const router = ctx.getStore(RouterStore);
|
|
892
|
-
|
|
893
565
|
// Info about the current route is exported as a set of Readables. Query params are also Writable through $$query:
|
|
894
|
-
const { $path, $pattern, $params,
|
|
566
|
+
const { $path, $pattern, $params, $query } = Dolla.router;
|
|
895
567
|
|
|
896
|
-
//
|
|
897
|
-
|
|
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.
|
|
898
570
|
|
|
899
|
-
|
|
900
|
-
|
|
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.
|
|
901
573
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
navigate("/things/152"); // Navigate to another path within the same app.
|
|
906
|
-
navigate("https://www.example.com/another/site"); // Navigate to another domain entirely.
|
|
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.
|
|
907
576
|
|
|
908
577
|
// Three ways to confirm with the user that they wish to navigate before actually doing it.
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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 });
|
|
912
581
|
|
|
913
582
|
// Get the live value of `{name}` from the current path.
|
|
914
|
-
const $name =
|
|
583
|
+
const $name = Dolla.derive([$params], (p) => p.name);
|
|
915
584
|
|
|
916
585
|
// Render it into a <p> tag. The name portion will update if the URL changes.
|
|
917
586
|
return <p>The person is: {$name}</p>;
|
|
918
587
|
}
|
|
919
588
|
```
|
|
920
589
|
|
|
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();
|
|
601
|
+
|
|
602
|
+
// Could do something with the response here.
|
|
603
|
+
|
|
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
|
+
```
|
|
612
|
+
|
|
613
|
+
## Localization
|
|
614
|
+
|
|
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
|
+
```
|
|
643
|
+
|
|
921
644
|
---
|
|
922
645
|
|
|
923
646
|
[🦆](https://www.manyducks.co)
|