@manyducks.co/dolla 2.0.0-alpha.9 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +222 -512
  2. package/dist/core/app.d.ts +24 -0
  3. package/dist/core/context.d.ts +147 -0
  4. package/dist/core/env.d.ts +3 -0
  5. package/dist/core/hooks.d.ts +70 -0
  6. package/dist/core/hooks.test.d.ts +1 -0
  7. package/dist/core/index.d.ts +25 -0
  8. package/dist/core/logger.d.ts +42 -0
  9. package/dist/core/logger.test.d.ts +0 -0
  10. package/dist/core/markup.d.ts +82 -0
  11. package/dist/core/markup.test.d.ts +0 -0
  12. package/dist/core/nodes/_markup.d.ts +36 -0
  13. package/dist/core/nodes/dom.d.ts +13 -0
  14. package/dist/core/nodes/dynamic.d.ts +22 -0
  15. package/dist/core/nodes/element.d.ts +27 -0
  16. package/dist/core/nodes/portal.d.ts +18 -0
  17. package/dist/core/nodes/repeat.d.ts +27 -0
  18. package/dist/core/nodes/view.d.ts +25 -0
  19. package/dist/core/ref.d.ts +19 -0
  20. package/dist/core/ref.test.d.ts +1 -0
  21. package/dist/core/signals.d.ts +100 -0
  22. package/dist/core/signals.test.d.ts +1 -0
  23. package/dist/{views → core/views}/default-crash-view.d.ts +11 -4
  24. package/dist/core/views/for.d.ts +21 -0
  25. package/dist/core/views/fragment.d.ts +7 -0
  26. package/dist/core/views/portal.d.ts +16 -0
  27. package/dist/core/views/show.d.ts +25 -0
  28. package/dist/fragment-BahD_BJA.js +7 -0
  29. package/dist/fragment-BahD_BJA.js.map +1 -0
  30. package/dist/{modules/http.d.ts → http/index.d.ts} +3 -5
  31. package/dist/http.js +150 -0
  32. package/dist/http.js.map +1 -0
  33. package/dist/i18n/index.d.ts +134 -0
  34. package/dist/i18n.js +309 -0
  35. package/dist/i18n.js.map +1 -0
  36. package/dist/index-DRJlxs-Q.js +535 -0
  37. package/dist/index-DRJlxs-Q.js.map +1 -0
  38. package/dist/index.js +160 -1414
  39. package/dist/index.js.map +1 -1
  40. package/dist/jsx-dev-runtime.d.ts +3 -2
  41. package/dist/jsx-dev-runtime.js +5 -12
  42. package/dist/jsx-dev-runtime.js.map +1 -1
  43. package/dist/jsx-runtime.d.ts +4 -3
  44. package/dist/jsx-runtime.js +9 -15
  45. package/dist/jsx-runtime.js.map +1 -1
  46. package/dist/logger-Aqi9m1CF.js +565 -0
  47. package/dist/logger-Aqi9m1CF.js.map +1 -0
  48. package/dist/markup-8jNhoqDe.js +1089 -0
  49. package/dist/markup-8jNhoqDe.js.map +1 -0
  50. package/dist/router/hooks.d.ts +2 -0
  51. package/dist/router/index.d.ts +3 -0
  52. package/dist/router/router.d.ts +166 -0
  53. package/dist/{routing.d.ts → router/router.utils.d.ts} +17 -3
  54. package/dist/router/router.utils.test.d.ts +1 -0
  55. package/dist/router.js +6 -0
  56. package/dist/router.js.map +1 -0
  57. package/dist/typeChecking-5kmX0ulW.js +65 -0
  58. package/dist/typeChecking-5kmX0ulW.js.map +1 -0
  59. package/dist/typeChecking.d.ts +2 -98
  60. package/dist/typeChecking.test.d.ts +1 -0
  61. package/dist/types.d.ts +97 -25
  62. package/dist/utils.d.ts +25 -3
  63. package/docs/buildless.md +132 -0
  64. package/docs/components.md +238 -0
  65. package/docs/hooks.md +356 -0
  66. package/docs/http.md +178 -0
  67. package/docs/i18n.md +220 -0
  68. package/docs/index.md +10 -0
  69. package/docs/markup.md +136 -0
  70. package/docs/mixins.md +176 -0
  71. package/docs/ref.md +77 -0
  72. package/docs/router.md +281 -0
  73. package/docs/setup.md +137 -0
  74. package/docs/signals.md +262 -0
  75. package/docs/stores.md +113 -0
  76. package/docs/views.md +356 -0
  77. package/index.d.ts +2 -2
  78. package/notes/atomic.md +452 -0
  79. package/notes/elimination.md +33 -0
  80. package/notes/observable.md +180 -0
  81. package/notes/scratch.md +346 -14
  82. package/notes/splitting.md +5 -0
  83. package/package.json +29 -15
  84. package/vite.config.js +5 -11
  85. package/build.js +0 -34
  86. package/dist/index.d.ts +0 -21
  87. package/dist/markup.d.ts +0 -108
  88. package/dist/modules/dolla.d.ts +0 -111
  89. package/dist/modules/i18n.d.ts +0 -59
  90. package/dist/modules/render.d.ts +0 -17
  91. package/dist/modules/router.d.ts +0 -152
  92. package/dist/nodes/cond.d.ts +0 -26
  93. package/dist/nodes/html.d.ts +0 -31
  94. package/dist/nodes/observer.d.ts +0 -29
  95. package/dist/nodes/outlet.d.ts +0 -22
  96. package/dist/nodes/portal.d.ts +0 -19
  97. package/dist/nodes/repeat.d.ts +0 -34
  98. package/dist/nodes/text.d.ts +0 -19
  99. package/dist/passthrough-9kwwjgWk.js +0 -1279
  100. package/dist/passthrough-9kwwjgWk.js.map +0 -1
  101. package/dist/state.d.ts +0 -101
  102. package/dist/view.d.ts +0 -65
  103. package/dist/views/passthrough.d.ts +0 -5
  104. package/notes/context-vars.md +0 -21
  105. package/notes/readme-scratch.md +0 -222
  106. package/notes/route-middleware.md +0 -42
  107. package/tests/state.test.js +0 -135
  108. /package/dist/{routing.test.d.ts → core/context.test.d.ts} +0 -0
package/README.md CHANGED
@@ -3,644 +3,354 @@
3
3
  ![bundle size](https://img.shields.io/bundlephobia/min/@manyducks.co/dolla)
4
4
  ![bundle size](https://img.shields.io/bundlephobia/minzip/@manyducks.co/dolla)
5
5
 
6
- > WARNING: This package is in active development. It may contain serious bugs and docs may be outdated or inaccurate. Use at your own risk.
6
+ Alright, so Dolla is this new web framework. Ngl, it's pretty sick. It feels like you're writing React, which is cool, but it's also got this crazy fast reactivity thing going on under the hood. It’s built to feel super familiar, but like, way easier to figure out and it comes with all the stuff you actually need to build something.
7
7
 
8
- Dolla is a batteries-included JavaScript frontend framework covering the needs of moderate-to-complex single page apps:
8
+ - ⚡️ [**Signals**](./docs/signals.md) make your UI updates hit different. Your DOM just refreshes instantly, it's lowkey magic.
9
+ - 📦 You got options for [components](./docs/components.md), three different vibes:
10
+ - 🖥️ [**Views**](./docs/views.md) are for the UI glow up. You know the drill.
11
+ - 💾 [**Stores**](./docs/stores.md) are for when your components need to share state without all the drama. We don't do prop drilling in this house.
12
+ - ✨ [**Mixins**](./docs/mixins.md) give your plain HTML elements some extra rizz. Slay.
13
+ - 🪝 [**Hooks**](./docs/hooks.md) let your components actually cook. They're your familiar, React-style toolkit for state (`useSignal`), lifecycle (`useMount`), and more.
14
+ - 🔀 The client-side [**router**](./docs/router.md) actually understands the assignment. Nested routes, middleware for gatekeeping pages (iykyk), preloading data so it's not laggy... it's all there.
15
+ - 🐕 It comes with its own [**HTTP client**](./docs/http.md) so you can stop installing axios. It's got middleware too, so adding auth headers to every request is easy. We stan.
16
+ - 📍 A lowkey [**i18n system**](./docs/i18n.md). Just yeet your translations into a JSON file and the `t` function pulls them. Simple.
17
+ - 🍳 And the biggest flex? The build system is optional. You can [write your JSX like always](./docs/setup.md), or just [use tagged template literals](./docs/buildless.md) straight in the browser with [HTM](https://github.com/developit/htm). It's a whole vibe.
9
18
 
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.
19
+ ## Check it out: The Counter Example
16
20
 
17
- Let's first get into some examples.
18
-
19
- ## State
20
-
21
- ### Basic State API
22
-
23
- ```jsx
24
- import { createState, toState, valueOf, derive } from "@manyducks.co/dolla";
25
-
26
- const [$count, setCount] = createState(72);
27
-
28
- // Get value
29
- $count.get(): // 72
30
-
31
- // Replace the stored value with something else
32
- setCount(300);
33
- $count.get(); // 300
34
-
35
- // You can also pass a function that takes the current value and returns a new one
36
- setCount((current) => current + 1);
37
- $count.get(); // 301
38
-
39
- // Watch for changes to the value
40
- const unwatch = $count.watch((value) => {
41
- // This function is called immediately with the current value, then again each time the value changes.
42
- });
43
- unwatch(); // Stop watching for changes
44
-
45
- // Returns the value of a state. If the value is not a state it is returned as is.
46
- const count = valueOf($count);
47
- const bool = valueOf(true);
48
-
49
- // Creates a state from a value. If the value is already a state it is returned as is.
50
- const $bool = toState(true);
51
- const $anotherCount = toState($count);
52
-
53
- // Derive a new state from one or more other states. Whenever $count changes, $doubled will follow.
54
- const $doubled = derive([$count], (count) => count * 2);
55
- const $sum = derive([$count, $doubled], (count, doubled) => count + doubled);
56
- ```
57
-
58
- States also come in a settable variety that includes the setter on the same object. Sometimes you want to pass around a two-way binding and this is what SettableState is for.
21
+ The best way to get it is to just see it. If you've ever touched React, you'll know what's up, but peep the little things that make your life way easier.
59
22
 
60
23
  ```jsx
61
- import { createSettableState, fromSettable, toSettable } from "@manyducks.co/dolla";
62
-
63
- // Settable states have their setter included.
64
- const $$value = createSettableState("Test");
65
- $$value.set("New Value");
66
-
67
- // They can also be split into a State and Setter
68
- const [$value, setValue] = fromSettableState($$value);
24
+ import { useSignal, useEffect, createApp } from "@manyducks.co/dolla";
69
25
 
70
- // And a State and Setter can be combined into a SettableState.
71
- const $$otherValue = toSettableState($value, setValue);
72
-
73
- // Or discard the setter and make it read-only using the good old toState function:
74
- const $value = toState($$value);
75
- ```
76
-
77
- You can also do weird proxy things like this:
78
-
79
- ```jsx
80
- // Create an original place for the state to live
81
- const [$value, setValue] = createState(5);
82
-
83
- // Derive a state that doubles the value
84
- const $doubled = derive([$value], (value) => value * 2);
85
-
86
- // Create a setter that takes the doubled value and sets the original $value accordingly.
87
- const setDoubled = createSetter($doubled, (next, current) => {
88
- setValue(next / 2);
89
- });
90
-
91
- // Bundle the derived state and setter into a SettableState to pass around.
92
- const $$doubled = toSettableState($doubled, setDoubled);
93
-
94
- // Setting the doubled state...
95
- $$doubled.set(100);
96
-
97
- // ... will be reflected everywhere.
98
- $$doubled.get(); // 100
99
- $doubled.get(); // 100
100
- $value.get(); // 50
101
- ```
102
-
103
- ## Views [id="section-views"]
104
-
105
- A basic view:
106
-
107
- ```js
108
- import Dolla, { createState, html } from "@manyducks.co/dolla";
109
-
110
- function Counter(props, ctx) {
111
- const [$count, setCount] = createState(0);
26
+ function Counter() {
27
+ // 1. Make a reactive thingy, we call it a "signal".
28
+ const [$count, setCount] = useSignal(0);
112
29
 
113
30
  function increment() {
114
- setCount((count) => count + 1);
31
+ setCount((current) => current + 1);
115
32
  }
116
33
 
117
- return html`
118
- <div>
119
- <p>Clicks: ${$count}</p>
120
- <button onclick=${increment}>+1</button>
121
- </div>
122
- `;
123
- }
124
-
125
- Dolla.mount(document.body, Counter);
126
- ```
127
-
128
- If you've ever used React before (and chances are you have if you're interested in obscure frameworks like this one) this should look very familiar to you.
129
-
130
- The biggest difference is that the Counter function runs only once when the component is mounted. All updates after that point are a direct result of `$count` being updated.
131
-
132
- ## Advanced Componentry
133
-
134
- Component functions take two arguments; props and a `Context` object. Props are passed from parent components to child components, and `Context` is provided by the app.
135
-
136
- > The following examples are shown in TypeScript for clarity. Feel free to omit the type annotations in your own code if you prefer vanilla JS.
137
-
138
- ### Props
139
-
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.
141
-
142
- ```tsx
143
- import { type State, type Context, html } from "@manyducks.co/dolla";
144
-
145
- type HeadingProps = {
146
- $text: State<string>;
147
- };
148
-
149
- function Heading(props: HeadingProps, c: Context) {
150
- return html`<h1>${props.$text}</h1>`;
151
- }
152
-
153
- function Layout() {
154
- const [$text, setText] = signal("HELLO THERE!");
34
+ // 2. This effect just works. It knows to re-run when $count changes. No drama.
35
+ useEffect(() => {
36
+ // to get a signal's value, just call it like a function. easy.
37
+ console.log("Count is: " + $count());
38
+ });
155
39
 
156
40
  return (
157
- <section>
158
- <Heading $text={$text}>
159
- </section>
41
+ <div>
42
+ {/* 3. In your HTML, just drop the signal right in. */}
43
+ <p>Counter: {$count}</p>
44
+
45
+ {/* 4. Using signals with helpers like <Show> is a total breeze. */}
46
+ <Show when={() => $count() > 100}>
47
+ <p>Whoa, that's a lotta clicks!</p>
48
+ </Show>
49
+
50
+ <div>
51
+ <button onClick={increment}>+1</button>
52
+ {/* ... other buttons */}
53
+ </div>
54
+ </div>
160
55
  );
161
56
  }
162
57
  ```
163
58
 
164
- ### Context
165
-
166
- ```tsx
167
- import { type State, type Context, html } from "@manyducks.co/dolla";
59
+ ### 1\. `useSignal` - State that's actually simple
168
60
 
169
- type HeadingProps = {
170
- $text: State<string>;
171
- };
61
+ You make state with `useSignal()`, and it gives you back a `[getter, setter]` pair, just like `useState` in React.
172
62
 
173
- function Heading(props: HeadingProps, c: Context) {
174
- // A full compliment of logging functions:
175
- // Log levels that get printed can be set at the app level.
63
+ - `$count`: This is the **signal**. We just use a `$` at the start by convention. Think of it as a reactive value you can just plop into your JSX.
64
+ - `setCount`: This is how you change the value. Works just like you'd think.
176
65
 
177
- c.trace("What's going on? Let's find out.");
178
- c.info("This is low priority info.");
179
- c.log("This is normal priority info.");
180
- c.warn("Hey! This could be serious.");
181
- c.error("NOT GOOD! DEFINITELY NOT GOOD!!1");
66
+ When you need the value in your JS code (like in a `useEffect`), just call it like a function: `$count()`.
182
67
 
183
- // And sometimes things are just too borked to press on:
184
- c.crash(new Error("STOP THE PRESSES! BURN IT ALL DOWN!!!"));
185
-
186
- // The four lifecycle hooks:
187
-
188
- // c.beforeMount(() => {
189
- // c.info("Heading is going to be mounted. Good time to set things up.");
190
- // });
191
-
192
- c.onMount(() => {
193
- c.info("Heading has just been mounted. Good time to access the DOM and finalize setup.");
194
- });
195
-
196
- // c.beforeUnmount(() => {
197
- // c.info("Heading is going to be unmounted. Good time to begin teardown.");
198
- // });
199
-
200
- c.onUnmount(() => {
201
- c.info("Heading has just been unmounted. Good time to finalize teardown.");
202
- });
203
-
204
- // States can be watched by the component context.
205
- // Watchers created this way are cleaned up automatically when the component unmounts.
206
-
207
- c.watch(props.$text, (value) => {
208
- c.warn(`text has changed to: ${value}`);
209
- });
68
+ ### 2\. Effects without the headache
210
69
 
211
- return html`<h1>${props.$text}</h1>`;
212
- }
213
- ```
70
+ `useEffect` and `useMemo` are here, but they're way more chill.
214
71
 
215
- ## Signals
72
+ - **Automatic Tracking (by default\!)**: You literally don't have to do anything. Dolla just sees what signals you used and re-runs your code when they change.
73
+ - **Manual Tracking (if you're feeling extra)**: If you _really_ want to, you can give it an array of signals to watch. Then it'll _only_ pay attention to those.
216
74
 
217
- Basics
75
+ <!-- end list -->
218
76
 
219
77
  ```jsx
220
- const [$count, setCount] = signal(0);
221
-
222
- // Set the value directly.
223
- setCount(1);
224
- setCount(2);
225
-
226
- // Transform the previous value into a new one.
227
- setCount((current) => current + 1);
228
-
229
- // This can be used to create easy helper functions:
230
- function increment(amount = 1) {
231
- setCount((current) => current + amount);
232
- }
233
- increment();
234
- increment(5);
235
- increment(-362);
78
+ const [$count, setCount] = useSignal(0);
79
+ const [$name, setName] = useSignal("Dolla");
236
80
 
237
- // Get the current value
238
- $count.get(); // -354
239
-
240
- // Watch for new values. Don't forget to call stop() to clean up!
241
- const stop = $count.watch((current) => {
242
- console.log(`count is now ${current}`);
81
+ // AUTOMATIC: Runs if $count OR $name changes. Simple.
82
+ useEffect(() => {
83
+ console.log(`Yo ${$name()}, the count is ${$count()}`);
243
84
  });
244
85
 
245
- increment(); // "count is now -353"
246
- increment(); // "count is now -352"
247
-
248
- stop();
86
+ // MANUAL: This ONLY runs when $count changes.
87
+ // We're using $name() in here, but the effect is like "I don't see it" lol.
88
+ useEffect(() => {
89
+ console.log(`Yo ${$name()}, the count is ${$count()}`);
90
+ }, [$count]);
249
91
  ```
250
92
 
251
- Derive
93
+ ### 3\. No VDOM, no problem
252
94
 
253
- ```jsx
254
- import { signal, derive } from "@manyducks.co/dolla";
95
+ Behind the scenes, Dolla isn't re-running your whole component all the time. Nah. It makes a direct connection from your signal to the exact spot in the HTML that uses it.
255
96
 
256
- const [$names, setNames] = signal(["Morg", "Ton", "Bon"]);
257
- const [$index, setIndex] = signal(0);
97
+ When you `setCount(1)`, Dolla knows only the `<p>` tag and the `<Show>` component care. So it just updates those two things. It's like a ninja. This means no VDOM overhead and it's fast af by default.
258
98
 
259
- // Create a new signal that depends on two existing signals:
260
- const $selected = derive([$names, $index], (names, index) => names[index]);
99
+ ## The Dolla Building Blocks
261
100
 
262
- $selected.get(); // "Morg"
101
+ Dolla gives you a few types of components to keep your code from becoming a mess: **Views**, **Stores**, and **Mixins**. They're all connected by this thing called **context**, which you can grab with `useContext()`.
263
102
 
264
- setIndex(2);
265
-
266
- $selected.get(); // "Bon"
267
- ```
103
+ ### 1\. Views: Your UI stuff
268
104
 
269
- Proxy
105
+ **Views** are your normal, everyday components for putting stuff on the screen. If you know React components, you're already a pro. They get `props`, use hooks, and return JSX.
270
106
 
271
107
  ```jsx
272
- import { createState, createProxyState } from "@manyducks.co/dolla";
273
-
274
- const [$names, setNames] = createState(["Morg", "Ton", "Bon"]);
275
- const [$index, setIndex] = createState(0);
276
-
277
- const [$selected, setSelected] = createProxyState([$names, $index], {
278
- get(names, index) {
279
- return names[index];
280
- },
281
- set(next, names, _) {
282
- const index = names.indexOf(next);
283
- if (index === -1) {
284
- throw new Error("Name is not in the list!");
285
- }
286
- setIndex(index);
287
- },
288
- });
108
+ function ExampleView(props) {
109
+ const context = useContext();
110
+ const [$count, setCount] = useSignal(0);
289
111
 
290
- $selected.get(); // "Morg"
291
- $index.get(); // 0
112
+ // The logger automatically knows the component's name!
113
+ context.log("sup from ExampleView");
292
114
 
293
- // Set selected directly by name through the proxy.
294
- setSelected("Ton");
115
+ useMount(() => context.log("we're live!"));
116
+ useUnmount(() => context.log("aight, i'm out"));
295
117
 
296
- // Selected and the index have been updated to match.
297
- $selected.get(); // "Ton"
298
- $index.get(); // 1
118
+ return <div>{$count}</div>;
119
+ }
299
120
  ```
300
121
 
301
- ## Views
122
+ [More on views.](./docs/views.md)
302
123
 
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.
124
+ ### 2\. Stores: For your shared state
304
125
 
305
- At its most basic, a view is a function that returns elements.
126
+ Got some state you need to use in a bunch of different places? **Stores** are for that. It's Dolla's built-in way to handle state so you don't have to go install another library.
306
127
 
307
128
  ```jsx
308
- function ExampleView() {
309
- return <h1>Hello World!</h1>;
310
- }
311
- ```
312
-
313
- #### View Props
129
+ function CounterStore() {
130
+ const [$value, setValue] = useSignal(0);
314
131
 
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.
132
+ // You can return functions to control how the state gets changed.
133
+ const increment = () => setValue((current) => current + 1);
134
+ const decrement = () => setValue((current) => current - 1);
316
135
 
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>`;
136
+ return { $value, increment, decrement };
332
137
  }
333
138
  ```
334
139
 
140
+ You "provide" a store to a part of your app, and any component inside can just grab it.
141
+
335
142
  ```jsx
336
- function ListView() {
143
+ function App() {
144
+ // Now, App and any components inside it can use CounterStore.
145
+ const counter = useStoreProvider(CounterStore);
146
+
337
147
  return (
338
- <ul>
339
- <ListItemView label="Squirrel" />
340
- <ListItemView label="Chipmunk" />
341
- <ListItemView label="Groundhog" />
342
- </ul>
148
+ <div>
149
+ <CounterView />
150
+ <button onClick={counter.increment}>Increment</button>
151
+ </div>
343
152
  );
344
153
  }
345
154
 
346
- function ListItemView(props) {
347
- return <li>{props.label}</li>;
155
+ function CounterView() {
156
+ // Just ask for the store you need!
157
+ const counter = useStore(CounterStore);
158
+ return <p>Current value: {counter.$value}</p>;
348
159
  }
349
160
  ```
350
161
 
351
- As you may have guessed, you can pass States as props and slot them in in exactly the same way. This is important because Views do not re-render the way you might expect from other frameworks. Whatever you pass as props is what the View gets for its entire lifecycle.
352
-
353
- ### View Helpers
162
+ [More on stores.](./docs/stores.md)
354
163
 
355
- #### `cond($condition, whenTruthy, whenFalsy)`
164
+ ### 3\. Mixins: Reusable superpowers
356
165
 
357
- The `cond` helper does conditional rendering. When `$condition` is truthy, the second argument is rendered. When `$condition` is falsy the third argument is rendered. Either case can be left null or undefined if you don't want to render something for that condition.
166
+ **Mixins** are a super cool way to add reusable behaviors to your HTML elements. A mixin is just a function you can slap onto any element, and it can have its own state and lifecycle. It's perfect for stuff like logging, animations, or whatever else you can dream up.
358
167
 
359
168
  ```jsx
360
- function ConditionalListView({ $show }) {
169
+ function logLifecycle() {
170
+ // A mixin is just a function...
171
+ return (element) => {
172
+ // ...that takes an element and can use hooks inside.
173
+ const context = useContext();
174
+ useMount(() => context.log("element mounted!", element));
175
+ useUnmount(() => context.log("element unmounted!"));
176
+ };
177
+ }
178
+
179
+ // Then you can use it in any View.
180
+ function MyComponent() {
361
181
  return (
362
182
  <div>
363
- {cond(
364
- $show,
365
-
366
- // Visible when truthy
367
- <ul>
368
- <ListItemView label="Squirrel" />
369
- <ListItemView label="Chipmunk" />
370
- <ListItemView label="Groundhog" />
371
- </ul>,
372
-
373
- // Visible when falsy
374
- <span>List is hidden</span>,
375
- )}
183
+ {/* Just call it in the `mixin` prop. */}
184
+ <h1 mixin={logLifecycle()}>I'll log when I show up and leave.</h1>
185
+
186
+ {/* You can even use an array of 'em! */}
187
+ <p mixin={[logLifecycle(), otherMixin()]}>Me too!</p>
376
188
  </div>
377
189
  );
378
190
  }
379
191
  ```
380
192
 
381
- #### `repeat($items, keyFn, renderFn)`
193
+ [More on mixins.](./docs/mixins.md)
382
194
 
383
- The `repeat` helper repeats a render function for each item in a list. The `keyFn` takes an item's value and returns a number, string or Symbol that uniquely identifies that list item. If `$items` changes or gets reordered, all rendered items with matching keys will be reused, those no longer in the list will be removed and those that didn't previously have a matching key are created.
195
+ ### 4\. So, what's this "Context" thing anyway?
384
196
 
385
- ```jsx
386
- function RepeatedListView() {
387
- const $items = Dolla.toState(["Squirrel", "Chipmunk", "Groundhog"]);
197
+ **Context** is basically the glue that holds all your components together. Think of it like a family tree. Every component has its own context, but it's linked to its parent. This lets you do some cool stuff:
388
198
 
389
- return (
390
- <ul>
391
- {repeat(
392
- $items,
393
- (item) => item, // Using the string itself as the key
394
- ($item, $index, ctx) => {
395
- return <ListItemView label={$item} />;
396
- },
397
- )}
398
- </ul>
399
- );
400
- }
401
- ```
199
+ - **Finding Things**: When you do `useStore(CounterStore)`, Dolla just climbs up the family tree from your component until it finds where you provided the store. It's how some component deep in your app can get state from way up top.
200
+ - **Helpful Tools**: The context itself has some neat tricks. The logging methods (`.log()`, `.warn()`, etc.) automatically know your component's name, which is awesome for debugging. There's even a `.crash()` that'll just stop the app and show an error page if things go really wrong. You can get and set the component's name with `context.getName()` and `context.setName()`.
201
+ - **Deep-level State**: Context has its own key-value state system with `setState()` and `getState()`. The framework uses this a bunch. It's there if you wanna get wild, but tbh, for your app's state, **you should probably just use Stores.** They're way easier.
402
202
 
403
- #### `portal(content, parentNode)`
203
+ ## Batteries Included: All The Stuff You Get\! 🧰
404
204
 
405
- The `portal` helper displays DOM elements from a view as children of a parent element elsewhere in the document. Portals are typically used to display modals and other content that needs to appear at the top level of a document.
205
+ Dolla isn't just for rendering. We threw in a bunch of tools so you can stop hunting around on npm.
406
206
 
407
- ```jsx
408
- function PortalView() {
409
- const content = (
410
- <div class="modal">
411
- <p>This is a modal.</p>
412
- </div>
413
- );
207
+ ### A Router that Doesn't Suck
414
208
 
415
- // Content will be appended to `document.body` while this view is connected.
416
- return portal(document.body, content);
417
- }
418
- ```
209
+ Dolla has a router for making multi-page apps. It just works. Back/forward buttons, bookmarks, all that jazz. It's also smart and always picks the _most specific_ route, so you don't get weird bugs based on the order you write your routes.
419
210
 
420
- ### View Context
211
+ #### Route Patterns
421
212
 
422
- A view function takes a context object as its second argument. The context provides a set of functions you can use to respond to lifecycle events, observe dynamic data, print debug messages and display child elements among other things.
213
+ - **Static**: `/dashboard/settings`
214
+ - **Number Param** (only matches numbers): `/users/{#id}`
215
+ - **Anything Param**: `/users/{name}`
216
+ - **Wildcard**: `/files/*`
423
217
 
424
- #### Printing Debug Messages
218
+ #### Setting it Up
425
219
 
426
220
  ```jsx
427
- function ExampleView(props, ctx) {
428
- // Set the name of this view's context. Console messages are prefixed with name.
429
- ctx.name = "CustomName";
221
+ import { createApp } from "@manyducks.co/dolla";
222
+ import { createRouter } from "@manyducks.co/dolla/router";
223
+ import { ThingIndex, ThingDetails, ThingEdit } from "./views.js";
430
224
 
431
- // Print messages to the console. These are suppressed by default in the app's "production" mode.
432
- // You can also change which of these are printed and filter messages from certain contexts in the `createApp` options object.
433
- ctx.info("Verbose debugging info that might be useful to know");
434
- ctx.log("Standard messages");
435
- ctx.warn("Something bad might be happening");
436
- ctx.error("Uh oh!");
437
-
438
- // If you encounter a bad enough situation, you can halt and disconnect the entire app.
439
- ctx.crash(new Error("BOOM"));
225
+ const router = createRouter({
226
+ routes: [
227
+ {
228
+ path: "/things",
229
+ view: null, // a null view just groups routes
230
+ routes: [
231
+ { path: "/", view: ThingIndex }, // matches `/things`
232
+ { path: "/{#id}", view: ThingDetails }, // matches `/things/123`
233
+ { path: "/{#id}/edit", view: ThingEdit }, // matches `/things/123/edit`
234
+ ],
235
+ },
236
+ { path: "*", redirect: "/things" }, // catch-all
237
+ ],
238
+ });
440
239
 
441
- return <h1>Hello World!</h1>;
442
- }
240
+ const app = createApp(router);
241
+ app.mount(document.body);
443
242
  ```
444
243
 
445
- #### Lifecycle Events
244
+ #### Using It
446
245
 
447
- ```jsx
448
- function ExampleView(props, ctx) {
449
- ctx.beforeConnect(() => {
450
- // Do something before this view's DOM nodes are created.
451
- });
246
+ Just use the `useRouter()` hook.
452
247
 
453
- ctx.onConnected(() => {
454
- // Do something immediately after this view is connected to the DOM.
455
- });
248
+ ```jsx
249
+ import { useEffect } from "@manyducks.co/dolla";
250
+ import { useRouter } from "@manyducks.co/dolla/router";
456
251
 
457
- ctx.beforeDisconnect(() => {
458
- // Do something before removing this view from the DOM.
459
- });
252
+ function ThingDetails() {
253
+ const router = useRouter();
254
+ const { $params } = router; // get reactive params
460
255
 
461
- ctx.onDisconnected(() => {
462
- // Do some cleanup after this view is disconnected from the DOM.
256
+ useEffect(() => {
257
+ console.log("Current thing ID:", $params().id);
463
258
  });
464
259
 
465
- return <h1>Hello World!</h1>;
466
- }
467
- ```
468
-
469
- #### Displaying Children
470
-
471
- The context object has an `outlet` function that can be used to display children at a location of your choosing.
260
+ function goToNext() {
261
+ const nextId = $params().id + 1;
262
+ router.go(`/things/${nextId}`);
263
+ }
472
264
 
473
- ```js
474
- function LayoutView(props, ctx) {
475
265
  return (
476
- <div className="layout">
477
- <OtherView />
478
- <div className="content">{ctx.outlet()}</div>
266
+ <div>
267
+ <p>Viewing thing #{$params().id}</p>
268
+ <button onClick={goToNext}>View Next Thing</button>
479
269
  </div>
480
270
  );
481
271
  }
482
-
483
- function ExampleView() {
484
- // <h1> and <p> are displayed inside LayoutView's outlet.
485
- return (
486
- <LayoutView>
487
- <h1>Hello</h1>
488
- <p>This is inside the box.</p>
489
- </LayoutView>
490
- );
491
- }
492
272
  ```
493
273
 
494
- #### Observing States
274
+ ### A Built-in HTTP Client
495
275
 
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.
276
+ Dolla has a little `http` client for API calls. It automatically parses JSON and has a sick middleware system.
497
277
 
498
278
  ```jsx
499
- function ExampleView(props, ctx) {
500
- const { $someValue } = ctx.getStore(SomeStore);
279
+ import { http } from "@manyducks.co/dolla/http";
501
280
 
502
- ctx.observe($someValue, (value) => {
503
- ctx.log("someValue is now", value);
504
- });
505
-
506
- return <h1>Hello World!</h1>;
507
- }
508
- ```
509
-
510
- #### Routing
511
-
512
- Dolla makes heavy use of client-side routing. You can define as many routes as you have views, and the URL
513
- will determine which one the app shows at any given time. By building an app around routes, lots of things one expects
514
- from a web app will just work; back and forward buttons, sharable URLs, bookmarks, etc.
515
-
516
- Routes are matched by highest specificity regardless of the order they were registered.
517
- This avoids some confusing situations that come up with order-based routers like that of `express`.
518
- On the other hand, order-based routers can support regular expressions as patterns which Dolla's router cannot.
519
-
520
- #### Route Patterns
521
-
522
- Routes are defined with strings called patterns. A pattern defines the shape the URL path must match, with special
523
- placeholders for variables that appear within the route. Values matched by those placeholders are parsed out and exposed
524
- to your code (`router` store, `$params` readable). Below are some examples of patterns and how they work.
525
-
526
- - Static: `/this/is/static` has no params and will match only when the route is exactly `/this/is/static`.
527
- - Numeric params: `/users/{#id}/edit` has the named param `{#id}` which matches numbers only, such as `123` or `52`. The
528
- resulting value will be parsed as a number.
529
- - Generic params: `/users/{name}` has the named param `{name}` which matches anything in that position in the path. The
530
- resulting value will be a string.
531
- - Wildcard: `/users/*` will match anything beginning with `/users` and store everything after that in params
532
- as `wildcard`. `*` is valid only at the end of a route.
533
-
534
- Now, here are some route examples in the context of an app:
535
-
536
- ```js
537
- import Dolla from "@manyducks.co/dolla";
538
- import { PersonDetails, ThingIndex, ThingDetails, ThingEdit, ThingDelete } from "./views.js";
539
-
540
- Dolla.router.setup({
541
- routes: [
542
- { path: "/people/{name}", view: PersonDetails },
543
- {
544
- // A `null` component with subroutes acts as a namespace for those subroutes.
545
- // Passing a view instead of `null` results in subroutes being rendered inside that view wherever `ctx.outlet()` is called.
546
- path: "/things",
547
- view: null,
548
- routes: [
549
- { path: "/", view: ThingIndex }, // matches `/things`
550
- { path: "/{#id}", view: ThingDetails }, // matches `/things/{#id}`
551
- { path: "/{#id}/edit", view: ThingEdit }, // matches `/things/{#id}/edit`
552
- { path: "/{#id}/delete", view: ThingDelete }, // matches `/things/{#id}/delete`
553
- ],
554
- },
555
- ],
281
+ // Automatically add an auth header to all API calls
282
+ http.use(async (req, next) => {
283
+ if (req.url.pathname.startsWith("/api/")) {
284
+ req.headers.set("authorization", `Bearer ${localStorage.getItem("api-key")}`);
285
+ }
286
+ await next(); // don't forget this part!
556
287
  });
557
- ```
558
288
 
559
- As you may have inferred from the code above, when the URL matches a pattern the corresponding view is displayed. If we
560
- visit `/people/john`, we will see the `PersonDetails` view and the params will be `{ name: "john" }`. Params can be
561
- accessed anywhere through `Dolla.router`.
562
-
563
- ```js
564
- function PersonDetails(props, ctx) {
565
- // Info about the current route is exported as a set of Readables. Query params are also Writable through $$query:
566
- const { $path, $pattern, $params, $query } = Dolla.router;
567
-
568
- Dolla.router.back(); // Step back in the history to the previous route, if any.
569
- Dolla.router.back(2); // Hit the back button twice.
289
+ const res = await http.get("/api/users");
290
+ console.log(res.body); // already parsed JSON, leggo
291
+ ```
570
292
 
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.
293
+ ### Internationalization (i18n)
573
294
 
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.
295
+ Wanna make your app speak different languages? We got you. Dolla's i18n stuff is super simple.
576
296
 
577
- // Three ways to confirm with the user that they wish to navigate before actually doing it.
578
- Dolla.router.go("/another/page", { prompt: true });
579
- Dolla.router.go("/another/page", { prompt: "Are you sure you want to leave and go to /another/page?" });
580
- Dolla.router.go("/another/page", { prompt: PromptView });
297
+ The best part? `t()` gives you back a **signal**. So if the user switches languages, your whole app just updates. Automatically. It's kinda magic.
581
298
 
582
- // Get the live value of `{name}` from the current path.
583
- const $name = Dolla.derive([$params], (p) => p.name);
299
+ ```jsx
300
+ import { createApp, useSignal } from "@manyducks.co/dolla";
301
+ import { i18n, t } from "@manyducks.co/dolla/i18n";
584
302
 
585
- // Render it into a <p> tag. The name portion will update if the URL changes.
586
- return <p>The person is: {$name}</p>;
303
+ function CounterView() {
304
+ return <button>{t("buttonLabel")}</button>;
587
305
  }
306
+
307
+ const app = createApp(CounterView);
308
+
309
+ // Set up your languages before you start the app
310
+ i18n
311
+ .setup({
312
+ locale: "en",
313
+ translations: [
314
+ { locale: "en", strings: { buttonLabel: "Click me!" } },
315
+ { locale: "ja", strings: { buttonLabel: "押してね!" } },
316
+ ],
317
+ })
318
+ .then(() => app.mount(document.body));
588
319
  ```
589
320
 
590
- ## HTTP Client
321
+ ## The Tea: How's Dolla Different?
591
322
 
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
- }
323
+ ### vs. React
599
324
 
600
- const response = await next();
325
+ - **No More Re-Renders (Fr!)**: This is the big one. In React, when you call `setCount(1)`, your _entire component function runs all over again_. React then has to figure out what changed with the VDOM. In Dolla, your component function only runs once, ever. When you call `setCount(1)`, Dolla just goes and changes the text in that one `<p>` tag. That's it. This makes it way faster and easier to reason about.
326
+ - **Goodbye, `useCallback`**: Because React re-renders all the time, it creates new functions and objects on every single render. This is why you have to wrap everything in `useCallback` and `useMemo` so you don't cause a chain reaction of re-renders in your child components. Since Dolla components don't re-render, you can just pass a regular function as a prop without a single worry. No more referential equality drama.
327
+ - **Easier Effects**: You know that annoying dependency array? Gone. Unless you, like, _want_ to use it for manual control. This means no more stale closure bugs.
328
+ - **State Management Included**: `Stores` are built in, so you can probably skip installing Redux or Zustand and all the boilerplate that comes with them.
601
329
 
602
- // Could do something with the response here.
330
+ ### vs. Angular
603
331
 
604
- return response;
605
- });
332
+ - **Way Less Boilerplate**: Angular is a whole ceremony. Modules, decorators, dependency injection... it's a lot. Dolla is just functions. You write a function, it becomes a component. It's a much more direct vibe.
333
+ - **Stores are like Services, but chill**: Ngl, our `Stores` were lowkey inspired by Angular's services. It's the same idea of providing a thing in one place and using it somewhere else. But instead of all the module and decorator ceremony, you just use a couple of simple hooks.
334
+ - **Signals First, Not an Add-on**: Yeah, Angular has signals now, but the whole framework was built on a different system (Zone.js). Dolla was born with signals as its main character, so the whole DX is built around them. It's not an optional extra, it's the whole point.
606
335
 
607
- const exampleResponse = await Dolla.http.get("/api/example");
336
+ ### vs. Svelte
608
337
 
609
- // Body is already parsed from JSON into an object.
610
- exampleResponse.body.someValue;
611
- ```
338
+ - **It's Just JS**: Dolla is just functions and JSX. Svelte has its own `.svelte` file type and special syntax. Dolla just fits into the normal JS world.
339
+ - **Clearer Updates**: In Dolla, you `setCount(1)`. You know what's happening. In Svelte, `count += 1` is kinda magic.
340
+ - **Consistent Vibe**: Everything in Dolla uses the same hook style. It all feels the same.
612
341
 
613
- ## Localization
342
+ ### vs. SolidJS
614
343
 
615
- ```js
616
- import Dolla, { html, t } from "@manyducks.co/dolla";
344
+ Okay, Solid is sick, ngl. It uses signals too. The main difference is the vibe. Solid is like getting a super-tuned car engine. **Dolla is the whole car.** With heated seats and a good sound system.
617
345
 
618
- function Counter(props, ctx) {
619
- const [$count, setCount] = Dolla.createState(0);
346
+ Choose **SolidJS** if you wanna build your car from scratch.
620
347
 
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
- }
348
+ Choose **Dolla** if you wanna just get in and drive.
632
349
 
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
- });
350
+ ---
640
351
 
641
- Dolla.mount(document.body, Counter);
642
- ```
352
+ For more detail [check out the Docs](./docs/index.md).
643
353
 
644
354
  ---
645
355
 
646
- [🦆](https://www.manyducks.co)
356
+ [🦆 That's a lot of ducks.](https://www.manyducks.co)