@manyducks.co/dolla 0.78.2 → 1.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.
- package/README.md +241 -25
- package/lib/classes/DebugHub.d.ts +1 -0
- package/lib/index.d.ts +2 -2
- package/lib/index.js +606 -427
- package/lib/index.js.map +4 -4
- package/lib/markup.d.ts +17 -9
- package/lib/nodes/cond.d.ts +3 -3
- package/lib/nodes/html.d.ts +1 -1
- package/lib/nodes/observer.d.ts +3 -3
- package/lib/nodes/outlet.d.ts +3 -3
- package/lib/nodes/repeat.d.ts +9 -7
- package/lib/nodes/text.d.ts +3 -3
- package/lib/signals.d.ts +118 -0
- package/lib/signals.test.d.ts +1 -0
- package/lib/state.d.ts +3 -2
- package/lib/store.d.ts +2 -19
- package/lib/stores/dialog.d.ts +16 -14
- package/lib/stores/document.d.ts +5 -4
- package/lib/stores/language.d.ts +4 -4
- package/lib/stores/router.d.ts +5 -4
- package/lib/testing/makeMockFetch._test.d.ts +1 -0
- package/lib/testing/makeMockFetch.test_skip.d.ts +1 -0
- package/lib/testing/wrapStore._test.d.ts +1 -0
- package/lib/testing/wrapStore.test_skip.d.ts +1 -0
- package/lib/types.d.ts +16 -32
- package/lib/view.d.ts +18 -19
- package/notes/scratch.md +76 -0
- package/package.json +1 -1
- package/tests/signals.test.js +135 -0
- package/tests/state.test.js +0 -290
package/README.md
CHANGED
|
@@ -5,17 +5,236 @@
|
|
|
5
5
|
|
|
6
6
|
> WARNING: This package is pre-1.0 and therefore may contain serious bugs and releases may introduce breaking changes without notice.
|
|
7
7
|
|
|
8
|
-
Dolla is a frontend framework
|
|
8
|
+
Dolla is a batteries-included JavaScript frontend framework covering the needs of complex modern single page apps (SPA). Dolla provides:
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
- Components
|
|
11
|
+
- Routing
|
|
12
|
+
- State management
|
|
13
|
+
- Declarative templating and DOM updates via signals
|
|
14
|
+
- An HTTP client
|
|
15
|
+
-
|
|
11
16
|
|
|
12
|
-
|
|
17
|
+
Let's first get into some examples.
|
|
13
18
|
|
|
14
|
-
|
|
19
|
+
## A Basic Component
|
|
15
20
|
|
|
16
|
-
|
|
21
|
+
```tsx
|
|
22
|
+
import { dolla, signal } from "@manyducks.co/dolla";
|
|
23
|
+
|
|
24
|
+
function Counter(props, c) {
|
|
25
|
+
const [$count, setCount] = signal(0);
|
|
26
|
+
|
|
27
|
+
function increment() {
|
|
28
|
+
setCount((count) => count + 1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div>
|
|
33
|
+
<p>Clicks: {$count}</p>
|
|
34
|
+
<button onClick={increment}>Click Me</button>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// return html`
|
|
39
|
+
// <div>
|
|
40
|
+
// <p>Clicks: ${$count}</p>
|
|
41
|
+
// <button onclick=${increment}>Click Me</button>
|
|
42
|
+
// </div>
|
|
43
|
+
// `;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Create a new dolla app...
|
|
47
|
+
const app = dolla();
|
|
48
|
+
|
|
49
|
+
// Mount this counter component at the root...
|
|
50
|
+
app.route("/", Counter);
|
|
51
|
+
|
|
52
|
+
app.route({
|
|
53
|
+
path: "/{#projectId}",
|
|
54
|
+
component: Counter,
|
|
55
|
+
routes: [{ path: "/", redirect: "../" }],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// And mount the app to the page.
|
|
59
|
+
app.mount("body");
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
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.
|
|
63
|
+
|
|
64
|
+
The biggest difference is that the Counter function runs only once when the component is mounted. All updates after that point are a direct result of the `$count` signal being updated.
|
|
65
|
+
|
|
66
|
+
You'll notice that signals are typically named with a `$` at the beginning to indicate that they contain special values that may change over time.
|
|
67
|
+
|
|
68
|
+
## Advanced Componentry
|
|
69
|
+
|
|
70
|
+
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.
|
|
71
|
+
|
|
72
|
+
> 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.
|
|
73
|
+
|
|
74
|
+
### Props
|
|
75
|
+
|
|
76
|
+
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.
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { type Signal, type Context, html } from "@manyducks.co/dolla";
|
|
80
|
+
|
|
81
|
+
type HeadingProps = {
|
|
82
|
+
$text: Signal<string>;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
function Heading(props: HeadingProps, c: Context) {
|
|
86
|
+
return html`<h1>${props.$text}</h1>`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function Layout() {
|
|
90
|
+
const [$text, setText] = signal("HELLO THERE!");
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<section>
|
|
94
|
+
<Heading $text={$text}>
|
|
95
|
+
</section>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Context
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
import { type Signal, type Context, html } from "@manyducks.co/dolla";
|
|
104
|
+
|
|
105
|
+
type HeadingProps = {
|
|
106
|
+
$text: Signal<string>;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
function Heading(props: HeadingProps, c: Context) {
|
|
110
|
+
// A full compliment of logging functions:
|
|
111
|
+
// Log levels that get printed can be set at the app level.
|
|
112
|
+
|
|
113
|
+
c.trace("What's going on? Let's find out.");
|
|
114
|
+
c.info("This is low priority info.");
|
|
115
|
+
c.log("This is normal priority info.");
|
|
116
|
+
c.warn("Hey! This could be serious.");
|
|
117
|
+
c.error("NOT GOOD! DEFINITELY NOT GOOD!!1");
|
|
118
|
+
|
|
119
|
+
// And sometimes things are just too borked to press on:
|
|
120
|
+
c.crash(new Error("STOP THE PRESSES! BURN IT ALL DOWN!!!"));
|
|
121
|
+
|
|
122
|
+
// The four lifecycle hooks:
|
|
123
|
+
|
|
124
|
+
// c.beforeMount(() => {
|
|
125
|
+
// c.info("Heading is going to be mounted. Good time to set things up.");
|
|
126
|
+
// });
|
|
127
|
+
|
|
128
|
+
c.onMount(() => {
|
|
129
|
+
c.info("Heading has just been mounted. Good time to access the DOM and finalize setup.");
|
|
130
|
+
});
|
|
17
131
|
|
|
18
|
-
|
|
132
|
+
// c.beforeUnmount(() => {
|
|
133
|
+
// c.info("Heading is going to be unmounted. Good time to begin teardown.");
|
|
134
|
+
// });
|
|
135
|
+
|
|
136
|
+
c.onUnmount(() => {
|
|
137
|
+
c.info("Heading has just been unmounted. Good time to finalize teardown.");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Signals can be watched by the component context.
|
|
141
|
+
// Watchers created this way are cleaned up automatically when the component unmounts.
|
|
142
|
+
|
|
143
|
+
c.watch(props.$text, (value) => {
|
|
144
|
+
c.warn(`text has changed to: ${value}`);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return html`<h1>${props.$text}</h1>`;
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Signals
|
|
152
|
+
|
|
153
|
+
Basics
|
|
154
|
+
|
|
155
|
+
```jsx
|
|
156
|
+
const [$count, setCount] = signal(0);
|
|
157
|
+
|
|
158
|
+
// Set the value directly.
|
|
159
|
+
setCount(1);
|
|
160
|
+
setCount(2);
|
|
161
|
+
|
|
162
|
+
// Transform the previous value into a new one.
|
|
163
|
+
setCount((current) => current + 1);
|
|
164
|
+
|
|
165
|
+
// This can be used to create easy helper functions:
|
|
166
|
+
function increment(amount = 1) {
|
|
167
|
+
setCount((current) => current + amount);
|
|
168
|
+
}
|
|
169
|
+
increment();
|
|
170
|
+
increment(5);
|
|
171
|
+
increment(-362);
|
|
172
|
+
|
|
173
|
+
// Get the current value
|
|
174
|
+
$count.get(); // -354
|
|
175
|
+
|
|
176
|
+
// Watch for new values. Don't forget to call stop() to clean up!
|
|
177
|
+
const stop = $count.watch((current) => {
|
|
178
|
+
console.log(`count is now ${current}`);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
increment(); // "count is now -353"
|
|
182
|
+
increment(); // "count is now -352"
|
|
183
|
+
|
|
184
|
+
stop();
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Derive
|
|
188
|
+
|
|
189
|
+
```jsx
|
|
190
|
+
import { signal, derive } from "@manyducks.co/dolla";
|
|
191
|
+
|
|
192
|
+
const [$names, setNames] = signal(["Morg", "Ton", "Bon"]);
|
|
193
|
+
const [$index, setIndex] = signal(0);
|
|
194
|
+
|
|
195
|
+
// Create a new signal that depends on two existing signals:
|
|
196
|
+
const $selected = derive([$names, $index], (names, index) => names[index]);
|
|
197
|
+
|
|
198
|
+
$selected.get(); // "Morg"
|
|
199
|
+
|
|
200
|
+
setIndex(2);
|
|
201
|
+
|
|
202
|
+
$selected.get(); // "Bon"
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Proxy
|
|
206
|
+
|
|
207
|
+
```jsx
|
|
208
|
+
import { signal, proxy } from "@manyducks.co/dolla";
|
|
209
|
+
|
|
210
|
+
const [$names, setNames] = signal(["Morg", "Ton", "Bon"]);
|
|
211
|
+
const [$index, setIndex] = signal(0);
|
|
212
|
+
|
|
213
|
+
const [$selected, setSelected] = proxy([$names, $index], {
|
|
214
|
+
get(names, index) {
|
|
215
|
+
return names[index];
|
|
216
|
+
},
|
|
217
|
+
set(next) {
|
|
218
|
+
const index = $names.get().indexOf(next);
|
|
219
|
+
if (index === -1) {
|
|
220
|
+
throw new Error("Name is not in the list!");
|
|
221
|
+
}
|
|
222
|
+
setIndex(index);
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
$selected.get(); // "Morg"
|
|
227
|
+
$index.get(); // 0
|
|
228
|
+
|
|
229
|
+
// Set selected directly by name through the proxy.
|
|
230
|
+
setSelected("Ton");
|
|
231
|
+
|
|
232
|
+
// Selected and the index have been updated to match.
|
|
233
|
+
$selected.get(); // "Ton"
|
|
234
|
+
$index.get(); // 1
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
##
|
|
19
238
|
|
|
20
239
|
States come in two varieties, each with a constructor function and a TypeScript type to match. These are:
|
|
21
240
|
|
|
@@ -27,47 +246,44 @@ States come in two varieties, each with a constructor function and a TypeScript
|
|
|
27
246
|
The constructor functions are `$` for `Readable` and `$$` for `Writable`. By convention, the names of each are prefixed with `$` or `$$` to indicate its type, making the data flow a lot easier to understand at a glance.
|
|
28
247
|
|
|
29
248
|
```js
|
|
30
|
-
import {
|
|
249
|
+
import { signal } from "@manyducks.co/dolla";
|
|
31
250
|
|
|
32
251
|
// By convention, Writable names are prefixed with two dollar signs and Readable with one.
|
|
33
|
-
const
|
|
252
|
+
const [$number, setNumber] = signal(5);
|
|
34
253
|
|
|
35
254
|
// Returns the current value held by the Writable.
|
|
36
|
-
|
|
255
|
+
$number.get();
|
|
37
256
|
// Stores a new value to the Writable.
|
|
38
|
-
|
|
257
|
+
setNumber(12);
|
|
39
258
|
// Uses a callback to update the value. Takes the current value and returns the next.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// Convert to a read-only Readable with the same live value.
|
|
43
|
-
const $readOnlyNumber = $($$number);
|
|
259
|
+
setNumber((current) => current + 1);
|
|
44
260
|
|
|
45
261
|
// Derive a new state from an existing one.
|
|
46
|
-
const $doubled = $
|
|
47
|
-
$doubled.get(); // 26 (
|
|
262
|
+
const $doubled = derive([$number], (value) => value * 2);
|
|
263
|
+
$doubled.get(); // 26 ($number is 13)
|
|
48
264
|
|
|
49
265
|
// Derive one new state from the latest values of many other states.
|
|
50
|
-
const $many = $
|
|
266
|
+
const $many = derive([$number, $doubled], (num, doubled) => num + doubled);
|
|
51
267
|
```
|
|
52
268
|
|
|
53
269
|
Now how do we use it? For a real example, a simple greeter app. The user types their name into a text input and that value is reflected in a heading above the input. For this we will use the `writable` function to create a state container. That container can be slotted into our JSX as a text node or DOM property. Any changes to the value will now be reflected in the DOM.
|
|
54
270
|
|
|
55
271
|
```jsx
|
|
56
|
-
import {
|
|
272
|
+
import { signal } from "@manyducks.co/dolla";
|
|
57
273
|
|
|
58
|
-
function
|
|
59
|
-
const
|
|
274
|
+
function Greeter() {
|
|
275
|
+
const [$name, setName] = signal("Valued Customer");
|
|
60
276
|
|
|
61
277
|
return (
|
|
62
278
|
<section>
|
|
63
279
|
<header>
|
|
64
|
-
<h1>Hello, {
|
|
280
|
+
<h1>Hello, {$name}!</h1>
|
|
65
281
|
</header>
|
|
66
282
|
|
|
67
283
|
<input
|
|
68
|
-
value={
|
|
284
|
+
value={$name}
|
|
69
285
|
onChange={(e) => {
|
|
70
|
-
|
|
286
|
+
setName(e.target.value);
|
|
71
287
|
}}
|
|
72
288
|
/>
|
|
73
289
|
</section>
|
|
@@ -204,7 +420,7 @@ function ConditionalListView({ $show }) {
|
|
|
204
420
|
</ul>,
|
|
205
421
|
|
|
206
422
|
// Visible when falsy
|
|
207
|
-
<span>List is hidden</span
|
|
423
|
+
<span>List is hidden</span>,
|
|
208
424
|
)}
|
|
209
425
|
</div>
|
|
210
426
|
);
|
|
@@ -226,7 +442,7 @@ function RepeatedListView() {
|
|
|
226
442
|
(item) => item, // Using the string itself as the key
|
|
227
443
|
($item, $index, ctx) => {
|
|
228
444
|
return <ListItemView label={$item} />;
|
|
229
|
-
}
|
|
445
|
+
},
|
|
230
446
|
)}
|
|
231
447
|
</ul>
|
|
232
448
|
);
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { App } from "./app.js";
|
|
2
|
-
export
|
|
3
|
-
export { m, cond, repeat, portal } from "./markup.js";
|
|
2
|
+
export * from "./signals.js";
|
|
3
|
+
export { type Ref, isRef, ref, m, cond, repeat, portal } from "./markup.js";
|
|
4
4
|
export { Fragment } from "./views/fragment.js";
|
|
5
5
|
export { StoreScope, type StoreScopeProps } from "./views/store-scope.js";
|
|
6
6
|
export { RouterStore, type RouteMatchContext } from "./stores/router.js";
|