@manyducks.co/dolla 2.0.0-alpha.6 → 2.0.0-alpha.60
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 +86 -591
- package/dist/core/context.d.ts +144 -0
- package/dist/core/env.d.ts +3 -0
- package/dist/core/index.d.ts +22 -0
- package/dist/core/logger.d.ts +42 -0
- package/dist/core/markup.d.ts +125 -0
- package/dist/core/mount.d.ts +15 -0
- package/dist/core/nodes/dom.d.ts +14 -0
- package/dist/core/nodes/dynamic.d.ts +29 -0
- package/dist/core/nodes/html.d.ts +24 -0
- package/dist/core/nodes/portal.d.ts +19 -0
- package/dist/core/nodes/repeat.d.ts +25 -0
- package/dist/core/nodes/view.d.ts +24 -0
- package/dist/core/ref.d.ts +18 -0
- package/dist/core/signals.d.ts +43 -0
- package/dist/core/symbols.d.ts +2 -0
- package/dist/{views → core/views}/default-crash-view.d.ts +6 -2
- package/dist/core/views/fragment.d.ts +7 -0
- package/dist/fragment-BahD_BJA.js +7 -0
- package/dist/fragment-BahD_BJA.js.map +1 -0
- package/dist/{modules/http.d.ts → http/index.d.ts} +3 -5
- package/dist/http.js +163 -0
- package/dist/http.js.map +1 -0
- package/dist/i18n/index.d.ts +134 -0
- package/dist/i18n.js +318 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.js +105 -1385
- package/dist/index.js.map +1 -1
- package/dist/jsx-dev-runtime.d.ts +3 -2
- package/dist/jsx-dev-runtime.js +5 -12
- package/dist/jsx-dev-runtime.js.map +1 -1
- package/dist/jsx-runtime.d.ts +4 -3
- package/dist/jsx-runtime.js +9 -15
- package/dist/jsx-runtime.js.map +1 -1
- package/dist/logger-MPwl-Xqu.js +524 -0
- package/dist/logger-MPwl-Xqu.js.map +1 -0
- package/dist/markup-BGlfQYQk.js +996 -0
- package/dist/markup-BGlfQYQk.js.map +1 -0
- package/dist/router/index.d.ts +2 -0
- package/dist/router/router.d.ts +149 -0
- package/dist/{routing.d.ts → router/router.utils.d.ts} +17 -3
- package/dist/router/router.utils.test.d.ts +1 -0
- package/dist/router-BpuJQ6OA.js +516 -0
- package/dist/router-BpuJQ6OA.js.map +1 -0
- package/dist/router.js +8 -0
- package/dist/router.js.map +1 -0
- package/dist/typeChecking-CbltMOUt.js +71 -0
- package/dist/typeChecking-CbltMOUt.js.map +1 -0
- package/dist/typeChecking.d.ts +2 -98
- package/dist/typeChecking.test.d.ts +1 -0
- package/dist/types.d.ts +98 -25
- package/dist/utils.d.ts +20 -3
- package/docs/http.md +29 -0
- package/docs/i18n.md +43 -0
- package/docs/index.md +10 -0
- package/docs/markup.md +16 -0
- package/docs/mixins.md +32 -0
- package/docs/ref.md +93 -0
- package/docs/router.md +80 -0
- package/docs/setup.md +31 -0
- package/docs/signals.md +166 -0
- package/docs/state.md +141 -0
- package/docs/stores.md +62 -0
- package/docs/views.md +208 -0
- package/examples/webcomponent/index.html +14 -0
- package/examples/webcomponent/main.js +165 -0
- package/index.d.ts +2 -2
- package/notes/TODO.md +6 -0
- package/notes/atomic.md +452 -0
- package/notes/context-routes.md +61 -0
- package/notes/custom-nodes.md +17 -0
- package/notes/effection-idea.md +34 -0
- package/notes/elimination.md +33 -0
- package/notes/mixins.md +22 -0
- package/notes/molecule.md +35 -0
- package/notes/observable.md +180 -0
- package/notes/readme-scratch.md +45 -7
- package/notes/route-middleware.md +42 -0
- package/notes/scratch.md +353 -6
- package/notes/splitting.md +5 -0
- package/notes/stores.md +79 -0
- package/package.json +27 -12
- package/vite.config.js +5 -11
- package/build.js +0 -34
- package/dist/index.d.ts +0 -21
- package/dist/markup.d.ts +0 -100
- package/dist/modules/dolla.d.ts +0 -111
- package/dist/modules/language.d.ts +0 -41
- package/dist/modules/render.d.ts +0 -17
- package/dist/modules/router.d.ts +0 -152
- package/dist/nodes/cond.d.ts +0 -26
- package/dist/nodes/html.d.ts +0 -31
- package/dist/nodes/observer.d.ts +0 -29
- package/dist/nodes/outlet.d.ts +0 -22
- package/dist/nodes/portal.d.ts +0 -19
- package/dist/nodes/repeat.d.ts +0 -34
- package/dist/nodes/text.d.ts +0 -19
- package/dist/passthrough-CW8Ezjg-.js +0 -1244
- package/dist/passthrough-CW8Ezjg-.js.map +0 -1
- package/dist/state.d.ts +0 -101
- package/dist/view.d.ts +0 -50
- package/dist/views/passthrough.d.ts +0 -5
- package/tests/state.test.js +0 -135
- /package/dist/{routing.test.d.ts → core/signals.test.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -3,643 +3,138 @@
|
|
|
3
3
|

|
|
4
4
|

|
|
5
5
|
|
|
6
|
-
> WARNING: This package is in active development. It may contain serious bugs and
|
|
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 with [
|
|
11
|
-
- 📦 Reusable components with [Views](
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
- Built
|
|
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
|
-
|
|
18
|
+
Dolla's goals include:
|
|
17
19
|
|
|
18
|
-
|
|
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
|
-
|
|
24
|
+
## Why Dolla?
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
import { createState, toState, valueOf, derive } from "@manyducks.co/dolla";
|
|
24
|
-
|
|
25
|
-
const [$count, setCount] = createState(72);
|
|
26
|
-
|
|
27
|
-
// Get value
|
|
28
|
-
$count.get(): // 72
|
|
29
|
-
|
|
30
|
-
// Replace the stored value with something else
|
|
31
|
-
setCount(300);
|
|
32
|
-
$count.get(); // 300
|
|
33
|
-
|
|
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
|
|
37
|
-
|
|
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
|
|
43
|
-
|
|
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);
|
|
47
|
-
|
|
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);
|
|
51
|
-
|
|
52
|
-
// Derive a new state from one or more other states. Whenever $count changes, $doubled will follow.
|
|
53
|
-
const $doubled = derive([$count], (count) => count * 2);
|
|
54
|
-
const $sum = derive([$count, $doubled], (count, doubled) => count + doubled);
|
|
55
|
-
```
|
|
56
|
-
|
|
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.
|
|
58
|
-
|
|
59
|
-
```jsx
|
|
60
|
-
import { createSettableState, fromSettable, toSettable } from "@manyducks.co/dolla";
|
|
61
|
-
|
|
62
|
-
// Settable states have their setter included.
|
|
63
|
-
const $$value = createSettableState("Test");
|
|
64
|
-
$$value.set("New Value");
|
|
26
|
+
> TODO: Write about why Dolla was started and what it's all about.
|
|
65
27
|
|
|
66
|
-
|
|
67
|
-
|
|
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).
|
|
68
32
|
|
|
69
|
-
|
|
70
|
-
const $$otherValue = toSettableState($value, setValue);
|
|
33
|
+
## An Example
|
|
71
34
|
|
|
72
|
-
|
|
73
|
-
const $value = toState($$value);
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
You can also do weird proxy things like this:
|
|
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.
|
|
77
36
|
|
|
78
37
|
```jsx
|
|
79
|
-
|
|
80
|
-
const [$value, setValue] = createState(5);
|
|
81
|
-
|
|
82
|
-
// Derive a state that doubles the value
|
|
83
|
-
const $doubled = derive([$value], (value) => value * 2);
|
|
84
|
-
|
|
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
|
-
});
|
|
89
|
-
|
|
90
|
-
// Bundle the derived state and setter into a SettableState to pass around.
|
|
91
|
-
const $$doubled = toSettableState($doubled, setDoubled);
|
|
92
|
-
|
|
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
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
## Views [id="section-views"]
|
|
103
|
-
|
|
104
|
-
A basic view:
|
|
105
|
-
|
|
106
|
-
```js
|
|
107
|
-
import Dolla, { html } from "@manyducks.co/dolla";
|
|
38
|
+
import { $, when, mount } from "@manyducks.co/dolla";
|
|
108
39
|
|
|
109
40
|
function Counter(props, ctx) {
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
function increment() {
|
|
113
|
-
setCount((count) => count + 1);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return html`
|
|
117
|
-
<div>
|
|
118
|
-
<p>Clicks: ${$count}</p>
|
|
119
|
-
<button onclick=${increment}>+1</button>
|
|
120
|
-
</div>
|
|
121
|
-
`;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
Dolla.mount(document.body, Counter);
|
|
125
|
-
```
|
|
126
|
-
|
|
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.
|
|
128
|
-
|
|
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.
|
|
130
|
-
|
|
131
|
-
## Advanced Componentry
|
|
132
|
-
|
|
133
|
-
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.
|
|
134
|
-
|
|
135
|
-
> 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.
|
|
136
|
-
|
|
137
|
-
### Props
|
|
138
|
-
|
|
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.
|
|
140
|
-
|
|
141
|
-
```tsx
|
|
142
|
-
import { type State, type Context, html } from "@manyducks.co/dolla";
|
|
143
|
-
|
|
144
|
-
type HeadingProps = {
|
|
145
|
-
$text: State<string>;
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
function Heading(props: HeadingProps, c: Context) {
|
|
149
|
-
return html`<h1>${props.$text}</h1>`;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function Layout() {
|
|
153
|
-
const [$text, setText] = signal("HELLO THERE!");
|
|
154
|
-
|
|
155
|
-
return (
|
|
156
|
-
<section>
|
|
157
|
-
<Heading $text={$text}>
|
|
158
|
-
</section>
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
### Context
|
|
164
|
-
|
|
165
|
-
```tsx
|
|
166
|
-
import { type State, type Context, html } from "@manyducks.co/dolla";
|
|
167
|
-
|
|
168
|
-
type HeadingProps = {
|
|
169
|
-
$text: State<string>;
|
|
170
|
-
};
|
|
41
|
+
const $count = $(0);
|
|
171
42
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
c.trace("What's going on? Let's find out.");
|
|
177
|
-
c.info("This is low priority info.");
|
|
178
|
-
c.log("This is normal priority info.");
|
|
179
|
-
c.warn("Hey! This could be serious.");
|
|
180
|
-
c.error("NOT GOOD! DEFINITELY NOT GOOD!!1");
|
|
181
|
-
|
|
182
|
-
// And sometimes things are just too borked to press on:
|
|
183
|
-
c.crash(new Error("STOP THE PRESSES! BURN IT ALL DOWN!!!"));
|
|
184
|
-
|
|
185
|
-
// The four lifecycle hooks:
|
|
186
|
-
|
|
187
|
-
// c.beforeMount(() => {
|
|
188
|
-
// c.info("Heading is going to be mounted. Good time to set things up.");
|
|
189
|
-
// });
|
|
190
|
-
|
|
191
|
-
c.onMount(() => {
|
|
192
|
-
c.info("Heading has just been mounted. Good time to access the DOM and finalize setup.");
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
// c.beforeUnmount(() => {
|
|
196
|
-
// c.info("Heading is going to be unmounted. Good time to begin teardown.");
|
|
197
|
-
// });
|
|
198
|
-
|
|
199
|
-
c.onUnmount(() => {
|
|
200
|
-
c.info("Heading has just been unmounted. Good time to finalize teardown.");
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// States can be watched by the component context.
|
|
204
|
-
// Watchers created this way are cleaned up automatically when the component unmounts.
|
|
205
|
-
|
|
206
|
-
c.watch(props.$text, (value) => {
|
|
207
|
-
c.warn(`text has changed to: ${value}`);
|
|
43
|
+
// An effect will re-run whenever any signal value accessed inside it changes.
|
|
44
|
+
ctx.effect(() => {
|
|
45
|
+
console.log(`Count is: ${$count()}`);
|
|
208
46
|
});
|
|
209
47
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
## Signals
|
|
215
|
-
|
|
216
|
-
Basics
|
|
217
|
-
|
|
218
|
-
```jsx
|
|
219
|
-
const [$count, setCount] = signal(0);
|
|
220
|
-
|
|
221
|
-
// Set the value directly.
|
|
222
|
-
setCount(1);
|
|
223
|
-
setCount(2);
|
|
224
|
-
|
|
225
|
-
// Transform the previous value into a new one.
|
|
226
|
-
setCount((current) => current + 1);
|
|
227
|
-
|
|
228
|
-
// This can be used to create easy helper functions:
|
|
229
|
-
function increment(amount = 1) {
|
|
230
|
-
setCount((current) => current + amount);
|
|
231
|
-
}
|
|
232
|
-
increment();
|
|
233
|
-
increment(5);
|
|
234
|
-
increment(-362);
|
|
235
|
-
|
|
236
|
-
// Get the current value
|
|
237
|
-
$count.get(); // -354
|
|
238
|
-
|
|
239
|
-
// Watch for new values. Don't forget to call stop() to clean up!
|
|
240
|
-
const stop = $count.watch((current) => {
|
|
241
|
-
console.log(`count is now ${current}`);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
increment(); // "count is now -353"
|
|
245
|
-
increment(); // "count is now -352"
|
|
246
|
-
|
|
247
|
-
stop();
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
Derive
|
|
251
|
-
|
|
252
|
-
```jsx
|
|
253
|
-
import { signal, derive } from "@manyducks.co/dolla";
|
|
254
|
-
|
|
255
|
-
const [$names, setNames] = signal(["Morg", "Ton", "Bon"]);
|
|
256
|
-
const [$index, setIndex] = signal(0);
|
|
257
|
-
|
|
258
|
-
// Create a new signal that depends on two existing signals:
|
|
259
|
-
const $selected = derive([$names, $index], (names, index) => names[index]);
|
|
260
|
-
|
|
261
|
-
$selected.get(); // "Morg"
|
|
262
|
-
|
|
263
|
-
setIndex(2);
|
|
264
|
-
|
|
265
|
-
$selected.get(); // "Bon"
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
Proxy
|
|
269
|
-
|
|
270
|
-
```jsx
|
|
271
|
-
import { createState, createProxyState } from "@manyducks.co/dolla";
|
|
272
|
-
|
|
273
|
-
const [$names, setNames] = createState(["Morg", "Ton", "Bon"]);
|
|
274
|
-
const [$index, setIndex] = createState(0);
|
|
275
|
-
|
|
276
|
-
const [$selected, setSelected] = createProxyState([$names, $index], {
|
|
277
|
-
get(names, index) {
|
|
278
|
-
return names[index];
|
|
279
|
-
},
|
|
280
|
-
set(next, names, _) {
|
|
281
|
-
const index = names.indexOf(next);
|
|
282
|
-
if (index === -1) {
|
|
283
|
-
throw new Error("Name is not in the list!");
|
|
284
|
-
}
|
|
285
|
-
setIndex(index);
|
|
286
|
-
},
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
$selected.get(); // "Morg"
|
|
290
|
-
$index.get(); // 0
|
|
291
|
-
|
|
292
|
-
// Set selected directly by name through the proxy.
|
|
293
|
-
setSelected("Ton");
|
|
294
|
-
|
|
295
|
-
// Selected and the index have been updated to match.
|
|
296
|
-
$selected.get(); // "Ton"
|
|
297
|
-
$index.get(); // 1
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
## Views
|
|
301
|
-
|
|
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.
|
|
303
|
-
|
|
304
|
-
At its most basic, a view is a function that returns elements.
|
|
305
|
-
|
|
306
|
-
```jsx
|
|
307
|
-
function ExampleView() {
|
|
308
|
-
return <h1>Hello World!</h1>;
|
|
309
|
-
}
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
#### View Props
|
|
313
|
-
|
|
314
|
-
A view function takes a `props` object as its first argument. This object contains all properties passed to the view when it's invoked.
|
|
315
|
-
|
|
316
|
-
```js
|
|
317
|
-
import { html } from "@manyducks.co/dolla";
|
|
318
|
-
|
|
319
|
-
function ListView(props, ctx) {
|
|
320
|
-
return html`
|
|
321
|
-
<ul>
|
|
322
|
-
<${ListItemView} label="Squirrel" />
|
|
323
|
-
<${ListItemView} label="Chipmunk" />
|
|
324
|
-
<${ListItemView} label="Groundhog" />
|
|
325
|
-
</ul>
|
|
326
|
-
`;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function ListItemView(props, ctx) {
|
|
330
|
-
return html`<li>${props.label}</li>`;
|
|
331
|
-
}
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
```jsx
|
|
335
|
-
function ListView() {
|
|
336
|
-
return (
|
|
337
|
-
<ul>
|
|
338
|
-
<ListItemView label="Squirrel" />
|
|
339
|
-
<ListItemView label="Chipmunk" />
|
|
340
|
-
<ListItemView label="Groundhog" />
|
|
341
|
-
</ul>
|
|
342
|
-
);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function ListItemView(props) {
|
|
346
|
-
return <li>{props.label}</li>;
|
|
347
|
-
}
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
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.
|
|
351
|
-
|
|
352
|
-
### View Helpers
|
|
48
|
+
function increment() {
|
|
49
|
+
// Pass a function that takes the current value and returns a new one.
|
|
50
|
+
$count((x) => x + 1);
|
|
51
|
+
}
|
|
353
52
|
|
|
354
|
-
|
|
53
|
+
function decrement() {
|
|
54
|
+
$count((x) => x - 1);
|
|
55
|
+
}
|
|
355
56
|
|
|
356
|
-
|
|
57
|
+
function reset() {
|
|
58
|
+
// Set state directly by passing a (non-function) value.
|
|
59
|
+
$count(0);
|
|
60
|
+
}
|
|
357
61
|
|
|
358
|
-
```jsx
|
|
359
|
-
function ConditionalListView({ $show }) {
|
|
360
62
|
return (
|
|
361
63
|
<div>
|
|
362
|
-
{
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
<
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
<span>List is hidden</span>,
|
|
64
|
+
{/* Signals can be slotted into the DOM to render them */}
|
|
65
|
+
<p>Counter: {$count}</p>
|
|
66
|
+
<div>
|
|
67
|
+
<button on:click={increment}>+1</button>
|
|
68
|
+
<button on:click={decrement}>-1</button>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{/* We can derive a new signal on the fly and conditionally render something based on that condition */}
|
|
72
|
+
{when(
|
|
73
|
+
$(() => $count() > 10),
|
|
74
|
+
<span>That's a lot of clicks!</span>,
|
|
374
75
|
)}
|
|
375
76
|
</div>
|
|
376
77
|
);
|
|
377
78
|
}
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
#### `repeat($items, keyFn, renderFn)`
|
|
381
|
-
|
|
382
|
-
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.
|
|
383
|
-
|
|
384
|
-
```jsx
|
|
385
|
-
function RepeatedListView() {
|
|
386
|
-
const $items = Dolla.toState(["Squirrel", "Chipmunk", "Groundhog"]);
|
|
387
|
-
|
|
388
|
-
return (
|
|
389
|
-
<ul>
|
|
390
|
-
{repeat(
|
|
391
|
-
$items,
|
|
392
|
-
(item) => item, // Using the string itself as the key
|
|
393
|
-
($item, $index, ctx) => {
|
|
394
|
-
return <ListItemView label={$item} />;
|
|
395
|
-
},
|
|
396
|
-
)}
|
|
397
|
-
</ul>
|
|
398
|
-
);
|
|
399
|
-
}
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
#### `portal(content, parentNode)`
|
|
403
|
-
|
|
404
|
-
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.
|
|
405
|
-
|
|
406
|
-
```jsx
|
|
407
|
-
function PortalView() {
|
|
408
|
-
const content = (
|
|
409
|
-
<div class="modal">
|
|
410
|
-
<p>This is a modal.</p>
|
|
411
|
-
</div>
|
|
412
|
-
);
|
|
413
|
-
|
|
414
|
-
// Content will be appended to `document.body` while this view is connected.
|
|
415
|
-
return portal(document.body, content);
|
|
416
|
-
}
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
### View Context
|
|
420
|
-
|
|
421
|
-
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.
|
|
422
|
-
|
|
423
|
-
#### Printing Debug Messages
|
|
424
|
-
|
|
425
|
-
```jsx
|
|
426
|
-
function ExampleView(props, ctx) {
|
|
427
|
-
// Set the name of this view's context. Console messages are prefixed with name.
|
|
428
|
-
ctx.name = "CustomName";
|
|
429
|
-
|
|
430
|
-
// Print messages to the console. These are suppressed by default in the app's "production" mode.
|
|
431
|
-
// You can also change which of these are printed and filter messages from certain contexts in the `createApp` options object.
|
|
432
|
-
ctx.info("Verbose debugging info that might be useful to know");
|
|
433
|
-
ctx.log("Standard messages");
|
|
434
|
-
ctx.warn("Something bad might be happening");
|
|
435
|
-
ctx.error("Uh oh!");
|
|
436
79
|
|
|
437
|
-
|
|
438
|
-
ctx.crash(new Error("BOOM"));
|
|
439
|
-
|
|
440
|
-
return <h1>Hello World!</h1>;
|
|
441
|
-
}
|
|
80
|
+
mount(Counter, document.body);
|
|
442
81
|
```
|
|
443
82
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
```jsx
|
|
447
|
-
function ExampleView(props, ctx) {
|
|
448
|
-
ctx.beforeConnect(() => {
|
|
449
|
-
// Do something before this view's DOM nodes are created.
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
ctx.onConnected(() => {
|
|
453
|
-
// Do something immediately after this view is connected to the DOM.
|
|
454
|
-
});
|
|
83
|
+
> TODO: Show small examples for routing and stores.
|
|
455
84
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
85
|
+
```js
|
|
86
|
+
function MessageStore(options, ctx) {
|
|
87
|
+
const message = $("Hello world!");
|
|
459
88
|
|
|
460
|
-
ctx.
|
|
461
|
-
|
|
89
|
+
ctx.effect(() => {
|
|
90
|
+
ctx.log(`Message is now: ${message()}`);
|
|
91
|
+
// Calling `get()` inside an effect (or compose) function will track that reactive value as a dependency.
|
|
92
|
+
// Effects will re-run when a dependency updates.
|
|
462
93
|
});
|
|
94
|
+
// `ctx` refers to the context object; StoreContext in a store and ViewContext in a view.
|
|
95
|
+
// Context objects contain methods for controlling the component, logging and attaching lifecycle hooks.
|
|
463
96
|
|
|
464
|
-
return
|
|
97
|
+
return {
|
|
98
|
+
message: $(() => message()),
|
|
99
|
+
setMessage: (value: string) => message(value),
|
|
100
|
+
};
|
|
465
101
|
}
|
|
466
|
-
```
|
|
467
102
|
|
|
468
|
-
|
|
103
|
+
function App(props, ctx) {
|
|
104
|
+
// Provide a store for this and all child views.
|
|
105
|
+
ctx.addStore(MessageStore);
|
|
469
106
|
|
|
470
|
-
|
|
107
|
+
const { message, setMessage } = ctx.getStore(MessageStore);
|
|
108
|
+
// Provides a MessageStore on this context and any child contexts.
|
|
109
|
+
// When a store is provided its value is returned right away.
|
|
471
110
|
|
|
472
|
-
```js
|
|
473
|
-
function LayoutView(props, ctx) {
|
|
474
111
|
return (
|
|
475
|
-
<div
|
|
476
|
-
<
|
|
477
|
-
<
|
|
112
|
+
<div>
|
|
113
|
+
<MessageView />
|
|
114
|
+
<MessageView />
|
|
115
|
+
<MessageView />
|
|
116
|
+
|
|
117
|
+
<input
|
|
118
|
+
type="text"
|
|
119
|
+
value={message}
|
|
120
|
+
on:input={(e) => {
|
|
121
|
+
setMessage(e.currentTarget.value);
|
|
122
|
+
}}
|
|
123
|
+
/>
|
|
478
124
|
</div>
|
|
479
125
|
);
|
|
480
126
|
}
|
|
481
127
|
|
|
482
|
-
function
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
<LayoutView>
|
|
486
|
-
<h1>Hello</h1>
|
|
487
|
-
<p>This is inside the box.</p>
|
|
488
|
-
</LayoutView>
|
|
489
|
-
);
|
|
490
|
-
}
|
|
491
|
-
```
|
|
492
|
-
|
|
493
|
-
#### Observing States
|
|
494
|
-
|
|
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.
|
|
496
|
-
|
|
497
|
-
```jsx
|
|
498
|
-
function ExampleView(props, ctx) {
|
|
499
|
-
const { $someValue } = ctx.getStore(SomeStore);
|
|
500
|
-
|
|
501
|
-
ctx.observe($someValue, (value) => {
|
|
502
|
-
ctx.log("someValue is now", value);
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
return <h1>Hello World!</h1>;
|
|
506
|
-
}
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
#### Routing
|
|
510
|
-
|
|
511
|
-
Dolla makes heavy use of client-side routing. You can define as many routes as you have views, and the URL
|
|
512
|
-
will determine which one the app shows at any given time. By building an app around routes, lots of things one expects
|
|
513
|
-
from a web app will just work; back and forward buttons, sharable URLs, bookmarks, etc.
|
|
514
|
-
|
|
515
|
-
Routes are matched by highest specificity regardless of the order they were registered.
|
|
516
|
-
This avoids some confusing situations that come up with order-based routers like that of `express`.
|
|
517
|
-
On the other hand, order-based routers can support regular expressions as patterns which Dolla's router cannot.
|
|
518
|
-
|
|
519
|
-
#### Route Patterns
|
|
520
|
-
|
|
521
|
-
Routes are defined with strings called patterns. A pattern defines the shape the URL path must match, with special
|
|
522
|
-
placeholders for variables that appear within the route. Values matched by those placeholders are parsed out and exposed
|
|
523
|
-
to your code (`router` store, `$params` readable). Below are some examples of patterns and how they work.
|
|
524
|
-
|
|
525
|
-
- Static: `/this/is/static` has no params and will match only when the route is exactly `/this/is/static`.
|
|
526
|
-
- Numeric params: `/users/{#id}/edit` has the named param `{#id}` which matches numbers only, such as `123` or `52`. The
|
|
527
|
-
resulting value will be parsed as a number.
|
|
528
|
-
- Generic params: `/users/{name}` has the named param `{name}` which matches anything in that position in the path. The
|
|
529
|
-
resulting value will be a string.
|
|
530
|
-
- Wildcard: `/users/*` will match anything beginning with `/users` and store everything after that in params
|
|
531
|
-
as `wildcard`. `*` is valid only at the end of a route.
|
|
532
|
-
|
|
533
|
-
Now, here are some route examples in the context of an app:
|
|
534
|
-
|
|
535
|
-
```js
|
|
536
|
-
import Dolla from "@manyducks.co/dolla";
|
|
537
|
-
import { PersonDetails, ThingIndex, ThingDetails, ThingEdit, ThingDelete } from "./views.js";
|
|
538
|
-
|
|
539
|
-
Dolla.router.setup({
|
|
540
|
-
routes: [
|
|
541
|
-
{ path: "/people/{name}", view: PersonDetails },
|
|
542
|
-
{
|
|
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
|
-
],
|
|
553
|
-
},
|
|
554
|
-
],
|
|
555
|
-
});
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
As you may have inferred from the code above, when the URL matches a pattern the corresponding view is displayed. If we
|
|
559
|
-
visit `/people/john`, we will see the `PersonDetails` view and the params will be `{ name: "john" }`. Params can be
|
|
560
|
-
accessed anywhere through `Dolla.router`.
|
|
561
|
-
|
|
562
|
-
```js
|
|
563
|
-
function PersonDetails(props, ctx) {
|
|
564
|
-
// Info about the current route is exported as a set of Readables. Query params are also Writable through $$query:
|
|
565
|
-
const { $path, $pattern, $params, $query } = Dolla.router;
|
|
566
|
-
|
|
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.
|
|
569
|
-
|
|
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.
|
|
128
|
+
function MessageView(props, ctx) {
|
|
129
|
+
const { message } = ctx.getStore(MessageStore);
|
|
130
|
+
// Gets the nearest instance of MessageStore. In this case the one provided at the parent.
|
|
572
131
|
|
|
573
|
-
|
|
574
|
-
Dolla.router.go("https://www.example.com/another/site"); // Navigate to another domain entirely.
|
|
575
|
-
|
|
576
|
-
// Three ways to confirm with the user that they wish to navigate before actually doing it.
|
|
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 });
|
|
580
|
-
|
|
581
|
-
// Get the live value of `{name}` from the current path.
|
|
582
|
-
const $name = Dolla.derive([$params], (p) => p.name);
|
|
583
|
-
|
|
584
|
-
// Render it into a <p> tag. The name portion will update if the URL changes.
|
|
585
|
-
return <p>The person is: {$name}</p>;
|
|
132
|
+
return <span>{message}</span>;
|
|
586
133
|
}
|
|
587
134
|
```
|
|
588
135
|
|
|
589
|
-
|
|
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
|
-
```
|
|
136
|
+
For more detail [check out the Docs](./docs/index.md).
|
|
642
137
|
|
|
643
138
|
---
|
|
644
139
|
|
|
645
|
-
[🦆](https://www.manyducks.co)
|
|
140
|
+
[🦆 That's a lot of ducks.](https://www.manyducks.co)
|