@loadbare/app 0.12.0 → 0.14.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 (56) hide show
  1. package/dist/build/assemble.d.ts.map +1 -1
  2. package/dist/build/assemble.js +87 -1
  3. package/dist/build/assemble.js.map +1 -1
  4. package/dist/build/cli.js +2 -1
  5. package/dist/build/cli.js.map +1 -1
  6. package/dist/build/elements.d.ts +9 -1
  7. package/dist/build/elements.d.ts.map +1 -1
  8. package/dist/build/elements.js +50 -0
  9. package/dist/build/elements.js.map +1 -1
  10. package/dist/build/origins.d.ts +2 -0
  11. package/dist/build/origins.d.ts.map +1 -1
  12. package/dist/build/origins.js +1 -1
  13. package/dist/build/origins.js.map +1 -1
  14. package/dist/core/lb-constants.d.ts +34 -1
  15. package/dist/core/lb-constants.d.ts.map +1 -1
  16. package/dist/core/lb-constants.js +105 -0
  17. package/dist/core/lb-constants.js.map +1 -1
  18. package/dist/core/lb-types.d.ts +74 -18
  19. package/dist/core/lb-types.d.ts.map +1 -1
  20. package/dist/core/lb-types.js +74 -11
  21. package/dist/core/lb-types.js.map +1 -1
  22. package/dist/hub/lb-apply.d.ts +57 -18
  23. package/dist/hub/lb-apply.d.ts.map +1 -1
  24. package/dist/hub/lb-apply.js +661 -63
  25. package/dist/hub/lb-apply.js.map +1 -1
  26. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  27. package/dist/hub/lb-hub.browser.js +400 -33
  28. package/dist/hub/lb-hub.browser.js.map +1 -1
  29. package/dist/server/lb-express.d.ts +19 -7
  30. package/dist/server/lb-express.d.ts.map +1 -1
  31. package/dist/server/lb-express.js +62 -26
  32. package/dist/server/lb-express.js.map +1 -1
  33. package/dist/server/lb-server.d.ts +80 -11
  34. package/dist/server/lb-server.d.ts.map +1 -1
  35. package/dist/server/lb-server.js +185 -33
  36. package/dist/server/lb-server.js.map +1 -1
  37. package/docs/TECHREF-1.0.md +584 -111
  38. package/docs/comparison.md +17 -13
  39. package/docs/possible-ideas.md +308 -0
  40. package/docs/reference/chrome.md +7 -1
  41. package/docs/reference/custom-elements.md +134 -39
  42. package/docs/reference/data-binding.md +405 -17
  43. package/docs/reference/page-files.md +101 -19
  44. package/docs/reference/server.md +24 -0
  45. package/docs/reference/widgets.md +118 -25
  46. package/docs/roadmap.md +17 -6
  47. package/docs/testing.md +21 -10
  48. package/package.json +2 -3
  49. package/skills/loadbare-app/SKILL.md +92 -30
  50. package/skills/loadbare-app/references/TECHREF-1.0.md +584 -111
  51. package/skills/loadbare-app/references/chrome.md +7 -1
  52. package/skills/loadbare-app/references/custom-elements.md +134 -39
  53. package/skills/loadbare-app/references/data-binding.md +405 -17
  54. package/skills/loadbare-app/references/page-files.md +101 -19
  55. package/skills/loadbare-app/references/server.md +24 -0
  56. package/skills/loadbare-app/references/widgets.md +118 -25
@@ -48,7 +48,8 @@ page touches that page's files.
48
48
  ## HTML
49
49
 
50
50
  Write the page as a fragment. The fragment lands in `<main>`, which
51
- [chrome.html](./chrome.md) supplies.
51
+ [chrome.html](./chrome.md) supplies. A page carries no `<main>` and no
52
+ `<lb-hub>` of its own; the builder rejects one that does.
52
53
 
53
54
  ```html
54
55
  <!-- src/pages/about.page.html -->
@@ -89,14 +90,18 @@ export const queries: Queries = {
89
90
  page: "directory",
90
91
  count: String(await ctx.db.visitCount()),
91
92
  })),
92
- directory: rows("id", (ctx) => ctx.db.directory()),
93
+ directory: rows("id", (ctx) => ctx.db.directory(), { order: "name" }),
93
94
  };
94
95
  ```
95
96
 
96
- | Declared with | Answers with |
97
- |-------------------|-------------------------------------------|
98
- | `row(key, run)` | One row, an object |
99
- | `rows(key, run)` | All its rows, an array of objects, in order |
97
+ | Declared with | Answers with |
98
+ |----------------------------|-------------------------------------------|
99
+ | `row(key, run)` | One row, an object |
100
+ | `rows(key, run, options)` | All its rows, an array of objects |
101
+
102
+ `options` are a `rows` query's order: `order`, the columns the hub places
103
+ its rows by, and `serverSortedByUrl`, set when the query reads the user's
104
+ order parm itself. See [Order](./data-binding.md#order).
100
105
 
101
106
  `key` names the column that identifies a row. Give every row that column,
102
107
  including a `row` query's: an aggregate row answers with a constant key. The
@@ -109,8 +114,8 @@ page that needs the same data as one row and as a set declares two queries.
109
114
  The hub hands each value to the browser untouched, so what a number, a date
110
115
  or a null looks like is decided here, in the query.
111
116
 
112
- Return the full answer every time, and the rows in the order the page shows
113
- them. Sending only what changed is a request's job — see
117
+ Return the full answer every time. With no `order`, the rows show in the
118
+ order they are returned. Sending only what changed is a request's job — see
114
119
  [refresh and patch](#refresh-and-patch).
115
120
 
116
121
  ## Requests
@@ -120,7 +125,7 @@ optional:
120
125
 
121
126
  | Key | Runs |
122
127
  |---------------|--------------------------------------------------------|
123
- | `onPageEnter` | Before the page's queries, when the page loads |
128
+ | `onPageEnter` | Before the page's queries, when the page loads; may move the URL |
124
129
  | `handlers` | The page's declared requests, by request name |
125
130
  | `crud` | The requests Loadbare provides, by query name |
126
131
 
@@ -132,10 +137,14 @@ name:
132
137
  | `name` | The request name, from `lb-request` |
133
138
  | `query` | The query the request is for |
134
139
  | `key` | The key of the row the request is for |
140
+ | `keys` | The keys of the rows selected, in place of `key` |
141
+ | `rows` | A form's nested rows, by query: each `key` and `values` |
135
142
  | `values` | The gathered control values, by column |
136
143
 
137
144
  The hub sends `query`, `key` and `values` when it finds them; see
138
- [Gathering](./data-binding.md#gathering).
145
+ [Gathering](./data-binding.md#gathering). It sends `keys` for a request that
146
+ acts on the rows the user selected; see
147
+ [Selecting rows](./data-binding.md#selecting-rows).
139
148
 
140
149
  ### onPageEnter
141
150
 
@@ -150,6 +159,12 @@ export const requests: Requests = {
150
159
 
151
160
  Declare no refresh set here. The page's queries run afterward.
152
161
 
162
+ It may return `url()` instead, to move the page to other query parms before
163
+ anything shows, as restoring the filters and order the user last had does.
164
+ The page loads there in the same round trip, and that load does not follow
165
+ `onPageEnter` again. A `url()` leading to the parms already in hand is
166
+ ignored. See [Moving the URL](#moving-the-url).
167
+
153
168
  ### handlers
154
169
 
155
170
  Declare a request under the name the HTML gives `lb-request`. Pair what it
@@ -180,10 +195,34 @@ the query they operate on:
180
195
 
181
196
  | Request name | Runs | `run` receives |
182
197
  |-----------------|-------------|------------------|
183
- | `lb-row-insert` | `rowInsert` | `query`, `values` |
184
- | `lb-row-update` | `rowUpdate` | `query`, `key`, `values` |
198
+ | `lb-row-insert` | `rowInsert` | `query`, `values`, and `rows` from a form |
199
+ | `lb-row-update` | `rowUpdate` | `query`, `key`, `values`, and `rows` from a form |
185
200
  | `lb-row-delete` | `rowDelete` | `query`, `key` |
186
201
 
202
+ An `lb-row-update` or `lb-row-delete` carrying `keys` runs `rowUpdate` or
203
+ `rowDelete` once for each key, with that key, so a handler written for one
204
+ row serves several. The patches the runs return are laid together, and the
205
+ refresh set runs once. A `url()` returned for one of several rows is
206
+ reported and ignored. A declared request receives `keys` as it is.
207
+
208
+ `rows` holds the rows of a query nested in the form the request came from,
209
+ by query name, each with its `key` and the `values` of its own controls; see
210
+ [A form over a set of rows](./data-binding.md#a-form-over-a-set-of-rows).
211
+ The handler writes the master and its details together, deciding what each
212
+ row's values mean, such as an empty amount being no row at all:
213
+
214
+ ```ts
215
+ rowInsert: {
216
+ run: async (ctx, { values, rows }) => {
217
+ const id = await ctx.db.insertPaycheck(values);
218
+ for (const { key, values: row } of rows?.deduction_types ?? []) {
219
+ if (row.amount !== "") await ctx.db.insertDeduction(id, key, row.amount);
220
+ }
221
+ },
222
+ refresh: ["paychecks"],
223
+ },
224
+ ```
225
+
187
226
  ```ts
188
227
  // src/pages/directory.requests.ts
189
228
  import { patch, type Requests } from "@loadbare/app/server";
@@ -240,16 +279,14 @@ queries:
240
279
 
241
280
  Return a patch for a change the request knows the extent of — one row added,
242
281
  one row dropped, one row edited — and leave `refresh` empty. A refreshed
243
- `rows` query sends every row, and the hub places every row again, which moves
244
- each element and takes focus from the control the user is in.
282
+ `rows` query sends every row to say what one row could.
245
283
 
246
284
  A change that seems to need a refresh usually has a patch:
247
285
 
248
- - A placeholder row standing in for an empty set or section: drop its key in
249
- the patch that adds the first real row, and add it back in the patch that
250
- drops the last.
251
- - A row whose position changes: the rows host places it, as
252
- [`lb-table`](./widgets.md#lb-table) does by `data-sort`.
286
+ - A row whose position changes: the hub places it by the query's
287
+ [order](./data-binding.md#order).
288
+ - A group that appears or goes: the hub makes a group with its first row
289
+ and removes it with its last, so no row stands in for an empty one.
253
290
  - A row whose other columns change with the edit: re-read the row and patch
254
291
  it.
255
292
  - Rows the database removes with a deleted one: select their keys before the
@@ -324,3 +361,48 @@ Post/Redirect/Get without the redirect. The refresh set does not run, and
324
361
  nothing else `run` returned is sent, since both answered for the URL the page
325
362
  left. The loaded page reads the new parms through `contextFor`; see
326
363
  [the Express server](./server.md#database-layer).
364
+
365
+ ### Refusing a request
366
+
367
+ A handler refuses a request by throwing `refusal()`, with words written for
368
+ the user:
369
+
370
+ ```ts
371
+ import { refusal, type Requests } from "@loadbare/app/server";
372
+
373
+ export const requests: Requests = {
374
+ handlers: {
375
+ enterPaycheck: {
376
+ run: async (ctx, { values }) => {
377
+ if (Number(values!.amount) <= 0) {
378
+ throw refusal({ amount: "Must be more than zero" });
379
+ }
380
+ const overlap = await ctx.db.overlappingSeries(values!);
381
+ if (overlap) {
382
+ throw refusal(
383
+ `That series already has paychecks from ${overlap.from} through ${overlap.to}.`,
384
+ );
385
+ }
386
+ await ctx.db.enterPaycheck(values!);
387
+ },
388
+ refresh: ["paychecks"],
389
+ },
390
+ },
391
+ };
392
+ ```
393
+
394
+ | Call | The browser shows |
395
+ |----------------------------------|--------------------------------------------|
396
+ | `refusal("message")` | The message, for the request as a whole |
397
+ | `refusal({ column: "message" })` | Each message on the control for its column |
398
+ | `refusal("message", { ... })` | Both |
399
+
400
+ Throw it, from the handler or from anything the handler calls, and whatever
401
+ the application has open rolls back as for any throw. A column names a
402
+ control the request gathered. See [Refusals](./data-binding.md#refusals)
403
+ for how the browser shows them.
404
+
405
+ A refusal is the only error whose words reach the browser. Anything else a
406
+ handler throws is a failure, answered with nothing in it, so a stack or a
407
+ database message never reaches a page.
408
+
@@ -72,6 +72,26 @@ path, so the application cannot be hosted under a subpath such as
72
72
  whole path space through unchanged.
73
73
 
74
74
  Leave `express.json()` to `hubRoutes`, which mounts it on its own routes.
75
+ Pass its options through `hubRoutes` instead.
76
+
77
+ ## Options
78
+
79
+ `hubRoutes` takes an optional third argument, an object with one key per
80
+ concern:
81
+
82
+ | Option | Description |
83
+ |--------|----------------------------------------------------------|
84
+ | `json` | Options for `express.json()`, passed to it unchanged |
85
+
86
+ A request body over 100 KB, the `express.json()` default, is refused with a
87
+ 413 before any handler runs, and the server log names the option that raises
88
+ it. Raise it for an application whose pages send a file's text as a value:
89
+
90
+ ```ts
91
+ app.use(hubRoutes(hub, contextFor, { json: { limit: "5mb" } }));
92
+ ```
93
+
94
+ The limit covers every page's requests.
75
95
 
76
96
  ## Running the server
77
97
 
@@ -91,6 +111,10 @@ TypeScript types itself. `dist/pages.ts` imports each `.requests.ts` and
91
111
  `.queries.ts` file by its full name, extension included, because Node looks
92
112
  for exactly the path an import names.
93
113
 
114
+ The `hub` it exports comes from `createHub`, the only implementation of
115
+ the `Hub` interface. That interface may gain members in any release, so an
116
+ application never implements it.
117
+
94
118
  Enable `allowImportingTsExtensions` in the application's `tsconfig.json`
95
119
  when `tsc` type-checks the server. Without it, `tsc` rejects the `.ts`
96
120
  extensions in `dist/pages.ts`:
@@ -1,7 +1,8 @@
1
1
  # The Basic Widget Library
2
2
 
3
- Six widgets: `lb-input`, `lb-select`, `lb-options`, `lb-picker`, `lb-table`,
4
- `lb-unknown-page`. Every one of them is an ordinary custom element, written
3
+ Eleven widgets: `lb-input`, `lb-select`, `lb-options`, `lb-picker`,
4
+ `lb-table`, `lb-unknown-page`, `lb-confirm`, `lb-form`, `lb-insert-dialog`,
5
+ `lb-master` and `lb-checkbox`. Every one of them is an ordinary custom element, written
5
6
  against the contracts in [Custom Elements](./custom-elements.md#html) for its
6
7
  definition and [Custom Elements](./custom-elements.md#code) for its class.
7
8
 
@@ -25,7 +26,9 @@ for where a listed package sits in the cascade.
25
26
  `lb-input`, `lb-select`, `lb-options` and `lb-picker` are controls:
26
27
  form-associated custom elements with a `value` property that fire `change`.
27
28
  Each carries `lb-column` and `lb-request` itself, the way an `<input>` does;
28
- see [Being a control](./custom-elements.md#being-a-control).
29
+ see [Being a control](./custom-elements.md#being-a-control). Each passes a
30
+ refusal's message for its column to the native control inside it; see
31
+ [Refusals](./data-binding.md#refusals).
29
32
 
30
33
  ## `lb-input`
31
34
 
@@ -43,6 +46,7 @@ write the same edit twice.
43
46
  | Parameter | Fills |
44
47
  |----------------|----------------------------------|
45
48
  | `exp-label` | The visible `<label>` text |
49
+ | `exp-type` | The input's `type`, `text` when absent |
46
50
  | `exp-readonly` | The input's `readonly` attribute |
47
51
 
48
52
  ## `lb-select`
@@ -67,9 +71,7 @@ writes the row template inside the widget:
67
71
 
68
72
  ```html
69
73
  <lb-options lb-query="statuses" lb-column="status" lb-request="lb-row-update" exp-label="Status">
70
- <template data-group="category">
71
- <option lb-column="label"></option>
72
- </template>
74
+ <template><option lb-column="label"></option></template>
73
75
  </lb-options>
74
76
  ```
75
77
 
@@ -79,9 +81,15 @@ writes the row template inside the widget:
79
81
 
80
82
  - Each option's `value` is its row's key, the `lb-key-value` the hub stamps
81
83
  on the live row, so the widget holds a key and shows a label.
82
- - `data-group` on the row template sections the options into `<optgroup>`s,
83
- one per distinct value of that column, added and removed as rows arrive
84
- and leave.
84
+ - A group template holding an `<optgroup>` puts the options in groups, one
85
+ per run of the query's order's leading term. The widget labels each
86
+ `<optgroup>` from the `lb-group-value` the hub stamps on it:
87
+
88
+ ```html
89
+ <template lb-group>
90
+ <optgroup><template><option lb-column="label"></option></template></optgroup>
91
+ </template>
92
+ ```
85
93
  - Its `value` selects the option with that key, including an option that
86
94
  arrives after the value did.
87
95
 
@@ -100,7 +108,6 @@ one option showing one column:
100
108
  lb-request="lb-row-update"
101
109
  exp-label="Status"
102
110
  exp-column="label"
103
- exp-group="category"
104
111
  ></lb-picker>
105
112
  ```
106
113
 
@@ -108,10 +115,9 @@ one option showing one column:
108
115
  |--------------|-----------------------------------------|
109
116
  | `exp-label` | The visible `<label>` text |
110
117
  | `exp-column` | The column each option shows |
111
- | `exp-group` | The row template's `data-group` |
112
118
 
113
- An option built from two columns, or a row with a second element, is
114
- `lb-options` with the page's own row template.
119
+ An option built from two columns, a row with a second element, or options
120
+ in groups, is `lb-options` with the page's own templates.
115
121
 
116
122
  ## `lb-table`
117
123
 
@@ -123,11 +129,14 @@ row, the row template, and optionally a footer, each as a `<template>`:
123
129
  <template lb-exp-template="head">
124
130
  <tr><th>Date</th><th>Amount</th></tr>
125
131
  </template>
126
- <template data-sort="date" data-group="month">
127
- <tr><td lb-column="date"></td><td lb-column="amount"></td></tr>
132
+ <template lb-group>
133
+ <tr><th colspan="2" lb-column="month"></th></tr>
134
+ <template>
135
+ <tr><td lb-column="date"></td><td lb-column="amount"></td></tr>
136
+ </template>
128
137
  </template>
129
138
  <template lb-exp-template="foot">
130
- <tr lb-query="ledger-total"><td>Total</td><td lb-column="total"></td></tr>
139
+ <tr><td>Total</td><td lb-sum="amount"></td></tr>
131
140
  </template>
132
141
  </lb-table>
133
142
  ```
@@ -140,15 +149,99 @@ row, the row template, and optionally a footer, each as a `<template>`:
140
149
  |-----------------------------------|-----------------------------------------|
141
150
  | `<template lb-exp-template="head">` | The `<thead>` |
142
151
  | `<template lb-exp-template="foot">` | The `<tfoot>` |
143
- | Any other `<template>` | The row template, in the `<tbody>` |
144
-
145
- - `data-group` on the row template sections rows under a heading row, one
146
- per distinct value of that column, spanning every column of the row.
147
- - `data-sort` orders rows within a section, or within the whole body with no
148
- grouping, by the text of that column.
149
- - The footer is not a row of `ledger`. Its `<tr>` names a `row` query of its
150
- own, which lands on the `<tr>` itself. A grand total is a second query over
151
- the same data.
152
+ | Any other `<template>` | The row or group template, in the `<tbody>` |
153
+
154
+ - The hub places the rows by the query's order and builds its groups. The
155
+ rows land in the one `<tbody>`, so a group here is a heading row with its
156
+ rows after it.
157
+ - The footer is not a row of `ledger`. An aggregate there covers every row;
158
+ a figure that is not a sum of the rows, such as a budget, is a `<tr>`
159
+ naming a `row` query of its own.
160
+ - A `<template lb-exp-template="ghost">` is a blank row for entering a new
161
+ one, in a `<tbody>` of its own. The widget joins its controls to a form
162
+ outside the table, since a `<form>` cannot wrap a `<tr>`, and puts the
163
+ cursor back in it once an insert from it succeeds.
164
+ - After rows land, the widget scrolls the row the page's own request created
165
+ or moved into view.
166
+
167
+ ## `lb-confirm`
168
+
169
+ A button that asks before it sends. The page writes the question inside the
170
+ tag, and the request goes on the dialog's confirm button, so nothing reaches
171
+ the hub until the user says yes:
172
+
173
+ ```html
174
+ <lb-confirm exp-label="Delete" exp-request="lb-row-delete">
175
+ Delete <b lb-column="name"></b>?
176
+ </lb-confirm>
177
+ ```
178
+
179
+ | Parameter | Fills |
180
+ |---------------|--------------------------------------------------|
181
+ | `exp-label` | The text of the button that opens the dialog |
182
+ | `exp-request` | The confirm button's `lb-request` |
183
+ | `exp-confirm` | The confirm button's text, `Confirm` when absent |
184
+
185
+ The widget closes the dialog once `lb-request-done` says the request
186
+ succeeded, and leaves it open on a refusal.
187
+
188
+ ## `lb-form`
189
+
190
+ A `<form>` over one row's controls. A row with a key saves each control's
191
+ column on `change`; a row whose key is empty is new, and the hub sends no
192
+ update for it, so its controls wait for a submit. A definition only.
193
+
194
+ | Parameter | Fills |
195
+ |-----------|-----------------|
196
+ | `exp-id` | The form's `id` |
197
+
198
+ ## `lb-insert-dialog`
199
+
200
+ A row entered in a modal dialog, written once outside every row and opened
201
+ with `commandfor` and `command="show-modal"`. Save is the form's
202
+ `lb-row-insert`; on success the widget chooses the new row in the nearest
203
+ `lb-options` around the button that opened it, and closes. The handler
204
+ answers with a patch holding the new row.
205
+
206
+ | Parameter | Fills |
207
+ |-------------|---------------------------------------------------|
208
+ | `exp-id` | The dialog's `id` |
209
+ | `exp-title` | The dialog's heading |
210
+ | `exp-query` | The form's `lb-query`, which it inserts into |
211
+ | `exp-save` | The Save button's text, `Save` when absent |
212
+
213
+ ## `lb-master`
214
+
215
+ A set, one of its rows, and the buttons that act on the set: New, Copy,
216
+ Delete, Save new and Cancel, each shown only for a row on record or a new
217
+ one. A definition only, built from `lb-form` and `lb-confirm`. New sends
218
+ `new` and Copy sends `copy`, requests the page declares; `new` returns
219
+ `url()` emptying the parm that names the row.
220
+
221
+ | Parameter | Fills |
222
+ |---------------|--------------------------------------------|
223
+ | `exp-row` | The `lb-query` of the row the form shows |
224
+ | `exp-form-id` | The form's `id`, `master-form` when absent |
225
+
226
+ ## `lb-checkbox`
227
+
228
+ A checkbox and its label as one element, for an application to style. A
229
+ definition only: a native checkbox is already a control (see
230
+ [Checkboxes](./data-binding.md#checkboxes)), so the widget is the markup, a
231
+ `<label>` holding the `<input type="checkbox">` and a `<span>` with the
232
+ text. The column and request go on the checkbox inside, so they arrive as
233
+ expansion attributes.
234
+
235
+ Unlike `lb-input`, it has no script making the tag the control. A value
236
+ that lands before a widget's script has loaded arrives only as the string
237
+ stamp `lb-column-value`, and a script checkbox would read `"false"` as
238
+ checked. The hub sets a native checkbox's `checked` directly.
239
+
240
+ | Parameter | Fills |
241
+ |---------------|-----------------------------------------------|
242
+ | `exp-column` | The checkbox's `lb-column` |
243
+ | `exp-label` | The label's text |
244
+ | `exp-request` | The checkbox's `lb-request`, none when absent |
152
245
 
153
246
  ## `lb-unknown-page`
154
247