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

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 (83) hide show
  1. package/README.md +81 -933
  2. package/dist/core/context.d.ts +65 -0
  3. package/dist/{modules → core}/dolla.d.ts +43 -26
  4. package/dist/core/markup.d.ts +102 -0
  5. package/dist/core/nodes/dom.d.ts +13 -0
  6. package/dist/core/nodes/dynamic.d.ts +30 -0
  7. package/dist/core/nodes/fragment.d.ts +19 -0
  8. package/dist/core/nodes/html.d.ts +34 -0
  9. package/dist/core/nodes/outlet.d.ts +20 -0
  10. package/dist/core/nodes/portal.d.ts +22 -0
  11. package/dist/core/nodes/repeat.d.ts +28 -0
  12. package/dist/core/nodes/view.d.ts +97 -0
  13. package/dist/core/signals-api.d.ts +42 -0
  14. package/dist/core/signals.d.ts +22 -0
  15. package/dist/core/store.d.ts +52 -0
  16. package/dist/core/symbols.d.ts +4 -0
  17. package/dist/{views → core/views}/default-crash-view.d.ts +1 -1
  18. package/dist/core/views/fragment.d.ts +8 -0
  19. package/dist/{views → core/views}/passthrough.d.ts +2 -2
  20. package/dist/fragment-Bvuvw3ue.js +8 -0
  21. package/dist/fragment-Bvuvw3ue.js.map +1 -0
  22. package/dist/{modules/http.d.ts → http/index.d.ts} +3 -5
  23. package/dist/index.d.ts +15 -10
  24. package/dist/index.js +1056 -1216
  25. package/dist/index.js.map +1 -1
  26. package/dist/jsx-dev-runtime.d.ts +2 -2
  27. package/dist/jsx-dev-runtime.js +8 -8
  28. package/dist/jsx-dev-runtime.js.map +1 -1
  29. package/dist/jsx-runtime.d.ts +3 -3
  30. package/dist/jsx-runtime.js +10 -10
  31. package/dist/jsx-runtime.js.map +1 -1
  32. package/dist/markup-QqAGIoYP.js +1501 -0
  33. package/dist/markup-QqAGIoYP.js.map +1 -0
  34. package/dist/{modules/router.d.ts → router/index.d.ts} +53 -60
  35. package/dist/{routing.d.ts → router/router.utils.d.ts} +17 -3
  36. package/dist/router/router.utils.test.d.ts +1 -0
  37. package/dist/translate/index.d.ts +133 -0
  38. package/dist/typeChecking.d.ts +2 -98
  39. package/dist/typeChecking.test.d.ts +1 -0
  40. package/dist/types.d.ts +17 -7
  41. package/dist/utils.d.ts +18 -3
  42. package/docs/http.md +29 -0
  43. package/docs/i18n.md +38 -0
  44. package/docs/index.md +10 -0
  45. package/docs/router.md +80 -0
  46. package/docs/setup.md +31 -0
  47. package/docs/signals.md +166 -0
  48. package/docs/state.md +141 -0
  49. package/docs/stores.md +62 -0
  50. package/docs/views.md +208 -0
  51. package/examples/webcomponent/index.html +14 -0
  52. package/examples/webcomponent/main.js +165 -0
  53. package/index.d.ts +2 -2
  54. package/notes/TODO.md +6 -0
  55. package/notes/atomic.md +452 -0
  56. package/notes/context-routes.md +61 -0
  57. package/notes/custom-nodes.md +17 -0
  58. package/notes/elimination.md +33 -0
  59. package/notes/molecule.md +35 -0
  60. package/notes/readme-scratch.md +260 -0
  61. package/notes/route-middleware.md +42 -0
  62. package/notes/scratch.md +330 -7
  63. package/notes/splitting.md +5 -0
  64. package/notes/stores.md +53 -0
  65. package/package.json +13 -10
  66. package/vite.config.js +5 -10
  67. package/build.js +0 -34
  68. package/dist/markup.d.ts +0 -100
  69. package/dist/modules/language.d.ts +0 -41
  70. package/dist/modules/render.d.ts +0 -17
  71. package/dist/nodes/cond.d.ts +0 -26
  72. package/dist/nodes/html.d.ts +0 -31
  73. package/dist/nodes/observer.d.ts +0 -29
  74. package/dist/nodes/outlet.d.ts +0 -22
  75. package/dist/nodes/portal.d.ts +0 -19
  76. package/dist/nodes/repeat.d.ts +0 -34
  77. package/dist/nodes/text.d.ts +0 -19
  78. package/dist/passthrough-CtoBcpag.js +0 -1245
  79. package/dist/passthrough-CtoBcpag.js.map +0 -1
  80. package/dist/signals.d.ts +0 -101
  81. package/dist/view.d.ts +0 -50
  82. package/tests/signals.test.js +0 -135
  83. /package/dist/{routing.test.d.ts → core/signals-api.test.d.ts} +0 -0
package/README.md CHANGED
@@ -3,994 +3,142 @@
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 releases may introduce breaking changes without notice.
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
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 [Signals](./docs/state.md).
11
+ - 📦 Reusable components with [Views](./docs/views.md).
12
+ - 💾 Reusable state management with [Stores](./docs/stores.md).
13
+ - 🔀 Client side [routing](./docs/router.md) with nested routes and middleware support (check login status, preload data, etc).
14
+ - 🐕 Built-in [HTTP](./docs/http.md) client with middleware support (set auth headers, etc).
15
+ - 📍 Lightweight [localization](./docs/i18n.md) system (store translated strings in JSON files and call the `t` function to get them).
16
+ - 🍳 Build system optional. [Write views in JSX](./docs/setup.md) or bring in [HTM](https://github.com/developit/htm) and use tagged template literals directly in the browser.
15
17
 
16
- Let's first get into some examples.
18
+ Dolla's goals include:
17
19
 
18
- ## Signals
20
+ - Be fun to create with.
21
+ - Be snappy and responsive for real life apps.
22
+ - Be compact as possible but not at the expense of necessary features.
19
23
 
20
- ### Signals API
24
+ ## Why Dolla?
21
25
 
22
- ```jsx
23
- import { createSignal, derive } from "@manyducks.co/dolla";
24
-
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
26
+ > TODO: Write about why Dolla was started and what it's all about.
33
27
 
34
- ```jsx
35
- import { createSignal } from "@manyducks.co/dolla";
28
+ - Borne of frustration using React and similar libs (useEffect, referential equality, a pain to integrate other libs into its lifecycle, need to hunt for libraries to move much beyond Hello World).
29
+ - Merges ideas from my favorite libraries and frameworks (Solid/Knockout, Choo, Svelte, i18next, etc) into one curated set designed to work well together.
30
+ - Opinionated (with the _correct_ opinions).
31
+ - Many mainstream libraries seem too big for what they do. The entirety of Dolla is less than half the size of [`react-router`](https://bundlephobia.com/package/react-router@7.1.5).
36
32
 
37
- const [$count, setCount] = createSignal(0);
38
-
39
- // Set Style 1: Set value explicitly.
40
- setCount(1); // $count = 1
41
-
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);
45
-
46
- increment(); // $count = 2
47
- increment(); // $count = 3
48
- decrement(); // $count = 2
49
-
50
- console.log($count.get()); // 2
51
- ```
33
+ ## An Example
52
34
 
53
- ### Derived State
35
+ A basic view. Note that the view function is called exactly once when the view is first mounted. All changes to DOM nodes thereafter happen as a result of `$state` values changing.
54
36
 
55
37
  ```jsx
56
- import { createSignal, derive } from "@manyducks.co/dolla";
57
-
58
- const [$count, setCount] = createSignal(0);
59
- 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
64
- ```
65
-
66
- ## A Basic View
67
-
68
- ```js
69
- import Dolla, { html } from "@manyducks.co/dolla";
38
+ import Dolla, { $ } from "@manyducks.co/dolla";
70
39
 
71
40
  function Counter(props, ctx) {
72
- const [$count, setCount] = Dolla.createSignal(0);
73
-
74
- function increment() {
75
- setCount((count) => count + 1);
76
- }
77
-
78
- return html`
79
- <div>
80
- <p>Clicks: ${$count}</p>
81
- <button onclick=${increment}>Click here to increment</button>
82
- </div>
83
- `;
84
- }
85
-
86
- Dolla.mount(document.body, Counter);
87
- ```
88
-
89
- The above example, annotated:
41
+ const count = $(0);
90
42
 
91
- ```js
92
- import Dolla, { html } from "@manyducks.co/dolla";
93
-
94
- function Counter(props, ctx) {
95
- const [$count, setCount] = Dolla.createSignal(0);
43
+ // An effect will re-run whenever any signal value accessed inside it changes.
44
+ ctx.effect(() => {
45
+ console.log(`Count is: ${count()}`);
46
+ });
96
47
 
97
48
  function increment() {
98
- setCount((count) => count + 1);
49
+ // Call signal function with a new value to set it...
50
+ count(count() + 1);
99
51
  }
100
52
 
101
- return html`
102
- <div>
103
- <p>Clicks: ${$count}</p>
104
- <button onclick=${increment}>Click here to increment</button>
105
- </div>
106
- `;
107
- }
108
-
109
- Dolla.mount(document.body, Counter);
110
- ```
111
-
112
- Localized:
113
-
114
- ```js
115
- import Dolla, { html, t } from "@manyducks.co/dolla";
116
-
117
- function Counter(props, ctx) {
118
- const [$count, setCount] = Dolla.createSignal(0);
119
-
120
- function increment() {
121
- setCount((count) => count + 1);
53
+ function decrement() {
54
+ // ... or pass a function that takes the current value and returns the next.
55
+ count((value) => value - 1);
122
56
  }
123
57
 
124
- return html`
125
- <div>
126
- <p>Clicks: ${$count}</p>
127
- <button onclick=${increment}>${t("buttonLabel")}</button>
128
- </div>
129
- `;
130
- }
131
-
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
- Dolla.mount(document.body, Counter);
141
- ```
142
-
143
- 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
-
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.
146
-
147
- ## Advanced Componentry
148
-
149
- 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.
150
-
151
- > 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.
152
-
153
- ### Props
154
-
155
- 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
-
157
- ```tsx
158
- import { type Signal, type Context, html } from "@manyducks.co/dolla";
159
-
160
- type HeadingProps = {
161
- $text: Signal<string>;
162
- };
163
-
164
- function Heading(props: HeadingProps, c: Context) {
165
- return html`<h1>${props.$text}</h1>`;
166
- }
167
-
168
- function Layout() {
169
- const [$text, setText] = signal("HELLO THERE!");
170
-
171
- return (
172
- <section>
173
- <Heading $text={$text}>
174
- </section>
175
- );
176
- }
177
- ```
178
-
179
- ### Context
180
-
181
- ```tsx
182
- import { type Signal, type Context, html } from "@manyducks.co/dolla";
183
-
184
- type HeadingProps = {
185
- $text: Signal<string>;
186
- };
187
-
188
- function Heading(props: HeadingProps, c: Context) {
189
- // A full compliment of logging functions:
190
- // Log levels that get printed can be set at the app level.
191
-
192
- c.trace("What's going on? Let's find out.");
193
- c.info("This is low priority info.");
194
- c.log("This is normal priority info.");
195
- c.warn("Hey! This could be serious.");
196
- c.error("NOT GOOD! DEFINITELY NOT GOOD!!1");
197
-
198
- // And sometimes things are just too borked to press on:
199
- c.crash(new Error("STOP THE PRESSES! BURN IT ALL DOWN!!!"));
200
-
201
- // The four lifecycle hooks:
202
-
203
- // c.beforeMount(() => {
204
- // c.info("Heading is going to be mounted. Good time to set things up.");
205
- // });
206
-
207
- c.onMount(() => {
208
- c.info("Heading has just been mounted. Good time to access the DOM and finalize setup.");
209
- });
210
-
211
- // c.beforeUnmount(() => {
212
- // c.info("Heading is going to be unmounted. Good time to begin teardown.");
213
- // });
214
-
215
- c.onUnmount(() => {
216
- c.info("Heading has just been unmounted. Good time to finalize teardown.");
217
- });
218
-
219
- // Signals can be watched by the component context.
220
- // Watchers created this way are cleaned up automatically when the component unmounts.
221
-
222
- c.watch(props.$text, (value) => {
223
- c.warn(`text has changed to: ${value}`);
224
- });
225
-
226
- return html`<h1>${props.$text}</h1>`;
227
- }
228
- ```
229
-
230
- ## Signals
231
-
232
- Basics
233
-
234
- ```jsx
235
- const [$count, setCount] = signal(0);
236
-
237
- // Set the value directly.
238
- setCount(1);
239
- setCount(2);
240
-
241
- // Transform the previous value into a new one.
242
- setCount((current) => current + 1);
243
-
244
- // This can be used to create easy helper functions:
245
- function increment(amount = 1) {
246
- setCount((current) => current + amount);
247
- }
248
- increment();
249
- increment(5);
250
- increment(-362);
251
-
252
- // Get the current value
253
- $count.get(); // -354
254
-
255
- // Watch for new values. Don't forget to call stop() to clean up!
256
- const stop = $count.watch((current) => {
257
- console.log(`count is now ${current}`);
258
- });
259
-
260
- increment(); // "count is now -353"
261
- increment(); // "count is now -352"
262
-
263
- stop();
264
- ```
265
-
266
- Derive
267
-
268
- ```jsx
269
- import { signal, derive } from "@manyducks.co/dolla";
270
-
271
- const [$names, setNames] = signal(["Morg", "Ton", "Bon"]);
272
- const [$index, setIndex] = signal(0);
273
-
274
- // Create a new signal that depends on two existing signals:
275
- const $selected = derive([$names, $index], (names, index) => names[index]);
276
-
277
- $selected.get(); // "Morg"
278
-
279
- setIndex(2);
280
-
281
- $selected.get(); // "Bon"
282
- ```
283
-
284
- Proxy
285
-
286
- ```jsx
287
- import { signal, proxy } from "@manyducks.co/dolla";
288
-
289
- const [$names, setNames] = signal(["Morg", "Ton", "Bon"]);
290
- const [$index, setIndex] = signal(0);
291
-
292
- const [$selected, setSelected] = proxy([$names, $index], {
293
- get(names, index) {
294
- return names[index];
295
- },
296
- set(next) {
297
- const index = $names.get().indexOf(next);
298
- if (index === -1) {
299
- throw new Error("Name is not in the list!");
300
- }
301
- setIndex(index);
302
- },
303
- });
304
-
305
- $selected.get(); // "Morg"
306
- $index.get(); // 0
307
-
308
- // Set selected directly by name through the proxy.
309
- setSelected("Ton");
310
-
311
- // Selected and the index have been updated to match.
312
- $selected.get(); // "Ton"
313
- $index.get(); // 1
314
- ```
315
-
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
- ## Views
448
-
449
- 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.
450
-
451
- At its most basic, a view is a function that returns elements.
452
-
453
- ```jsx
454
- function ExampleView() {
455
- return <h1>Hello World!</h1>;
456
- }
457
- ```
458
-
459
- #### View Props
460
-
461
- A view function takes a `props` object as its first argument. This object contains all properties passed to the view when it's invoked.
462
-
463
- ```js
464
- import { html } from "@manyducks.co/dolla";
465
-
466
- function ListView(props, ctx) {
467
- return html`
468
- <ul>
469
- <${ListItemView} label="Squirrel" />
470
- <${ListItemView} label="Chipmunk" />
471
- <${ListItemView} label="Groundhog" />
472
- </ul>
473
- `;
474
- }
475
-
476
- function ListItemView(props, ctx) {
477
- return html`<li>${props.label}</li>`;
478
- }
479
- ```
480
-
481
- ```jsx
482
- function ListView() {
483
- return (
484
- <ul>
485
- <ListItemView label="Squirrel" />
486
- <ListItemView label="Chipmunk" />
487
- <ListItemView label="Groundhog" />
488
- </ul>
489
- );
490
- }
491
-
492
- function ListItemView(props) {
493
- return <li>{props.label}</li>;
494
- }
495
- ```
496
-
497
- 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.
498
-
499
- ### View Helpers
500
-
501
- #### `cond($condition, whenTruthy, whenFalsy)`
502
-
503
- 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.
504
-
505
- ```jsx
506
- function ConditionalListView({ $show }) {
507
58
  return (
508
59
  <div>
509
- {cond(
510
- $show,
511
-
512
- // Visible when truthy
513
- <ul>
514
- <ListItemView label="Squirrel" />
515
- <ListItemView label="Chipmunk" />
516
- <ListItemView label="Groundhog" />
517
- </ul>,
518
-
519
- // Visible when falsy
520
- <span>List is hidden</span>,
521
- )}
522
- </div>
523
- );
524
- }
525
- ```
526
-
527
- #### `repeat($items, keyFn, renderFn)`
528
-
529
- 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.
530
-
531
- ```jsx
532
- function RepeatedListView() {
533
- const $items = $(["Squirrel", "Chipmunk", "Groundhog"]);
60
+ {/* Signals can be slotted into the DOM to render them */}
61
+ <p>Counter: {count}</p>
62
+ <div>
63
+ <button on:click={increment}>+1</button>
64
+ <button on:click={decrement}>-1</button>
65
+ </div>
534
66
 
535
- return (
536
- <ul>
537
- {repeat(
538
- $items,
539
- (item) => item, // Using the string itself as the key
540
- ($item, $index, ctx) => {
541
- return <ListItemView label={$item} />;
542
- },
67
+ {/* We can derive a new signal on the fly and conditionally render something based on that condition */}
68
+ {when(
69
+ $(() => count() > 10),
70
+ <span>That's a lot of clicks!</span>,
543
71
  )}
544
- </ul>
545
- );
546
- }
547
- ```
548
-
549
- #### `portal(content, parentNode)`
550
-
551
- 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.
552
72
 
553
- ```jsx
554
- function PortalView() {
555
- const content = (
556
- <div class="modal">
557
- <p>This is a modal.</p>
73
+ {/* ALT: Or slot a getter function into the DOM and have it conditionally render an element */}
74
+ {() => {
75
+ // DEV NOTE
76
+ // If we get Dynamic to track its rendered elements and diff them by keys
77
+ // then we may be able to do away with repeat and when and just do things like:
78
+ // <ul>{() => items().map(item => <li>{item}</li>)}</ul>
79
+ if (count() > 10) {
80
+ return <span>That's a lot of clicks!</span>;
81
+ }
82
+ }}
558
83
  </div>
559
84
  );
560
-
561
- // Content will be appended to `document.body` while this view is connected.
562
- return portal(content, document.body);
563
- }
564
- ```
565
-
566
- ### View Context
567
-
568
- 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.
569
-
570
- #### Printing Debug Messages
571
-
572
- ```jsx
573
- function ExampleView(props, ctx) {
574
- // Set the name of this view's context. Console messages are prefixed with name.
575
- ctx.name = "CustomName";
576
-
577
- // Print messages to the console. These are suppressed by default in the app's "production" mode.
578
- // You can also change which of these are printed and filter messages from certain contexts in the `createApp` options object.
579
- ctx.info("Verbose debugging info that might be useful to know");
580
- ctx.log("Standard messages");
581
- ctx.warn("Something bad might be happening");
582
- ctx.error("Uh oh!");
583
-
584
- // If you encounter a bad enough situation, you can halt and disconnect the entire app.
585
- ctx.crash(new Error("BOOM"));
586
-
587
- return <h1>Hello World!</h1>;
588
85
  }
589
- ```
590
-
591
- #### Lifecycle Events
592
-
593
- ```jsx
594
- function ExampleView(props, ctx) {
595
- ctx.beforeConnect(() => {
596
- // Do something before this view's DOM nodes are created.
597
- });
598
-
599
- ctx.onConnected(() => {
600
- // Do something immediately after this view is connected to the DOM.
601
- });
602
-
603
- ctx.beforeDisconnect(() => {
604
- // Do something before removing this view from the DOM.
605
- });
606
-
607
- ctx.onDisconnected(() => {
608
- // Do some cleanup after this view is disconnected from the DOM.
609
- });
610
86
 
611
- return <h1>Hello World!</h1>;
612
- }
87
+ Dolla.mount(document.body, Counter);
613
88
  ```
614
89
 
615
- #### Displaying Children
616
-
617
- The context object has an `outlet` function that can be used to display children at a location of your choosing.
90
+ > TODO: Show small examples for routing and stores.
618
91
 
619
92
  ```js
620
- function LayoutView(props, ctx) {
621
- return (
622
- <div className="layout">
623
- <OtherView />
624
- <div className="content">{ctx.outlet()}</div>
625
- </div>
626
- );
627
- }
628
-
629
- function ExampleView() {
630
- // <h1> and <p> are displayed inside LayoutView's outlet.
631
- return (
632
- <LayoutView>
633
- <h1>Hello</h1>
634
- <p>This is inside the box.</p>
635
- </LayoutView>
636
- );
637
- }
638
- ```
639
-
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
- #### Observing States
653
-
654
- 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.
93
+ function MessageStore(options, ctx) {
94
+ const message = $("Hello world!");
655
95
 
656
- ```jsx
657
- function ExampleView(props, ctx) {
658
- const { $someValue } = ctx.getStore(SomeStore);
659
-
660
- ctx.observe($someValue, (value) => {
661
- ctx.log("someValue is now", value);
96
+ ctx.effect(() => {
97
+ ctx.log(`Message is now: ${message()}`);
98
+ // Calling `get()` inside an effect (or compose) function will track that reactive value as a dependency.
99
+ // Effects will re-run when a dependency updates.
662
100
  });
101
+ // `ctx` refers to the context object; StoreContext in a store and ViewContext in a view.
102
+ // Context objects contain methods for controlling the component, logging and attaching lifecycle hooks.
663
103
 
664
- return <h1>Hello World!</h1>;
665
- }
666
- ```
667
-
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
104
  return {
722
- message: "Hello from the message store!",
105
+ message: $(() => message()),
106
+ setMessage: (value: string) => message(value),
723
107
  };
724
108
  }
725
109
 
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
- }
110
+ function App(props, ctx) {
111
+ const { message, setMessage } = ctx.provide(MessageStore);
112
+ // Provides a MessageStore on this context and any child contexts.
113
+ // When a store is provided its value is returned right away.
732
114
 
733
- // And a layout view with five MessageViews inside.
734
- function LayoutView() {
735
115
  return (
736
116
  <div>
737
- <h1>Title</h1>
738
- <MessageView />
739
- <MessageView />
740
117
  <MessageView />
741
118
  <MessageView />
742
119
  <MessageView />
120
+
121
+ <input
122
+ type="text"
123
+ value={message}
124
+ on:input={(e) => {
125
+ setMessage(e.currentTarget.value);
126
+ }}
127
+ />
743
128
  </div>
744
129
  );
745
130
  }
746
131
 
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
- }
132
+ function MessageView(props, ctx) {
133
+ const { message } = ctx.get(MessageStore);
134
+ // Gets the nearest instance of MessageStore. In this case the one provided at the parent.
780
135
 
781
- function LayoutView() {
782
- return (
783
- <StoreScope stores={[ExampleStore]}>
784
- <ExampleView />
785
- </StoreScope>
786
- );
136
+ return <span>{message}</span>;
787
137
  }
788
138
  ```
789
139
 
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
- #### Routing
902
-
903
- Dolla makes heavy use of client-side routing. You can define as many routes as you have views, and the URL
904
- will determine which one the app shows at any given time. By building an app around routes, lots of things one expects
905
- from a web app will just work; back and forward buttons, sharable URLs, bookmarks, etc.
906
-
907
- Routes are matched by highest specificity regardless of the order they were registered.
908
- This avoids some confusing situations that come up with order-based routers like that of `express`.
909
- On the other hand, order-based routers can support regular expressions as patterns which Dolla's router cannot.
910
-
911
- #### Route Patterns
912
-
913
- Routes are defined with strings called patterns. A pattern defines the shape the URL path must match, with special
914
- placeholders for variables that appear within the route. Values matched by those placeholders are parsed out and exposed
915
- to your code (`router` store, `$params` readable). Below are some examples of patterns and how they work.
916
-
917
- - Static: `/this/is/static` has no params and will match only when the route is exactly `/this/is/static`.
918
- - Numeric params: `/users/{#id}/edit` has the named param `{#id}` which matches numbers only, such as `123` or `52`. The
919
- resulting value will be parsed as a number.
920
- - Generic params: `/users/{name}` has the named param `{name}` which matches anything in that position in the path. The
921
- resulting value will be a string.
922
- - Wildcard: `/users/*` will match anything beginning with `/users` and store everything after that in params
923
- as `wildcard`. `*` is valid only at the end of a route.
924
-
925
- Now, here are some route examples in the context of an app:
926
-
927
- ```js
928
- import { App, RouterStore } from "@manyducks.co/dolla";
929
- import { PersonDetails, ThingIndex, ThingDetails, ThingEdit, ThingDelete } from "./components.js";
930
-
931
- const app = App({
932
- stores: [
933
- {
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
- },
952
- },
953
- ],
954
- });
955
- ```
956
-
957
- As you may have inferred from the code above, when the URL matches a pattern the corresponding view is displayed. If we
958
- 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`.
960
-
961
- ```js
962
- 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
- // 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;
971
-
972
- back(); // Step back in the history to the previous route, if any.
973
- back(2); // Hit the back button twice.
974
-
975
- forward(); // Step forward in the history to the next route, if any.
976
- forward(4); // Hit the forward button 4 times.
977
-
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.
980
-
981
- // 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 });
985
-
986
- // Get the live value of `{name}` from the current path.
987
- const $name = computed($params, (p) => p.name);
988
-
989
- // Render it into a <p> tag. The name portion will update if the URL changes.
990
- return <p>The person is: {$name}</p>;
991
- }
992
- ```
140
+ For more detail [check out the Docs](./docs/index.md).
993
141
 
994
142
  ---
995
143
 
996
- [🦆](https://www.manyducks.co)
144
+ [🦆 That's a lot of ducks.](https://www.manyducks.co)