@manyducks.co/dolla 2.0.0-alpha.27 → 2.0.0-alpha.29
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 +27 -612
- package/dist/core/context.d.ts +88 -0
- package/dist/core/dolla.d.ts +30 -12
- package/dist/core/markup.d.ts +1 -23
- package/dist/core/nodes/html.d.ts +2 -1
- package/dist/core/nodes/observer.d.ts +2 -1
- package/dist/core/nodes/portal.d.ts +3 -2
- package/dist/core/nodes/repeat.d.ts +3 -2
- package/dist/core/nodes/view.d.ts +10 -13
- package/dist/core/store.d.ts +61 -0
- package/dist/core/symbols.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +501 -457
- package/dist/index.js.map +1 -1
- package/dist/jsx-dev-runtime.js +2 -2
- package/dist/jsx-runtime.js +2 -2
- package/dist/passthrough-eH8w4zJi.js +1511 -0
- package/dist/passthrough-eH8w4zJi.js.map +1 -0
- package/docs/http.md +8 -0
- package/docs/i18n.md +35 -2
- package/docs/index.md +10 -0
- package/docs/router.md +65 -12
- package/docs/setup.md +10 -1
- package/docs/state.md +141 -0
- package/docs/stores.md +62 -0
- package/docs/views.md +305 -2
- package/notes/stores.md +73 -0
- package/package.json +1 -1
- package/dist/passthrough-D9NjRov5.js +0 -1319
- package/dist/passthrough-D9NjRov5.js.map +0 -1
- package/docs/states.md +0 -41
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, createView } from "@manyducks.co/dolla";
|
|
125
|
+
|
|
126
|
+
const UserNameView = createView(function (props) {
|
|
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, { createStore, createState, createView } from "@manyducks.co/dolla";
|
|
7
|
+
|
|
8
|
+
const CounterStore = createStore(function (initialValue: number) {
|
|
9
|
+
const [$count, setCount] = createState(initialValue);
|
|
10
|
+
|
|
11
|
+
// Respond to context events which bubble up from views.
|
|
12
|
+
this.on("counter:increment", () => {
|
|
13
|
+
setCount((count) => count + 1);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
this.on("counter:decrement", () => {
|
|
17
|
+
setCount((count) => count - 1);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
this.on("counter:reset", () => {
|
|
21
|
+
setCount(0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return $count;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Stores can be attached to the app itself.
|
|
28
|
+
// createStore returns a factory function that is called with the store's options to create a new instance.
|
|
29
|
+
Dolla.attachStore(CounterStore(0));
|
|
30
|
+
|
|
31
|
+
const CounterView = createView(function () {
|
|
32
|
+
// Store instances can also be attached 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
|
+
this.attachStore(CounterStore(0));
|
|
35
|
+
|
|
36
|
+
// Store return values can be accessed with `useStore`.
|
|
37
|
+
// This method will traverse up the view tree to find the nearest attached instance.
|
|
38
|
+
// An error will be thrown if no instances are attached at or above this level in the tree.
|
|
39
|
+
const $count = this.useStore(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={() => this.emit("counter:decrement")}>-1</button>
|
|
48
|
+
<button onClick={() => this.emit("counter:reset")}>Reset</button>
|
|
49
|
+
<button onClick={() => this.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)
|
package/docs/views.md
CHANGED
|
@@ -1,5 +1,308 @@
|
|
|
1
1
|
# Views
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Views are one of two component types in Dolla. We call them views because they deal specifically with presenting visible things to the user. The other type of component, [Stores](./stores.md), deal with data and events.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
At its most basic, a view is a function that returns elements.
|
|
6
|
+
|
|
7
|
+
```jsx
|
|
8
|
+
const ExampleView = createView(function () {
|
|
9
|
+
return <h1>Hello World!</h1>;
|
|
10
|
+
});
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## View Props
|
|
14
|
+
|
|
15
|
+
A view function takes a `props` object as its first argument. This object contains all properties passed to the view when it's invoked.
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
const ListItemView = createView(function (props) {
|
|
19
|
+
return <li>{props.label}</li>;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const ListView = createView(function () {
|
|
23
|
+
return (
|
|
24
|
+
<ul>
|
|
25
|
+
<ListItemView label="Squirrel" />
|
|
26
|
+
<ListItemView label="Chipmunk" />
|
|
27
|
+
<ListItemView label="Groundhog" />
|
|
28
|
+
</ul>
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
As you may have guessed, you can pass States as props and slot them in in exactly the same way. This is important because Views do not re-render the way you might expect from other frameworks. Whatever you pass as props is what the View gets for its entire lifecycle.
|
|
34
|
+
|
|
35
|
+
## View Helpers
|
|
36
|
+
|
|
37
|
+
### `cond($condition, whenTruthy, whenFalsy)`
|
|
38
|
+
|
|
39
|
+
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.
|
|
40
|
+
|
|
41
|
+
```jsx
|
|
42
|
+
const ConditionalListView = createView(function (props) {
|
|
43
|
+
return (
|
|
44
|
+
<div>
|
|
45
|
+
{cond(
|
|
46
|
+
props.$show,
|
|
47
|
+
|
|
48
|
+
// Visible when truthy
|
|
49
|
+
<ul>
|
|
50
|
+
<ListItemView label="Squirrel" />
|
|
51
|
+
<ListItemView label="Chipmunk" />
|
|
52
|
+
<ListItemView label="Groundhog" />
|
|
53
|
+
</ul>,
|
|
54
|
+
|
|
55
|
+
// Visible when falsy
|
|
56
|
+
<span>List is hidden</span>,
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### `repeat($items, keyFn, renderFn)`
|
|
64
|
+
|
|
65
|
+
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.
|
|
66
|
+
|
|
67
|
+
```jsx
|
|
68
|
+
const RepeatedListView = createView(function () {
|
|
69
|
+
const [$items, setItems] = createState(["Squirrel", "Chipmunk", "Groundhog"]);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<ul>
|
|
73
|
+
{repeat(
|
|
74
|
+
$items,
|
|
75
|
+
(item, index) => item, // Using the string itself as the key
|
|
76
|
+
($item, $index, context) => {
|
|
77
|
+
return <ListItemView label={$item} />;
|
|
78
|
+
},
|
|
79
|
+
)}
|
|
80
|
+
</ul>
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `portal(content, parentNode)`
|
|
86
|
+
|
|
87
|
+
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.
|
|
88
|
+
|
|
89
|
+
```jsx
|
|
90
|
+
const PortalView = createView(function () {
|
|
91
|
+
const content = (
|
|
92
|
+
<div class="modal">
|
|
93
|
+
<p>This is a modal.</p>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Content will be appended to `document.body` while this view is connected.
|
|
98
|
+
return portal(document.body, content);
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## View Context
|
|
103
|
+
|
|
104
|
+
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.
|
|
105
|
+
|
|
106
|
+
The context can be accessed in one of two ways; as `this` when you pass a non-arrow function, or as the second parameter passed after the props object.
|
|
107
|
+
|
|
108
|
+
```jsx
|
|
109
|
+
// Option 1: Access through `this`
|
|
110
|
+
const ExampleView = createView(function (props) {
|
|
111
|
+
this.onMount(() => {
|
|
112
|
+
this.log("HELLO!");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return <h1>Hello World!</h1>;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Option 2: Access as second argument (for arrow functions)
|
|
119
|
+
const ExampleView = createView((props, ctx) => {
|
|
120
|
+
ctx.onMount(() => {
|
|
121
|
+
ctx.log("HELLO!");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return <h1>Hello World!</h1>;
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Which one you use is just an aesthetic preference, but I kind of like the classic `function` syntax with `this`.
|
|
129
|
+
|
|
130
|
+
### Printing Debug Messages
|
|
131
|
+
|
|
132
|
+
```jsx
|
|
133
|
+
const ExampleView = createView(function (props) {
|
|
134
|
+
// Set the name of this view's context. Console messages are prefixed with name.
|
|
135
|
+
this.setName("CustomName");
|
|
136
|
+
|
|
137
|
+
// Print messages to the console. These are suppressed by default in the app's "production" mode.
|
|
138
|
+
// You can also change which of these are printed and filter messages from certain contexts in the `createApp` options object.
|
|
139
|
+
this.info("Verbose debugging info that might be useful to know");
|
|
140
|
+
this.log("Standard messages");
|
|
141
|
+
this.warn("Something bad might be happening");
|
|
142
|
+
this.error("Uh oh!");
|
|
143
|
+
|
|
144
|
+
// If you encounter a bad enough situation, you can halt and disconnect the entire app.
|
|
145
|
+
this.crash(new Error("BOOM"));
|
|
146
|
+
|
|
147
|
+
return <h1>Hello World!</h1>;
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Lifecycle Events
|
|
152
|
+
|
|
153
|
+
```jsx
|
|
154
|
+
const ExampleView = createView(function (props) {
|
|
155
|
+
this.beforeMount(() => {
|
|
156
|
+
// Do something before this view's DOM nodes are created.
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
this.onMount(() => {
|
|
160
|
+
// Do something immediately after this view is connected to the DOM.
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
this.beforeUnmount(() => {
|
|
164
|
+
// Do something before removing this view from the DOM.
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
this.onUnmount(() => {
|
|
168
|
+
// Do some cleanup after this view is disconnected from the DOM.
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return <h1>Hello World!</h1>;
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Displaying Children
|
|
176
|
+
|
|
177
|
+
The context has an `outlet` function that can be used to display children at a location of your choosing.
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
const LayoutView = createView(function (props) {
|
|
181
|
+
return (
|
|
182
|
+
<div className="layout">
|
|
183
|
+
<div className="content">{this.outlet()}</div>
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const ExampleView = createView(function () {
|
|
189
|
+
// <h1> and <p> are displayed inside LayoutView's outlet.
|
|
190
|
+
return (
|
|
191
|
+
<LayoutView>
|
|
192
|
+
<h1>Hello</h1>
|
|
193
|
+
<p>This is inside the box.</p>
|
|
194
|
+
</LayoutView>
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Watching States
|
|
200
|
+
|
|
201
|
+
The `watch` function starts observing when the view is connected and stops when disconnected. This takes care of cleaning up watchers so you don't have to worry about memory leaks.
|
|
202
|
+
|
|
203
|
+
```jsx
|
|
204
|
+
const ExampleView = createView(function (props) {
|
|
205
|
+
const [$count, setCount] = createState(0);
|
|
206
|
+
|
|
207
|
+
// This callback will run when any states in the dependency array receive new values.
|
|
208
|
+
this.watch([$count], (count) => {
|
|
209
|
+
this.log("count is now", count);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ...
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Context Variables
|
|
217
|
+
|
|
218
|
+
> TODO: Write about context state (`.get` and `.set`)
|
|
219
|
+
|
|
220
|
+
### Context Events
|
|
221
|
+
|
|
222
|
+
Events can be emitted from views and [stores](./stores.md) using `this.emit(eventName, data)`. Context events will bubble up the view tree just like native browser events bubble up the DOM tree.
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
this.on("eventName", (event) => {
|
|
226
|
+
event.type; // "eventName"
|
|
227
|
+
event.detail; // the value that was passed when the event was emitted (or undefined if none)
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
this.once("eventName", (event) => {
|
|
231
|
+
// Receive only once and then stop listening.
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Remove a listener by reference.
|
|
235
|
+
// Listener must be the same exact function that was passed to `on` or `once`.
|
|
236
|
+
this.off("eventName", listener);
|
|
237
|
+
|
|
238
|
+
// Emit an event.
|
|
239
|
+
this.emit("eventName", { value: "This object will be exposed as event.detail" });
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Bubbling
|
|
243
|
+
|
|
244
|
+
Events bubble up through the view tree unless `stopPropagation` is called by a listener. In the following example we have a view listening for events that are emitted from a child of a child.
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
const ParentView = createView(function () {
|
|
248
|
+
// Listen for greetings that bubble up.
|
|
249
|
+
this.on("greeting", (event) => {
|
|
250
|
+
const { name, message } = event.detail;
|
|
251
|
+
|
|
252
|
+
this.log(`${name} says "${message}"!`);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
<div>
|
|
257
|
+
<ChildView />
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const ChildView = createView(function () {
|
|
263
|
+
this.on("greeting", (event) => {
|
|
264
|
+
// Let's perform some censorship.
|
|
265
|
+
// If propagation is stopped this event will not bubble any further and ParentView won't see it.
|
|
266
|
+
if (containsForbiddenKnowledge(event.message)) {
|
|
267
|
+
event.stopPropagation();
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<div>
|
|
273
|
+
<ChildOfChildView />
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const ChildOfChildView = createView(function () {
|
|
279
|
+
return (
|
|
280
|
+
<form
|
|
281
|
+
onSubmit={(e) => {
|
|
282
|
+
// This is a browser event handler.
|
|
283
|
+
// Browser events can't be listened for with `this.on`, but we can emit context events from here that can be.
|
|
284
|
+
e.preventDefault();
|
|
285
|
+
|
|
286
|
+
// Pluck the values from the form.
|
|
287
|
+
const name = e.currentTarget.name.value;
|
|
288
|
+
const message = e.currentTarget.message.value;
|
|
289
|
+
|
|
290
|
+
// Emit!
|
|
291
|
+
this.emit("greeting", { name, message });
|
|
292
|
+
}}
|
|
293
|
+
>
|
|
294
|
+
<input type="text" name="name" placeholder="Your Name" />
|
|
295
|
+
<input type="text" name="message" placeholder="Your Message" />
|
|
296
|
+
<button type="submit">Submit</button>
|
|
297
|
+
</form>
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
End.
|
|
305
|
+
|
|
306
|
+
- [🗂️ Docs](./index.md)
|
|
307
|
+
- [🏠 README](../README.md)
|
|
308
|
+
- [🦆 That's a lot of ducks.](https://www.manyducks.co)
|
package/notes/stores.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Stores
|
|
2
|
+
|
|
3
|
+
Ideas for updating the API.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { createStore, attachStore, useStore, createView } from "@manyducks.co/dolla";
|
|
7
|
+
|
|
8
|
+
const Counter = createStore(function (initialCount: number) {
|
|
9
|
+
const [$value, setValue] = createState(initialCount);
|
|
10
|
+
|
|
11
|
+
this.on("counter:increment", (e) => {
|
|
12
|
+
e.stopPropagation(); // Stop this event from bubbling up to counters at higher levels (if any).
|
|
13
|
+
setValue((current) => current + 1);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
this.on("counter:decrement", (e) => {
|
|
17
|
+
e.stopPropagation();
|
|
18
|
+
setValue((current) => current - 1);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Events can be emitted from this context in a store.
|
|
22
|
+
this.emit("otherEvent");
|
|
23
|
+
|
|
24
|
+
this.onMount(() => {
|
|
25
|
+
// Setup
|
|
26
|
+
// This is called based on the context the store is attached to.
|
|
27
|
+
// If Dolla, it's called when the app is mounted. If ViewContext, it's called when the view is mounted.
|
|
28
|
+
});
|
|
29
|
+
this.onUnmount(() => {
|
|
30
|
+
// Cleanup
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Context variables will be accessible on the same context (e.g. the view this is attached to and below)
|
|
34
|
+
this.get("context variable");
|
|
35
|
+
this.set("context variable", "context variable value");
|
|
36
|
+
|
|
37
|
+
// Stores don't have to return anything, but if they do it becomes accessible by using `useStore(ctx, Store)`.
|
|
38
|
+
return $value;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Attach it to the app.
|
|
42
|
+
Dolla.attachStore(Counter(0));
|
|
43
|
+
|
|
44
|
+
const ExampleView = createView(function () {
|
|
45
|
+
// useStore lets you access the return value
|
|
46
|
+
// but the events will still be received and handled regardless
|
|
47
|
+
const $count = this.useStore(Counter);
|
|
48
|
+
|
|
49
|
+
// Convenience helper to attach and use in one step?
|
|
50
|
+
const $count = this.attachAndUseStore(Counter(0));
|
|
51
|
+
|
|
52
|
+
return html`
|
|
53
|
+
<button onclick=${() => this.emit("counter:decrement")}>-1</button>
|
|
54
|
+
<span>${$count}</span>
|
|
55
|
+
<button onclick=${() => this.emit("counter:increment")}>+1</button>
|
|
56
|
+
`;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ViewContext is also still passed as a second argument if you'd rather use arrow functions to define views.
|
|
60
|
+
const ExampleView = createView((props, self) => {
|
|
61
|
+
// useStore lets you access the return value
|
|
62
|
+
// but the events will still be received and handled regardless
|
|
63
|
+
const $count = self.useStore(Counter);
|
|
64
|
+
|
|
65
|
+
return html`
|
|
66
|
+
<button onclick=${() => self.emit("counter:decrement")}>-1</button>
|
|
67
|
+
<span>${$count}</span>
|
|
68
|
+
<button onclick=${() => self.emit("counter:increment")}>+1</button>
|
|
69
|
+
`;
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This means `createStore` returns a function that is called to create a Store instance. The instance is
|