@manyducks.co/dolla 2.0.0-alpha.46 → 2.0.0-alpha.47
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 +31 -15
- package/dist/core/context.d.ts +2 -2
- package/dist/core/markup.d.ts +25 -24
- package/dist/core/nodes/dynamic.d.ts +2 -2
- package/dist/core/nodes/fragment.d.ts +2 -2
- package/dist/core/nodes/outlet.d.ts +2 -2
- package/dist/core/nodes/{list.d.ts → repeat.d.ts} +6 -6
- package/dist/core/nodes/view.d.ts +3 -3
- package/dist/core/signals-api.d.ts +42 -0
- package/dist/core/signals.d.ts +16 -194
- package/dist/core/store.d.ts +2 -2
- package/dist/fragment-vl5kFl1d.js +8 -0
- package/dist/fragment-vl5kFl1d.js.map +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +443 -398
- package/dist/index.js.map +1 -1
- package/dist/jsx-dev-runtime.js +2 -2
- package/dist/jsx-runtime.js +2 -2
- package/dist/{markup-BJffA2Ls.js → markup-B-2w-v-S.js} +641 -736
- package/dist/markup-B-2w-v-S.js.map +1 -0
- package/dist/router/index.d.ts +22 -8
- package/dist/router/router.utils.d.ts +3 -3
- package/dist/translate/index.d.ts +10 -10
- package/dist/types.d.ts +9 -9
- package/docs/signals.md +82 -65
- package/notes/molecule.md +35 -0
- package/package.json +1 -1
- package/dist/core/ref.d.ts +0 -28
- package/dist/core/views/for.d.ts +0 -9
- package/dist/fragment-CYt92o5I.js +0 -8
- package/dist/fragment-CYt92o5I.js.map +0 -1
- package/dist/markup-BJffA2Ls.js.map +0 -1
- package/dist/router/router.d.ts +0 -0
- /package/dist/core/{signals.test.d.ts → signals-api.test.d.ts} +0 -0
package/docs/signals.md
CHANGED
|
@@ -1,50 +1,38 @@
|
|
|
1
1
|
## ⚡ Reactive Updates with `State`
|
|
2
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
|
|
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
4
|
|
|
5
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
6
|
|
|
7
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
8
|
|
|
9
|
-
|
|
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
|
-
- `atom`
|
|
19
|
-
- `compose`
|
|
20
|
-
- `effect`
|
|
21
|
-
- `get`
|
|
22
|
-
- `peek`
|
|
23
|
-
- `set`
|
|
9
|
+
The Signals API in Dolla has just four functions:
|
|
24
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.
|
|
25
15
|
|
|
26
16
|
### Basic State API
|
|
27
17
|
|
|
28
18
|
```js
|
|
29
|
-
import {
|
|
19
|
+
import { $ } from "@manyducks.co/dolla";
|
|
30
20
|
|
|
31
|
-
|
|
32
|
-
// A new read-only State and linked Setter are created.
|
|
33
|
-
const [$count, setCount] = createState(72);
|
|
21
|
+
const count = $(72);
|
|
34
22
|
|
|
35
23
|
// Get the current value.
|
|
36
|
-
|
|
24
|
+
count(): // 72
|
|
37
25
|
|
|
38
26
|
// Set a new value.
|
|
39
|
-
|
|
27
|
+
count(300);
|
|
40
28
|
|
|
41
29
|
// The State now reflects the latest value.
|
|
42
|
-
|
|
30
|
+
count(); // 300
|
|
43
31
|
|
|
44
|
-
// Data can also be updated by passing
|
|
45
|
-
// This function takes the current state and returns
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
48
36
|
```
|
|
49
37
|
|
|
50
38
|
### Deriving States from other States
|
|
@@ -52,39 +40,53 @@ $count.get(); // 301
|
|
|
52
40
|
#### Example 1: Doubled
|
|
53
41
|
|
|
54
42
|
```js
|
|
55
|
-
import {
|
|
43
|
+
import { $ } from "@manyducks.co/dolla";
|
|
56
44
|
|
|
57
|
-
|
|
45
|
+
// Passing a value to $() results in a Source...
|
|
46
|
+
const count = $(1);
|
|
58
47
|
|
|
59
|
-
|
|
48
|
+
// ...while passing a function results in a Signal with a derived value.
|
|
49
|
+
const doubled = $(() => count() * 2);
|
|
60
50
|
|
|
61
|
-
|
|
62
|
-
|
|
51
|
+
count(10);
|
|
52
|
+
doubled(); // 20
|
|
63
53
|
```
|
|
64
54
|
|
|
65
|
-
|
|
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.
|
|
66
70
|
|
|
67
71
|
#### Example 2: Selecting a User
|
|
68
72
|
|
|
69
73
|
```js
|
|
70
|
-
import {
|
|
74
|
+
import { $ } from "@manyducks.co/dolla";
|
|
71
75
|
|
|
72
|
-
const
|
|
76
|
+
const users = $([
|
|
73
77
|
{ id: 1, name: "Audie" },
|
|
74
78
|
{ id: 2, name: "Bob" },
|
|
75
79
|
{ id: 3, name: "Cabel" },
|
|
76
80
|
]);
|
|
77
|
-
const
|
|
81
|
+
const userId = $(1);
|
|
78
82
|
|
|
79
|
-
const
|
|
80
|
-
return users.find((user) => user.id === id);
|
|
81
|
-
});
|
|
83
|
+
const selectedUser = $(() => users().find((user) => user.id === userId()));
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
selectedUser(); // { id: 1, name: "Audie" }
|
|
84
86
|
|
|
85
|
-
|
|
87
|
+
userId(3);
|
|
86
88
|
|
|
87
|
-
|
|
89
|
+
selectedUser(); // { id: 3, name: "Cabel" }
|
|
88
90
|
```
|
|
89
91
|
|
|
90
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.
|
|
@@ -94,52 +96,67 @@ The strength of setting up a join like this is that the `$users` array can be up
|
|
|
94
96
|
#### Example 3: Narrowing Complex Data
|
|
95
97
|
|
|
96
98
|
```jsx
|
|
97
|
-
import {
|
|
99
|
+
import { $ } from "@manyducks.co/dolla";
|
|
98
100
|
|
|
99
|
-
const
|
|
101
|
+
const user = $({ id: 1, name: "Audie" });
|
|
102
|
+
const name = $(() => user().name);
|
|
100
103
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
$name.get(); // "Audie"
|
|
104
|
+
name(); // "Audie"
|
|
104
105
|
|
|
105
106
|
// In a view:
|
|
106
|
-
<span class="user-name">{
|
|
107
|
+
<span class="user-name">{name}</span>;
|
|
107
108
|
```
|
|
108
109
|
|
|
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.
|
|
110
111
|
|
|
111
|
-
### Converting to and from
|
|
112
|
+
### Converting to and from Signals
|
|
112
113
|
|
|
113
114
|
```js
|
|
114
|
-
import {
|
|
115
|
+
import { $, get } from "@manyducks.co/dolla";
|
|
115
116
|
|
|
116
|
-
const
|
|
117
|
+
const count = state(512);
|
|
117
118
|
|
|
118
|
-
// Unwrap the value of
|
|
119
|
-
const
|
|
119
|
+
// Unwrap the value of count. Returns 512.
|
|
120
|
+
const value = get(count);
|
|
120
121
|
// Passing a non-state value will simply return it.
|
|
121
|
-
const name =
|
|
122
|
+
const name = get("World");
|
|
122
123
|
|
|
123
|
-
//
|
|
124
|
-
const
|
|
125
|
-
// Passing a state will simply return that same state.
|
|
126
|
-
const $number = toState($count);
|
|
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";
|
|
127
126
|
```
|
|
128
127
|
|
|
129
128
|
### In Views
|
|
130
129
|
|
|
131
130
|
```jsx
|
|
132
|
-
import {
|
|
131
|
+
import { $ } from "@manyducks.co/dolla";
|
|
133
132
|
|
|
134
133
|
function UserNameView(props, ctx) {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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.
|
|
139
158
|
```
|
|
140
159
|
|
|
141
|
-
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.
|
|
142
|
-
|
|
143
160
|
---
|
|
144
161
|
|
|
145
162
|
End.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Like `compose` but it takes an initial value and a function that can set its value asynchronously. Its callback is a tracking context, so it will be re-run when signals called within are updated.
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
interface MoleculeGetter<T> {
|
|
5
|
+
(): T;
|
|
6
|
+
<X>(source: MaybeReactive<X>): X;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface MoleculeSetter<T> {
|
|
10
|
+
(next: T): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface MoleculeFunction<T> {
|
|
14
|
+
(get: MoleculeGetter<T>, set: MoleculeSetter<T>): void | (() => void);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function molecule<T>(initialValue: T, fn: MoleculeFunction<T>) {}
|
|
18
|
+
|
|
19
|
+
const value = molecule(5, (get, set) => {
|
|
20
|
+
// get() returns the current value stored in this hadron
|
|
21
|
+
// get(reactive) returns the value of that reactive and tracks it (== reactive.get())
|
|
22
|
+
// set(value) updates the value stored in this hadron
|
|
23
|
+
// This function will not be called unless there is at least one observer.
|
|
24
|
+
|
|
25
|
+
let interval = setInterval(() => {
|
|
26
|
+
set(get() + 1);
|
|
27
|
+
}, 1000);
|
|
28
|
+
|
|
29
|
+
// Can return a cleanup function to run between invocations.
|
|
30
|
+
// Also called when the last observer stops observing.
|
|
31
|
+
return () => {
|
|
32
|
+
clearInterval(interval);
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
```
|
package/package.json
CHANGED
package/dist/core/ref.d.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A `Ref` is a function that stores a value when called with a single argument,
|
|
3
|
-
* and returns the most recently stored value when called with no arguments.
|
|
4
|
-
*/
|
|
5
|
-
export interface Ref<T> {
|
|
6
|
-
/**
|
|
7
|
-
* Get: returns the current value stored in the ref (or undefined).
|
|
8
|
-
*/
|
|
9
|
-
(): T | undefined;
|
|
10
|
-
/**
|
|
11
|
-
* Set: stores a new `value` in the ref.
|
|
12
|
-
*/
|
|
13
|
-
<T>(value: T | undefined): void;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* A Ref is a function that returns the last argument it was called with.
|
|
17
|
-
* Calling it with no arguments will simply return the latest value.
|
|
18
|
-
* Calling it with an argument will store that value and immediately return it.
|
|
19
|
-
*
|
|
20
|
-
* @param value - An (optional) initial value to store.
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* const number = ref(5);
|
|
24
|
-
* number(); // 5
|
|
25
|
-
* number(500);
|
|
26
|
-
* number(); // 500
|
|
27
|
-
*/
|
|
28
|
-
export declare function ref<T>(value?: T): Ref<T>;
|
package/dist/core/views/for.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { ViewContext, ViewResult } from "../nodes/view";
|
|
2
|
-
import { MaybeReactive, Reactive } from "../signals";
|
|
3
|
-
interface ForProps<T> {
|
|
4
|
-
each: MaybeReactive<Iterable<T>>;
|
|
5
|
-
key: (item: T, index: number) => any;
|
|
6
|
-
children: (item: Reactive<T>, index: Reactive<number>, ctx: ViewContext) => ViewResult;
|
|
7
|
-
}
|
|
8
|
-
export declare function For<T = any>({ each, key, children }: ForProps<T>): import("../markup").Markup;
|
|
9
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"fragment-CYt92o5I.js","sources":["../src/core/views/fragment.ts"],"sourcesContent":["import type { Renderable } from \"../../types.js\";\nimport { markup } from \"../markup.js\";\nimport { type ViewContext } from \"../nodes/view.js\";\nimport { compose } from \"../signals.js\";\n\n/**\n * A utility view that displays its children.\n */\nexport function Fragment(props: { children?: Renderable }, ctx: ViewContext) {\n return markup(\"$dynamic\", { source: compose(() => props.children) });\n}\n"],"names":["Fragment","props","ctx","markup","compose"],"mappings":";AAQgB,SAAAA,EAASC,GAAkCC,GAAkB;AACpE,SAAAC,EAAO,YAAY,EAAE,QAAQC,EAAQ,MAAMH,EAAM,QAAQ,GAAG;AACrE;"}
|