@loadbare/app 0.9.0 → 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 -24
  17. package/dist/core/lb-constants.d.ts.map +1 -1
  18. package/dist/core/lb-constants.js +95 -168
  19. package/dist/core/lb-constants.js.map +1 -1
  20. package/dist/core/lb-types.d.ts +64 -77
  21. package/dist/core/lb-types.d.ts.map +1 -1
  22. package/dist/core/lb-types.js +40 -7
  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 +411 -449
  31. package/dist/hub/lb-hub.browser.js.map +1 -1
  32. package/dist/server/lb-express.d.ts +5 -5
  33. package/dist/server/lb-express.d.ts.map +1 -1
  34. package/dist/server/lb-express.js +35 -66
  35. package/dist/server/lb-express.js.map +1 -1
  36. package/dist/server/lb-server.d.ts +77 -135
  37. package/dist/server/lb-server.d.ts.map +1 -1
  38. package/dist/server/lb-server.js +132 -79
  39. package/dist/server/lb-server.js.map +1 -1
  40. package/docs/TECHREF-1.0.md +861 -587
  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 +374 -374
  47. package/docs/reference/overview.md +12 -10
  48. package/docs/reference/page-files.md +135 -99
  49. package/docs/reference/server.md +2 -2
  50. package/docs/reference/widgets.md +104 -110
  51. package/docs/roadmap.md +32 -39
  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 -122
  70. package/skills/loadbare-app/references/TECHREF-1.0.md +861 -587
  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 +374 -374
  75. package/skills/loadbare-app/references/overview.md +12 -10
  76. package/skills/loadbare-app/references/page-files.md +135 -99
  77. package/skills/loadbare-app/references/server.md +2 -2
  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:
134
-
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.
149
+ Declare what the three requests Loadbare provides do under `crud`, keyed by
150
+ the query they operate on:
145
151
 
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: {
@@ -193,18 +217,16 @@ resetRoster: {
193
217
  },
194
218
  ```
195
219
 
196
- Return `queryParms()` instead when the server response changes the query
197
- parms: after an insert, only the server knows the new key, and after a delete,
198
- only the server knows the parm should go. The page loads at the query string
199
- with those parms set, in the same round trip, and the hub writes them into the
200
- URL, replacing the history entry. This is Post/Redirect/Get without the
201
- redirect. The refresh set does not run, and nothing else `run` returned is
202
- sent, since both answered for the query string the page left. Name at least
203
- one parm, give each a string, and use an empty string to take one out:
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).
204
226
 
205
227
  ```ts
206
228
  // src/pages/accounts.requests.ts
207
- import { queryParms, type Requests } from "@loadbare/app/server";
229
+ import { url, type Requests } from "@loadbare/app/server";
208
230
 
209
231
  export const requests: Requests = {
210
232
  crud: {
@@ -212,14 +234,14 @@ export const requests: Requests = {
212
234
  rowInsert: {
213
235
  run: async (ctx, { values }) => {
214
236
  const id = await ctx.db.addAccount(values);
215
- return queryParms({ acct: String(id) });
237
+ return url({ acct: String(id) });
216
238
  },
217
239
  refresh: [],
218
240
  },
219
241
  rowDelete: {
220
242
  run: async (ctx, { key }) => {
221
243
  await ctx.db.deleteAccount(key);
222
- if (key === ctx.acct) return queryParms({ acct: "" });
244
+ if (key === ctx.acct) return url({ acct: "" });
223
245
  },
224
246
  refresh: ["accounts"],
225
247
  },
@@ -228,6 +250,20 @@ export const requests: Requests = {
228
250
  };
229
251
  ```
230
252
 
231
- Only parms: `run` cannot send the browser to another page. The loaded page
232
- reads the new parms through `contextFor` like any others; see
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
233
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
@@ -149,7 +149,7 @@ function contextFor(req: Request, parms: URLSearchParams): HubContext {
149
149
 
150
150
  Read the parms from that argument, not from `req.query`. When a server
151
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 [refresh and patch](./page-files.md#refresh-and-patch). Write it to
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
153
  be safe to call twice, and make a write visible to the second context by the
154
154
  time `run` returns: a handle opened per request, with no transaction held
155
155
  open across the two, is both.