@manyducks.co/dolla 0.67.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.
- package/README.md +643 -0
- package/build.js +34 -0
- package/index.d.ts +12 -0
- package/jsx-dev-runtime.d.ts +1 -0
- package/jsx-runtime.d.ts +1 -0
- package/lib/app.d.ts +138 -0
- package/lib/classes/CrashCollector.d.ts +30 -0
- package/lib/classes/DebugHub.d.ts +60 -0
- package/lib/index.d.ts +23 -0
- package/lib/index.js +4062 -0
- package/lib/index.js.map +7 -0
- package/lib/jsx/jsx-dev-runtime.d.ts +3 -0
- package/lib/jsx/jsx-dev-runtime.js +20 -0
- package/lib/jsx/jsx-dev-runtime.js.map +7 -0
- package/lib/jsx/jsx-runtime.d.ts +10 -0
- package/lib/jsx/jsx-runtime.js +22 -0
- package/lib/jsx/jsx-runtime.js.map +7 -0
- package/lib/markup.d.ts +81 -0
- package/lib/nodes/cond.d.ts +28 -0
- package/lib/nodes/html.d.ts +30 -0
- package/lib/nodes/observer.d.ts +33 -0
- package/lib/nodes/outlet.d.ts +26 -0
- package/lib/nodes/portal.d.ts +22 -0
- package/lib/nodes/repeat.d.ts +36 -0
- package/lib/nodes/text.d.ts +20 -0
- package/lib/spring.d.ts +40 -0
- package/lib/state.d.ts +84 -0
- package/lib/store.d.ts +67 -0
- package/lib/stores/dialog.d.ts +30 -0
- package/lib/stores/document.d.ts +10 -0
- package/lib/stores/http.d.ts +60 -0
- package/lib/stores/language.d.ts +39 -0
- package/lib/stores/render.d.ts +18 -0
- package/lib/stores/router.d.ts +118 -0
- package/lib/testing/classes/MockHTTP.d.ts +10 -0
- package/lib/testing/index.d.ts +4 -0
- package/lib/testing/makeMockDOMNode.d.ts +10 -0
- package/lib/testing/makeMockFetch.d.ts +36 -0
- package/lib/testing/makeMockFetch.test.d.ts +1 -0
- package/lib/testing/stores/dialog.d.ts +6 -0
- package/lib/testing/stores/http.d.ts +13 -0
- package/lib/testing/stores/page.d.ts +7 -0
- package/lib/testing/stores/router.d.ts +12 -0
- package/lib/testing/wrapStore.d.ts +8 -0
- package/lib/testing/wrapStore.test.d.ts +1 -0
- package/lib/testing/wrapView.d.ts +0 -0
- package/lib/types.d.ts +3388 -0
- package/lib/utils.d.ts +14 -0
- package/lib/view.d.ts +80 -0
- package/lib/views/fragment.d.ts +2 -0
- package/lib/views/store-scope.d.ts +10 -0
- package/package.json +56 -0
- package/tests/state.test.js +290 -0
package/README.md
ADDED
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
# 🖥 @manyducks.co/dolla
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
> WARNING: This package is pre-1.0 and therefore may contain serious bugs and releases may introduce breaking changes without notice.
|
|
7
|
+
|
|
8
|
+
Dolla is a frontend framework that covers the common needs of complex apps, such as routing, components and state management. Where Dolla differs from other frameworks is in its approach to state management and how state changes translate to DOM updates.
|
|
9
|
+
|
|
10
|
+
Dolla gives you a set of composable state container primitives. Everything that happens in your app is a direct result of a value changing inside one of these containers. There is no VDOM. There is no other way to make the app function than to use these containers correctly. However, the advantage is that state, transformations and their side effects are expressed right in front of your eyes rather than being hidden deep in the framework. It's a bit more work to understand up front, but when you do the whole app becomes easier to understand and maintain.
|
|
11
|
+
|
|
12
|
+
Let's first get into some examples.
|
|
13
|
+
|
|
14
|
+
## State
|
|
15
|
+
|
|
16
|
+
### Writables
|
|
17
|
+
|
|
18
|
+
Writables have a few methods:
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
const $$number = writable(5);
|
|
22
|
+
|
|
23
|
+
// Returns the current value held by the Writable.
|
|
24
|
+
$$number.get();
|
|
25
|
+
|
|
26
|
+
// Stores a new value to the Writable.
|
|
27
|
+
$$number.set(12);
|
|
28
|
+
|
|
29
|
+
// Uses a callback to update the value. Takes the current value and returns the next.
|
|
30
|
+
$$number.update((current) => current + 1);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
For the first 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.
|
|
34
|
+
|
|
35
|
+
```jsx
|
|
36
|
+
import { writable } from "@manyducks.co/dolla";
|
|
37
|
+
|
|
38
|
+
function UserView() {
|
|
39
|
+
// By convention writables start with '$$'
|
|
40
|
+
const $$name = writable("Valued Customer");
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<section>
|
|
44
|
+
<header>
|
|
45
|
+
<h1>Hello, {$$name}!</h1>
|
|
46
|
+
</header>
|
|
47
|
+
|
|
48
|
+
<input
|
|
49
|
+
value={$$name}
|
|
50
|
+
onChange={(e) => {
|
|
51
|
+
$$name.set(e.target.value);
|
|
52
|
+
}}
|
|
53
|
+
/>
|
|
54
|
+
</section>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Readables
|
|
60
|
+
|
|
61
|
+
Readables are like Writables with only a `get` function. Typically, readables are derived from a Writable or derived from other states with `computed`.
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
import { writable, readable } from "@manyducks.co/dolla";
|
|
65
|
+
|
|
66
|
+
const $$value = writable("This is the value.");
|
|
67
|
+
|
|
68
|
+
// By convention Readable names start with '$'.
|
|
69
|
+
const $value = readable($$value);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
You can now safely pass `$value` around without worrying about that code changing it. `$value` will always reflect the value of `$$value`.
|
|
73
|
+
|
|
74
|
+
### Computed
|
|
75
|
+
|
|
76
|
+
Computed states take one or more Readables or Writables and produce a new value _computed_ from those.
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
import { writable, computed } from "@manyducks.co/dolla";
|
|
80
|
+
|
|
81
|
+
const $$count = writable(100);
|
|
82
|
+
|
|
83
|
+
const $double = computed($$count, (value) => value * 2);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
In that example, `$$double` will always have a value derived from that of `$$count`.
|
|
87
|
+
|
|
88
|
+
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.
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
import { writable, computed } from "@manyducks.co/dolla";
|
|
92
|
+
|
|
93
|
+
// Let's assume this list of users was fetched from an API somewhere.
|
|
94
|
+
const $$people = writable([
|
|
95
|
+
{
|
|
96
|
+
id: 1,
|
|
97
|
+
name: "Bob",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 2,
|
|
101
|
+
name: "Bex",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 3,
|
|
105
|
+
name: "Bleeblop",
|
|
106
|
+
},
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
// Let's assume this ID was chosen from an input where the above users were displayed.
|
|
110
|
+
const $$selectedId = writable(2);
|
|
111
|
+
|
|
112
|
+
// Now we get the object of the person who is selected.
|
|
113
|
+
const $selectedPerson = computed([$$people, $$selectedId], ([people, selectedId]) => {
|
|
114
|
+
return people.find((person) => person.id === selectedId);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Now we get a Readable of just that person's name. Say we're going to display it on the page somewhere.
|
|
118
|
+
const $personName = computed($selectedPerson, (person) => person.name);
|
|
119
|
+
|
|
120
|
+
console.log($personName.get()); // "Bex"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
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.
|
|
124
|
+
|
|
125
|
+
### Unwrap
|
|
126
|
+
|
|
127
|
+
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.
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
import { readable, writable, unwrap } from "@manyducks.co/dolla";
|
|
131
|
+
|
|
132
|
+
const $$number = writable(5);
|
|
133
|
+
|
|
134
|
+
unwrap($$number); // 5
|
|
135
|
+
unwrap(readable(5)); // 5
|
|
136
|
+
unwrap(5); // 5
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Advanced Use Cases
|
|
140
|
+
|
|
141
|
+
<details>
|
|
142
|
+
<summary><code>observe</code> and <code>proxy</code></summary>
|
|
143
|
+
|
|
144
|
+
> TO DO
|
|
145
|
+
|
|
146
|
+
</details>
|
|
147
|
+
|
|
148
|
+
## Views
|
|
149
|
+
|
|
150
|
+
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.
|
|
151
|
+
|
|
152
|
+
At its most basic, a view is a function that returns elements.
|
|
153
|
+
|
|
154
|
+
```jsx
|
|
155
|
+
function ExampleView() {
|
|
156
|
+
return <h1>Hello World!</h1>;
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### View Props
|
|
161
|
+
|
|
162
|
+
A view function takes a `props` object as its first argument. This object contains all properties passed to the view when it's invoked.
|
|
163
|
+
|
|
164
|
+
```jsx
|
|
165
|
+
function ListView() {
|
|
166
|
+
return (
|
|
167
|
+
<ul>
|
|
168
|
+
<ListItemView label="Squirrel" />
|
|
169
|
+
<ListItemView label="Chipmunk" />
|
|
170
|
+
<ListItemView label="Groundhog" />
|
|
171
|
+
</ul>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function ListItemView(props) {
|
|
176
|
+
return <li>{props.label}</li>;
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
As you may have guessed, you can pass Readables and Writables 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.
|
|
181
|
+
|
|
182
|
+
### View Helpers
|
|
183
|
+
|
|
184
|
+
#### `cond($condition, whenTruthy, whenFalsy)`
|
|
185
|
+
|
|
186
|
+
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.
|
|
187
|
+
|
|
188
|
+
```jsx
|
|
189
|
+
function ConditionalListView({ $show }) {
|
|
190
|
+
return (
|
|
191
|
+
<div>
|
|
192
|
+
{cond(
|
|
193
|
+
$show,
|
|
194
|
+
|
|
195
|
+
// Visible when truthy
|
|
196
|
+
<ul>
|
|
197
|
+
<ListItemView label="Squirrel" />
|
|
198
|
+
<ListItemView label="Chipmunk" />
|
|
199
|
+
<ListItemView label="Groundhog" />
|
|
200
|
+
</ul>,
|
|
201
|
+
|
|
202
|
+
// Visible when falsy
|
|
203
|
+
<span>List is hidden</span>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
#### `repeat($items, keyFn, renderFn)`
|
|
211
|
+
|
|
212
|
+
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.
|
|
213
|
+
|
|
214
|
+
```jsx
|
|
215
|
+
function RepeatedListView() {
|
|
216
|
+
const $items = readable(["Squirrel", "Chipmunk", "Groundhog"]);
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<ul>
|
|
220
|
+
{repeat(
|
|
221
|
+
$items,
|
|
222
|
+
(item) => item, // Using the string itself as the key
|
|
223
|
+
($item, $index, ctx) => {
|
|
224
|
+
return <ListItemView label={$item} />;
|
|
225
|
+
}
|
|
226
|
+
)}
|
|
227
|
+
</ul>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
#### `portal(content, parentNode)`
|
|
233
|
+
|
|
234
|
+
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.
|
|
235
|
+
|
|
236
|
+
```jsx
|
|
237
|
+
function PortalView() {
|
|
238
|
+
const content = (
|
|
239
|
+
<div class="modal">
|
|
240
|
+
<p>This is a modal.</p>
|
|
241
|
+
</div>
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Content will be appended to `document.body` while this view is connected.
|
|
245
|
+
return portal(content, document.body);
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### View Context
|
|
250
|
+
|
|
251
|
+
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.
|
|
252
|
+
|
|
253
|
+
#### Printing Debug Messages
|
|
254
|
+
|
|
255
|
+
```jsx
|
|
256
|
+
function ExampleView(props, ctx) {
|
|
257
|
+
// Set the name of this view's context. Console messages are prefixed with name.
|
|
258
|
+
ctx.name = "CustomName";
|
|
259
|
+
|
|
260
|
+
// Print messages to the console. These are suppressed by default in the app's "production" mode.
|
|
261
|
+
// You can also change which of these are printed and filter messages from certain contexts in the `createApp` options object.
|
|
262
|
+
ctx.info("Verbose debugging info that might be useful to know");
|
|
263
|
+
ctx.log("Standard messages");
|
|
264
|
+
ctx.warn("Something bad might be happening");
|
|
265
|
+
ctx.error("Uh oh!");
|
|
266
|
+
|
|
267
|
+
// If you encounter a bad enough situation, you can halt and disconnect the entire app.
|
|
268
|
+
ctx.crash(new Error("BOOM"));
|
|
269
|
+
|
|
270
|
+
return <h1>Hello World!</h1>;
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
#### Lifecycle Events
|
|
275
|
+
|
|
276
|
+
```jsx
|
|
277
|
+
function ExampleView(props, ctx) {
|
|
278
|
+
ctx.beforeConnect(() => {
|
|
279
|
+
// Do something before this view's DOM nodes are created.
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
ctx.onConnected(() => {
|
|
283
|
+
// Do something immediately after this view is connected to the DOM.
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
ctx.beforeDisconnect(() => {
|
|
287
|
+
// Do something before removing this view from the DOM.
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
ctx.onDisconnected(() => {
|
|
291
|
+
// Do some cleanup after this view is disconnected from the DOM.
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
return <h1>Hello World!</h1>;
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
#### Displaying Children
|
|
299
|
+
|
|
300
|
+
The context object has an `outlet` function that can be used to display children at a location of your choosing.
|
|
301
|
+
|
|
302
|
+
```js
|
|
303
|
+
function LayoutView(props, ctx) {
|
|
304
|
+
return (
|
|
305
|
+
<div className="layout">
|
|
306
|
+
<OtherView />
|
|
307
|
+
<div className="content">{ctx.outlet()}</div>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function ExampleView() {
|
|
313
|
+
// <h1> and <p> are displayed inside LayoutView's outlet.
|
|
314
|
+
return (
|
|
315
|
+
<LayoutView>
|
|
316
|
+
<h1>Hello</h1>
|
|
317
|
+
<p>This is inside the box.</p>
|
|
318
|
+
</LayoutView>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
#### Using Stores
|
|
324
|
+
|
|
325
|
+
```jsx
|
|
326
|
+
import { UserStore } from "../stores/UserStore.js";
|
|
327
|
+
|
|
328
|
+
function ExampleView(props, ctx) {
|
|
329
|
+
const { $name } = ctx.getStore(UserStore);
|
|
330
|
+
|
|
331
|
+
return <h1>Hello {$name}!</h1>;
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
#### Observing Readables
|
|
336
|
+
|
|
337
|
+
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.
|
|
338
|
+
|
|
339
|
+
```jsx
|
|
340
|
+
function ExampleView(props, ctx) {
|
|
341
|
+
const { $someValue } = ctx.getStore(SomeStore);
|
|
342
|
+
|
|
343
|
+
ctx.observe($someValue, (value) => {
|
|
344
|
+
ctx.log("someValue is now", value);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
return <h1>Hello World!</h1>;
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
#### Example: Counter View
|
|
352
|
+
|
|
353
|
+
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.
|
|
354
|
+
|
|
355
|
+
```jsx
|
|
356
|
+
import { writable } from "@manyducks.co/dolla";
|
|
357
|
+
|
|
358
|
+
function CounterView(props, ctx) {
|
|
359
|
+
const $$count = writable(0);
|
|
360
|
+
|
|
361
|
+
function increment() {
|
|
362
|
+
$$count.update((n) => n + 1);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function decrement() {
|
|
366
|
+
$$count.update((n) => n - 1);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function reset() {
|
|
370
|
+
$$count.set(0);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return (
|
|
374
|
+
<div>
|
|
375
|
+
<p>The count is {$$count}</p>
|
|
376
|
+
<div>
|
|
377
|
+
<button onClick={increment}>+1</button>
|
|
378
|
+
<button onClick={decrement}>-1</button>
|
|
379
|
+
<button onClick={reset}>Reset</button>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Stores
|
|
387
|
+
|
|
388
|
+
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`.
|
|
389
|
+
|
|
390
|
+
Stores are accessed with the `getStore` function available on the context object in views and other stores.
|
|
391
|
+
|
|
392
|
+
Stores are helpful for managing persistent state that needs to be accessed in many places.
|
|
393
|
+
|
|
394
|
+
```js
|
|
395
|
+
import { makeApp } from "@manyducks.co/dolla";
|
|
396
|
+
|
|
397
|
+
const app = makeApp();
|
|
398
|
+
|
|
399
|
+
// We define a store that just exports a message.
|
|
400
|
+
function MessageStore() {
|
|
401
|
+
return {
|
|
402
|
+
message: "Hello from the message store!",
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Register it on the app.
|
|
407
|
+
app.store(MessageStore);
|
|
408
|
+
|
|
409
|
+
// All instances of MessageView will share just one instance of MessageStore.
|
|
410
|
+
function MessageView(props, ctx) {
|
|
411
|
+
const store = ctx.getStore(MessageStore);
|
|
412
|
+
|
|
413
|
+
return <p>{store.message}</p>;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// And a layout view with five MessageViews inside.
|
|
417
|
+
function LayoutView() {
|
|
418
|
+
return (
|
|
419
|
+
<div>
|
|
420
|
+
<h1>Title</h1>
|
|
421
|
+
<MessageView />
|
|
422
|
+
<MessageView />
|
|
423
|
+
<MessageView />
|
|
424
|
+
<MessageView />
|
|
425
|
+
<MessageView />
|
|
426
|
+
</div>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Use LayoutView as the app's main view.
|
|
431
|
+
app.main(LayoutView);
|
|
432
|
+
|
|
433
|
+
// Connect the app.
|
|
434
|
+
app.connect("#app");
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
The output:
|
|
438
|
+
|
|
439
|
+
```html
|
|
440
|
+
<div id="app">
|
|
441
|
+
<div>
|
|
442
|
+
<h1>Title</h1>
|
|
443
|
+
<p>Hello from the message store!</p>
|
|
444
|
+
<p>Hello from the message store!</p>
|
|
445
|
+
<p>Hello from the message store!</p>
|
|
446
|
+
<p>Hello from the message store!</p>
|
|
447
|
+
<p>Hello from the message store!</p>
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### StoreScope
|
|
453
|
+
|
|
454
|
+
Stores relevant to only a part of the view tree can be scoped using a `StoreScope`.
|
|
455
|
+
|
|
456
|
+
```jsx
|
|
457
|
+
function ExampleStore() {
|
|
458
|
+
return { value: 5 };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function ExampleView(props, ctx) {
|
|
462
|
+
const store = ctx.getStore(ExampleStore);
|
|
463
|
+
|
|
464
|
+
return <div>{store.value}</div>;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function LayoutView() {
|
|
468
|
+
return (
|
|
469
|
+
<StoreScope store={ExampleStore}>
|
|
470
|
+
<ExampleView />
|
|
471
|
+
</StoreScope>
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
## Apps and Routing
|
|
477
|
+
|
|
478
|
+
```jsx
|
|
479
|
+
import { makeApp } from "@manyducks.co/dolla";
|
|
480
|
+
|
|
481
|
+
const app = makeApp({
|
|
482
|
+
// Debug options control what gets printed from messages logged through view and store contexts.
|
|
483
|
+
debug: {
|
|
484
|
+
// A comma-separated list of filters. '*' means allow everything and '-dolla/*' means suppress messages with labels beginning with 'dolla/'.
|
|
485
|
+
filter: "*,-dolla/*",
|
|
486
|
+
|
|
487
|
+
// Never print ctx.info() messages
|
|
488
|
+
info: false,
|
|
489
|
+
|
|
490
|
+
// Only print ctx.log() and ctx.warn() messages in development mode
|
|
491
|
+
log: "development",
|
|
492
|
+
warn: "development",
|
|
493
|
+
|
|
494
|
+
// Always print ctx.error() messages
|
|
495
|
+
error: true,
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
// Router options control how routes are matched
|
|
499
|
+
router: {
|
|
500
|
+
hash: true, // Use hash-based routing
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
mode: "development", // or "production" (enables additional debug features and logging in "development")
|
|
504
|
+
});
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
#### Main View, Routes and Outlets
|
|
508
|
+
|
|
509
|
+
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.
|
|
510
|
+
|
|
511
|
+
```jsx
|
|
512
|
+
// Here is a hypothetical main view with a layout and navigation:
|
|
513
|
+
app.main((props, ctx) => {
|
|
514
|
+
return (
|
|
515
|
+
<div className="todo-layout">
|
|
516
|
+
<nav>
|
|
517
|
+
<ul>
|
|
518
|
+
<li>
|
|
519
|
+
<a href="/tasks">Tasks</a>
|
|
520
|
+
</li>
|
|
521
|
+
<li>
|
|
522
|
+
<a href="/completed">Completed</a>
|
|
523
|
+
</li>
|
|
524
|
+
</ul>
|
|
525
|
+
</nav>
|
|
526
|
+
{/*
|
|
527
|
+
* An outlet is where children of a view are shown.
|
|
528
|
+
* Because this is a main view, children in this case
|
|
529
|
+
* are the views that correspond to matched routes.
|
|
530
|
+
*/}
|
|
531
|
+
{ctx.outlet()}
|
|
532
|
+
</div>
|
|
533
|
+
);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// Here are a couple of routes to be rendered into our layout:
|
|
537
|
+
app.route("/tasks", TasksView);
|
|
538
|
+
app.route("/completed", CompletedView);
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Routes can also be nested. Just like the main view and its routes, subroutes will be displayed in the outlet of their parent view.
|
|
542
|
+
|
|
543
|
+
```jsx
|
|
544
|
+
app.route("/tasks", TasksView, (sub) => {
|
|
545
|
+
sub.route("/", TaskListView);
|
|
546
|
+
|
|
547
|
+
// In routes, `{value}` is a dynamic value that matches anything,
|
|
548
|
+
// and `{#value}` is a dynamic value that matches a number.
|
|
549
|
+
sub.route("/{#id}", TaskDetailsView);
|
|
550
|
+
sub.route("/{#id}/edit", TaskEditView);
|
|
551
|
+
|
|
552
|
+
// If the route is any other than the ones defined above, redirect to the list.
|
|
553
|
+
// Redirects support './' and '../' style relative paths.
|
|
554
|
+
sub.redirect("*", "./");
|
|
555
|
+
});
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
#### Routing
|
|
559
|
+
|
|
560
|
+
Dolla makes heavy use of client-side routing. You can define as many routes as you have views, and the URL
|
|
561
|
+
will determine which one the app shows at any given time. By building an app around routes, lots of things one expects
|
|
562
|
+
from a web app will just work; back and forward buttons, sharable URLs, bookmarks, etc.
|
|
563
|
+
|
|
564
|
+
Routing in Dolla is aesthetically inspired by [choo.js](https://www.choo.io/docs/routing)
|
|
565
|
+
with technical inspiration from [@reach/router](https://reach.tech/router/), as routes are matched by highest
|
|
566
|
+
specificity regardless of the order they were registered. This avoids some confusing situations that come up with
|
|
567
|
+
order-based routers like that of `express`. On the other hand, order-based routers can support regular expressions as
|
|
568
|
+
patterns which Dolla's router cannot.
|
|
569
|
+
|
|
570
|
+
#### Route Patterns
|
|
571
|
+
|
|
572
|
+
Routes are defined with strings called patterns. A pattern defines the shape the URL path must match, with special
|
|
573
|
+
placeholders for variables that appear within the route. Values matched by those placeholders are parsed out and exposed
|
|
574
|
+
to your code (`router` store, `$params` readable). Below are some examples of patterns and how they work.
|
|
575
|
+
|
|
576
|
+
- Static: `/this/is/static` has no params and will match only when the route is exactly `/this/is/static`.
|
|
577
|
+
- Numeric params: `/users/{#id}/edit` has the named param `{#id}` which matches numbers only, such as `123` or `52`. The
|
|
578
|
+
resulting value will be parsed as a number.
|
|
579
|
+
- Generic params: `/users/{name}` has the named param `{name}` which matches anything in that position in the path. The
|
|
580
|
+
resulting value will be a string.
|
|
581
|
+
- Wildcard: `/users/*` will match anything beginning with `/users` and store everything after that in params
|
|
582
|
+
as `wildcard`. `*` is valid only at the end of a route.
|
|
583
|
+
|
|
584
|
+
Now, here are some route examples in the context of an app:
|
|
585
|
+
|
|
586
|
+
```js
|
|
587
|
+
import { PersonDetails, ThingIndex, ThingDetails, ThingEdit, ThingDelete } from "./components.js";
|
|
588
|
+
|
|
589
|
+
const app = createApp();
|
|
590
|
+
|
|
591
|
+
app
|
|
592
|
+
.route("/people/{name}", PersonDetails)
|
|
593
|
+
|
|
594
|
+
// Routes can be nested. Also, a `null` component with subroutes acts as a namespace for those subroutes.
|
|
595
|
+
// Passing a view instead of `null` results in subroutes being rendered inside that view wherever `ctx.outlet()` is called.
|
|
596
|
+
.route("/things", null, (sub) => {
|
|
597
|
+
sub.route("/", ThingIndex); // matches `/things`
|
|
598
|
+
sub.route("/{#id}", ThingDetails); // matches `/things/{#id}`
|
|
599
|
+
sub.route("/{#id}/edit", ThingEdit); // matches `/things/{#id}/edit`
|
|
600
|
+
sub.route("/{#id}/delete", ThingDelete); // matches `/things/{#id}/delete`
|
|
601
|
+
});
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
As you may have inferred from the code above, when the URL matches a pattern the corresponding view is displayed. If we
|
|
605
|
+
visit `/people/john`, we will see the `PersonDetails` view and the params will be `{ name: "john" }`. Params can be
|
|
606
|
+
accessed inside those views through the built-in `router` store.
|
|
607
|
+
|
|
608
|
+
```js
|
|
609
|
+
function PersonDetails(props, ctx) {
|
|
610
|
+
// `router` store allows you to work with the router from inside the app.
|
|
611
|
+
const router = ctx.getStore("router");
|
|
612
|
+
|
|
613
|
+
// Info about the current route is exported as a set of Readables. Query params are also Writable through $$query:
|
|
614
|
+
const { $path, $pattern, $params, $$query } = router;
|
|
615
|
+
|
|
616
|
+
// Functions are exported for navigation:
|
|
617
|
+
const { back, forward, navigate } = router;
|
|
618
|
+
|
|
619
|
+
back(); // Step back in the history to the previous route, if any.
|
|
620
|
+
back(2); // Hit the back button twice.
|
|
621
|
+
|
|
622
|
+
forward(); // Step forward in the history to the next route, if any.
|
|
623
|
+
forward(4); // Hit the forward button 4 times.
|
|
624
|
+
|
|
625
|
+
navigate("/things/152"); // Navigate to another path within the same app.
|
|
626
|
+
navigate("https://www.example.com/another/site"); // Navigate to another domain entirely.
|
|
627
|
+
|
|
628
|
+
// Three ways to confirm with the user that they wish to navigate before actually doing it.
|
|
629
|
+
navigate("/another/page", { prompt: true });
|
|
630
|
+
navigate("/another/page", { prompt: "Are you sure you want to leave and go to /another/page?" });
|
|
631
|
+
navigate("/another/page", { prompt: PromptView });
|
|
632
|
+
|
|
633
|
+
// Get the live value of `{name}` from the current path.
|
|
634
|
+
const $name = computed($params, (p) => p.name);
|
|
635
|
+
|
|
636
|
+
// Render it into a <p> tag. The name portion will update if the URL changes.
|
|
637
|
+
return <p>The person is: {$name}</p>;
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
---
|
|
642
|
+
|
|
643
|
+
[🦆](https://www.manyducks.co)
|
package/build.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import esbuild from "esbuild";
|
|
3
|
+
|
|
4
|
+
esbuild
|
|
5
|
+
.build({
|
|
6
|
+
entryPoints: ["src/index.ts"],
|
|
7
|
+
bundle: true,
|
|
8
|
+
metafile: true,
|
|
9
|
+
sourcemap: true,
|
|
10
|
+
// minify: process.env.NODE_ENV === "production",
|
|
11
|
+
outdir: "lib",
|
|
12
|
+
format: "esm",
|
|
13
|
+
})
|
|
14
|
+
.then((result) => {
|
|
15
|
+
fs.writeFileSync("esbuild-meta.json", JSON.stringify(result.metafile));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
esbuild.build({
|
|
19
|
+
entryPoints: ["src/jsx/jsx-runtime.js"],
|
|
20
|
+
bundle: false,
|
|
21
|
+
minify: false,
|
|
22
|
+
sourcemap: true,
|
|
23
|
+
outdir: "lib/jsx",
|
|
24
|
+
format: "esm",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
esbuild.build({
|
|
28
|
+
entryPoints: ["src/jsx/jsx-dev-runtime.js"],
|
|
29
|
+
bundle: false,
|
|
30
|
+
minify: false,
|
|
31
|
+
sourcemap: true,
|
|
32
|
+
outdir: "lib/jsx",
|
|
33
|
+
format: "esm",
|
|
34
|
+
});
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./lib/index";
|
|
2
|
+
|
|
3
|
+
import type { IntrinsicElements as Elements } from "./lib/core/types";
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
namespace JSX {
|
|
7
|
+
interface IntrinsicElements extends Elements {
|
|
8
|
+
// Catch all for custom elements
|
|
9
|
+
[tag: string]: any;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./";
|
package/jsx-runtime.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./";
|