@manyducks.co/dolla 2.0.0-alpha.6 → 2.0.0-alpha.61
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 +142 -0
- package/dist/core/env.d.ts +3 -0
- package/dist/core/index.d.ts +21 -0
- package/dist/core/logger.d.ts +42 -0
- package/dist/core/logger.test.d.ts +0 -0
- package/dist/core/markup.d.ts +104 -0
- package/dist/core/markup.test.d.ts +0 -0
- package/dist/core/mount.d.ts +15 -0
- package/dist/core/mount.test.d.ts +0 -0
- package/dist/core/nodes/_markup.d.ts +36 -0
- package/dist/core/nodes/dom.d.ts +13 -0
- package/dist/core/nodes/dynamic.d.ts +22 -0
- package/dist/core/nodes/element.d.ts +25 -0
- package/dist/core/nodes/portal.d.ts +18 -0
- package/dist/core/nodes/repeat.d.ts +27 -0
- package/dist/core/nodes/view.d.ts +25 -0
- package/dist/core/ref.d.ts +18 -0
- package/dist/core/ref.test.d.ts +1 -0
- package/dist/core/signals.d.ts +58 -0
- package/dist/core/signals.test.d.ts +1 -0
- package/dist/{views → core/views}/default-crash-view.d.ts +11 -4
- 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/hooks/index.d.ts +64 -0
- package/dist/hooks/index.test.d.ts +1 -0
- package/dist/hooks.js +69 -0
- package/dist/hooks.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 +98 -1388
- 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-Bl496yfY.js +91 -0
- package/dist/logger-Bl496yfY.js.map +1 -0
- package/dist/markup-CX27GJ1M.js +1030 -0
- package/dist/markup-CX27GJ1M.js.map +1 -0
- package/dist/ref-BD79iqlg.js +15 -0
- package/dist/ref-BD79iqlg.js.map +1 -0
- package/dist/router/index.d.ts +2 -0
- package/dist/router/router.d.ts +160 -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-CjCkk4dA.js +543 -0
- package/dist/router-CjCkk4dA.js.map +1 -0
- package/dist/router.js +8 -0
- package/dist/router.js.map +1 -0
- package/dist/signals-gCwiIe5X.js +450 -0
- package/dist/signals-gCwiIe5X.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/hooks.md +211 -0
- 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 +31 -12
- package/vite.config.js +6 -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/context.test.d.ts} +0 -0
package/docs/i18n.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Internationalization (i18n) Support
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
import { $, mount } from "@manyducks.co/dolla";
|
|
5
|
+
import { i18n, t } from "@manyducks.co/dolla/i18n";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
function CounterView(props) {
|
|
9
|
+
const $count = $(0);
|
|
10
|
+
|
|
11
|
+
const increment = () => {
|
|
12
|
+
$count(count => count + 1);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div>
|
|
17
|
+
<p>Clicks: {$count}</p>
|
|
18
|
+
<button onClick={increment}>{t("buttonLabel")}</button>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Await i18n setup before mounting the app to make sure translations are loaded.
|
|
24
|
+
i18n
|
|
25
|
+
.setup({
|
|
26
|
+
locale: "en",
|
|
27
|
+
translations: [
|
|
28
|
+
{ locale: "en", strings: { buttonLabel: "Click here to increment" } },
|
|
29
|
+
{ locale: "ja", strings: { buttonLabel: "ここに押して増加する" } },
|
|
30
|
+
],
|
|
31
|
+
})
|
|
32
|
+
.then(() => {
|
|
33
|
+
return mount(CounterView, document.body);
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
End.
|
|
40
|
+
|
|
41
|
+
- [🗂️ Docs](./index.md)
|
|
42
|
+
- [🏠 README](../README.md)
|
|
43
|
+
- [🦆 That's a lot of ducks.](https://www.manyducks.co)
|
package/docs/index.md
ADDED
package/docs/markup.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Markup
|
|
2
|
+
|
|
3
|
+
Dolla creates a tree of views that manage the DOM, updating attributes and recreating parts of the DOM as signal values change.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { $, m, render } from "@manyducks.co/dolla";
|
|
7
|
+
|
|
8
|
+
const $count = $(0);
|
|
9
|
+
const labelMarkup = m("span", { children: $count });
|
|
10
|
+
// or in JSX:
|
|
11
|
+
const labelMarkup = <span>{$count}</span>;
|
|
12
|
+
|
|
13
|
+
const rendered = render(labelMarkup);
|
|
14
|
+
|
|
15
|
+
rendered.mount(document.body);
|
|
16
|
+
```
|
package/docs/mixins.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Mixins
|
|
2
|
+
|
|
3
|
+
Mixins are a way to add custom lifecycle handlers to plain DOM nodes without creating an entire view. You can encapsulate reusable logic in mixin functions and apply them like CSS classes.
|
|
4
|
+
|
|
5
|
+
Mixin functions take a reference to the element and a `MixinContext` object which adds lifecycle hooks similar to those of `ViewContext`.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { type Mixin } from "@manyducks.co/dolla";
|
|
9
|
+
|
|
10
|
+
const logMe: Mixin = (element, ctx) => {
|
|
11
|
+
ctx.onMount(() => {
|
|
12
|
+
ctx.log("element mounted");
|
|
13
|
+
});
|
|
14
|
+
ctx.onUnmount(() => {
|
|
15
|
+
ctx.log("element unmounted");
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Pass one mixin
|
|
20
|
+
<h1 mixin={logMe}>Title</h1>;
|
|
21
|
+
|
|
22
|
+
// Or an array of mixins
|
|
23
|
+
<p mixin={[logMe, anotherMixin, yetAnotherMixin]}>Text goes here...</p>;
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
End.
|
|
29
|
+
|
|
30
|
+
- [🗂️ Docs](./index.md)
|
|
31
|
+
- [🏠 README](../README.md)
|
|
32
|
+
- [🦆 That's a lot of ducks.](https://www.manyducks.co)
|
package/docs/ref.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Ref
|
|
2
|
+
|
|
3
|
+
Refs are functions that serve as a getter and setter for a stored value. Calling a ref with no arguments will return its stored value, or throw an error if no value has yet been stored. Calling a ref with a single argument will store a new value.
|
|
4
|
+
|
|
5
|
+
This signature is very similar to that of a `Source` signal, with the differences being their error throwing behavior while empty and that refs are _not_ trackable in a signal context.
|
|
6
|
+
|
|
7
|
+
## Pattern #1: Referencing DOM nodes
|
|
8
|
+
|
|
9
|
+
The main pattern for refs is as a DOM node reference. Markup elements take a `ref` attribute to which they will pass their DOM node when they are mounted.
|
|
10
|
+
|
|
11
|
+
Once you have this reference you can manipulate the node outside the usual declarative template workflow.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { ref } from "@manyducks.co/dolla";
|
|
15
|
+
|
|
16
|
+
function ExampleView() {
|
|
17
|
+
const element = ref<HTMLElement>();
|
|
18
|
+
|
|
19
|
+
ctx.onMount(() => {
|
|
20
|
+
element().innerText = "GOODBYE THERE";
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return <div ref={element}>HELLO THERE</div>;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Pattern #2: Controlling a child view from a parent view
|
|
28
|
+
|
|
29
|
+
Another useful pattern is to pass an API object from a child view to the parent, allowing the parent to call methods to control the child view in an imperative way.
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { ref } from "@manyducks.co/dolla";
|
|
33
|
+
|
|
34
|
+
// First we'll define the view to be controlled.
|
|
35
|
+
|
|
36
|
+
interface CounterViewControls {
|
|
37
|
+
increment(): void;
|
|
38
|
+
decrement(): void;
|
|
39
|
+
reset(): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface CounterViewProps {
|
|
43
|
+
controls: Ref<CounterViewControls>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function CounterView({ controls }: CounterViewProps) {
|
|
47
|
+
const $count = $(count);
|
|
48
|
+
|
|
49
|
+
// Passing a `controls` object to the ref whose methods reference internal state.
|
|
50
|
+
controls({
|
|
51
|
+
increment() {
|
|
52
|
+
$count((current) => current + 1);
|
|
53
|
+
},
|
|
54
|
+
decrement() {
|
|
55
|
+
$count((current) => current - 1);
|
|
56
|
+
},
|
|
57
|
+
reset() {
|
|
58
|
+
$count(0);
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return <span>Count: {$count}</span>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Then we'll use it in the parent:
|
|
66
|
+
|
|
67
|
+
function ParentView() {
|
|
68
|
+
// Create a Ref to store the controls object.
|
|
69
|
+
const controls = ref<CounterViewControls>();
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<section>
|
|
73
|
+
<h1>Counter</h1>
|
|
74
|
+
|
|
75
|
+
{/* CounterView will set the controls object */}
|
|
76
|
+
<CounterView controls={controls} />
|
|
77
|
+
|
|
78
|
+
{/* Our buttons will call the methods on the controls object causing state changes within CounterView */}
|
|
79
|
+
<button onClick={() => controls.increment()}>+1</button>
|
|
80
|
+
<button onClick={() => controls.decrement()}>-1</button>
|
|
81
|
+
<button onClick={() => controls.reset()}>=0</button>
|
|
82
|
+
</section>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
End.
|
|
90
|
+
|
|
91
|
+
- [🗂️ Docs](./index.md)
|
|
92
|
+
- [🏠 README](../README.md)
|
|
93
|
+
- [🦆 That's a lot of ducks.](https://www.manyducks.co)
|
package/docs/router.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Router
|
|
2
|
+
|
|
3
|
+
Dolla makes heavy use of client-side routing. You can define as many routes as you have views, and the URL
|
|
4
|
+
will determine which one the app shows at any given time. By building an app around routes, lots of things one expects
|
|
5
|
+
from a web app will just work; back and forward buttons, sharable URLs, bookmarks, etc.
|
|
6
|
+
|
|
7
|
+
Routes are matched by highest specificity regardless of the order they were registered.
|
|
8
|
+
This avoids some confusing situations that come up with order-based routers like that of `express`.
|
|
9
|
+
On the other hand, order-based routers can support regular expressions as patterns which Dolla's router cannot.
|
|
10
|
+
|
|
11
|
+
## Route Patterns
|
|
12
|
+
|
|
13
|
+
Routes are defined with strings called patterns. A pattern defines the shape the URL path must match, with special
|
|
14
|
+
placeholders for variables that appear within the route. Values matched by those placeholders are parsed out and exposed
|
|
15
|
+
to your code (`router` store, `$params` readable). Below are some examples of patterns and how they work.
|
|
16
|
+
|
|
17
|
+
- Static: `/this/is/static` has no params and will match only when the route is exactly `/this/is/static`.
|
|
18
|
+
- Numeric params: `/users/{#id}/edit` has the named param `{#id}` which matches numbers only, such as `123` or `52`. The
|
|
19
|
+
resulting value will be parsed as a number.
|
|
20
|
+
- Generic params: `/users/{name}` has the named param `{name}` which matches anything in that position in the path. The
|
|
21
|
+
resulting value will be a string.
|
|
22
|
+
- Wildcard: `/users/*` will match anything beginning with `/users` and store everything after that in params
|
|
23
|
+
as `wildcard`. `*` is valid only at the end of a route.
|
|
24
|
+
|
|
25
|
+
Now, here are some route examples in the context of an app:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import Dolla, { createRouter } from "@manyducks.co/dolla";
|
|
29
|
+
import { ThingIndex, ThingDetails, ThingEdit, ThingDelete } from "./views.js";
|
|
30
|
+
|
|
31
|
+
const router = createRouter({
|
|
32
|
+
routes: [
|
|
33
|
+
{
|
|
34
|
+
// A `null` component with subroutes acts as a namespace for those subroutes.
|
|
35
|
+
// Passing a view instead of `null` results in subroutes being rendered inside that view wherever `ctx.outlet()` is called.
|
|
36
|
+
path: "/things",
|
|
37
|
+
view: null,
|
|
38
|
+
routes: [
|
|
39
|
+
{ path: "/", view: ThingIndex }, // matches `/things`
|
|
40
|
+
{ path: "/{#id}", view: ThingDetails }, // matches `/things/{#id}`
|
|
41
|
+
{ path: "/{#id}/edit", view: ThingEdit }, // matches `/things/{#id}/edit`
|
|
42
|
+
{ path: "/{#id}/delete", view: ThingDelete }, // matches `/things/{#id}/delete`
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
// All routes that don't match anything else will redirect to `/things`
|
|
46
|
+
{ path: "*", redirect: "/things" },
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Mount the router in place of a view.
|
|
51
|
+
Dolla.mount(document.body, router);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
When the URL matches a pattern the corresponding view is displayed. If we visit `/people/john`,
|
|
55
|
+
we will see the `PersonDetails` view and the params will be `{ name: "john" }`. Params can be
|
|
56
|
+
accessed from anywhere in the app through `Dolla.router`.
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import Dolla from "@manyducks.co/dolla";
|
|
60
|
+
|
|
61
|
+
// Info about the current route is exported as a set of Readables. Query params are also Writable through $$query:
|
|
62
|
+
const { $path, $pattern, $params, $query } = router;
|
|
63
|
+
|
|
64
|
+
router.back(); // Step back in the history to the previous route, if any.
|
|
65
|
+
router.back(2); // Hit the back button twice.
|
|
66
|
+
|
|
67
|
+
router.forward(); // Step forward in the history to the next route, if any.
|
|
68
|
+
router.forward(4); // Hit the forward button 4 times.
|
|
69
|
+
|
|
70
|
+
router.go("/things/152"); // Navigate to another path within the same app.
|
|
71
|
+
router.go("https://www.example.com/another/site"); // Navigate to another domain entirely.
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
End.
|
|
77
|
+
|
|
78
|
+
- [🗂️ Docs](./index.md)
|
|
79
|
+
- [🏠 README](../README.md)
|
|
80
|
+
- [🦆 That's a lot of ducks.](https://www.manyducks.co)
|
package/docs/setup.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Setting up Dolla
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
Dolla is published on npm as `@manyducks.co/dolla`. You can install it in your project with the following command:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm i @manyducks.co/dolla
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## JSX
|
|
12
|
+
|
|
13
|
+
If you want to use JSX in your app you can add the following options to your `tsconfig.json` or `jsconfig.json`. Modern build systems like [Vite](https://vite.dev) will pick these up automatically.
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"compilerOptions": {
|
|
18
|
+
// ... other options ...
|
|
19
|
+
"jsx": "react-jsx",
|
|
20
|
+
"jsxImportSource": "@manyducks.co/dolla"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
End.
|
|
28
|
+
|
|
29
|
+
- [🗂️ Docs](./index.md)
|
|
30
|
+
- [🏠 README](../README.md)
|
|
31
|
+
- [🦆 That's a lot of ducks.](https://www.manyducks.co)
|
package/docs/signals.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
## ⚡ Reactive Updates with `Signals`
|
|
2
|
+
|
|
3
|
+
Dolla sets out to solve the challenge of keeping your UI in sync with your data. All apps have state that changes at runtime, and your UI must update itself to stay in sync with that state as it changes. JavaScript frameworks all have their own ways of doing this, but there are two main ones; virtual DOM and signals. Dolla follows the Signals philosophy.
|
|
4
|
+
|
|
5
|
+
[React](https://react.dev) and similar frameworks make use of a [virtual DOM](https://svelte.dev/blog/virtual-dom-is-pure-overhead), in which every state change causes a "diff" of the real DOM nodes on the page against a lightweight representation of what those nodes _should_ look like, followed by a "patch" where the minimal updates are performed to bring the DOM in line with the ideal virtual DOM.
|
|
6
|
+
|
|
7
|
+
[Solid](https://www.solidjs.com) and similar frameworks make use of [signals](https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob), which are containers for data that will change over time. Signal values are accessed through special getter functions that can be called inside of a "scope" to track their values. When the value of a tracked signal changes, any computations that happened in scopes that depend on those signals are re-run. In an app like this, all of your DOM updates are performed with pinpoint accuracy without diffing as signal values change.
|
|
8
|
+
|
|
9
|
+
The Signals API in Dolla has just four functions:
|
|
10
|
+
|
|
11
|
+
- `$` to create a new Source or derived Signal.
|
|
12
|
+
- `get` to unwrap a possible Signal value.
|
|
13
|
+
- `peek` to unwrap a possible Signal value without tracking it.
|
|
14
|
+
- `effect` to run side effects when tracked signals change.
|
|
15
|
+
|
|
16
|
+
### Basic State API
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import { $ } from "@manyducks.co/dolla";
|
|
20
|
+
|
|
21
|
+
const count = $(72);
|
|
22
|
+
|
|
23
|
+
// Get the current value.
|
|
24
|
+
count(): // 72
|
|
25
|
+
|
|
26
|
+
// Set a new value.
|
|
27
|
+
count(300);
|
|
28
|
+
|
|
29
|
+
// The State now reflects the latest value.
|
|
30
|
+
count(); // 300
|
|
31
|
+
|
|
32
|
+
// Data can also be updated by passing an update function.
|
|
33
|
+
// This function takes the current state and returns the next.
|
|
34
|
+
count((value) => value + 1);
|
|
35
|
+
count(); // 301
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Deriving States from other States
|
|
39
|
+
|
|
40
|
+
#### Example 1: Doubled
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
import { $ } from "@manyducks.co/dolla";
|
|
44
|
+
|
|
45
|
+
// Passing a value to $() results in a Source...
|
|
46
|
+
const count = $(1);
|
|
47
|
+
|
|
48
|
+
// ...while passing a function results in a Signal with a derived value.
|
|
49
|
+
const doubled = $(() => count() * 2);
|
|
50
|
+
|
|
51
|
+
count(10);
|
|
52
|
+
doubled(); // 20
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
##### A note on derived signals.
|
|
56
|
+
|
|
57
|
+
Because signals are simply functions that return a value, you can also derive state by simply defining a function that returns a value. Any `Source` called in this function will therefore be tracked when this function is called in a tracked scope.
|
|
58
|
+
|
|
59
|
+
The difference is that the value of the plain function is computed again each and every time that function is called. Wrapping it with `$()` will result in the computed value being cached until one of its dependencies changes. If you are coming from React then you may want to think of this like `useMemo`.
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
// Plain getter: OK
|
|
63
|
+
const plainCount = () => count() * 2;
|
|
64
|
+
|
|
65
|
+
// Signal: OK
|
|
66
|
+
const cachedCount = $(() => count() * 2);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Using plain getters to derive values is perfectly fine. It may be a waste to cache a very simple getter, but if the value is accessed frequently or involves expensive computations then you can get better performance by wrapping it in a Signal.
|
|
70
|
+
|
|
71
|
+
#### Example 2: Selecting a User
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
import { $ } from "@manyducks.co/dolla";
|
|
75
|
+
|
|
76
|
+
const users = $([
|
|
77
|
+
{ id: 1, name: "Audie" },
|
|
78
|
+
{ id: 2, name: "Bob" },
|
|
79
|
+
{ id: 3, name: "Cabel" },
|
|
80
|
+
]);
|
|
81
|
+
const userId = $(1);
|
|
82
|
+
|
|
83
|
+
const selectedUser = $(() => users().find((user) => user.id === userId()));
|
|
84
|
+
|
|
85
|
+
selectedUser(); // { id: 1, name: "Audie" }
|
|
86
|
+
|
|
87
|
+
userId(3);
|
|
88
|
+
|
|
89
|
+
selectedUser(); // { id: 3, name: "Cabel" }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
That was a more realistic example you might actually use in real life. Here we are selecting a user from a list based on its `id` field. This is kind of similar to a `JOIN` operation in a SQL database. I use this kind of pattern constantly in my apps.
|
|
93
|
+
|
|
94
|
+
The strength of setting up a join like this is that the `$users` array can be updated (by API call, websockets, etc.) and your `$selectedUser` will always be pointing to the latest version of the user data.
|
|
95
|
+
|
|
96
|
+
#### Example 3: Narrowing Complex Data
|
|
97
|
+
|
|
98
|
+
```jsx
|
|
99
|
+
import { $ } from "@manyducks.co/dolla";
|
|
100
|
+
|
|
101
|
+
const user = $({ id: 1, name: "Audie" });
|
|
102
|
+
const name = $(() => user().name);
|
|
103
|
+
|
|
104
|
+
name(); // "Audie"
|
|
105
|
+
|
|
106
|
+
// In a view:
|
|
107
|
+
<span class="user-name">{name}</span>;
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Another common pattern. In a real app, most data is stored as arrays of objects. But what you need in order to slot it into a view is just a string. In the example above we've selected the user's name and slotted it into a `span`. If the `$user` value ever changes, the name will stay in sync.
|
|
111
|
+
|
|
112
|
+
### Converting to and from Signals
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
import { $, get } from "@manyducks.co/dolla";
|
|
116
|
+
|
|
117
|
+
const count = state(512);
|
|
118
|
+
|
|
119
|
+
// Unwrap the value of count. Returns 512.
|
|
120
|
+
const value = get(count);
|
|
121
|
+
// Passing a non-state value will simply return it.
|
|
122
|
+
const name = get("World");
|
|
123
|
+
|
|
124
|
+
// If you need to convert a static piece of data into a Signal you can simply wrap it in a getter function.
|
|
125
|
+
const value = () => "Hello";
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### In Views
|
|
129
|
+
|
|
130
|
+
```jsx
|
|
131
|
+
import { $ } from "@manyducks.co/dolla";
|
|
132
|
+
|
|
133
|
+
function UserNameView(props, ctx) {
|
|
134
|
+
const name = $(() => props.user().name);
|
|
135
|
+
|
|
136
|
+
// Passing an object to `class` results in keys with a truthy value being applied as classes.
|
|
137
|
+
// Those with falsy values will be ignored.
|
|
138
|
+
// Signals can be given as values and they will be tracked.
|
|
139
|
+
return (
|
|
140
|
+
<span
|
|
141
|
+
class={{
|
|
142
|
+
"user-name": true,
|
|
143
|
+
"is-selected": props.selected
|
|
144
|
+
}}>
|
|
145
|
+
{name}
|
|
146
|
+
</span>
|
|
147
|
+
);
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
// In parent view:
|
|
151
|
+
|
|
152
|
+
const selected = $(false);
|
|
153
|
+
const user = $({ id: 1, name: "Audie" });
|
|
154
|
+
|
|
155
|
+
<UserNameView selected={selected} user={user} />
|
|
156
|
+
|
|
157
|
+
// Changing signal values out here will now update the UserNameView internals.
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
End.
|
|
163
|
+
|
|
164
|
+
- [🗂️ Docs](./index.md)
|
|
165
|
+
- [🏠 README](../README.md)
|
|
166
|
+
- [🦆 That's a lot of ducks.](https://www.manyducks.co)
|
package/docs/state.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
## ⚡ Reactive Updates with `State`
|
|
2
|
+
|
|
3
|
+
Dolla sets out to solve the challenge of keeping your UI in sync with your data. All apps have state that changes at runtime, and as those values change your UI must update itself to stay in sync with that state. JavaScript frameworks all have their own ways of meeting this challenge, but there are two main ones; virtual DOM and signals.
|
|
4
|
+
|
|
5
|
+
[React](https://react.dev) and similar frameworks make use of a [virtual DOM](https://svelte.dev/blog/virtual-dom-is-pure-overhead), in which every state change causes a "diff" of the real DOM nodes on the page against a lightweight representation of what those nodes _should_ look like, followed by a "patch" where the minimal updates are performed to bring the DOM in line with the ideal virtual DOM.
|
|
6
|
+
|
|
7
|
+
[Solid](https://www.solidjs.com) and similar frameworks make use of [signals](https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob), which are containers for data that will change over time. Signal values are accessed through special getter functions that can be called inside of a "scope" to track their values. When the value of a tracked signal changes, any computations that happened in scopes that depend on those signals are re-run. In an app like this, all of your DOM updates are performed with pinpoint accuracy without diffing as signal values change.
|
|
8
|
+
|
|
9
|
+
Dolla uses a concept of a `State`, which is a signal-like container for values that change over time. Where `State` differs from signals, however, is that there is no magical scope tracking going on behind the scenes. All States that depend on others do so explicity, so your code is easier to read and understand.
|
|
10
|
+
|
|
11
|
+
The `State` API has just four functions:
|
|
12
|
+
|
|
13
|
+
- `createState` to create a new state and a linked setter function.
|
|
14
|
+
- `derive` to create a new state whose value depends on one or more other states.
|
|
15
|
+
- `toState` to ensure that a value is a state object.
|
|
16
|
+
- `toValue` to ensure that a value is a plain value.
|
|
17
|
+
|
|
18
|
+
### Basic State API
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import { createState } from "@manyducks.co/dolla";
|
|
22
|
+
|
|
23
|
+
// Equivalent to React's `useState` or Solid's `createSignal`.
|
|
24
|
+
// A new read-only State and linked Setter are created.
|
|
25
|
+
const [$count, setCount] = createState(72);
|
|
26
|
+
|
|
27
|
+
// Get the current value.
|
|
28
|
+
$count.get(): // 72
|
|
29
|
+
|
|
30
|
+
// Set a new value.
|
|
31
|
+
setCount(300);
|
|
32
|
+
|
|
33
|
+
// The State now reflects the latest value.
|
|
34
|
+
$count.get(); // 300
|
|
35
|
+
|
|
36
|
+
// Data can also be updated by passing a function.
|
|
37
|
+
// This function takes the current state and returns a new one.
|
|
38
|
+
setCount((current) => current + 1);
|
|
39
|
+
$count.get(); // 301
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Deriving States from other States
|
|
43
|
+
|
|
44
|
+
#### Example 1: Doubled
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import { createState, derive } from "@manyducks.co/dolla";
|
|
48
|
+
|
|
49
|
+
const [$count, setCount] = createState(1);
|
|
50
|
+
|
|
51
|
+
const $doubled = derive([$count], (count) => count * 2);
|
|
52
|
+
|
|
53
|
+
setCount(10);
|
|
54
|
+
$doubled.get(); // 20
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
That was a typical toy example where we create a `$doubled` state that always contains the value of `$count`... doubled! This is the essential basic example of computed properties, as written in Dolla.
|
|
58
|
+
|
|
59
|
+
#### Example 2: Selecting a User
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
import { createState, derive } from "@manyducks.co/dolla";
|
|
63
|
+
|
|
64
|
+
const [$users, setUsers] = createState([
|
|
65
|
+
{ id: 1, name: "Audie" },
|
|
66
|
+
{ id: 2, name: "Bob" },
|
|
67
|
+
{ id: 3, name: "Cabel" },
|
|
68
|
+
]);
|
|
69
|
+
const [$selectedUserId, setSelectedUserId] = createState(1);
|
|
70
|
+
|
|
71
|
+
const $selectedUser = derive([$users, $selectedUserId], (users, id) => {
|
|
72
|
+
return users.find((user) => user.id === id);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
$selectedUser.get(); // { id: 1, name: "Audie" }
|
|
76
|
+
|
|
77
|
+
setSelectedId(3);
|
|
78
|
+
|
|
79
|
+
$selectedUser.get(); // { id: 3, name: "Cabel" }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
That was a more realistic example you might actually use in real life. Here we are selecting a user from a list based on its `id` field. This is kind of similar to a `JOIN` operation in a SQL database. I use this kind of pattern constantly in my apps.
|
|
83
|
+
|
|
84
|
+
The strength of setting up a join like this is that the `$users` array can be updated (by API call, websockets, etc.) and your `$selectedUser` will always be pointing to the latest version of the user data.
|
|
85
|
+
|
|
86
|
+
#### Example 3: Narrowing Complex Data
|
|
87
|
+
|
|
88
|
+
```jsx
|
|
89
|
+
import { createState, derive } from "@manyducks.co/dolla";
|
|
90
|
+
|
|
91
|
+
const [$user, setUser] = createState({ id: 1, name: "Audie" });
|
|
92
|
+
|
|
93
|
+
const $name = derive([$user], (user) => user.name);
|
|
94
|
+
|
|
95
|
+
$name.get(); // "Audie"
|
|
96
|
+
|
|
97
|
+
// In a view:
|
|
98
|
+
<span class="user-name">{$name}</span>;
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Another common pattern. In a real app, most data is stored as arrays of objects. But what you need in order to slot it into a view is just a string. In the example above we've selected the user's name and slotted it into a `span`. If the `$user` value ever changes, the name will stay in sync.
|
|
102
|
+
|
|
103
|
+
### Converting to and from States
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
import { createState, toState, toValue } from "@manyducks.co/dolla";
|
|
107
|
+
|
|
108
|
+
const [$count, setCount] = createState(512);
|
|
109
|
+
|
|
110
|
+
// Unwrap the value of $count. Returns 512.
|
|
111
|
+
const count = toValue($count);
|
|
112
|
+
// Passing a non-state value will simply return it.
|
|
113
|
+
const name = toValue("World");
|
|
114
|
+
|
|
115
|
+
// Wrap "Hello" into a State containing "Hello"
|
|
116
|
+
const $value = toState("Hello");
|
|
117
|
+
// Passing a state will simply return that same state.
|
|
118
|
+
const $number = toState($count);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### In Views
|
|
122
|
+
|
|
123
|
+
```jsx
|
|
124
|
+
import { derive } from "@manyducks.co/dolla";
|
|
125
|
+
|
|
126
|
+
function UserNameView(props, ctx) {
|
|
127
|
+
const $name = derive([props.$user], (user) => user.name);
|
|
128
|
+
|
|
129
|
+
return <span class={{ "user-name": true, "is-selected": props.$selected }}>{$name}</span>;
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
In the example above we've displayed the `name` field from a `$user` object inside of a span. We are also assigning an `is-selected` class dynamically based on whether the `$selected` prop contains a truthy or falsy value.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
End.
|
|
138
|
+
|
|
139
|
+
- [🗂️ Docs](./index.md)
|
|
140
|
+
- [🏠 README](../README.md)
|
|
141
|
+
- [🦆 That's a lot of ducks.](https://www.manyducks.co)
|
package/docs/stores.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Stores
|
|
2
|
+
|
|
3
|
+
> TODO: Write about stores
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import Dolla, { createState } from "@manyducks.co/dolla";
|
|
7
|
+
|
|
8
|
+
function CounterStore (initialValue, ctx) {
|
|
9
|
+
const [$count, setCount] = createState(initialValue);
|
|
10
|
+
|
|
11
|
+
// Respond to context events which bubble up from views.
|
|
12
|
+
ctx.on("counter:increment", (e) => {
|
|
13
|
+
e.stop(); // call to stop events bubbling to parent contexts.
|
|
14
|
+
setCount((count) => count + 1);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
ctx.on("counter:decrement", () => {
|
|
18
|
+
setCount((count) => count - 1);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
ctx.on("counter:reset", () => {
|
|
22
|
+
setCount(0);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return $count;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Stores can be provided by the app itself.
|
|
29
|
+
Dolla.provide(CounterStore, 0);
|
|
30
|
+
|
|
31
|
+
function CounterView(props, ctx) {
|
|
32
|
+
// Store instances can also be provided at the view level to provide them to the current scope and those of child views.
|
|
33
|
+
// Views that are not children of this CounterView will not be able to access this particular instance of CounterStore.
|
|
34
|
+
ctx.provide(CounterStore, 0);
|
|
35
|
+
|
|
36
|
+
// Store return values can be accessed with `use`.
|
|
37
|
+
// This method will check the current context for an instance, then recursively check up the view tree until it finds one.
|
|
38
|
+
// An error will be thrown if no instances of the store are provided.
|
|
39
|
+
const $count = ctx.use(CounterStore);
|
|
40
|
+
|
|
41
|
+
// The buttons increment the value inside the store by emitting events.
|
|
42
|
+
// Child views at any depth could also emit these events to update the store.
|
|
43
|
+
return (
|
|
44
|
+
<div>
|
|
45
|
+
<p>Clicks: {$count}</p>
|
|
46
|
+
<div>
|
|
47
|
+
<button onClick={() => ctx.emit("counter:decrement")}>-1</button>
|
|
48
|
+
<button onClick={() => ctx.emit("counter:reset")}>Reset</button>
|
|
49
|
+
<button onClick={() => ctx.emit("counter:increment")}>+1</button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
End.
|
|
59
|
+
|
|
60
|
+
- [🗂️ Docs](./index.md)
|
|
61
|
+
- [🏠 README](../README.md)
|
|
62
|
+
- [🦆 That's a lot of ducks.](https://www.manyducks.co)
|