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

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 CHANGED
@@ -7,115 +7,107 @@
7
7
 
8
8
  Dolla is a batteries-included JavaScript frontend framework covering the needs of moderate-to-complex single page apps:
9
9
 
10
- - Reactive DOM updates (Signals)
11
- - Reusable components (Views)
12
- - Routing
13
- - HTTP client
14
- - Localization (translations as JSON files and a `t` function to get strings)
10
+ - Reactive DOM updates with [State](). A similar concept to Signals without behind the scenes magic.
11
+ - 📦 Reusable components with [Views](#section-views).
12
+ - 🗺️ Built in [router]() supporting nested routes and preloading.
13
+ - Built in [HTTP]() client with middleware support.
14
+ - Built in [Localization] (translations as JSON files and a `t` function to get strings)
15
15
 
16
16
  Let's first get into some examples.
17
17
 
18
- ## Signals
19
-
20
- ### Signals API
21
-
22
- ```jsx
23
- import { createSignal, derive } from "@manyducks.co/dolla";
18
+ ## State
24
19
 
25
- // Create a readable state and setter.
26
- const [$count, setCount] = createSignal(0);
27
-
28
- // Derive a new state from one or more states.
29
- const $doubled = derive([$$count], (count) => count * 2);
30
- ```
31
-
32
- ### Basic State
20
+ ### Basic State API
33
21
 
34
22
  ```jsx
35
- import { createSignal } from "@manyducks.co/dolla";
23
+ import { createState, toState, valueOf, derive } from "@manyducks.co/dolla";
36
24
 
37
- const [$count, setCount] = createSignal(0);
25
+ const [$count, setCount] = createState(72);
38
26
 
39
- // Set Style 1: Set value explicitly.
40
- setCount(1); // $count = 1
27
+ // Get value
28
+ $count.get(): // 72
41
29
 
42
- // Set Style 2: Set value based on the current value using a callback.
43
- const increment = () => setCount((current) => current + 1);
44
- const decrement = () => setCount((current) => current - 1);
30
+ // Replace the stored value with something else
31
+ setCount(300);
32
+ $count.get(); // 300
45
33
 
46
- increment(); // $count = 2
47
- increment(); // $count = 3
48
- decrement(); // $count = 2
34
+ // You can also pass a function that takes the current value and returns a new one
35
+ setCount((current) => current + 1);
36
+ $count.get(); // 301
49
37
 
50
- console.log($count.get()); // 2
51
- ```
38
+ // Watch for changes to the value
39
+ const unwatch = $count.watch((value) => {
40
+ // This function is called immediately with the current value, then again each time the value changes.
41
+ });
42
+ unwatch(); // Stop watching for changes
52
43
 
53
- ### Derived State
44
+ // Returns the value of a state. If the value is not a state it is returned as is.
45
+ const count = valueOf($count);
46
+ const bool = valueOf(true);
54
47
 
55
- ```jsx
56
- import { createSignal, derive } from "@manyducks.co/dolla";
48
+ // Creates a state from a value. If the value is already a state it is returned as is.
49
+ const $bool = toState(true);
50
+ const $anotherCount = toState($count);
57
51
 
58
- const [$count, setCount] = createSignal(0);
52
+ // Derive a new state from one or more other states. Whenever $count changes, $doubled will follow.
59
53
  const $doubled = derive([$count], (count) => count * 2);
60
-
61
- setCount(1); // $count = 1, $doubled = 2
62
- setCount(256); // $count = 256, $doubled = 512
63
- setCount(-37); // $count = -37, $doubled = -74
54
+ const $sum = derive([$count, $doubled], (count, doubled) => count + doubled);
64
55
  ```
65
56
 
66
- ## A Basic View
57
+ 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.
67
58
 
68
- ```js
69
- import Dolla, { html } from "@manyducks.co/dolla";
59
+ ```jsx
60
+ import { createSettableState, fromSettable, toSettable } from "@manyducks.co/dolla";
70
61
 
71
- function Counter(props, ctx) {
72
- const [$count, setCount] = Dolla.createSignal(0);
62
+ // Settable states have their setter included.
63
+ const $$value = createSettableState("Test");
64
+ $$value.set("New Value");
73
65
 
74
- function increment() {
75
- setCount((count) => count + 1);
76
- }
66
+ // They can also be split into a State and Setter
67
+ const [$value, setValue] = fromSettableState($$value);
77
68
 
78
- return html`
79
- <div>
80
- <p>Clicks: ${$count}</p>
81
- <button onclick=${increment}>Click here to increment</button>
82
- </div>
83
- `;
84
- }
69
+ // And a State and Setter can be combined into a SettableState.
70
+ const $$otherValue = toSettableState($value, setValue);
85
71
 
86
- Dolla.mount(document.body, Counter);
72
+ // Or discard the setter and make it read-only using the good old toState function:
73
+ const $value = toState($$value);
87
74
  ```
88
75
 
89
- The above example, annotated:
76
+ You can also do weird proxy things like this:
90
77
 
91
- ```js
92
- import Dolla, { html } from "@manyducks.co/dolla";
78
+ ```jsx
79
+ // Create an original place for the state to live
80
+ const [$value, setValue] = createState(5);
93
81
 
94
- function Counter(props, ctx) {
95
- const [$count, setCount] = Dolla.createSignal(0);
82
+ // Derive a state that doubles the value
83
+ const $doubled = derive([$value], (value) => value * 2);
96
84
 
97
- function increment() {
98
- setCount((count) => count + 1);
99
- }
85
+ // Create a setter that takes the doubled value and sets the original $value accordingly.
86
+ const setDoubled = createSetter($doubled, (next, current) => {
87
+ setValue(next / 2);
88
+ });
100
89
 
101
- return html`
102
- <div>
103
- <p>Clicks: ${$count}</p>
104
- <button onclick=${increment}>Click here to increment</button>
105
- </div>
106
- `;
107
- }
90
+ // Bundle the derived state and setter into a SettableState to pass around.
91
+ const $$doubled = toSettableState($doubled, setDoubled);
108
92
 
109
- Dolla.mount(document.body, Counter);
93
+ // Setting the doubled state...
94
+ $$doubled.set(100);
95
+
96
+ // ... will be reflected everywhere.
97
+ $$doubled.get(); // 100
98
+ $doubled.get(); // 100
99
+ $value.get(); // 50
110
100
  ```
111
101
 
112
- Localized:
102
+ ## Views [id="section-views"]
103
+
104
+ A basic view:
113
105
 
114
106
  ```js
115
- import Dolla, { html, t } from "@manyducks.co/dolla";
107
+ import Dolla, { html } from "@manyducks.co/dolla";
116
108
 
117
109
  function Counter(props, ctx) {
118
- const [$count, setCount] = Dolla.createSignal(0);
110
+ const [$count, setCount] = Dolla.createState(0);
119
111
 
120
112
  function increment() {
121
113
  setCount((count) => count + 1);
@@ -124,25 +116,17 @@ function Counter(props, ctx) {
124
116
  return html`
125
117
  <div>
126
118
  <p>Clicks: ${$count}</p>
127
- <button onclick=${increment}>${t("buttonLabel")}</button>
119
+ <button onclick=${increment}>+1</button>
128
120
  </div>
129
121
  `;
130
122
  }
131
123
 
132
- Dolla.language.setup({
133
- initialLanguage: "en",
134
- languages: [
135
- { name: "en", strings: { buttonLabel: "Click here to increment" } },
136
- { name: "ja", strings: { buttonLabel: "ここに押して増加する" } },
137
- ],
138
- });
139
-
140
124
  Dolla.mount(document.body, Counter);
141
125
  ```
142
126
 
143
127
  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.
144
128
 
145
- 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 the `$count` signal being updated.
129
+ 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.
146
130
 
147
131
  ## Advanced Componentry
148
132
 
@@ -155,10 +139,10 @@ Component functions take two arguments; props and a `Context` object. Props are
155
139
  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.
156
140
 
157
141
  ```tsx
158
- import { type Signal, type Context, html } from "@manyducks.co/dolla";
142
+ import { type State, type Context, html } from "@manyducks.co/dolla";
159
143
 
160
144
  type HeadingProps = {
161
- $text: Signal<string>;
145
+ $text: State<string>;
162
146
  };
163
147
 
164
148
  function Heading(props: HeadingProps, c: Context) {
@@ -179,10 +163,10 @@ function Layout() {
179
163
  ### Context
180
164
 
181
165
  ```tsx
182
- import { type Signal, type Context, html } from "@manyducks.co/dolla";
166
+ import { type State, type Context, html } from "@manyducks.co/dolla";
183
167
 
184
168
  type HeadingProps = {
185
- $text: Signal<string>;
169
+ $text: State<string>;
186
170
  };
187
171
 
188
172
  function Heading(props: HeadingProps, c: Context) {
@@ -216,7 +200,7 @@ function Heading(props: HeadingProps, c: Context) {
216
200
  c.info("Heading has just been unmounted. Good time to finalize teardown.");
217
201
  });
218
202
 
219
- // Signals can be watched by the component context.
203
+ // States can be watched by the component context.
220
204
  // Watchers created this way are cleaned up automatically when the component unmounts.
221
205
 
222
206
  c.watch(props.$text, (value) => {
@@ -284,17 +268,17 @@ $selected.get(); // "Bon"
284
268
  Proxy
285
269
 
286
270
  ```jsx
287
- import { signal, proxy } from "@manyducks.co/dolla";
271
+ import { createState, createProxyState } from "@manyducks.co/dolla";
288
272
 
289
- const [$names, setNames] = signal(["Morg", "Ton", "Bon"]);
290
- const [$index, setIndex] = signal(0);
273
+ const [$names, setNames] = createState(["Morg", "Ton", "Bon"]);
274
+ const [$index, setIndex] = createState(0);
291
275
 
292
- const [$selected, setSelected] = proxy([$names, $index], {
276
+ const [$selected, setSelected] = createProxyState([$names, $index], {
293
277
  get(names, index) {
294
278
  return names[index];
295
279
  },
296
- set(next) {
297
- const index = $names.get().indexOf(next);
280
+ set(next, names, _) {
281
+ const index = names.indexOf(next);
298
282
  if (index === -1) {
299
283
  throw new Error("Name is not in the list!");
300
284
  }
@@ -313,137 +297,6 @@ $selected.get(); // "Ton"
313
297
  $index.get(); // 1
314
298
  ```
315
299
 
316
- ##
317
-
318
- States come in two varieties, each with a constructor function and a TypeScript type to match. These are:
319
-
320
- - `Readable<T>`, which has only a `.get()` method that returns the current value.
321
- - `Writable<T>`, which extends `Readable<T>` and adds a couple methods:
322
- - `.set(value: T)` to replace the stored value.
323
- - `.update(callback: (current: T) => T)` which takes a function that receives the current value and returns a new one.
324
-
325
- 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.
326
-
327
- ```js
328
- import { signal } from "@manyducks.co/dolla";
329
-
330
- // By convention, Writable names are prefixed with two dollar signs and Readable with one.
331
- const [$number, setNumber] = signal(5);
332
-
333
- // Returns the current value held by the Writable.
334
- $number.get();
335
- // Stores a new value to the Writable.
336
- setNumber(12);
337
- // Uses a callback to update the value. Takes the current value and returns the next.
338
- setNumber((current) => current + 1);
339
-
340
- // Derive a new state from an existing one.
341
- const $doubled = derive([$number], (value) => value * 2);
342
- $doubled.get(); // 26 ($number is 13)
343
-
344
- // Derive one new state from the latest values of many other states.
345
- const $many = derive([$number, $doubled], (num, doubled) => num + doubled);
346
- ```
347
-
348
- 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.
349
-
350
- ```jsx
351
- import { signal } from "@manyducks.co/dolla";
352
-
353
- function Greeter() {
354
- const [$name, setName] = signal("Valued Customer");
355
-
356
- return (
357
- <section>
358
- <header>
359
- <h1>Hello, {$name}!</h1>
360
- </header>
361
-
362
- <input
363
- value={$name}
364
- onChange={(e) => {
365
- setName(e.target.value);
366
- }}
367
- />
368
- </section>
369
- );
370
- }
371
- ```
372
-
373
- ### Computed
374
-
375
- Computed states take one or more Readables or Writables and produce a new value _computed_ from those.
376
-
377
- ```js
378
- import { $, $$ } from "@manyducks.co/dolla";
379
-
380
- const $$count = $$(100);
381
-
382
- const $double = $($$count, (value) => value * 2);
383
- ```
384
-
385
- In that example, `$$double` will always have a value derived from that of `$$count`.
386
-
387
- 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.
388
-
389
- ```js
390
- import { $, $$ } from "@manyducks.co/dolla";
391
-
392
- // Let's assume this list of users was fetched from an API somewhere.
393
- const $$people = $$([
394
- {
395
- id: 1,
396
- name: "Borb",
397
- },
398
- {
399
- id: 2,
400
- name: "Bex",
401
- },
402
- {
403
- id: 3,
404
- name: "Bleeblop",
405
- },
406
- ]);
407
-
408
- // Let's assume this ID was chosen from an input where the above users were displayed.
409
- const $$selectedId = $$(2);
410
-
411
- // Now we get the object of the person who is selected.
412
- const $selectedPerson = $($$people, $$selectedId, (people, selectedId) => {
413
- return people.find((person) => person.id === selectedId);
414
- });
415
-
416
- // Now we get a Readable of just that person's name. Say we're going to display it on the page somewhere.
417
- const $personName = $($selectedPerson, (person) => person.name);
418
-
419
- console.log($personName.get()); // "Bex"
420
- ```
421
-
422
- 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.
423
-
424
- ### Unwrap
425
-
426
- 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.
427
-
428
- ```js
429
- import { $, $$, unwrap } from "@manyducks.co/dolla";
430
-
431
- const $$number = $$(5);
432
-
433
- unwrap($$number); // 5
434
- unwrap($(5)); // 5
435
- unwrap(5); // 5
436
- ```
437
-
438
- ### Advanced Use Cases
439
-
440
- <details>
441
- <summary><code>observe</code> and <code>proxy</code></summary>
442
-
443
- > TO DO
444
-
445
- </details>
446
-
447
300
  ## Views
448
301
 
449
302
  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.
@@ -530,7 +383,7 @@ The `repeat` helper repeats a render function for each item in a list. The `keyF
530
383
 
531
384
  ```jsx
532
385
  function RepeatedListView() {
533
- const $items = $(["Squirrel", "Chipmunk", "Groundhog"]);
386
+ const $items = Dolla.toState(["Squirrel", "Chipmunk", "Groundhog"]);
534
387
 
535
388
  return (
536
389
  <ul>
@@ -559,7 +412,7 @@ function PortalView() {
559
412
  );
560
413
 
561
414
  // Content will be appended to `document.body` while this view is connected.
562
- return portal(content, document.body);
415
+ return portal(document.body, content);
563
416
  }
564
417
  ```
565
418
 
@@ -637,18 +490,6 @@ function ExampleView() {
637
490
  }
638
491
  ```
639
492
 
640
- #### Using Stores
641
-
642
- ```jsx
643
- import { UserStore } from "../stores/UserStore.js";
644
-
645
- function ExampleView(props, ctx) {
646
- const { $name } = ctx.getStore(UserStore);
647
-
648
- return <h1>Hello {$name}!</h1>;
649
- }
650
- ```
651
-
652
493
  #### Observing States
653
494
 
654
495
  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.
@@ -665,239 +506,6 @@ function ExampleView(props, ctx) {
665
506
  }
666
507
  ```
667
508
 
668
- #### Example: Counter View
669
-
670
- 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.
671
-
672
- ```jsx
673
- import { $$ } from "@manyducks.co/dolla";
674
-
675
- function CounterView(props, ctx) {
676
- const $$count = $$(0);
677
-
678
- function increment() {
679
- $$count.update((n) => n + 1);
680
- }
681
-
682
- function decrement() {
683
- $$count.update((n) => n - 1);
684
- }
685
-
686
- function reset() {
687
- $$count.set(0);
688
- }
689
-
690
- return (
691
- <div>
692
- <p>The count is {$$count}</p>
693
- <div>
694
- <button onClick={increment}>+1</button>
695
- <button onClick={decrement}>-1</button>
696
- <button onClick={reset}>Reset</button>
697
- </div>
698
- </div>
699
- );
700
- }
701
- ```
702
-
703
- ## Stores
704
-
705
- 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`.
706
-
707
- Stores are accessed with the `getStore` function available on the context object in views and other stores.
708
-
709
- Stores are helpful for managing persistent state that needs to be accessed in many places.
710
-
711
- ```js
712
- import { App } from "@manyducks.co/dolla";
713
-
714
- const app = App({
715
- view: LayoutView,
716
- stores: [MessageStore],
717
- });
718
-
719
- // We define a store that just exports a message.
720
- function MessageStore() {
721
- return {
722
- message: "Hello from the message store!",
723
- };
724
- }
725
-
726
- // All instances of MessageView will share just one instance of MessageStore.
727
- function MessageView(props, ctx) {
728
- const store = ctx.getStore(MessageStore);
729
-
730
- return <p>{store.message}</p>;
731
- }
732
-
733
- // And a layout view with five MessageViews inside.
734
- function LayoutView() {
735
- return (
736
- <div>
737
- <h1>Title</h1>
738
- <MessageView />
739
- <MessageView />
740
- <MessageView />
741
- <MessageView />
742
- <MessageView />
743
- </div>
744
- );
745
- }
746
-
747
- // Connect the app.
748
- app.connect("#app");
749
- ```
750
-
751
- The output:
752
-
753
- ```html
754
- <div id="app">
755
- <div>
756
- <h1>Title</h1>
757
- <p>Hello from the message store!</p>
758
- <p>Hello from the message store!</p>
759
- <p>Hello from the message store!</p>
760
- <p>Hello from the message store!</p>
761
- <p>Hello from the message store!</p>
762
- </div>
763
- </div>
764
- ```
765
-
766
- ### StoreScope
767
-
768
- Stores relevant to only a part of the view tree can be scoped using a `StoreScope`.
769
-
770
- ```jsx
771
- function ExampleStore() {
772
- return { value: 5 };
773
- }
774
-
775
- function ExampleView(props, ctx) {
776
- const store = ctx.getStore(ExampleStore);
777
-
778
- return <div>{store.value}</div>;
779
- }
780
-
781
- function LayoutView() {
782
- return (
783
- <StoreScope stores={[ExampleStore]}>
784
- <ExampleView />
785
- </StoreScope>
786
- );
787
- }
788
- ```
789
-
790
- ## Apps and Routing
791
-
792
- ```jsx
793
- import { App } from "@manyducks.co/dolla";
794
-
795
- const app = App({
796
- // Debug options control what gets printed from messages logged through view and store contexts.
797
- debug: {
798
- // A comma-separated list of filters. '*' means allow everything and '-dolla/*' means suppress messages with labels beginning with 'dolla/'.
799
- filter: "*,-dolla/*",
800
-
801
- // Never print ctx.info() messages
802
- info: false,
803
-
804
- // Only print ctx.log() and ctx.warn() messages in development mode
805
- log: "development",
806
- warn: "development",
807
-
808
- // Always print ctx.error() messages
809
- error: true,
810
- },
811
-
812
- mode: "development", // or "production" (enables additional debug features and logging in "development")
813
-
814
- view: (_, ctx) => {
815
- // Define a custom root view. By default this just renders any routes like so:
816
- return ctx.outlet();
817
- },
818
- });
819
- ```
820
-
821
- #### Routes and Outlets
822
-
823
- 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.
824
-
825
- ```jsx
826
- // Here is an app with a hypothetical main view with a layout and navigation:
827
- const app = App({
828
- view: (_, ctx) => {
829
- return (
830
- <div class="todo-layout">
831
- <nav>
832
- <ul>
833
- <li>
834
- <a href="/tasks">Tasks</a>
835
- </li>
836
- <li>
837
- <a href="/completed">Completed</a>
838
- </li>
839
- </ul>
840
- </nav>
841
- {/*
842
- * An outlet is where children of a view are shown.
843
- * Because this is a main view, children in this case
844
- * are the views that correspond to matched routes.
845
- */}
846
- {ctx.outlet()}
847
- </div>
848
- );
849
- },
850
-
851
- stores: [
852
- {
853
- store: RouterStore,
854
- options: {
855
- hash: true, // Use hash-based routing (default false)
856
-
857
- // Here are a couple of routes to be rendered into our layout:
858
- routes: [
859
- { path: "/tasks", view: TasksView },
860
- { path: "/completed", view: CompletedView },
861
- ],
862
- },
863
- },
864
- ],
865
- });
866
- ```
867
-
868
- Routes can also be nested. Just like the main view and its routes, subroutes will be displayed in the outlet of their parent view.
869
-
870
- ```jsx
871
- const app = App({
872
- stores: [
873
- {
874
- store: RouterStore,
875
- options: {
876
- routes: [
877
- {
878
- path: "/tasks",
879
- view: TasksView,
880
- routes: [
881
- { path: "/", view: TaskListView },
882
-
883
- // In routes, `{value}` is a dynamic value that matches anything,
884
- // and `{#value}` is a dynamic value that matches a number.
885
- { path: "/{#id}", view: TaskDetailsView },
886
- { path: "/{#id}/edit", view: TaskEditView },
887
-
888
- // If the route is any other than the ones defined above, redirect to the list.
889
- // Redirects support './' and '../' style relative paths.
890
- { path: "*", redirect: "./" },
891
- ],
892
- },
893
- { path: "/completed", view: CompletedView },
894
- ],
895
- },
896
- },
897
- ],
898
- });
899
- ```
900
-
901
509
  #### Routing
902
510
 
903
511
  Dolla makes heavy use of client-side routing. You can define as many routes as you have views, and the URL
@@ -925,30 +533,23 @@ to your code (`router` store, `$params` readable). Below are some examples of pa
925
533
  Now, here are some route examples in the context of an app:
926
534
 
927
535
  ```js
928
- import { App, RouterStore } from "@manyducks.co/dolla";
929
- import { PersonDetails, ThingIndex, ThingDetails, ThingEdit, ThingDelete } from "./components.js";
536
+ import Dolla from "@manyducks.co/dolla";
537
+ import { PersonDetails, ThingIndex, ThingDetails, ThingEdit, ThingDelete } from "./views.js";
930
538
 
931
- const app = App({
932
- stores: [
539
+ Dolla.router.setup({
540
+ routes: [
541
+ { path: "/people/{name}", view: PersonDetails },
933
542
  {
934
- store: RouterStore,
935
- options: {
936
- routes: [
937
- { path: "/people/{name}", view: PersonDetails },
938
- {
939
- // A `null` component with subroutes acts as a namespace for those subroutes.
940
- // Passing a view instead of `null` results in subroutes being rendered inside that view wherever `ctx.outlet()` is called.
941
- path: "/things",
942
- view: null,
943
- routes: [
944
- { path: "/", view: ThingIndex }, // matches `/things`
945
- { path: "/{#id}", view: ThingDetails }, // matches `/things/{#id}`
946
- { path: "/{#id}/edit", view: ThingEdit }, // matches `/things/{#id}/edit`
947
- { path: "/{#id}/delete", view: ThingDelete }, // matches `/things/{#id}/delete`
948
- ],
949
- },
950
- ],
951
- },
543
+ // A `null` component with subroutes acts as a namespace for those subroutes.
544
+ // Passing a view instead of `null` results in subroutes being rendered inside that view wherever `ctx.outlet()` is called.
545
+ path: "/things",
546
+ view: null,
547
+ routes: [
548
+ { path: "/", view: ThingIndex }, // matches `/things`
549
+ { path: "/{#id}", view: ThingDetails }, // matches `/things/{#id}`
550
+ { path: "/{#id}/edit", view: ThingEdit }, // matches `/things/{#id}/edit`
551
+ { path: "/{#id}/delete", view: ThingDelete }, // matches `/things/{#id}/delete`
552
+ ],
952
553
  },
953
554
  ],
954
555
  });
@@ -956,41 +557,89 @@ const app = App({
956
557
 
957
558
  As you may have inferred from the code above, when the URL matches a pattern the corresponding view is displayed. If we
958
559
  visit `/people/john`, we will see the `PersonDetails` view and the params will be `{ name: "john" }`. Params can be
959
- accessed inside those views through `RouterStore`.
560
+ accessed anywhere through `Dolla.router`.
960
561
 
961
562
  ```js
962
563
  function PersonDetails(props, ctx) {
963
- // `router` store allows you to work with the router from inside the app.
964
- const router = ctx.getStore(RouterStore);
965
-
966
564
  // Info about the current route is exported as a set of Readables. Query params are also Writable through $$query:
967
- const { $path, $pattern, $params, $$query } = router;
968
-
969
- // Functions are exported for navigation:
970
- const { back, forward, navigate } = router;
565
+ const { $path, $pattern, $params, $query } = Dolla.router;
971
566
 
972
- back(); // Step back in the history to the previous route, if any.
973
- back(2); // Hit the back button twice.
567
+ Dolla.router.back(); // Step back in the history to the previous route, if any.
568
+ Dolla.router.back(2); // Hit the back button twice.
974
569
 
975
- forward(); // Step forward in the history to the next route, if any.
976
- forward(4); // Hit the forward button 4 times.
570
+ Dolla.router.forward(); // Step forward in the history to the next route, if any.
571
+ Dolla.router.forward(4); // Hit the forward button 4 times.
977
572
 
978
- navigate("/things/152"); // Navigate to another path within the same app.
979
- navigate("https://www.example.com/another/site"); // Navigate to another domain entirely.
573
+ Dolla.router.go("/things/152"); // Navigate to another path within the same app.
574
+ Dolla.router.go("https://www.example.com/another/site"); // Navigate to another domain entirely.
980
575
 
981
576
  // Three ways to confirm with the user that they wish to navigate before actually doing it.
982
- navigate("/another/page", { prompt: true });
983
- navigate("/another/page", { prompt: "Are you sure you want to leave and go to /another/page?" });
984
- navigate("/another/page", { prompt: PromptView });
577
+ Dolla.router.go("/another/page", { prompt: true });
578
+ Dolla.router.go("/another/page", { prompt: "Are you sure you want to leave and go to /another/page?" });
579
+ Dolla.router.go("/another/page", { prompt: PromptView });
985
580
 
986
581
  // Get the live value of `{name}` from the current path.
987
- const $name = computed($params, (p) => p.name);
582
+ const $name = Dolla.derive([$params], (p) => p.name);
988
583
 
989
584
  // Render it into a <p> tag. The name portion will update if the URL changes.
990
585
  return <p>The person is: {$name}</p>;
991
586
  }
992
587
  ```
993
588
 
589
+ ## HTTP Client
590
+
591
+ ```js
592
+ // Middleware!
593
+ Dolla.http.use((request, next) => {
594
+ // Add auth header for all requests going to the API.
595
+ if (request.url.pathname.startsWith("/api")) {
596
+ request.headers.set("authorization", `Bearer ${authToken}`);
597
+ }
598
+
599
+ const response = await next();
600
+
601
+ // Could do something with the response here.
602
+
603
+ return response;
604
+ });
605
+
606
+ const exampleResponse = await Dolla.http.get("/api/example");
607
+
608
+ // Body is already parsed from JSON into an object.
609
+ exampleResponse.body.someValue;
610
+ ```
611
+
612
+ ## Localization
613
+
614
+ ```js
615
+ import Dolla, { html, t } from "@manyducks.co/dolla";
616
+
617
+ function Counter(props, ctx) {
618
+ const [$count, setCount] = Dolla.createState(0);
619
+
620
+ function increment() {
621
+ setCount((count) => count + 1);
622
+ }
623
+
624
+ return html`
625
+ <div>
626
+ <p>Clicks: ${$count}</p>
627
+ <button onclick=${increment}>${t("buttonLabel")}</button>
628
+ </div>
629
+ `;
630
+ }
631
+
632
+ Dolla.language.setup({
633
+ initialLanguage: "en",
634
+ languages: [
635
+ { name: "en", strings: { buttonLabel: "Click here to increment" } },
636
+ { name: "ja", strings: { buttonLabel: "ここに押して増加する" } },
637
+ ],
638
+ });
639
+
640
+ Dolla.mount(document.body, Counter);
641
+ ```
642
+
994
643
  ---
995
644
 
996
645
  [🦆](https://www.manyducks.co)