@manyducks.co/dolla 0.78.2 → 1.0.1

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 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 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.
8
+ Dolla is a batteries-included JavaScript frontend framework covering the needs of complex modern single page apps (SPA). Dolla provides:
9
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.
10
+ - Components
11
+ - Routing
12
+ - State management
13
+ - Declarative templating and DOM updates via signals
14
+ - An HTTP client
15
+ -
11
16
 
12
- A Dolla app is like a house. In this house, State is to data as pipes are to water. Views are appliances that receive that data and make it do something for the user. ShowerView sprays liquid data over their head. FreezerView makes data ice cubes for later. Stores are parts of this system the user doesn't interact with directly, but that hold or process data. WaterHeaterStore keeps data nice and hot for ShowerView and SinkView. SewerStore drains used data away from the app for further processing.
17
+ Let's first get into some examples.
13
18
 
14
- But if you're reading this, you're probably a programmer and not a plumber. All of this is to say, State is a static structure that moves a constantly changing stream of data between Views where data informs the state of DOM nodes the user sees and interacts with. Stores are a type of stateful data component that can be used to share data between Views or provide methods to abstract away interacting with an API.
19
+ ## A Basic Component
15
20
 
16
- Let's first get into some examples.
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
- ## State
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 { $, $$ } from "@manyducks.co/dolla";
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 $$number = $$(5);
252
+ const [$number, setNumber] = signal(5);
34
253
 
35
254
  // Returns the current value held by the Writable.
36
- $$number.get();
255
+ $number.get();
37
256
  // Stores a new value to the Writable.
38
- $$number.set(12);
257
+ setNumber(12);
39
258
  // Uses a callback to update the value. Takes the current value and returns the next.
40
- $$number.update((current) => current + 1);
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 = $($$number, (value) => value * 2);
47
- $doubled.get(); // 26 ($$number is 13)
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 = $($$number, $doubled, (num, doubled) => num + doubled);
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 { $$ } from "@manyducks.co/dolla";
272
+ import { signal } from "@manyducks.co/dolla";
57
273
 
58
- function UserView() {
59
- const $$name = $$("Valued Customer");
274
+ function Greeter() {
275
+ const [$name, setName] = signal("Valued Customer");
60
276
 
61
277
  return (
62
278
  <section>
63
279
  <header>
64
- <h1>Hello, {$$name}!</h1>
280
+ <h1>Hello, {$name}!</h1>
65
281
  </header>
66
282
 
67
283
  <input
68
- value={$$name}
284
+ value={$name}
69
285
  onChange={(e) => {
70
- $$name.set(e.target.value);
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
  );
@@ -30,6 +30,7 @@ type DebugHubOptions = DebugOptions & {
30
30
  };
31
31
  export interface DebugChannelOptions {
32
32
  name: string;
33
+ id?: string;
33
34
  }
34
35
  export interface DebugChannel {
35
36
  info(...args: any[]): void;
package/lib/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { App } from "./app.js";
2
- export { $, $$, observe, unwrap, isReadable, isWritable, type Readable, type Writable } from "./state.js";
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";