@manyducks.co/dolla 2.0.0-alpha.30 → 2.0.0-alpha.32

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/docs/views.md CHANGED
@@ -2,12 +2,12 @@
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
- At its most basic, a view is a function that returns elements.
5
+ At its most basic, a view is a function that returns markup.
6
6
 
7
7
  ```jsx
8
- const ExampleView = createView(function () {
8
+ function ExampleView() {
9
9
  return <h1>Hello World!</h1>;
10
- });
10
+ }
11
11
  ```
12
12
 
13
13
  ## View Props
@@ -15,11 +15,11 @@ const ExampleView = createView(function () {
15
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
16
 
17
17
  ```jsx
18
- const ListItemView = createView(function (props) {
18
+ function ListItemView(props) {
19
19
  return <li>{props.label}</li>;
20
- });
20
+ }
21
21
 
22
- const ListView = createView(function () {
22
+ function ListView() {
23
23
  return (
24
24
  <ul>
25
25
  <ListItemView label="Squirrel" />
@@ -27,7 +27,7 @@ const ListView = createView(function () {
27
27
  <ListItemView label="Groundhog" />
28
28
  </ul>
29
29
  );
30
- });
30
+ }
31
31
  ```
32
32
 
33
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.
@@ -39,7 +39,7 @@ As you may have guessed, you can pass States as props and slot them in in exactl
39
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
40
 
41
41
  ```jsx
42
- const ConditionalListView = createView(function (props) {
42
+ function ConditionalListView(props) {
43
43
  return (
44
44
  <div>
45
45
  {cond(
@@ -57,7 +57,7 @@ const ConditionalListView = createView(function (props) {
57
57
  )}
58
58
  </div>
59
59
  );
60
- });
60
+ }
61
61
  ```
62
62
 
63
63
  ### `repeat($items, keyFn, renderFn)`
@@ -65,7 +65,7 @@ const ConditionalListView = createView(function (props) {
65
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
66
 
67
67
  ```jsx
68
- const RepeatedListView = createView(function () {
68
+ function RepeatedListView() {
69
69
  const [$items, setItems] = createState(["Squirrel", "Chipmunk", "Groundhog"]);
70
70
 
71
71
  return (
@@ -79,7 +79,7 @@ const RepeatedListView = createView(function () {
79
79
  )}
80
80
  </ul>
81
81
  );
82
- });
82
+ }
83
83
  ```
84
84
 
85
85
  ### `portal(content, parentNode)`
@@ -87,7 +87,7 @@ const RepeatedListView = createView(function () {
87
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
88
 
89
89
  ```jsx
90
- const PortalView = createView(function () {
90
+ function PortalView() {
91
91
  const content = (
92
92
  <div class="modal">
93
93
  <p>This is a modal.</p>
@@ -96,80 +96,66 @@ const PortalView = createView(function () {
96
96
 
97
97
  // Content will be appended to `document.body` while this view is connected.
98
98
  return portal(document.body, content);
99
- });
99
+ }
100
100
  ```
101
101
 
102
102
  ## View Context
103
103
 
104
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
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
106
  ```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) => {
107
+ function ExampleView(props, ctx) {
120
108
  ctx.onMount(() => {
121
109
  ctx.log("HELLO!");
122
110
  });
123
111
 
124
112
  return <h1>Hello World!</h1>;
125
- });
113
+ }
126
114
  ```
127
115
 
128
- Which one you use is just an aesthetic preference, but I kind of like the classic `function` syntax with `this`.
129
-
130
116
  ### Printing Debug Messages
131
117
 
132
118
  ```jsx
133
- const ExampleView = createView(function (props) {
119
+ function ExampleView(props, ctx) {
134
120
  // Set the name of this view's context. Console messages are prefixed with name.
135
- this.setName("CustomName");
121
+ ctx.setName("CustomName");
136
122
 
137
123
  // Print messages to the console. These are suppressed by default in the app's "production" mode.
138
124
  // 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!");
125
+ ctx.info("Verbose debugging info that might be useful to know");
126
+ ctx.log("Standard messages");
127
+ ctx.warn("Something bad might be happening");
128
+ ctx.error("Uh oh!");
143
129
 
144
130
  // If you encounter a bad enough situation, you can halt and disconnect the entire app.
145
- this.crash(new Error("BOOM"));
131
+ ctx.crash(new Error("BOOM"));
146
132
 
147
133
  return <h1>Hello World!</h1>;
148
- });
134
+ }
149
135
  ```
150
136
 
151
137
  ### Lifecycle Events
152
138
 
153
139
  ```jsx
154
- const ExampleView = createView(function (props) {
155
- this.beforeMount(() => {
140
+ function ExampleView(props, ctx) {
141
+ ctx.beforeMount(() => {
156
142
  // Do something before this view's DOM nodes are created.
157
143
  });
158
144
 
159
- this.onMount(() => {
145
+ ctx.onMount(() => {
160
146
  // Do something immediately after this view is connected to the DOM.
161
147
  });
162
148
 
163
- this.beforeUnmount(() => {
149
+ ctx.beforeUnmount(() => {
164
150
  // Do something before removing this view from the DOM.
165
151
  });
166
152
 
167
- this.onUnmount(() => {
153
+ ctx.onUnmount(() => {
168
154
  // Do some cleanup after this view is disconnected from the DOM.
169
155
  });
170
156
 
171
157
  return <h1>Hello World!</h1>;
172
- });
158
+ }
173
159
  ```
174
160
 
175
161
  ### Displaying Children
@@ -177,15 +163,15 @@ const ExampleView = createView(function (props) {
177
163
  The context has an `outlet` function that can be used to display children at a location of your choosing.
178
164
 
179
165
  ```js
180
- const LayoutView = createView(function (props) {
166
+ function LayoutView(props, ctx) {
181
167
  return (
182
168
  <div className="layout">
183
- <div className="content">{this.outlet()}</div>
169
+ <div className="content">{ctx.outlet()}</div>
184
170
  </div>
185
171
  );
186
- });
172
+ }
187
173
 
188
- const ExampleView = createView(function () {
174
+ function ExampleView() {
189
175
  // <h1> and <p> are displayed inside LayoutView's outlet.
190
176
  return (
191
177
  <LayoutView>
@@ -193,7 +179,7 @@ const ExampleView = createView(function () {
193
179
  <p>This is inside the box.</p>
194
180
  </LayoutView>
195
181
  );
196
- });
182
+ }
197
183
  ```
198
184
 
199
185
  ### Watching States
@@ -201,16 +187,16 @@ const ExampleView = createView(function () {
201
187
  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
188
 
203
189
  ```jsx
204
- const ExampleView = createView(function (props) {
190
+ function ExampleView(props, ctx) {
205
191
  const [$count, setCount] = createState(0);
206
192
 
207
193
  // 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);
194
+ ctx.watch([$count], (count) => {
195
+ ctx.log("count is now", count);
210
196
  });
211
197
 
212
198
  // ...
213
- });
199
+ }
214
200
  ```
215
201
 
216
202
  ### Context Variables
@@ -219,24 +205,24 @@ const ExampleView = createView(function (props) {
219
205
 
220
206
  ### Context Events
221
207
 
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.
208
+ Events can be emitted from views and [stores](./stores.md) using `ctx.emit(eventName, data)`. Context events will bubble up the view tree just like native browser events bubble up the DOM tree.
223
209
 
224
210
  ```js
225
- this.on("eventName", (event) => {
211
+ ctx.on("eventName", (event) => {
226
212
  event.type; // "eventName"
227
213
  event.detail; // the value that was passed when the event was emitted (or undefined if none)
228
214
  });
229
215
 
230
- this.once("eventName", (event) => {
216
+ ctx.once("eventName", (event) => {
231
217
  // Receive only once and then stop listening.
232
218
  });
233
219
 
234
220
  // Remove a listener by reference.
235
221
  // Listener must be the same exact function that was passed to `on` or `once`.
236
- this.off("eventName", listener);
222
+ ctx.off("eventName", listener);
237
223
 
238
224
  // Emit an event.
239
- this.emit("eventName", { value: "This object will be exposed as event.detail" });
225
+ ctx.emit("eventName", { value: "This object will be exposed as event.detail" });
240
226
  ```
241
227
 
242
228
  ### Bubbling
@@ -244,12 +230,12 @@ this.emit("eventName", { value: "This object will be exposed as event.detail" })
244
230
  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
231
 
246
232
  ```js
247
- const ParentView = createView(function () {
233
+ function ParentView(props, ctx) {
248
234
  // Listen for greetings that bubble up.
249
- this.on("greeting", (event) => {
235
+ ctx.on("greeting", (event) => {
250
236
  const { name, message } = event.detail;
251
237
 
252
- this.log(`${name} says "${message}"!`);
238
+ ctx.log(`${name} says "${message}"!`);
253
239
  });
254
240
 
255
241
  return (
@@ -257,10 +243,10 @@ const ParentView = createView(function () {
257
243
  <ChildView />
258
244
  </div>
259
245
  );
260
- });
246
+ }
261
247
 
262
- const ChildView = createView(function () {
263
- this.on("greeting", (event) => {
248
+ function ChildView(props, ctx) {
249
+ ctx.on("greeting", (event) => {
264
250
  // Let's perform some censorship.
265
251
  // If propagation is stopped this event will not bubble any further and ParentView won't see it.
266
252
  if (containsForbiddenKnowledge(event.message)) {
@@ -273,9 +259,9 @@ const ChildView = createView(function () {
273
259
  <ChildOfChildView />
274
260
  </div>
275
261
  );
276
- });
262
+ }
277
263
 
278
- const ChildOfChildView = createView(function () {
264
+ function ChildOfChildView(props, ctx) {
279
265
  return (
280
266
  <form
281
267
  onSubmit={(e) => {
@@ -288,7 +274,7 @@ const ChildOfChildView = createView(function () {
288
274
  const message = e.currentTarget.message.value;
289
275
 
290
276
  // Emit!
291
- this.emit("greeting", { name, message });
277
+ ctx.emit("greeting", { name, message });
292
278
  }}
293
279
  >
294
280
  <input type="text" name="name" placeholder="Your Name" />
@@ -296,7 +282,7 @@ const ChildOfChildView = createView(function () {
296
282
  <button type="submit">Submit</button>
297
283
  </form>
298
284
  );
299
- });
285
+ }
300
286
  ```
301
287
 
302
288
  ---
package/notes/scratch.md CHANGED
@@ -1,5 +1,140 @@
1
1
  # Scratch Note
2
2
 
3
+ Idea: Monomorphic app context. Replaces StoreContext, ViewContext, etc.
4
+
5
+ Routes are baked into the app once again, but
6
+
7
+ ```jsx
8
+ import { createRoot } from "@manyducks.co/dolla";
9
+ import { example } from "./stores/example.js";
10
+
11
+ const root = createRoot();
12
+
13
+ root.use(example());
14
+
15
+ async function auth(_, state, redirect) {
16
+ // route context
17
+ // Routes run through each callback until one resolves to a renderable value.
18
+ // If redirect is called, the route is re-matched and no further callbacks are run for this route.
19
+
20
+ if (state.auth == null) {
21
+ redirect("/login");
22
+ }
23
+ }
24
+
25
+ root.route("/users/*", auth, (C) => {
26
+ C.route("/{#id}/*", (C) => {
27
+ C.route("/", (C) => <UserDetailRoute userId={C.params.id} />);
28
+ C.route("*", "./");
29
+ });
30
+ });
31
+
32
+ root.route("/users/*", auth, (route) => {
33
+ route("/{#id}/*", (route) => {
34
+ // TODO: It's possible to reference the wrong 'route'
35
+ // Track active context and throw error if the one you call belongs to the wrong context?
36
+ route("/", (_, state) => <UserDetailView userId={state.params.id} />);
37
+ route("*", "./");
38
+ });
39
+ });
40
+
41
+ function ExampleView(props, ctx) {
42
+ // ctx.routes returns a special type of outlet that renders children based on
43
+ // the route segments that come after the ones at this ctx.
44
+
45
+ // The weakness of this idea is that routes can't be validated without initializing views.
46
+ return (
47
+ <div>
48
+ <Suspense fallback={<span>Loading...</span>}>
49
+ {ctx.routes((route) => {
50
+ route("/subroute", () => <OtherView />);
51
+
52
+ // Routes can be async.
53
+ route("/other", () => import("some-module"));
54
+ })}
55
+ </Suspense>
56
+ </div>
57
+ );
58
+
59
+ // Also Suspense. This can be simply implemented with events.
60
+ ctx.emit("suspense:begin", uniqueId);
61
+ // Then when done:
62
+ ctx.emit("suspense:end", uniqueId);
63
+
64
+ // The nearest Suspense view will track ids which are in suspense and show fallback content in the meantime.
65
+ }
66
+
67
+ function Suspense(props, ctx) {
68
+ const [$tracked, setTracked] = createState({});
69
+
70
+ ctx.on("suspense:begin", (e) => {
71
+ setTracked((tracked) => {
72
+ return {
73
+ ...tracked,
74
+ [e.detail]: new Date(),
75
+ };
76
+ });
77
+ });
78
+
79
+ ctx.on("suspense:end", (e) => {
80
+ setTracked((tracked) => {
81
+ const updated = Object.assign({}, tracked);
82
+ delete updated[e.detail];
83
+ return updated;
84
+ });
85
+ });
86
+
87
+ // TODO: Hide suspended view without unmounting it. This might take special logic.
88
+ }
89
+
90
+ // Can also pass markup directly if you don't need the context.
91
+ root.route("/", auth, <HomeRoute />);
92
+
93
+ // Static redirect.
94
+ root.route("*", "/");
95
+
96
+ // Programmatic redirect.
97
+ root.route("*", (C) => {
98
+ C.log("hit wildcard");
99
+ C.redirect("/");
100
+ });
101
+
102
+ root.mount(document.body);
103
+
104
+ // generate an HTML string for server side rendering.
105
+ root.toString("/some/path");
106
+ ```
107
+
108
+ ---
109
+
110
+ ```js
111
+ class ClockStore extends Store {
112
+
113
+
114
+ constructor() {
115
+
116
+ }
117
+ }
118
+
119
+ class CounterStore extends Store {
120
+ // Could have better name. This will catch any
121
+ // this.emit('counter:increment') or this.emit('counter:decrement') calls
122
+ // and update the state according to these functions.
123
+ value = new Emittable('counter', 0, {
124
+ increment: state => state + 1,
125
+ decrement: state => state - 1
126
+ });
127
+ }
128
+
129
+ type CounterEvents = {
130
+ increment: [amount: number];
131
+ decrement: [amount: number];
132
+ }
133
+
134
+
135
+
136
+ ```
137
+
3
138
  ---
4
139
 
5
140
  Bring the $ back and the name full circle.
package/notes/stores.md CHANGED
@@ -3,71 +3,53 @@
3
3
  Ideas for updating the API.
4
4
 
5
5
  ```js
6
- import { createStore, attachStore, useStore, createView } from "@manyducks.co/dolla";
6
+ import { attachStore, useStore } from "@manyducks.co/dolla";
7
7
 
8
- const Counter = createStore(function (initialCount: number) {
8
+ function CounterStore(initialCount = 0, ctx) {
9
9
  const [$value, setValue] = createState(initialCount);
10
10
 
11
- this.on("counter:increment", (e) => {
11
+ ctx.on("counter:increment", (e) => {
12
12
  e.stopPropagation(); // Stop this event from bubbling up to counters at higher levels (if any).
13
13
  setValue((current) => current + 1);
14
14
  });
15
15
 
16
- this.on("counter:decrement", (e) => {
16
+ ctx.on("counter:decrement", (e) => {
17
17
  e.stopPropagation();
18
18
  setValue((current) => current - 1);
19
19
  });
20
20
 
21
21
  // Events can be emitted from this context in a store.
22
- this.emit("otherEvent");
22
+ ctx.emit("otherEvent");
23
23
 
24
- this.onMount(() => {
24
+ ctx.onMount(() => {
25
25
  // Setup
26
26
  // This is called based on the context the store is attached to.
27
27
  // If Dolla, it's called when the app is mounted. If ViewContext, it's called when the view is mounted.
28
28
  });
29
- this.onUnmount(() => {
29
+ ctx.onUnmount(() => {
30
30
  // Cleanup
31
31
  });
32
32
 
33
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");
34
+ ctx.get("context variable");
35
+ ctx.set("context variable", "context variable value");
36
36
 
37
37
  // Stores don't have to return anything, but if they do it becomes accessible by using `useStore(ctx, Store)`.
38
38
  return $value;
39
- });
39
+ }
40
40
 
41
41
  // Attach it to the app.
42
- Dolla.attachStore(Counter(0));
42
+ Dolla.attachStore(CounterStore, 0);
43
43
 
44
- const ExampleView = createView(function () {
44
+ function ExampleView(props, ctx) {
45
45
  // useStore lets you access the return value
46
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));
47
+ const $count = ctx.useStore(Counter);
51
48
 
52
49
  return html`
53
50
  <button onclick=${() => this.emit("counter:decrement")}>-1</button>
54
51
  <span>${$count}</span>
55
52
  <button onclick=${() => this.emit("counter:increment")}>+1</button>
56
53
  `;
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
- });
54
+ }
71
55
  ```
72
-
73
- This means `createStore` returns a function that is called to create a Store instance. The instance is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manyducks.co/dolla",
3
- "version": "2.0.0-alpha.30",
3
+ "version": "2.0.0-alpha.32",
4
4
  "description": "Front-end components, routing and state management.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/build.js DELETED
@@ -1,34 +0,0 @@
1
- import fs from "node:fs";
2
- import esbuild from "esbuild";
3
-
4
- esbuild
5
- .build({
6
- entryPoints: ["src/index.ts"],
7
- bundle: true,
8
- metafile: true,
9
- sourcemap: true,
10
- // minify: process.env.NODE_ENV === "production",
11
- outdir: "dist",
12
- format: "esm",
13
- })
14
- .then((result) => {
15
- fs.writeFileSync("esbuild-meta.json", JSON.stringify(result.metafile));
16
- });
17
-
18
- esbuild.build({
19
- entryPoints: ["src/jsx-runtime.js"],
20
- bundle: false,
21
- minify: false,
22
- sourcemap: true,
23
- outdir: "dist",
24
- format: "esm",
25
- });
26
-
27
- esbuild.build({
28
- entryPoints: ["src/jsx-dev-runtime.js"],
29
- bundle: false,
30
- minify: false,
31
- sourcemap: true,
32
- outdir: "dist",
33
- format: "esm",
34
- });