@loadbare/app 0.8.2 → 0.10.0

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.
Files changed (80) hide show
  1. package/README.md +3 -3
  2. package/dist/build/assemble.d.ts.map +1 -1
  3. package/dist/build/assemble.js +74 -66
  4. package/dist/build/assemble.js.map +1 -1
  5. package/dist/build/expand.d.ts.map +1 -1
  6. package/dist/build/expand.js +20 -19
  7. package/dist/build/expand.js.map +1 -1
  8. package/dist/build/locations.d.ts +2 -3
  9. package/dist/build/locations.d.ts.map +1 -1
  10. package/dist/build/locations.js +2 -3
  11. package/dist/build/locations.js.map +1 -1
  12. package/dist/build/pages.d.ts +3 -4
  13. package/dist/build/pages.d.ts.map +1 -1
  14. package/dist/build/pages.js +3 -4
  15. package/dist/build/pages.js.map +1 -1
  16. package/dist/core/lb-constants.d.ts +25 -23
  17. package/dist/core/lb-constants.d.ts.map +1 -1
  18. package/dist/core/lb-constants.js +95 -158
  19. package/dist/core/lb-constants.js.map +1 -1
  20. package/dist/core/lb-types.d.ts +73 -75
  21. package/dist/core/lb-types.d.ts.map +1 -1
  22. package/dist/core/lb-types.js +58 -5
  23. package/dist/core/lb-types.js.map +1 -1
  24. package/dist/hub/lb-apply.d.ts +47 -37
  25. package/dist/hub/lb-apply.d.ts.map +1 -1
  26. package/dist/hub/lb-apply.js +174 -193
  27. package/dist/hub/lb-apply.js.map +1 -1
  28. package/dist/hub/lb-hub.browser.d.ts +1 -1
  29. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  30. package/dist/hub/lb-hub.browser.js +419 -415
  31. package/dist/hub/lb-hub.browser.js.map +1 -1
  32. package/dist/server/lb-express.d.ts +20 -13
  33. package/dist/server/lb-express.d.ts.map +1 -1
  34. package/dist/server/lb-express.js +50 -52
  35. package/dist/server/lb-express.js.map +1 -1
  36. package/dist/server/lb-server.d.ts +81 -116
  37. package/dist/server/lb-server.d.ts.map +1 -1
  38. package/dist/server/lb-server.js +151 -48
  39. package/dist/server/lb-server.js.map +1 -1
  40. package/docs/TECHREF-1.0.md +893 -558
  41. package/docs/comparison.md +222 -185
  42. package/docs/prior-art.md +15 -14
  43. package/docs/reference/builder.md +9 -3
  44. package/docs/reference/chrome.md +107 -56
  45. package/docs/reference/custom-elements.md +199 -173
  46. package/docs/reference/data-binding.md +375 -370
  47. package/docs/reference/overview.md +12 -10
  48. package/docs/reference/page-files.md +161 -86
  49. package/docs/reference/server.md +13 -7
  50. package/docs/reference/widgets.md +104 -110
  51. package/docs/roadmap.md +36 -31
  52. package/docs/terms-of-art.md +57 -0
  53. package/docs/testing.md +97 -68
  54. package/docs/theory.md +92 -58
  55. package/docs/tutorials/010-pages-and-navigation.md +20 -12
  56. package/docs/tutorials/020-css.md +6 -3
  57. package/docs/tutorials/030-html-decomposition.md +9 -7
  58. package/docs/tutorials/040-displaying-data.md +30 -13
  59. package/docs/tutorials/{050-actions.md → 050-requests.md} +25 -15
  60. package/docs/tutorials/060-custom-element-code.md +17 -16
  61. package/docs/tutorials/065-conditional-rendering.md +34 -23
  62. package/docs/tutorials/070-displaying-a-list.md +29 -21
  63. package/docs/tutorials/072-inserting-into-a-list.md +24 -16
  64. package/docs/tutorials/074-deleting-from-a-list.md +9 -7
  65. package/docs/tutorials/076-updating-a-list-item.md +11 -10
  66. package/docs/tutorials/080-widget-requests.md +71 -43
  67. package/docs/tutorials/090-using-widget-libraries.md +22 -22
  68. package/package.json +1 -1
  69. package/skills/loadbare-app/SKILL.md +178 -111
  70. package/skills/loadbare-app/references/TECHREF-1.0.md +893 -558
  71. package/skills/loadbare-app/references/builder.md +9 -3
  72. package/skills/loadbare-app/references/chrome.md +107 -56
  73. package/skills/loadbare-app/references/custom-elements.md +199 -173
  74. package/skills/loadbare-app/references/data-binding.md +375 -370
  75. package/skills/loadbare-app/references/overview.md +12 -10
  76. package/skills/loadbare-app/references/page-files.md +161 -86
  77. package/skills/loadbare-app/references/server.md +13 -7
  78. package/skills/loadbare-app/references/widgets.md +104 -110
  79. package/docs/analysis-accidental-complexity.md +0 -149
  80. package/docs/analysis-closed-set.md +0 -210
@@ -2,7 +2,7 @@
2
2
 
3
3
  This reference is organized into four groups that reflect how an application is
4
4
  put together: the shell it lives in, the build that assembles it, the pages it
5
- serves, and the widgets those pages are made of.
5
+ serves, and the custom elements those pages are made of.
6
6
 
7
7
  ## The application shell
8
8
 
@@ -24,15 +24,17 @@ serves, and the widgets those pages are made of.
24
24
 
25
25
  | Topic | Description |
26
26
  |---------------------------------------------------------|--------------------------------|
27
- | [`<name>.page.html`](./page-files.md#html) | The page's HTML |
28
- | [`<name>.queries.ts`](./page-files.md#queries) | The data the page displays |
29
- | [`<name>.requests.ts`](./page-files.md#requests-actions-crud) | Data Channel handlers |
27
+ | [`<stub>.page.html`](./page-files.md#html) | The page's HTML |
28
+ | [`<stub>.queries.ts`](./page-files.md#queries) | The queries the page shows |
29
+ | [`<stub>.requests.ts`](./page-files.md#requests) | The page's request handlers |
30
30
  | [Data Binding](./data-binding.md) | Connecting HTML to server data |
31
+ | [The URL](./chrome.md#the-url) | Pages, links and query parms |
31
32
 
32
- ## Widgets
33
+ ## Custom elements
33
34
 
34
- | Topic | Description |
35
- |------------------------------------------------|---------------------------------|
36
- | [What a widget is](./custom-elements.md) | An HTML file, a script, or both |
37
- | [`<tag-name>.html`](./custom-elements.md#html) | The markup the tag expands into |
38
- | [`<tag-name>.ts`](./custom-elements.md#code) | The class the tag registers |
35
+ | Topic | Description |
36
+ |--------------------------------------------------------|---------------------------------|
37
+ | [What a custom element is](./custom-elements.md) | An HTML file, a script, or both |
38
+ | [`<tag-name>.html`](./custom-elements.md#html) | The markup the tag expands into |
39
+ | [`<tag-name>.browser.ts`](./custom-elements.md#code) | The class the tag registers |
40
+ | [The Basic Widget Library](./widgets.md) | `@loadbare/widgets` |
@@ -1,92 +1,112 @@
1
1
  # Page Files
2
2
 
3
- A page is a set of files sharing one base name. The application writes the
4
- HTML, and adds queries and requests when the page shows data.
3
+ A page is a set of files sharing one stub, the file name less its suffix.
4
+ The application writes the HTML, and adds queries and requests when the page
5
+ shows data.
5
6
 
6
7
  | File | Holds |
7
8
  |----------------------|-------------------------------|
8
- | `<name>.page.html` | The page's HTML |
9
- | `<name>.queries.ts` | The data the page displays |
10
- | `<name>.requests.ts` | What the page does when asked |
9
+ | `<stub>.page.html` | The page's HTML |
10
+ | `<stub>.queries.ts` | The queries the page shows |
11
+ | `<stub>.requests.ts` | What the page does when asked |
11
12
 
12
13
  Name the landing page `index.page.html`. A bare `/` resolves to `index`, and
13
- every other path names the page of the same name.
14
+ every other path names the page whose stub it is.
14
15
 
15
- Put the files anywhere under `src/`. The builder pairs them by base name, not
16
- by directory; `src/pages/` is the convention.
16
+ Put the files anywhere under `src/`. The builder pairs them by stub, not by
17
+ directory; `src/pages/` is the convention.
17
18
 
18
19
  ## HTML
19
20
 
20
- Write the page as a fragment. The fragment will land in `<main>`, which
21
- is supplied by [chrome.html](./chrome.md).
21
+ Write the page as a fragment. The fragment lands in `<main>`, which
22
+ [chrome.html](./chrome.md) supplies.
22
23
 
23
24
  ```html
24
25
  <!-- src/pages/about.page.html -->
26
+ <title>About</title>
25
27
  <h1>About</h1>
26
28
  <p>This is the about page.</p>
27
29
 
28
- <div lb-row="visits">
29
- <p>This page has been visited <span lb-cell="count"></span> times.</p>
30
+ <div lb-query="visits">
31
+ <p>This page has been visited <span lb-column="count"></span> times.</p>
30
32
  </div>
31
33
  ```
32
34
 
33
- Bind elements to data with `lb-list` or `lb-row`, `lb-cell`, and the rest of the
34
- attribute vocabulary in [Data Binding](./data-binding.md).
35
+ Show data with `lb-query`, `lb-column`, and the rest of the attributes in
36
+ [Data Binding](./data-binding.md).
35
37
 
36
- A page that displays no data needs no other file.
38
+ A page that shows no data needs no other file.
39
+
40
+ ### The page title
41
+
42
+ Write a `<title>` in the page file to name the page. The builder removes it
43
+ from the page and stamps its text on the page's `<template>` as
44
+ `lb-page-title`. The hub shows it as the `lb-url` column `lb-page-label`, and
45
+ sets the document title to it; see [The URL](./chrome.md#the-url). A page
46
+ with no `<title>` is labeled by its stub.
37
47
 
38
48
  ## Queries
39
49
 
40
- Export `queries` from `<name>.queries.ts`. Each key is a name the HTML binds
41
- to with `lb-list` or `lb-row`, and each value takes the request context and returns
42
- that query's result:
50
+ Export `queries` from `<stub>.queries.ts`. Each key is a query name the HTML
51
+ names with `lb-query`, and each value declares the query's kind, its key
52
+ column, and the function that answers it:
43
53
 
44
54
  ```ts
45
- // src/pages/about.queries.ts
46
- import { row, type Queries } from "@loadbare/app/server";
55
+ // src/pages/directory.queries.ts
56
+ import { row, rows, type Queries } from "@loadbare/app/server";
47
57
 
48
58
  export const queries: Queries = {
49
- visits: row(async (ctx) => ({ count: String(await ctx.db.visitCount()) })),
59
+ visits: row("page", async (ctx) => ({
60
+ page: "directory",
61
+ count: String(await ctx.db.visitCount()),
62
+ })),
63
+ directory: rows("id", (ctx) => ctx.db.directory()),
50
64
  };
51
65
  ```
52
66
 
53
- Declare each query with `row()` or `list()`. Cardinality is a property of the
54
- name rather than of any one answer, so one name answers with one shape,
55
- always, and `createHub` refuses an answer that disagrees. A page that needs
56
- the same data as one row and as a set declares two queries.
67
+ | Declared with | Answers with |
68
+ |-------------------|-------------------------------------------|
69
+ | `row(key, run)` | One row, an object |
70
+ | `rows(key, run)` | All its rows, an array of objects, in order |
57
71
 
58
- The hub hands each cell to the browser untouched and takes no position on
59
- its type, so what a number, a date or a null looks like is decided here, in
60
- the query. Formatting it here means the browser displays a value it never
61
- computes.
72
+ `key` names the column that identifies a row. Give every row that column,
73
+ including a `row` query's: an aggregate row answers with a constant key. The
74
+ hub matches rows to live rows by key, and a request names a row by key.
62
75
 
63
- Declare a query that answers with many rows using `list()`, and return the
64
- array itself:
76
+ Kind and key belong to the name, so one name answers with one shape, always,
77
+ and `createHub` leaves out an answer that disagrees with its declaration. A
78
+ page that needs the same data as one row and as a set declares two queries.
65
79
 
66
- ```ts
67
- // src/pages/directory.queries.ts
68
- import { list, type Queries } from "@loadbare/app/server";
80
+ The hub hands each value to the browser untouched, so what a number, a date
81
+ or a null looks like is decided here, in the query.
69
82
 
70
- export const queries: Queries = {
71
- directory: list((ctx) => ctx.db.directory()),
72
- };
73
- ```
83
+ Return the full answer every time, and the rows in the order the page shows
84
+ them. Sending only what changed is a request's job — see
85
+ [refresh and patch](#refresh-and-patch).
86
+
87
+ ## Requests
74
88
 
75
- Give every row a column that identifies it, and name that column with
76
- `lb-key` in the HTML. Return the rows in the order the page shows them.
89
+ Export `requests` from `<stub>.requests.ts`. It holds three keys, each
90
+ optional:
77
91
 
78
- Return the full result every time. Sending only what changed is a request's job —
79
- see [refresh and patch](#refresh-and-patch).
92
+ | Key | Runs |
93
+ |---------------|--------------------------------------------------------|
94
+ | `onPageEnter` | Before the page's queries, when the page loads |
95
+ | `handlers` | The page's declared requests, by request name |
96
+ | `crud` | The requests Loadbare provides, by query name |
80
97
 
81
- ## Requests, actions, CRUD
98
+ Every request arrives with one shape, and a handler receives it less its
99
+ name:
82
100
 
83
- Export `requests` from `<name>.requests.ts`. It holds three keys, each optional:
101
+ | Field | Holds |
102
+ |----------|----------------------------------------------------------|
103
+ | `name` | The request name, from `lb-request` |
104
+ | `query` | The query the request is for |
105
+ | `key` | The key of the row the request is for |
106
+ | `values` | The gathered control values, by column |
84
107
 
85
- | Key | Runs |
86
- |---------------|------------------------------------------------------|
87
- | `onPageEnter` | Before the page's queries, on entering the page |
88
- | `actions` | What the page may be asked to do, by name |
89
- | `crud` | The three operations a list permits on its rows |
108
+ The hub sends `query`, `key` and `values` when it finds them; see
109
+ [Gathering](./data-binding.md#gathering).
90
110
 
91
111
  ### onPageEnter
92
112
 
@@ -101,51 +121,39 @@ export const requests: Requests = {
101
121
 
102
122
  Declare no refresh set here. The page's queries run afterward.
103
123
 
104
- ### actions
124
+ ### handlers
105
125
 
106
- Declare an action under the name the HTML gives `lb-action`. Pair what it
126
+ Declare a request under the name the HTML gives `lb-request`. Pair what it
107
127
  does with the queries to re-run once it has:
108
128
 
109
129
  ```ts
110
130
  export const requests: Requests = {
111
- actions: {
131
+ handlers: {
112
132
  resetVisits: {
113
133
  run: (ctx) => ctx.db.resetVisits(),
114
134
  refresh: ["visits"],
115
135
  },
136
+ sendReminder: {
137
+ run: (ctx, { key }) => ctx.mail.remind(key),
138
+ refresh: [],
139
+ },
116
140
  },
117
141
  };
118
142
  ```
119
143
 
120
- Declare every action the page allows. A name the page does not declare is
121
- refused.
122
-
123
- Read where the interaction happened from `run`'s second argument, which
124
- carries `list` or `row`, whichever attribute scoped the element, plus `key`,
125
- `cell` and `value` when the element that dispatched the request had them.
144
+ Declare every request the page allows. The server refuses a name the page
145
+ does not declare.
126
146
 
127
147
  ### crud
128
148
 
129
- Declare CRUD operations under `crud`, keyed by the list they operate on. All
130
- three are list operations: each needs a key, and a key exists only on a live
131
- row inside a list, so a single-row scope is read-only and a declared action is
132
- the only thing it can send. Each operation takes the binding its trigger
133
- supplies:
149
+ Declare what the three requests Loadbare provides do under `crud`, keyed by
150
+ the query they operate on:
134
151
 
135
- | Operation | The page writes | `run` receives |
136
- |-------------|------------------------------------------------------------------------|-----------------|
137
- | `rowDelete` | `lb-action="lb-row-delete"` | `key` |
138
- | `rowInsert` | `<form lb-action="lb-row-insert">` | `values` |
139
- | `rowUpdate` | `<form lb-action="lb-row-update">` or `<lb-input lb-action="lb-row-update">` | `key`, `values` |
140
-
141
- Write `rowUpdate` to set the columns `values` names and leave every other
142
- column as it is. A form sends the cells it holds, and a widget cell sends
143
- itself alone. Check the names in `values` against the columns the list lets
144
- the page edit.
145
-
146
- The operation names are reserved: a name beginning with `lb-` cannot be
147
- declared under `actions` or as a query, and `createHub` refuses a page that
148
- tries.
152
+ | Request name | Runs | `run` receives |
153
+ |-----------------|-------------|------------------|
154
+ | `lb-row-insert` | `rowInsert` | `query`, `values` |
155
+ | `lb-row-update` | `rowUpdate` | `query`, `key`, `values` |
156
+ | `lb-row-delete` | `rowDelete` | `query`, `key` |
149
157
 
150
158
  ```ts
151
159
  // src/pages/directory.requests.ts
@@ -161,30 +169,46 @@ export const requests: Requests = {
161
169
  },
162
170
  refresh: [],
163
171
  },
172
+ rowUpdate: {
173
+ run: async (ctx, { key, values }) => {
174
+ const entry = await ctx.db.updateDirectoryEntry(key, values);
175
+ return { directory: patch({ rows: [entry] }) };
176
+ },
177
+ refresh: [],
178
+ },
164
179
  },
165
180
  },
166
181
  };
167
182
  ```
168
183
 
169
- Declare every operation the list permits. An operation a list does not
170
- declare is refused, and a name with no `crud` entry permits none.
184
+ Declare every request the query permits. The server refuses a request a
185
+ query does not declare, and a query with no `crud` entry permits none.
186
+
187
+ Write `rowUpdate` to set the columns `values` names and leave every other
188
+ column as it is. A form or a live row sends every control it gathers, and a
189
+ single control sends itself alone. Check the names in `values` against the
190
+ columns the page may edit.
191
+
192
+ Every value in `values` is a string, as a control's value is.
171
193
 
172
194
  ### refresh and patch
173
195
 
174
- List in `refresh` every query whose whole answer the operation changed.
196
+ List in `refresh` every query whose whole answer the request changed.
175
197
 
176
- Return a result from `run` to state a narrower change than re-running a query
177
- would. What `run` returns is laid over the refreshed queries:
198
+ Return answers from `run` to state a narrower change than re-running a query
199
+ would. What `run` returns is keyed by query name and laid over the refreshed
200
+ queries:
178
201
 
179
- | Result | States |
202
+ | Answer | States |
180
203
  |--------------------------|---------------------------------------------|
181
- | `[...]` | The entire set, and its order |
204
+ | `{ ... }` | The row of a `row` query |
205
+ | `[...]` | All rows of a `rows` query, and their order |
182
206
  | `patch({ rows: [...] })` | These rows arrive or change; the rest stand |
183
207
  | `patch({ drop: [...] })` | These keys are gone; the rest stand |
184
208
 
185
- Return a patch for a change the operation knows the extent of — one row added,
209
+ Return a patch for a change the request knows the extent of — one row added,
186
210
  one row dropped, one row edited — and leave `refresh` empty. Re-run the query
187
- instead when membership or order changed in a way the operation cannot name:
211
+ instead when membership or order changed in a way the request cannot name:
188
212
 
189
213
  ```ts
190
214
  resetRoster: {
@@ -192,3 +216,54 @@ resetRoster: {
192
216
  refresh: ["roster"],
193
217
  },
194
218
  ```
219
+
220
+ ### Moving the URL
221
+
222
+ Return `url()` from `run` when the request changes what the URL should say:
223
+ after an insert, only the server knows the new key, and after a delete, only
224
+ the server knows the parm should go. `url()` takes columns of the `lb-url`
225
+ query; see [The URL](./chrome.md#the-url).
226
+
227
+ ```ts
228
+ // src/pages/accounts.requests.ts
229
+ import { url, type Requests } from "@loadbare/app/server";
230
+
231
+ export const requests: Requests = {
232
+ crud: {
233
+ accounts: {
234
+ rowInsert: {
235
+ run: async (ctx, { values }) => {
236
+ const id = await ctx.db.addAccount(values);
237
+ return url({ acct: String(id) });
238
+ },
239
+ refresh: [],
240
+ },
241
+ rowDelete: {
242
+ run: async (ctx, { key }) => {
243
+ await ctx.db.deleteAccount(key);
244
+ if (key === ctx.acct) return url({ acct: "" });
245
+ },
246
+ refresh: ["accounts"],
247
+ },
248
+ },
249
+ },
250
+ };
251
+ ```
252
+
253
+ A column other than `lb-path` is a query parm: set, or taken out when its
254
+ value is an empty string. Name at least one column, and give each a string.
255
+ With `lb-path` naming another page, the page is entered with only the parms
256
+ named:
257
+
258
+ ```ts
259
+ return url({ "lb-path": "/accounts", acct: String(id) });
260
+ ```
261
+
262
+ The server loads the page at the new URL in the same round trip, as a cold
263
+ load of it would, and the hub writes the URL: it pushes a history entry when
264
+ the page changes, or when the element that sent the request carries
265
+ `lb-url-push`, and replaces the current one otherwise. This is
266
+ Post/Redirect/Get without the redirect. The refresh set does not run, and
267
+ nothing else `run` returned is sent, since both answered for the URL the page
268
+ left. The loaded page reads the new parms through `contextFor`; see
269
+ [the Express server](./server.md#database-layer).
@@ -64,7 +64,7 @@ Register authentication before `hubRoutes`.
64
64
 
65
65
  Serve `dist/app.html` for every route the application does not claim,
66
66
  including a path that names no page. See [`chrome.html`](./chrome.md) for the
67
- `<dialog lb-unknown-page>` that announces that case to the user.
67
+ `<dialog lb-url-unknown>` that announces that case to the user.
68
68
 
69
69
  Give the server the origin root. The hub reaches its own route by absolute
70
70
  path, so the application cannot be hosted under a subpath such as
@@ -137,17 +137,23 @@ declare module "@loadbare/app/server" {
137
137
  }
138
138
  ```
139
139
 
140
- The query string the browser is showing arrives on every data request, so
141
- `req.query` holds the page's query parms. Put on the context whatever a query
142
- reads from them, and treat them as user input:
140
+ The query string the browser is showing arrives on every data request, and
141
+ `contextFor` gets its parms as a second argument. Put on the context whatever
142
+ a query reads from them, and treat them as user input:
143
143
 
144
144
  ```ts
145
- function contextFor(req: Request): HubContext {
146
- const { team } = req.query;
147
- return { db: openDb(), team: typeof team === "string" ? team : "" };
145
+ function contextFor(req: Request, parms: URLSearchParams): HubContext {
146
+ return { db: openDb(), team: parms.get("team") ?? "" };
148
147
  }
149
148
  ```
150
149
 
150
+ Read the parms from that argument, not from `req.query`. When a server
151
+ response changes the query parms, `contextFor` is called a second time for
152
+ that request, with the new parms, to load the page at them; see [Moving the URL](./page-files.md#moving-the-url). Write it to
153
+ be safe to call twice, and make a write visible to the second context by the
154
+ time `run` returns: a handle opened per request, with no transaction held
155
+ open across the two, is both.
156
+
151
157
  Add a field for anything else a request needs — the authenticated user, a
152
158
  request id, a feature flag set. Queries and requests read them from `ctx`; see
153
159
  [page files](./page-files.md).