@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
@@ -19,7 +19,8 @@ Loadbare/app enforces no data types on values between the database and the
19
19
  browser. A uniform and useful approach is difficult to discern, and we do
20
20
  not want to pollute 1.0 with a potentially sub-optimal solution. Until then
21
21
  a value is rendered by browser coercion when it lands on an HTML text
22
- element, or by whatever the widget does when it lands on a widget.
22
+ element or a control, and by whatever the custom element does with
23
+ `lb-column-value` when it lands on one that is not a control.
23
24
 
24
25
  We will then see if a useful solution emerges that Loadbare/app should
25
26
  handle.
@@ -27,24 +28,21 @@ handle.
27
28
  ### Form controls
28
29
 
29
30
  - **Checkboxes and radio buttons are not implemented.** A value does not
30
- land on one and a form does not gather one. Landing needs a decision on
31
+ land on one and the hub does not gather one. Landing needs a decision on
31
32
  what counts as checked, which waits on [Data types](#data-types), and a
32
- radio group is several elements answering to one cell.
33
+ radio group is several elements answering to one column.
33
34
 
34
- ### Run-time state attributes
35
+ ### Run-time stamps
35
36
 
36
- The hub stamps `lb-row-count`, `lb-pending` and `lb-error` for a
37
- stylesheet to read. Their consumer is CSS rather than code.
37
+ The hub stamps `lb-query-row-count`, `lb-request-pending` and
38
+ `lb-request-error` for a stylesheet to read. Their consumer is CSS rather
39
+ than code.
38
40
 
39
- - **Decide `data-` against `lb-`.** These are the only names the hub writes
40
- outside its own namespace, which is either a deliberate signal that CSS
41
- owns them or an inconsistency.
42
- - **Say what `lb-row-count` holds.** It is stamped and never explained.
43
- - **Decide whether an unarrived value needs a signal.** The roadmap proposes
44
- deriving it from an absent `lb-value`. Recommend confirming that and
45
- adding nothing.
41
+ - **Decide whether an unarrived value needs a signal.** An element whose
42
+ column has not landed carries no `lb-column-value`. Recommend confirming
43
+ that as the signal and adding nothing.
46
44
 
47
- ### The widget protocol
45
+ ### Custom element hooks
48
46
 
49
47
  Loadbare/app owns every method name beginning with `lb` on a custom element.
50
48
  That decision is firm; what the set contains is not.
@@ -52,45 +50,43 @@ That decision is firm; what the set contains is not.
52
50
  - **Decide what the build does with an unknown `lb*` method.** An element
53
51
  carrying a method beginning with `lb` that Loadbare/app does not define may
54
52
  be an error, a warning, or ignored.
55
- - **Decide whether a widget may receive a whole row.** A cell lands one
56
- element at a time and offers no escape hatch. An optional `lbAcceptRow`
57
- would sit beside `lbPlaceRow` and `lbRowsLanded`, and this is the most
58
- likely first request from a widget author.
53
+ - **Decide whether a custom element may receive a whole row.** A column
54
+ lands one element at a time and offers no escape hatch. An optional
55
+ `lbAcceptRow` would sit beside `lbPlaceRow` and `lbRowsLanded`, and this is
56
+ the most likely first request from a custom element author.
59
57
 
60
- ### Lists
58
+ ### Nested queries
61
59
 
62
- Imagine we have an HTML `<select>` that displays a fixed list for all
63
- rows in a table, such as `customer_type` for a table of customers. When
64
- all customers can be any customer type, our system as written today
65
- is fine, because all HTML `<select>` elements get the same list.
60
+ Imagine an HTML `<select>` that shows a fixed set of choices on every row of
61
+ a table, such as `customer_type` for a table of customers. When every
62
+ customer may take every customer type, the system as written today is
63
+ fine, because every `<select>` receives the same rows.
66
64
 
67
- But it may be that the list of allowed values is dependent on other values
68
- in the row.
65
+ But the allowed values may depend on other values in the row.
69
66
 
70
-
71
- - **Decide how a new row fills a nested list.** A row added to the outer
72
- list starts with an empty nested list, which fills only when the nested
73
- list's name lands again. See [Master-detail](#master-detail) for what a
74
- nested list is for.
67
+ - **Decide how a new live row fills a nested query.** A row added to the
68
+ outer query starts with its nested query empty, and it fills only when the
69
+ nested query's name lands again. See [Master-detail](#master-detail) for
70
+ what a nested query is for.
75
71
 
76
72
  ```html
77
- <tbody lb-list="accounts">
78
- <template lb-key="id"><tr>
79
- <td lb-cell="name"></td>
80
- <td><select lb-list="statuses"><template lb-key="id"><option lb-cell="label"></option></template></select></td>
73
+ <tbody lb-query="accounts">
74
+ <template><tr>
75
+ <td lb-column="name"></td>
76
+ <td><select lb-query="statuses" lb-column="status"><template><option lb-column="label"></option></template></select></td>
81
77
  </tr></template>
82
78
  </tbody>
83
79
  ```
84
80
 
85
81
  ### The server API
86
82
 
87
- - **Give the chrome a way to state its own queries.** A widget in the chrome
88
- bound to a query forces every page to declare that query, since the page
89
- data run covers one page's set. `lb-navigation` shows the pattern from the
83
+ - **Give the chrome a way to state its own queries.** A custom element in
84
+ the chrome naming a query forces every page to declare that query, since
85
+ the page load covers one page's set. `lb-url` shows the pattern from the
90
86
  framework side, and an application has no equivalent. Recommend a
91
87
  chrome-level query set on `createHub`.
92
- - **State that only `createHub` implements `Hub`.** Otherwise a sixth
93
- operation breaks anyone who wrote the interface by hand.
88
+ - **State that only `createHub` implements `Hub`.** Otherwise a third call
89
+ breaks anyone who wrote the interface by hand.
94
90
  - **Decide the options-argument shape once.** Global hooks, transaction
95
91
  wrapping, error handling and a CSRF token all want the same trailing
96
92
  parameter on `createHub` and `hubRoutes`. Build none of them, but pick
@@ -110,9 +106,9 @@ in the row.
110
106
  definition would let the builder ship only what survives expansion.
111
107
  Recommend deferring the mechanism and reserving the configuration key, so
112
108
  that it lands as an opt-in rather than as a silent drop.
113
- - **Land the remaining validations.** One `lb-hub`, one empty `<main>`, and
114
- every `lb-` attribute known. Recommend landing these now, because refusing
115
- markup that used to build is the kind of change 1.0 gives up.
109
+ - **Land the remaining validations.** One `lb-hub` and one empty `<main>`.
110
+ Recommend landing these now, because refusing markup that used to build is
111
+ the kind of change 1.0 gives up.
116
112
  - **Confirm the three output names.** `app.html`, `client.js` and `app.css`
117
113
  are about to be fixed in `staticRoutes` as well as in the builder.
118
114
 
@@ -162,12 +158,12 @@ The builder bundles `client.js` from a `client-entry.ts` it generates into
162
158
  `--out` and leaves there. Nothing reads it after the bundle, and an
163
159
  application neither writes nor imports it.
164
160
 
165
- Every `.css` file in every origin joins `app.css`. There is no widget, page
161
+ Every `.css` file in every origin joins `app.css`. There is no custom element, page
166
162
  or chrome stylesheet: the builder concatenates them all, with nothing added,
167
163
  removed or scoped, so what ships is what was authored. Within one origin
168
164
  they are ordered by filename, the full path breaking a tie, and a
169
165
  subdirectory never affects the order. Origins follow the cascade — this
170
- package's own widgets, then each package named in `imports.ts`, then `--src`
166
+ package's own custom elements, then each package named in `imports.ts`, then `--src`
171
167
  last — so an application's own stylesheet always lands after the ones it
172
168
  imported. A file named `00-global.css` sorts first by saying so.
173
169
 
@@ -210,71 +206,78 @@ the whole of `--src` is one flat namespace, as stated in
210
206
 
211
207
  Three kinds of file hold HTML.
212
208
 
213
- | File | Holds | Found in |
214
- | ------------------ | ------------------------------------ | ------------ |
215
- | `chrome.html` | The one HTML document | `--src` |
216
- | `<stub>.page.html` | One page's markup, as a fragment | `--src` |
217
- | `<tag-name>.html` | One widget definition, as a fragment | Every origin |
209
+ | File | Holds | Found in |
210
+ | ------------------ | ----------------------------------------- | ------------ |
211
+ | `chrome.html` | The one HTML document | `--src` |
212
+ | `<stub>.page.html` | One page's markup, as a fragment | `--src` |
213
+ | `<tag-name>.html` | One custom element's markup, as a fragment | Every origin |
218
214
 
219
215
  As stated in [Chrome](#chrome), `chrome.html` is a required singleton.
220
216
 
221
217
  As stated in [Pages](#pages), the `<stub>` of a `.page.html` is the path the
222
218
  page maps to. The builder puts all pages into `dist/app.html` as HTML `<template>` objects
223
- that carry `lb-page="<stub>"`.
219
+ that carry `lb-page="<stub>"`, and `lb-page-title` when the page file has a
220
+ `<title>`.
224
221
 
225
- A widget file is exactly `<kebab-case-name>.html`. A [widget](#widgets) is
226
- an HTML custom element, suitable for organizing large HTML trees, or adding
227
- custom behavior, or both. A widget definition is named for the tag it expands.
222
+ An element file is exactly `<kebab-case-name>.html`. A
223
+ [custom element](#custom-elements) is suitable for organizing large HTML
224
+ trees, or adding custom behavior, or both. An element file is named for the
225
+ tag it expands.
228
226
 
229
227
  Any other HTML file, one that is not `chrome.html`, or `<stub>.page.html` or
230
228
  `<kebab-case-name>.html` will either:
231
- - be an error if it looks like a widget with capitalization
229
+ - be an error if it looks like an element file with capitalization
232
230
  - be skipped
233
231
 
232
+
234
233
  ## HTML
235
234
 
236
235
  In a Loadbare/app application, HTML is static after the build, and once
237
236
  it is sent to the browser on initial page load, no HTML is ever sent
238
237
  again.
239
238
 
240
- ### Widget Expansion
239
+ The developer writes `lb-*` attributes in markup. Script never assigns
240
+ them. The hub and the builder write only their stamps. See
241
+ [The markup checks](#the-markup-checks).
242
+
243
+ ### Expansion
241
244
 
242
245
  The builder executes a process we call "expansion". When it finds
243
246
  a custom element in the HTML it is processing, such as `<my-element>`,
244
- it looks for the file `<my-element>.html`, and follows the expansion
245
- rules to place the contents of `<my-element>.html`.
247
+ it looks for the element file `my-element.html`, and follows the expansion
248
+ rules to place its contents.
246
249
 
247
- A custom element with no definition file is left alone. A custom element
248
- with neither a definition nor a `.browser.ts` script is a build error, as
249
- stated in [Widgets](#widgets).
250
+ A custom element with no element file is left alone. A custom element
251
+ with neither an element file nor a `.browser.ts` script is a build error, as
252
+ stated in [Custom elements](#custom-elements).
250
253
 
251
254
  #### Build time parameters
252
255
 
253
256
  A build time parameter is supplied as an attribute on a custom element,
254
257
  and is converted to a fixed value within the HTML by the builder.
255
258
 
256
- The attribute value carries an `exp-` prefix, as in `exp-label`, and
257
- the substitution locations use `{{label}}` (no exp-prefix) inside the
258
- widget's HTML definition file:
259
+ The attribute name carries an `exp-` prefix, as in `exp-label`, and
260
+ the substitution locations use `{{label}}` (no prefix) inside the
261
+ element file:
259
262
 
260
263
  ```html
261
- <!-- lb-input.html, the definition -->
264
+ <!-- note-field.html, the element file -->
262
265
  <label>{{label}} <input readonly="{{readonly}}" /></label>
263
266
  ```
264
267
 
265
268
  ```html
266
269
  <!-- a page -->
267
- <lb-input lb-cell="name" exp-label="Name"></lb-input>
270
+ <note-field exp-label="Name"></note-field>
268
271
  ```
269
272
 
270
273
  ```html
271
274
  <!-- dist/app.html -->
272
- <lb-input lb-cell="name" exp-label="Name"
275
+ <note-field exp-label="Name"
273
276
  ><label>Name <input /></label
274
- ></lb-input>
277
+ ></note-field>
275
278
  ```
276
279
 
277
- A definition states a default with `{{name|default}}`, as in
280
+ An element file states a default with `{{name|default}}`, as in
278
281
  `{{button-label|OK}}`. Whitespace around the name and around the default is
279
282
  discarded. The default is a literal.
280
283
 
@@ -297,48 +300,61 @@ A parameter value cannot become markup.
297
300
 
298
301
  #### Slots and templates
299
302
 
300
- A widget definition may contain any number of named templates and optionally
303
+ An element file may contain any number of named templates and optionally
301
304
  one slot.
302
305
 
303
- This simplified definition of `lb-table` names two templates, `head` and
306
+ | Attribute | The builder |
307
+ | ----------------- | ------------------------------------------------------- |
308
+ | `lb-exp-slot` | Puts in it the rest of the custom element's content |
309
+ | `lb-exp-template` | Puts in it the content of the template of the same name |
310
+ | `exp-<name>` | Replaces `{{name}}` in the element file with its value |
311
+
312
+ - `lb-exp-slot` goes on one element in an element file.
313
+ - `lb-exp-template` goes on an element in an element file, and on a
314
+ `<template>` in the custom element's content. Both name the same
315
+ template.
316
+ - `exp-<name>` goes on a custom element.
317
+
318
+ This simplified element file for `lb-table` names two templates, `head` and
304
319
  `foot`, and marks `<tbody>` as the slot. A page supplies whichever templates
305
320
  it wants, and everything else it writes goes to the slot.
306
321
 
307
322
  ```html
308
- <!-- lb-table.html, the definition -->
323
+ <!-- lb-table.html, the element file -->
309
324
  <table>
310
325
  <caption>
311
326
  {{caption}}
312
327
  </caption>
313
- <thead lb-template="head"></thead>
314
- <tbody lb-slot></tbody>
315
- <tfoot lb-template="foot"></tfoot>
328
+ <thead lb-exp-template="head"></thead>
329
+ <tbody lb-exp-slot></tbody>
330
+ <tfoot lb-exp-template="foot"></tfoot>
316
331
  </table>
317
332
  ```
318
333
 
319
- When we use `lb-table` in HTML, the example below supplies the header
320
- template and an unnamed template. The unnamed one names neither `head` nor
321
- `foot`, so it goes to the slot. This usage writes no footer.
334
+ The page below supplies the `head` template and a row template. The row
335
+ template names no destination, so it goes to the slot. This page writes no
336
+ footer.
322
337
 
323
338
  ```html
324
339
  <!-- a page -->
325
- <lb-table lb-list="staff" exp-caption="Everyone, by team">
326
- <template lb-template="head">
340
+ <lb-table lb-query="staff" exp-caption="Everyone, by team">
341
+ <template lb-exp-template="head">
327
342
  <tr><th>Name</th><th>Role</th></tr>
328
343
  </template>
329
- <template lb-key="id">
330
- <tr><td lb-cell="name"></td><td lb-cell="role"></td></tr>
344
+ <template>
345
+ <tr><td lb-column="name"></td><td lb-column="role"></td></tr>
331
346
  </template>
332
347
  </lb-table>
333
348
  ```
334
349
 
335
350
  When a named template is expanded, the `<template>` tag is discarded and its
336
- contents are placed as children of the tag that names it. The built document
337
- holds:
351
+ contents are placed as children of the element that names it. Both
352
+ `lb-exp-slot` and `lb-exp-template` are gone from what ships. The built
353
+ document holds:
338
354
 
339
355
  ```html
340
356
  <!-- dist/app.html -->
341
- <lb-table lb-list="staff" exp-caption="Everyone, by team"
357
+ <lb-table lb-query="staff" exp-caption="Everyone, by team"
342
358
  ><table>
343
359
  <caption>
344
360
  Everyone, by team
@@ -350,10 +366,10 @@ holds:
350
366
  </tr>
351
367
  </thead>
352
368
  <tbody>
353
- <template lb-key="id">
369
+ <template>
354
370
  <tr>
355
- <td lb-cell="name"></td>
356
- <td lb-cell="role"></td>
371
+ <td lb-column="name"></td>
372
+ <td lb-column="role"></td>
357
373
  </tr>
358
374
  </template>
359
375
  </tbody>
@@ -363,75 +379,147 @@ holds:
363
379
 
364
380
  These are build errors:
365
381
 
366
- - A definition with two templates of one name
367
- - A definition with more than one `lb-slot`
368
- - A page naming a template the definition does not have
382
+ - An element file with two destinations of one name
383
+ - An element file with more than one `lb-exp-slot`
384
+ - A page naming a template the element file does not have
369
385
  - A page giving two templates for one name
370
- - Content written in a widget whose definition has no slot
386
+ - Content written in a custom element whose element file has no slot
371
387
 
372
- ### Binding
388
+ ### Names
373
389
 
374
- Binding attributes determine how the hub updates the DOM, either by
375
- directly updating a plain HTML element or instructing
376
- custom widgets to update themselves.
390
+ Loadbare owns the `lb-` prefix in several namespaces: attribute names, the
391
+ values of its attributes, query names, the column names of its own queries,
392
+ request names, custom element names and custom event names. It also owns
393
+ custom element method names beginning with `lb`.
377
394
 
395
+ The application should never name anything with the `lb-` prefix anywhere.
378
396
 
379
- | Attribute | Assigned By | Behavior |
380
- | ------------ | ----------- | ----------------------------------------------------------------------------------------------- |
381
- | lb-list | Developer | Scopes DOM children to a named set of rows; a nested lb-list or lb-row begins a new scope |
382
- | lb-row | Developer | Scopes DOM children to one named row; a nested lb-list or lb-row begins a new scope |
383
- | lb-cell | Developer | This DOM node displays this column of the row in scope |
384
- | lb-show | Developer | This DOM node is present when this column of the row in scope is neither null nor false |
385
- | lb-key | Developer | Names the column that identifies a row, on the row template inside an lb-list |
386
- | lb-key-value | Hub | Stamped on a live row: that row's value of `lb-key` |
387
- | lb-value | Hub | The value that landed on a cell; a widget updates itself from it and a stylesheet selects on it |
397
+ `createHub` refuses, at startup, a page that declares a query or a handler
398
+ whose name begins with `lb-`.
388
399
 
389
- #### How a value lands
400
+ ### Landing
390
401
 
391
- A cell lands one of three ways, and the element decides which. Every cell
392
- that receives a value also carries it as `lb-value`.
402
+ A query is a name for rows, of kind `row` or `rows`, with a key. The server
403
+ declares a page's queries in `<stub>.queries.ts`, and answers every request,
404
+ a page load included, with response items. A response item carries a query
405
+ name, its kind, its key's column name, and one of a row, all rows, or a
406
+ patch. See [The wire](#the-wire).
393
407
 
394
- | Element | Receives the value as |
395
- | -------------------------------------- | --------------------------------- |
396
- | A custom element, tag hyphenated | Its `lb-value` attribute |
397
- | `<select>`, `<textarea>`, or `<input>` | Its `value`, and `lb-value` |
398
- | Any other native element | Its `textContent`, and `lb-value` |
408
+ The hub lands a response item on every element whose `lb-query` names its
409
+ query.
399
410
 
400
- A widget owns whatever control it wraps, so it is handed the value and
401
- renders it itself. A form control shows its state as its `value`, so a
402
- `<select>` keeps its options. Any other native element has no behavior of its
403
- own, so its value is its text.
411
+ | Attribute | Names | The hub |
412
+ | ----------- | -------- | -------------------------------------------------- |
413
+ | `lb-query` | a query | Puts its rows in the element's content |
414
+ | `lb-column` | a column | Sets the element from that column |
415
+ | `lb-show` | a column | Removes it while the named column is null or false |
404
416
 
405
- A form gathers from the same controls it lands on, so a value read back on
406
- submit is the one that landed.
417
+ | Stamp | The hub stamps it with |
418
+ | -------------------- | -------------------------------- |
419
+ | `lb-column-value` | The value it set |
420
+ | `lb-key-value` | The row's key |
421
+ | `lb-query-row-count` | The number of live rows it holds |
407
422
 
408
- An `<input>` of type `checkbox`, `radio`, or `file` receives nothing, not even
409
- `lb-value`, and the hub reports it to the console. A form gathering one skips it the same way.
423
+ The markup names a query and the columns it shows. The kind and the key are
424
+ the server's, and the markup states neither.
410
425
 
411
- A `<select>` with no option for the value shows no selection.
426
+ The row template is the first `<template>` among the descendants of an
427
+ element with `lb-query`, outside any nested `lb-query`. A live row is an
428
+ element the hub cloned from a row template for one row.
429
+
430
+ | Kind | Row template | The hub |
431
+ | ------ | ------------ | ----------------------------------- |
432
+ | `row` | no | Lands the row on the element itself |
433
+ | `row` | yes | Lands one live row |
434
+ | `rows` | yes | Lands one live row per row |
435
+ | `rows` | no | Lands nothing |
436
+
437
+ A `rows` query with no row template names the query without showing it,
438
+ which is what an insert form naming the query it adds to is.
439
+
440
+ The nearest ancestor row of an element is the nearest of: the element
441
+ itself when it is a live row, an ancestor that is a live row, and an
442
+ ancestor that carries `lb-query` of kind `row`. The hub reads `lb-column`
443
+ and `lb-show` from the nearest ancestor row. A nested `lb-query` begins a
444
+ new query, and what is inside it is that query's.
445
+
446
+ An element's own `lb-query` names what the element holds, and its own
447
+ `lb-column` reads from the row around it. So a
448
+ `<select lb-query="statuses" lb-column="status">` in a live row of
449
+ `accounts` holds the statuses as its options and shows that account's
450
+ `status`. `lb-column` on an element with `lb-query` is undefined unless the
451
+ element is a control.
412
452
 
413
- Only the hyphen and these three form controls are recognized. The hub holds
414
- no other knowledge of any particular element, and the hyphen also decides
415
- that the hub leaves a widget's own clicks alone — see [Requests](#requests).
453
+ Name one query on more than one element to show it in more than one place.
454
+ Every one of them receives it.
416
455
 
417
- Nothing an application writes ever sets `lb-value`. Loadbare writes it, and
418
- a widget or a stylesheet reads it.
456
+ A query that arrives with nowhere to land is reported to the console.
419
457
 
420
- Loadbare also removes it, when a successful insert resets the cells it
421
- gathered — see [The round trip](#the-round-trip). A widget reads a removed
422
- `lb-value` as "nothing landed here" and returns its control to its default,
423
- the way a form control with no `value` attribute shows its default. A blank
424
- `lb-value` is a value like any other.
458
+ #### How a column lands
459
+
460
+ | Element | The hub sets |
461
+ | ------------------------------------------------ | ------------------------------ |
462
+ | A control | Its `value` |
463
+ | A custom element that is not a control | `lb-column-value` only |
464
+ | Any other element | Its text content |
465
+
466
+ A control is an `<input>`, `<select>` or `<textarea>`, or a form-associated
467
+ custom element with a `value` property that fires `change`. A
468
+ form-associated custom element declares `static formAssociated = true`.
469
+
470
+ The hub stamps `lb-column-value` with the value on every element it sets.
471
+ It never replaces the content of a custom element, which the builder placed
472
+ there from its element file. A custom element that is not a control
473
+ renders `lb-column-value` itself.
474
+
475
+ The hub gathers from the same controls it lands on, so a value read back is
476
+ the one that landed.
477
+
478
+ An `<input>` of type `checkbox`, `radio`, or `file` receives nothing, not
479
+ even `lb-column-value`, and the hub reports it to the console. The hub does
480
+ not gather one either.
481
+
482
+ A `<select>` with no option for the value shows no selection.
483
+
484
+ An element with `lb-column` and no ancestor row receives nothing. The hub
485
+ still gathers from it.
425
486
 
426
487
  The value arrives as the query produced it, with no conversion, so the
427
488
  browser decides what a non-string looks like.
428
489
 
429
- A cell holds one value. A set of values is a list of its own, never an array
430
- in a cell.
490
+ A column holds one value. A set of values is a query of its own, never an
491
+ array in a column.
492
+
493
+ A custom element sees `attributeChangedCallback` for an `lb-column-value`
494
+ already present when it upgrades, so it cannot tell a first landing from a
495
+ refresh, and does not need to.
431
496
 
432
- A widget sees `attributeChangedCallback` for an `lb-value` already present
433
- when it upgrades, so a widget cannot tell a first landing from a refresh,
434
- and does not need to.
497
+ #### Rows
498
+
499
+ The hub matches each row to a live row by its key. It stamps
500
+ `lb-key-value` on each live row, and on an element a `row` lands on itself.
501
+
502
+ All rows decide membership and order: every row is placed in the order
503
+ given, and a live row whose key did not arrive is removed. A patch touches
504
+ only the rows it names and leaves every other live row's contents and
505
+ position alone.
506
+
507
+ A new live row lands immediately before the row template, so rows
508
+ accumulate in the order they arrive. A custom element carrying `lb-query`
509
+ and a row template may place them itself — see [Row hooks](#row-hooks).
510
+
511
+ After every landing the hub stamps the element with `lb-query-row-count`,
512
+ the number of live rows it holds. The server answers with rows and says
513
+ nothing about how many survived, so this is the one fact a page cannot be
514
+ sent. It makes an empty query a stylesheet rule:
515
+
516
+ ```css
517
+ [lb-query-row-count="0"] .roster-empty {
518
+ display: revert;
519
+ }
520
+ ```
521
+
522
+ A row missing its key column is reported to the console and not landed.
435
523
 
436
524
  #### Displaying by condition
437
525
 
@@ -440,166 +528,236 @@ and does not need to.
440
528
  so `"false"` is on, and a query spells a condition as a boolean or a null.
441
529
 
442
530
  ```html
443
- <template lb-key="id">
531
+ <template>
444
532
  <tr>
445
- <td lb-cell="name"></td>
446
- <td><button lb-action="lb-row-delete" lb-show="removable">Remove</button></td>
533
+ <td lb-column="name"></td>
534
+ <td><button lb-request="lb-row-delete" lb-show="removable">Remove</button></td>
447
535
  </tr>
448
536
  </template>
449
537
  ```
450
538
 
451
- It binds as `lb-cell` does, to the row on the nearest scoped ancestor, and on
452
- an element that is itself a scope the column belongs to the row around it. A row
453
- that does not carry the column leaves the element as it is.
454
-
455
- An element that is off is moved into a `<template lb-show="column">` standing
456
- where it stood, and moved back out when the column turns on. It is not
457
- rendered, focused, clicked, announced or gathered. It is moved and never
458
- rebuilt, so a widget keeps its instance. Landing reaches into that template,
459
- and nothing else does, so the element and every cell and scope inside it
460
- return current. A custom element sees `disconnectedCallback` then
461
- `adoptedCallback` going in, and `adoptedCallback` then `connectedCallback`
462
- coming out, and still receives `lb-value` while it is away.
539
+ It reads from the nearest ancestor row, as `lb-column` does, and on an
540
+ element that carries `lb-query` the column belongs to the row around it. A
541
+ row that does not carry the column leaves the element as it is.
542
+ `lb-show` with no ancestor row is undefined.
543
+
544
+ An element that is off is moved into a `<template lb-show="column">`
545
+ standing where it stood, and moved back out when the column turns on. It is
546
+ not rendered, focused, clicked, announced or gathered. It is moved and
547
+ never rebuilt, so a custom element keeps its instance and a control keeps
548
+ what was typed into it. Landing reaches into that template, so the element
549
+ and everything inside it return current. A custom element sees
550
+ `disconnectedCallback` then `adoptedCallback` going in, and
551
+ `adoptedCallback` then `connectedCallback` coming out, and still receives
552
+ `lb-column-value` while it is away.
463
553
 
464
554
  The builder ships every `lb-show` element already inside its template, so
465
555
  nothing conditional shows until its row lands. An absent element's template
466
556
  keeps its place among its siblings, and a position selector counts it.
467
557
 
468
- These are build errors: `lb-show` on a row template's root, with no row
469
- around it (outside every scope, on a scope with none around it, or in a list
470
- scope outside its row template), or on a `<template>`.
558
+ These are build errors: `lb-show` on a row template's root, `lb-show` with
559
+ no `lb-query` around it, and `lb-show` on a `<template>`.
471
560
 
472
561
  Hiding is presentation, and the server still refuses what a request may not
473
562
  do.
474
563
 
475
564
  #### Master-detail
476
565
 
477
- A master is a row and its detail is a list, as two names answered side by
478
- side. A cell never holds rows.
566
+ A master is a `row` query and its detail is a `rows` query, as two names
567
+ answered side by side. A column never holds rows.
479
568
 
480
569
  ```html
481
- <section lb-row="invoice">
482
- <h2 lb-cell="number"></h2>
483
- <span lb-cell="customer"></span>
570
+ <section lb-query="invoice">
571
+ <h2 lb-column="number"></h2>
572
+ <span lb-column="customer"></span>
484
573
  </section>
485
574
 
486
575
  <table>
487
- <tbody lb-list="invoiceLines">
488
- <template lb-key="id"><tr><td lb-cell="item"></td><td lb-cell="amount"></td></tr></template>
576
+ <tbody lb-query="invoiceLines">
577
+ <template><tr><td lb-column="item"></td><td lb-column="amount"></td></tr></template>
489
578
  </tbody>
490
579
  </table>
491
580
  ```
492
581
 
493
- Many masters, each with its own detail, is one list of joined rows: each
494
- detail row carries its master's columns. A widget's `lbPlaceRow` groups them
495
- for display, as `lb-table` builds sections and `lb-options` builds
496
- `<optgroup>`s.
582
+ Many masters, each with its own detail, is one `rows` query of joined rows:
583
+ each detail row carries its master's columns. A custom element's
584
+ `lbPlaceRow` groups them for display, as `lb-table` builds sections and
585
+ `lb-options` builds `<optgroup>`s.
497
586
 
498
- A list nested in another list's rows receives the same rows in every outer
499
- row. That serves a picker offering the same choices on every row, and is
500
- not a way to show a different detail per row.
587
+ A query nested in another query's row template receives the same rows in
588
+ every live row. That serves a picker offering the same choices on every
589
+ row, and is not a way to show a different detail per row.
501
590
 
502
591
  Which master a page shows is a query parm, which a query reads off `ctx`
503
592
  since it takes no argument from the browser — see [Query parms](#query-parms).
504
- When a write creates or removes the master, the server response changes that
505
- parm — see
506
- [When a server response changes the query parms](#when-a-server-response-changes-the-query-parms).
593
+ When a write creates or removes the master, the server response moves the
594
+ URL — see
595
+ [When a server response moves the URL](#when-a-server-response-moves-the-url).
507
596
 
508
597
  ### Requests
509
598
 
510
599
  ---- UNEDITED ----
511
600
 
512
- Any element can carry the `lb-action` attribute. The hub listens for
513
- events `click` and `submit`, and fires a server request when it catches
514
- one of those events.
515
-
516
- The hub does not act on `click` or `submit` on a custom widget, they
517
- must fire their own event. The assumption is a custom widget is present
518
- because custom behavior is desired, and we don't want the hub to conflict
519
- with that custom behavior. A widget is told apart by the hyphen in its tag
520
- name, the same test that decides how a value lands — see
521
- [How a value lands](#how-a-value-lands).
522
-
523
- | lb-action | Written on |
524
- | -------------- | ------------------------------------------------- |
525
- | lb-row-insert | a `<form>`, or a button in a `<tr>`, in a list |
526
- | lb-row-delete | anything inside a live row |
527
- | lb-row-update | a `<form>`, a button in a live row, or a widget cell |
528
- | anything else | must be a named routine in the page's server code |
529
-
530
- The wire format is not visible to the user, but uses the same lb-*
531
- attributes minus their prefix, so that it is intelligible when working on
532
- Loadbare/app itself.
533
-
534
- The hub scopes every request, whether a native element or a widget
535
- dispatched it. It reads the scope from the dispatching element before any
536
- ancestor sees the event, and never overwrites a field the request already
537
- carries.
538
-
539
- | `action` | Filled from scope | Required |
540
- | ---------------- | ------------------------------ | ---------------------- |
541
- | a declared name | `list` or `row`, `key`, `cell` | nothing |
542
- | `lb-row-insert` | `list` | `list`, and `values` |
543
- | `lb-row-delete` | `list`, `key` | both |
544
- | `lb-row-update` | `list`, `key` | both, and `values` |
545
-
546
- An element's own `lb-list` or `lb-row` names what it displays, never where
547
- its request goes. A request belongs to the scope around the element, the
548
- way a control belongs to the form around it. `list` or `row` comes from the
549
- nearest ancestor scope, `key` from the nearest live row inside that scope,
550
- and `cell` from the dispatching element's own `lb-cell`.
551
- A request missing a required field is not sent. The hub never fills
552
- `value`. An `lb-row-insert` or `lb-row-update` from an element carrying
553
- `lb-cell` is a record of one cell, the way a control has a value and a form
554
- has values: `values` holds that cell alone, taken from the `value` the widget
555
- sent or else read from the control it is or wraps, and `value` is not sent
556
- beside it. A widget that sends a `value` from an element carrying no
557
- `lb-cell` is refused, since nothing names the column. Any other
558
- `lb-row-insert` or `lb-row-update` gathers the row it belongs
559
- to: `values` holds every `lb-cell` with a control to read in the nearest
560
- `<form>`, `<tr>` or live row around the dispatching element, itself
561
- included, inside its scope. The cells are found the way a row lands, so a
562
- cell inside a scope nested in the row is that scope's and is not gathered,
563
- while an element carrying a scope and `lb-cell` both is the row's cell.
564
- This is a button's form owner: what else sits
565
- beside the element never changes what is sent. A `<tr>` counts because a
566
- form cannot go around a table row's controls, so a new row in a table is its
567
- own form; a live row counts because it is the row the key names. The scope
568
- itself is never the row, since its cells belong to other rows. A request
569
- from an element in no form or row is not sent, and the hub reports that its
570
- cells belong in a `<form>`. A request that already carries `values` keeps
571
- them, and one whose row has no cell to read is not sent. A click on a
572
- native element that carries either operation and is a cell or
573
- holds cells is not sent, since clicking into one of its controls
574
- would send the row: put the action on a form, on a button, or on a widget
575
- that decides when its cell has changed.
576
-
577
- A widget dispatches the action and, where it wraps a control, that control's
578
- value.
601
+ | Attribute | Names | The hub |
602
+ | ------------ | --------- | -------------------------------------------- |
603
+ | `lb-request` | a request | Issues that request when the element commits |
604
+
605
+ | Stamp | The hub stamps it |
606
+ | -------------------- | ------------------------------------------ |
607
+ | `lb-request-pending` | While the round trip is in flight |
608
+ | `lb-request-error` | When the round trip fails, until the next |
609
+
610
+ The request name is the attribute value of `lb-request`. It names either a
611
+ request Loadbare provides or a declared request: a name the page declares
612
+ under `handlers`, which runs application code.
613
+
614
+ Every request has one shape, on the event and on the wire:
579
615
 
580
616
  ```
581
- { action: "lb-row-insert", list: "rosterList", values: {...} }
582
- { action: "lb-row-delete", list: "rosterList", key: "42" }
583
- { action: "lb-row-update", list: "rosterList", key: "42", values: {...} }
584
- { action: "lb-row-update", list: "rosterList", key: "42", values: { name: "Ann" } }
585
- { action: "selectTab", row: "prefs", cell: "active_tab", value: "two" }
617
+ { name: "lb-row-insert", query: "roster", values: { name: "Ann" } }
618
+ { name: "lb-row-update", query: "roster", key: "42", values: { name: "Ann" } }
619
+ { name: "lb-row-delete", query: "roster", key: "42" }
620
+ { name: "mailRoster", query: "roster", key: "42" }
621
+ ```
622
+
623
+ The hub sends `query`, `key` and `values` as present.
624
+
625
+ #### Committing
626
+
627
+ An element commits on `submit` when it is a form, on `change` when it is a
628
+ control, and on `click` otherwise.
629
+
630
+ A submit button with a form owner never commits on `click`. On a form's
631
+ `submit`, the hub issues the submitter's `lb-request` when it carries one,
632
+ and the form's otherwise, the way `formaction` replaces `action`.
633
+
634
+ A click on a descendant of an element carrying `lb-request` commits that
635
+ element, as long as the element is inside `<lb-hub>` and no interactive
636
+ content lies between them. Interactive content is HTML's: `<a href>`,
637
+ `<button>`, `<input>` other than hidden, `<select>`, `<textarea>`, `<label>`,
638
+ `<details>`, `<iframe>`, `<embed>`, `<audio controls>`, `<video controls>`
639
+ and `<img usemap>`, plus any form-associated custom element. A click on
640
+ one of them belongs to it: a click into an input inside a deletable row
641
+ focuses the input and deletes nothing.
642
+
643
+ The hub ignores a commit on an element carrying `lb-request-pending`.
644
+
645
+ #### Gathering
646
+
647
+ The hub gathers values when it issues a request. It gathers each control
648
+ value under the column its `lb-column` names.
649
+
650
+ | Attribute | The hub |
651
+ | -------------- | ------------------------------------------- |
652
+ | `lb-query` | Issues the request for it |
653
+ | `lb-column` | Gathers the control value under that column |
654
+ | `lb-key-value` | Takes `key` from it |
655
+
656
+ - When the element carrying `lb-request` has `lb-column`, the hub gathers
657
+ its value alone, and sends `values` with one member.
658
+ - Otherwise, when the element is a `<form>`, the hub gathers every
659
+ `lb-column` whose form owner is the element.
660
+ - Otherwise, when the element is in a live row, the hub gathers every
661
+ `lb-column` in that live row.
662
+ - Otherwise the hub gathers every `lb-column` whose form owner is the
663
+ element's form owner.
664
+
665
+ The form owner includes a control that names the form with the HTML `form`
666
+ attribute from anywhere in the document, which is how a table row's
667
+ controls belong to a form that cannot wrap a `<tr>`:
668
+
669
+ ```html
670
+ <form id="add-member" lb-request="lb-row-insert"></form>
671
+ <table lb-query="roster">
672
+ <tbody>
673
+ <template>
674
+ <tr><td lb-column="name"></td><td lb-column="role"></td></tr>
675
+ </template>
676
+ </tbody>
677
+ <tfoot>
678
+ <tr>
679
+ <td><input lb-column="name" form="add-member" /></td>
680
+ <td>
681
+ <input lb-column="role" form="add-member" />
682
+ <button form="add-member">Add</button>
683
+ </td>
684
+ </tr>
685
+ </tfoot>
686
+ </table>
586
687
  ```
587
688
 
689
+ The hub gathers from controls only. It skips a control whose nearest
690
+ ancestor `lb-query` is a descendant of the live row or form, since that
691
+ control belongs to a nested query. An element carrying `lb-query` and
692
+ `lb-column` both, such as a picker, belongs to the row around it and is
693
+ gathered.
694
+
695
+ The hub issues the request for the nearest ancestor `lb-query` of the
696
+ controls it gathered, and takes `key` from their nearest ancestor row when
697
+ that row is of the same query. When the gathered controls have different
698
+ nearest ancestor `lb-query`, the hub reports an error and sends nothing.
699
+
700
+ When it gathers nothing, the hub issues the request for the nearest
701
+ ancestor `lb-query` of the element carrying `lb-request`, and takes `key`
702
+ from that element's nearest ancestor row when that row is of the same
703
+ query.
704
+
705
+ A field the request already carries is its issuer's and is kept. A custom
706
+ element that dispatches its own request with `values` is not gathered over.
707
+
708
+ #### The request names
709
+
710
+ | Request name | Needs | Runs under `crud` |
711
+ | --------------- | ------------------------ | ----------------- |
712
+ | `lb-row-insert` | `query`, `values` | `rowInsert` |
713
+ | `lb-row-update` | `query`, `key`, `values` | `rowUpdate` |
714
+ | `lb-row-delete` | `query`, `key` | `rowDelete` |
715
+
716
+ The hub issues one of these only when it has what the table lists. An
717
+ `lb-row-insert` or `lb-row-update` that gathers nothing is not issued. A
718
+ request that is not issued is reported to the console.
719
+
720
+ Loadbare provides the handlers for these names: each runs the page's `crud`
721
+ entry for the request's query. A declared request runs the application's
722
+ handler under `handlers`, and the hub issues it with whatever it found.
723
+
724
+ A request name beginning with `lb-` that is none of these three is refused
725
+ by the builder in markup, and by the hub when a script dispatches it.
726
+
727
+ For a query the hub serves, the hub answers the request itself, with no
728
+ round trip. The hub serves `lb-url`, and answers `lb-row-update` for it —
729
+ see [Query parms](#query-parms).
730
+
588
731
  #### The request event
589
732
 
590
- The hub does not send a request the moment it catches a click or a submit.
591
- It builds the request, dispatches it from the element that acted as a
592
- bubbling `lb-request` event carrying the request as its `detail`, and a
593
- listener on the hub sends it.
733
+ The hub dispatches every request as the bubbling `lb-request` event before
734
+ sending it, with the request as its `detail`. The event is dispatched from
735
+ the element that committed.
594
736
 
595
- A widget uses that same event, and it is the only channel a widget has for
596
- firing a request of its own. A native element and a hand-written widget
737
+ A custom element uses the same event to issue a request of its own, with at
738
+ least `name` in its `detail`. A native element and a custom element
597
739
  therefore produce identical events.
598
740
 
599
- An ancestor sees the request on its way up and may stop it. The event is
600
- not cancelable, so an interceptor calls `stopPropagation`, not
601
- `preventDefault`. This is what makes a confirmation wrapper possible
602
- without the wrapped element knowing about it.
741
+ The hub completes the request on the event's way down, so every ancestor
742
+ sees it whole on the way up. A request that cannot be issued is stopped
743
+ there. An ancestor may stop it too: the event is not cancelable, so an
744
+ interceptor calls `stopPropagation`, not `preventDefault`. This is what
745
+ makes a confirmation wrapper possible without the wrapped element knowing
746
+ about it.
747
+
748
+ #### Request state
749
+
750
+ The hub stamps `lb-request-pending` on the element that issued a request
751
+ while its round trip is in flight, together with `aria-busy="true"`, and
752
+ removes both when it settles. It stamps `lb-request-error` when the round
753
+ trip fails, and clears it when that element issues its next request.
754
+
755
+ A custom element observes them by naming them in `observedAttributes`. On
756
+ plain HTML a stylesheet is the only consumer: dim a pending button, mark a
757
+ failed one.
758
+
759
+ The hub ignores a commit on an element carrying `lb-request-pending`, so a
760
+ pending element is disabled in fact and a stylesheet only has to show it.
603
761
 
604
762
  #### The round trip
605
763
 
@@ -608,77 +766,63 @@ aborts it and treats it as a failure, since `fetch` imposes no deadline of
608
766
  its own. An application cannot change the deadline.
609
767
 
610
768
  A round trip fails on a server error, on a network failure, or on that
611
- deadline, and all three set `lb-error` on the element that dispatched the
612
- request — see [Request state](#request-state).
613
-
614
- After a write succeeds, the cells it gathered show what the server holds.
615
- An `lb-row-update` does this by landing the row on them. An
616
- `lb-row-insert` does it by resetting them, the way `form.reset()` resets a
617
- form, because the row they held now lives in the list. The hub resets
618
- after it lands the response, and only what it gathered:
619
-
620
- - Each control returns to its default: an `<input>` or `<textarea>` to its
621
- `defaultValue`, a `<select>` to the options marked `selected`. A page
622
- that wants a prefilled insert writes a `value` attribute, as in plain
623
- HTML.
624
- - A control showing something other than the value the hub read holds an
625
- edit made during the round trip. That edit was not sent, and is left.
626
- - `lb-value` comes off each reset cell, and off its control, before the
627
- control resets — see [How a value lands](#how-a-value-lands).
628
- - Values a widget supplied in the request were not gathered, and are not
629
- reset.
630
- - Nothing is dispatched, as `form.reset()` fires no `change`.
631
-
632
- A failed insert resets nothing, so the entry can be corrected.
633
-
634
- A navigation that fails to load its data sets nothing. No element
635
- dispatched it, so there is nothing to stamp, and the hub reports it to the
636
- console. A load started by a control writing a query parm stamps that
637
- control, as a request stamps its origin.
638
-
639
- ### Links
769
+ deadline, and all three stamp `lb-request-error` on the element that issued
770
+ the request — see [Request state](#request-state).
640
771
 
641
- ---- UNEDITED ----
772
+ After a successful `lb-row-insert` gathered from a form, the hub resets the
773
+ form, after it lands the response. A failed insert resets nothing, so the
774
+ entry can be corrected. An `lb-row-update` resets nothing: the row it sent
775
+ lands back on its controls.
642
776
 
643
- An anchor carrying `lb-nav-link` navigates inside the application: the hub
644
- catches the click, pushes the anchor's path and query string onto history,
645
- and swaps the page host in `<main>`. An anchor without it is left alone and behaves like any
646
- other link, so leaving the application is the default and staying in it is
647
- the opt-in.
777
+ A reset of any form, whether the hub's, a reset button's or a script's,
778
+ returns its controls to their defaults, and the hub removes
779
+ `lb-column-value` from each of them. A cancelled reset keeps both.
648
780
 
649
- | Attribute | Assigned By | Behavior |
650
- | ----------- | ----------- | --------------------------------------------------------------------------------------- |
651
- | lb-nav-link | Developer | On an `<a>`: the hub shows the page the anchor's path names, without loading a document |
781
+ A page load that fails lands nothing and is reported to the console. A load
782
+ started by a request for `lb-url` stamps the element that issued it, as any
783
+ request stamps its element.
652
784
 
653
- The attribute takes no value. The hub looks for the nearest ancestor
654
- link to determine the path.
785
+ ### The URL
655
786
 
656
- Path space is flat. A path such as `/members` links to the `members.*` files
657
- on the server.
787
+ ---- UNEDITED ----
658
788
 
659
- The query string is kept. `/transactions?date_begin=2026-09-01` opens the
660
- transactions page narrowed to those dates, and a link to the page it is
661
- already on loads that page again at the new URL without replacing its DOM —
662
- see [Query parms](#query-parms).
789
+ The hub serves the query `lb-url`, of kind `row`, keyed by `lb-path`. It
790
+ lands wherever an element names it, like any server query.
663
791
 
664
- The bare path `/` resolves to `index`.
792
+ | Column | The hub sets it to |
793
+ | ----------------- | ---------------------------------------------- |
794
+ | `lb-path` | The path. The row's key |
795
+ | `lb-page-label` | The page's title, or its stub when it has none |
796
+ | `lb-page-unknown` | Whether no page's stub matches the path |
797
+ | any other name | That query parm |
665
798
 
666
- A path that names no page is detected in the browser, after a successful
667
- 200: every route gets the same document, so there is no server-delivered
668
- 404. A chrome that declares an `lb-unknown-page` dialog gets it opened.
669
- One that declares none gets a console error and nothing on screen. See
670
- [Chrome](#chrome).
799
+ | Attribute | The hub |
800
+ | ---------------- | -------------------------------------------------------- |
801
+ | `lb-url-link` | On a plain primary click, sets `lb-path` from the `href` |
802
+ | `lb-url-push` | Pushes a history entry for this element's update |
803
+ | `lb-url-unknown` | Calls `showModal()` when `lb-page-unknown` is `true` |
804
+
805
+ - `lb-url-link` goes on an `<a>`.
806
+ - `lb-url-push` goes on an element with `lb-request`.
807
+ - `lb-url-unknown` goes on a `<dialog>` inside `<lb-hub>`.
808
+
809
+ The hub shows in `<main>` the page whose stub is `lb-path`. `/` is
810
+ `index`. When `lb-path` takes a new value, the hub loads the page: the
811
+ server runs its `onPageEnter`, then its queries. When a query parm takes a
812
+ new value, the hub reloads the page's queries, and keeps the page's DOM, so
813
+ live rows that come back keep their place.
814
+
815
+ The page's title is the text of the page file's `<title>`, which the builder
816
+ stamps on the page as `lb-page-title`. The hub sets the document title to
817
+ `lb-page-label` when it is not null.
671
818
 
672
819
  ```html
673
- <nav>
674
- <a href="/" lb-nav-link>Home</a>
675
- <a href="/members" lb-nav-link>Members</a>
676
- <a href="https://example.com/docs">Docs</a>
677
- </nav>
820
+ <header lb-query="lb-url">
821
+ <h1>Membership Roster</h1>
822
+ <h2 lb-column="lb-page-label"></h2>
823
+ </header>
678
824
  ```
679
825
 
680
- See also [Navigation Row lb-navigation](#lb-navigation).
681
-
682
826
  #### What a URL names
683
827
 
684
828
  The path names a page, a place in the application. It never names a
@@ -690,70 +834,115 @@ The query string describes what that page has on screen:
690
834
  remembers nothing, so a URL is a reproducible view. Reload it, bookmark it,
691
835
  or mail it to someone, and what they see is what the sender saw.
692
836
 
693
- A page declares what it shows, its queries, and what it allows, its actions.
694
- Loading a page runs its queries. A request performs an action at a position.
695
- A row is addressed by `list` and `key`, taken from where the element sits,
696
- which is how the database already names it. Nothing is fetched by URL, so an
697
- application designs no endpoints.
837
+ A page declares what it shows, its queries, and what it allows, its
838
+ requests. Loading a page runs its queries. A request names a query and,
839
+ where it has one, a key, taken from where the element sits, which is how the
840
+ database already names a row. Nothing is fetched by URL, so an application
841
+ designs no endpoints.
698
842
 
699
843
  That last is where query parms move Loadbare's position. A query still takes
700
844
  no argument from the browser, and a parm arrives the way the session cookie
701
845
  does, as part of the request the context is built from. But a user who
702
- types `?acct=99999` now influences what a query returns. A query parm is
703
- user input, validated like any other where the application reads it, and a
846
+ types `?acct=99999` influences what a query returns. A query parm is user
847
+ input, validated like any other where the application reads it, and a
704
848
  per-session database role means an id outside the caller's reach finds
705
849
  nothing.
706
850
 
707
851
  Loadbare is for applications, not sites. Every route is answered with the
708
852
  same document, and a path that names no page is found out in the browser —
709
- see [Links](#links).
853
+ see [An unknown page](#an-unknown-page).
710
854
 
711
- #### Query parms
855
+ #### Links
712
856
 
713
- A control that narrows what a page shows writes its value into the query
714
- string, rather than sending it:
857
+ An `<a>` carrying `lb-url-link` moves within the application. On a plain
858
+ primary click — the first button, no modifier key, not already handled — the
859
+ hub pushes a history entry for the `href`'s path and query string, and loads
860
+ the page there. Any other click, and any anchor without `lb-url-link`,
861
+ behaves like any other link, so leaving the application is the default and
862
+ staying in it is the opt-in.
715
863
 
716
864
  ```html
717
- <lb-options lb-list="teams" lb-query-parm="team" exp-label="Team:">
718
- <option value="">Every team</option>
719
- <template lb-key="id"><option lb-cell="name"></option></template>
720
- </lb-options>
865
+ <nav>
866
+ <a href="/" lb-url-link>Home</a>
867
+ <a href="/members" lb-url-link>Members</a>
868
+ <a href="https://example.com/docs">Docs</a>
869
+ </nav>
721
870
  ```
722
871
 
723
- | Attribute | Written by | Behavior |
724
- | -------------------- | ---------- | --------------------------------------------------------------- |
725
- | `lb-query-parm` | Developer | On a control: its `change` writes this parm and reloads the page |
726
- | `lb-query-parm-push` | Developer | With `lb-query-parm`: the write pushes a history entry |
872
+ The attribute takes no value.
727
873
 
728
- On `change`, the hub reads the control's value the way a form reads a cell,
729
- and sets that one parm in the URL. Every other parm is left as it is, since
730
- it may have arrived by link with no control on screen to say it again. An
731
- empty value takes the parm out, so a URL is as long as the user has narrowed
732
- the page. A value the URL already carries does nothing.
874
+ Path space is flat. A path such as `/members` links to the `members.*` files
875
+ on the server.
733
876
 
734
- The write replaces the current history entry: changing what a page shows is
735
- not going anywhere, so Back leaves the page rather than walking back through
736
- every choice. `lb-query-parm-push` makes the write push an entry instead.
877
+ The query string is kept. `/transactions?date_begin=2026-09-01` opens the
878
+ transactions page narrowed to those dates.
737
879
 
738
- The page then loads at the new URL exactly as a cold load of that URL would:
739
- `onPageEnter`, then every query. The page's DOM is kept, and the answer
740
- lands by key, so a list that gets its rows back keeps them and its scroll
741
- position.
880
+ The browser's Back and Forward load the page at the URL they arrive at, the
881
+ same way.
742
882
 
743
- After every load — cold, by link, by Back, by a write — the hub lands each
744
- parm on the control that writes it, and an absent parm lands empty. A
745
- control therefore shows what the address bar says.
883
+ #### Query parms
746
884
 
747
- A control that writes a query parm sends no request. One that also carries
748
- `lb-action` has that request refused, since the choice would otherwise be
749
- sent twice.
885
+ A control that narrows what a page shows updates `lb-url` rather than
886
+ sending a request. Put it inside an element naming `lb-url`, with
887
+ `lb-column` naming the parm and `lb-request="lb-row-update"`:
750
888
 
751
- Every round trip carries the query string the browser is showing, a page
752
- load and an action alike. The server hands its parms to `contextFor` — see
889
+ ```html
890
+ <div lb-query="lb-url">
891
+ <select lb-column="team" lb-request="lb-row-update">
892
+ <option value="">Every team</option>
893
+ <option value="Engines">Engines</option>
894
+ </select>
895
+ </div>
896
+ ```
897
+
898
+ The hub answers the request itself. It sets the parms the request names in
899
+ the URL and leaves every other parm as it is, since one may have arrived by
900
+ link with no control on screen to say it again. An empty value takes the
901
+ parm out, so a URL is as long as the user has narrowed the page. A request
902
+ that leaves the URL as it was does nothing. `lb-page-label` and
903
+ `lb-page-unknown` are the hub's, and a request never sets them.
904
+
905
+ The update replaces the current history entry: changing what a page shows is
906
+ not going anywhere, so Back leaves the page rather than walking back through
907
+ every choice. `lb-url-push` on the element carrying `lb-request` makes the
908
+ update push an entry instead.
909
+
910
+ When `lb-path` takes a new value, the hub pushes a history entry, and the
911
+ new URL carries only the query parms the request names.
912
+
913
+ Every load lands `lb-url`, and a parm the URL does not carry lands empty on
914
+ every element naming it as a column, so a control shows what the address
915
+ bar says after Back as well as after a reload. This is the one exception
916
+ to landing, where a row sets the columns it names and leaves the rest as
917
+ they were: the hub reads the column names under `lb-query="lb-url"` and
918
+ names each one in the row it lands.
919
+
920
+ The hub sends the query parms with every round trip, a page load and a
921
+ request alike. The server passes them to `contextFor` — see
753
922
  [The Express server](#the-express-server). Nothing in Loadbare assigns a
754
923
  parm a meaning.
755
924
 
756
- #### When a server response changes the query parms
925
+ #### An unknown page
926
+
927
+ When no page's stub matches the path, `lb-page-unknown` is `true` and
928
+ `lb-page-label` is null. The hub calls `showModal()` on the
929
+ `<dialog lb-url-unknown>`, where the chrome has one, and reports the path to
930
+ the console. A chrome with none shows nothing.
931
+
932
+ `lb-url` lands before the page is looked up, so the dialog shows it by
933
+ naming it like any other element:
934
+
935
+ ```html
936
+ <dialog lb-url-unknown lb-query="lb-url">
937
+ The URL <span lb-column="lb-path"></span> is not in this app.
938
+ </dialog>
939
+ ```
940
+
941
+ A path that names no page is detected in the browser, after a successful
942
+ 200: every route gets the same document, so there is no server-delivered
943
+ 404.
944
+
945
+ #### When a server response moves the URL
757
946
 
758
947
  Some query parms can only be known once a write has run. After an insert,
759
948
  the key of the new row exists only on the server. After a delete, only the
@@ -761,75 +950,66 @@ server knows that the row the page was showing is gone.
761
950
 
762
951
  The usual web answer is Post/Redirect/Get: the server answers the write with
763
952
  a redirect to a URL naming the result, and the browser makes a second request
764
- to load it. Loadbare/app does the same work in one round trip, and never
765
- changes the path.
953
+ to load it. Loadbare/app does the same work in one round trip.
766
954
 
767
- A request's `run` returns `queryParms()`, naming the parms the write decided.
768
- The server loads the page with those parms set, as a cold load of the
769
- resulting URL would: `onPageEnter`, then every query. The server response
770
- carries the parms and that load together. The hub sets the parms in the URL,
771
- replacing the history entry, and then lands the load.
955
+ A handler returns `url()`, a new row for `lb-url`. The server loads the page
956
+ at the resulting URL, as a cold load of it would: `onPageEnter`, then every
957
+ query. The response carries the `lb-url` item followed by that load. The
958
+ hub writes the URL and then lands the load.
772
959
 
773
960
  ```ts
774
961
  rowInsert: {
775
962
  run: async (ctx, { values }) => {
776
963
  const id = await ctx.db.addAccount(values);
777
- return queryParms({ acct: String(id) });
964
+ return url({ acct: String(id) });
778
965
  },
779
966
  refresh: [],
780
967
  },
781
968
  ```
782
969
 
783
- - Only query parms change. A server response cannot send the browser to
784
- another page.
785
- - Parms the response does not name are left as they are. An empty value
786
- removes its parm.
787
- - At least one parm is named, and every value is a string. Otherwise the
788
- server warns, ignores the parms, and runs the refresh set as usual.
789
- - The refresh set does not run, and anything else `run` returned is dropped.
790
- Both were answers for the query string the page is leaving.
970
+ - A column other than `lb-path` is a query parm. Parms the row does not
971
+ name are kept, and an empty value removes its parm.
972
+ - A row carrying `lb-path` for another page enters that page with only the
973
+ parms the row names, and pushes a history entry. Otherwise the update
974
+ replaces the history entry, or pushes one when the element that issued the
975
+ request carries `lb-url-push`.
976
+ - The row names at least one column, and every value is a string. Otherwise
977
+ the server warns, ignores the row, and runs the refresh set as usual.
978
+ - The refresh set does not run, and anything else the handler returned is
979
+ dropped. Both were answers for the URL the page is leaving.
791
980
  - If loading the page fails, the write has still happened. The response
792
- carries the parms alone, and the hub loads the page itself. A failure
793
- there is a page load's, and is not stamped on the element that sent the
794
- request.
981
+ carries the `lb-url` item alone, and the hub loads the page itself. A
982
+ failure there is a page load's, and is not stamped on the element that
983
+ issued the request.
795
984
  - A user who has left the page by the time the response arrives keeps the
796
985
  URL they are on.
797
986
 
798
987
  The page is loaded with a second context, built by `contextFor` from the new
799
988
  parms — see [The Express server](#the-express-server).
800
989
 
801
- ### lb-navigation
802
-
803
- ---- UNEDITED ----
804
-
805
- Status: 1.0-RC.
806
-
807
- Current page, published by the hub as a row. Can be bound anywhere just
808
- like a server-produced result.
809
-
810
- | Cell | Holds |
811
- | ------------ | ------------------------------------------------------------------- |
812
- | `page-label` | The text of the link to the current page, empty if no link names it |
813
- | `page-uri` | The path and query string as the browser has them, such as `/members?team=Engines` |
814
-
815
- The `page-uri` is taken from the current URL. The `page-label` is taken from the first
816
- `lb-nav-link` link in the document (presumably in a nav bar) whose path matches the
817
- current path, so the query string does not change it.
990
+ ### The markup checks
818
991
 
819
- The row lands before the page is looked up, so a path that names no page has
820
- it too, and an `lb-unknown-page` dialog, where the chrome has one, displays
821
- it by naming the row like any other subtree.
992
+ The builder checks the chrome and every page, after expansion, and refuses
993
+ to build on any of these:
822
994
 
823
- It lands only where a subtree names it. A chrome that displays no
824
- navigation is not warned about a row with nowhere to go.
995
+ - An `lb-` attribute that is not a developer attribute: `lb-query`,
996
+ `lb-column`, `lb-show`, `lb-request`, `lb-url-link`, `lb-url-push`,
997
+ `lb-url-unknown`. Every other `lb-` attribute is a stamp.
998
+ `lb-exp-slot` and `lb-exp-template` are consumed by expansion and never
999
+ reach the check.
1000
+ - `lb-request` naming a value that begins with `lb-` and is not one of the
1001
+ three request names.
1002
+ - `lb-url-link` on anything but an `<a>`.
1003
+ - `lb-url-push` on an element with no `lb-request`.
1004
+ - `lb-url-unknown` on anything but a `<dialog>`, and in the chrome, outside
1005
+ `<lb-hub>`.
1006
+ - `lb-show` on a `<template>`, on a row template's root, or with no
1007
+ `lb-query` around it.
825
1008
 
826
- ```html
827
- <header lb-row="lb-navigation">
828
- <h2 lb-cell="page-label"></h2>
829
- </header>
830
- ```
1009
+ `lb-column` with no ancestor row is allowed: the hub gathers from it.
831
1010
 
832
- See also [Links](#links).
1011
+ Script never assigns an `lb-` attribute, so the markup states every one
1012
+ that exists, and checking the markup is sound.
833
1013
 
834
1014
  ## Application files
835
1015
 
@@ -849,10 +1029,9 @@ The file is an HTML document, which must contain:
849
1029
 
850
1030
  It may also contain:
851
1031
  - `/app.css`, the stylesheet bundle
852
- - A `<dialog>` marked with attribute `lb-unknown-page`, which the hub
853
- will display to the user if an attempt is made to navigate to an
854
- unknown page. It goes inside `<lb-hub>`, like everything the hub acts
855
- on; the builder rejects one placed elsewhere.
1032
+ - A `<dialog lb-url-unknown>`, which the hub opens when the path names no
1033
+ page. It goes inside `<lb-hub>`, like everything the hub acts on; the
1034
+ builder rejects one placed elsewhere.
856
1035
  - The `hidden` attribute on `<body>`, which the hub removes once the first page has landed.
857
1036
 
858
1037
  ```html
@@ -867,52 +1046,66 @@ It may also contain:
867
1046
  </head>
868
1047
  <body hidden>
869
1048
  <lb-hub>
870
- <header lb-row="lb-navigation">
1049
+ <header lb-query="lb-url">
871
1050
  <h1>Membership Roster</h1>
872
- <h2 lb-cell="page-label"></h2>
1051
+ <h2 lb-column="lb-page-label"></h2>
873
1052
  </header>
874
1053
  <nav>
875
- <a href="/" lb-nav-link>Home</a>
876
- <a href="/members" lb-nav-link>Members</a>
1054
+ <a href="/" lb-url-link>Home</a>
1055
+ <a href="/members" lb-url-link>Members</a>
877
1056
  <a href="https://example.org/">Our website</a>
878
1057
  </nav>
879
1058
  <main></main>
880
- <dialog lb-unknown-page lb-row="lb-navigation">
881
- The URL <span lb-cell="page-uri"></span> is not in this app.
1059
+ <dialog lb-url-unknown lb-query="lb-url">
1060
+ The URL <span lb-column="lb-path"></span> is not in this app.
882
1061
  </dialog>
883
1062
  </lb-hub>
884
1063
  </body>
885
1064
  </html>
886
1065
  ```
887
1066
 
888
- The `lb-row`, `lb-cell` and `lb-nav-link` attributes in that example are
889
- ordinary Loadbare/app binding. See [Binding](#binding),
890
- [Links](#links) and [lb-navigation](#lb-navigation).
1067
+ The chrome's `<title>` shows until the first page lands, and each page's
1068
+ title replaces it — see [The URL](#the-url).
891
1069
 
892
1070
  ### Pages
893
1071
 
894
1072
  The page namespace is flat. Loadbare/app does not care where in the `--src`
895
1073
  the page files are located, but they must be unique across the application.
896
- A page `/deep/path/to/mypage.html` is routed to `/mypage`.
1074
+ A page `/deep/path/to/mypage.page.html` is routed to `/mypage`.
897
1075
 
898
1076
  Pages are grouped as
899
- - <stub>.page.html is recognized as navigable and capable of
1077
+ - `<stub>.page.html` is recognized as navigable and capable of
900
1078
  having associated queries and requests
901
- - <stub>.queries.ts are the queries for a page
902
- - <stub>.requests.ts respond to the requests from the browser
1079
+ - `<stub>.queries.ts` are the queries for a page
1080
+ - `<stub>.requests.ts` respond to the requests from the browser
903
1081
 
904
- The <stub> value for a page must match the path used in links,
905
- so that `<a href='/members' lb-nav-link>Members</a>` has matching
906
- files `members.pages.html` et al.
1082
+ The stub of a page must match the path used in links,
1083
+ so that `<a href="/members" lb-url-link>Members</a>` has matching
1084
+ files `members.page.html` et al.
907
1085
 
908
1086
  #### Page HTML
909
1087
 
910
1088
  The markup in `<stub>.page.html` is the same HTML as everywhere else
911
1089
  in a Loadbare/app application, see [HTML](#html).
912
1090
 
913
- The builder wraps each expanded page in `<template lb-page="<stub>">` and
914
- puts it in the built document. The application never writes `lb-page`; the
915
- hub reads it to find the page a path names.
1091
+ A page file may carry a `<title>`. The builder removes it and stamps its
1092
+ text content as `lb-page-title` on the page, and the hub shows it as
1093
+ `lb-page-label`.
1094
+
1095
+ ```html
1096
+ <!-- src/pages/members.page.html -->
1097
+ <title>Members</title>
1098
+ <h1>Members</h1>
1099
+ ```
1100
+
1101
+ The builder wraps each expanded page in
1102
+ `<template lb-page="<stub>" lb-page-title="...">` and puts it in the built
1103
+ document. The hub reads `lb-page` to find the page a path names.
1104
+
1105
+ | Stamp | The builder stamps it with |
1106
+ | --------------- | --------------------------------------------- |
1107
+ | `lb-page` | The page's stub, which the hub reads |
1108
+ | `lb-page-title` | The text content of the page file's `<title>` |
916
1109
 
917
1110
  ## The server
918
1111
 
@@ -976,7 +1169,7 @@ Explaining Express is beyond the scope of this technical reference. The
976
1169
  only real requirement is that the catch-all for app.html is at the end,
977
1170
  so it does not catch any other files.
978
1171
 
979
- `hubRoutes` answers `POST /lb/<page>`, with the query string the browser is
1172
+ `hubRoutes` answers `POST /lb/<stub>`, with the query string the browser is
980
1173
  showing after it, verbatim. It hands `contextFor` that query string's parms
981
1174
  as a second argument, and `contextFor` puts on the context whatever a query
982
1175
  reads from them. They are user input:
@@ -988,7 +1181,7 @@ function contextFor(req: Request, parms: URLSearchParams): HubContext {
988
1181
  ```
989
1182
 
990
1183
  Read the parms from that argument, not from `req.query`. After a request
991
- whose `run` returned `queryParms()`, `contextFor` is called a second time for
1184
+ whose handler returned `url()`, `contextFor` is called a second time for
992
1185
  the same request, with the new parms, to load the page at them. So it must
993
1186
  be safe to call twice, and a write must be visible to the second context by
994
1187
  the time its `run` returns. A handle opened per request without a
@@ -1002,28 +1195,37 @@ the whole path space through unchanged.
1002
1195
  ### Page Queries
1003
1196
 
1004
1197
  `<stub>.queries.ts` exports one object named `queries`, typed `Queries`.
1005
- Each key is a query name, and the markup binds to that name through
1006
- `lb-list` or `lb-row`.
1198
+ Each key is a query name, which the markup names with `lb-query`.
1007
1199
 
1008
- Cardinality belongs to the named query. One named query always returns a single
1009
- row or an array of rows. Build a query using `row()` or `list()` to return
1010
- the two shapes. A query cannot be built without one of these functions.
1011
-
1012
- Queries names cannot begin with `lb-`, that namespace is reserved for
1013
- Loadbare/app queries the hub makes available in the browser, such
1014
- as [lb-navigation](#lb-navigation).
1200
+ A query's kind and key belong to its name. Declare each query with
1201
+ `row(key, run)` or `rows(key, run)`, naming the key column first. A query
1202
+ cannot be built without one of these functions. Every query has a key; an
1203
+ aggregate row answers with a constant one.
1015
1204
 
1016
1205
  ```ts
1017
1206
  // members.queries.ts
1018
- import { list, row, type Queries } from "@loadbare/app/server";
1207
+ import { row, rows, type Queries } from "@loadbare/app/server";
1019
1208
 
1020
1209
  export const queries: Queries = {
1021
- roster: list((ctx) => ctx.db.members()),
1022
- summary: row(async (ctx) => ({ count: String(await ctx.db.memberCount()) })),
1210
+ roster: rows("id", (ctx) => ctx.db.members()),
1211
+ summary: row("id", async (ctx) => ({
1212
+ id: "all",
1213
+ count: String(await ctx.db.memberCount()),
1214
+ })),
1023
1215
  };
1024
1216
  ```
1025
1217
 
1026
- Every query and every request receives `ctx`, the application's own request
1218
+ A `row` query returns one object, and a `rows` query returns an array of
1219
+ objects in the order the page shows them. An answer that disagrees with
1220
+ its declared kind is left out of the response, with a warning naming the
1221
+ query.
1222
+
1223
+ Query names cannot begin with `lb-`, that namespace is reserved for
1224
+ Loadbare/app queries the hub serves in the browser, such as
1225
+ [`lb-url`](#the-url). `createHub` refuses at startup a page that declares
1226
+ such a name as a query, a handler or a `crud` entry.
1227
+
1228
+ Every query and every handler receives `ctx`, the application's own request
1027
1229
  context. The application builds it once per request and hands it to the hub,
1028
1230
  see [The Express server](#the-express-server). Loadbare/app declares it empty
1029
1231
  and never reads it, so a query finds exactly what the application put there,
@@ -1039,36 +1241,32 @@ It has three optional keys.
1039
1241
 
1040
1242
  | Key | Keyed by | Answers |
1041
1243
  | ------------- | --------------------- | -------------------------------------------------- |
1042
- | `actions` | The `lb-action` value | Anything the page chooses to declare |
1043
- | `crud` | A query name | The three reserved `lb-action` values |
1244
+ | `handlers` | The request name | The page's declared requests |
1245
+ | `crud` | A query name | The three requests Loadbare provides |
1044
1246
  | `onPageEnter` | Nothing | Runs once on entering the page, before its queries |
1045
1247
 
1248
+ The server resolves a declared request against `handlers`, and a request
1249
+ Loadbare provides against `crud` under the request's query. A name with no
1250
+ entry there logs a server console warning naming the page and the missing
1251
+ entry, and answers 200 with no response items, so nothing lands and
1252
+ `lb-request-pending` clears from the element that issued the request. A
1253
+ request carrying no name answers 400, and a `run` that throws answers 500.
1046
1254
 
1047
- The hub resolves an `lb-action` value against `actions`, and a reserved
1048
- operation against `crud` under the bound list name. A name with no entry
1049
- there logs a server console warning naming the page and the missing entry,
1050
- and answers 200 with `{}`. The browser applies `{}`, so no cell is set and
1051
- no row is added or removed, and `lb-pending` clears from the element that
1052
- sent the request. A request carrying no action answers 400, and a `run`
1053
- that throws answers 500.
1054
-
1055
- Every entry under `actions` and `crud` has the same two members. `run`
1056
- performs the work, and `refresh` names the queries to re-run once the action
1057
- is complete.
1255
+ Every handler under `handlers` and `crud` has the same two members. `run`
1256
+ performs the work, and `refresh` names the queries to re-run once it has.
1058
1257
 
1059
- `run` receives the same `ctx` and a `where`: the binding in scope when the
1060
- interaction happened, as `list`, `row`, `key`, `cell` and `value`. Under
1061
- `crud` the `where` carries only what that operation is typed to carry.
1258
+ `run` receives the same `ctx` and the request less its name: `query`, `key`
1259
+ and `values`, as present.
1062
1260
 
1063
- `run` may also return results of its own, which are laid over the refreshed
1064
- ones. That is how a delta reaches the browser: wrap it in `patch()`, naming
1065
- the rows that arrived or changed and the keys that went.
1261
+ `run` may also return answers of its own, by query name, which are laid over
1262
+ the refreshed ones. That is how a delta reaches the browser: wrap it in
1263
+ `patch()`, naming the rows that arrived or changed and the keys that went.
1066
1264
 
1067
- `run` may instead return `queryParms()`, naming query parms only the write
1068
- can know, such as the key of a row it inserted. The page then loads at them
1069
- in the same round trip, in place of the refresh set — see
1070
- [When a server response changes the query parms](#when-a-server-response-changes-the-query-parms).
1071
- A `rowDelete` that removes the row on screen returns `queryParms({ acct: "" })`
1265
+ `run` may instead return `url()`, a new row for `lb-url`, naming query parms
1266
+ only the write can know, such as the key of a row it inserted. The page
1267
+ then loads at them in the same round trip, in place of the refresh set — see
1268
+ [When a server response moves the URL](#when-a-server-response-moves-the-url).
1269
+ A `rowDelete` that removes the row on screen returns `url({ acct: "" })`
1072
1270
  and leaves any other delete to its refresh set.
1073
1271
 
1074
1272
  ```ts
@@ -1076,7 +1274,7 @@ and leaves any other delete to its refresh set.
1076
1274
  import { patch, type Requests } from "@loadbare/app/server";
1077
1275
 
1078
1276
  export const requests: Requests = {
1079
- actions: {
1277
+ handlers: {
1080
1278
  resetRoster: {
1081
1279
  run: (ctx) => ctx.db.resetMembers(),
1082
1280
  refresh: ["roster"],
@@ -1085,9 +1283,9 @@ export const requests: Requests = {
1085
1283
  crud: {
1086
1284
  roster: {
1087
1285
  rowDelete: {
1088
- run: async (ctx, where) => {
1089
- await ctx.db.deleteMember(where.key);
1090
- return { roster: patch({ drop: [where.key] }) };
1286
+ run: async (ctx, { key }) => {
1287
+ await ctx.db.deleteMember(key);
1288
+ return { roster: patch({ drop: [key] }) };
1091
1289
  },
1092
1290
  refresh: [],
1093
1291
  },
@@ -1096,86 +1294,118 @@ export const requests: Requests = {
1096
1294
  };
1097
1295
  ```
1098
1296
 
1099
- Each key under a `crud` entry is a reserved `lb-action` value with its prefix
1100
- stripped and the rest camel-cased, so the attribute, the wire and this key
1101
- are one vocabulary. All three operate on a list, because each needs a key and
1102
- a key exists only on a live row.
1297
+ Each key under a `crud` entry is a request name with its prefix stripped and
1298
+ the rest camel-cased, so the attribute, the wire and this key are one
1299
+ vocabulary. A query with no `crud` entry permits none of them.
1103
1300
 
1104
- | `lb-action` | Key under `crud` | `where` carries |
1105
- | ---------------- | ---------------- | ---------------------- |
1106
- | `lb-row-delete` | `rowDelete` | `key` |
1107
- | `lb-row-insert` | `rowInsert` | `values` |
1108
- | `lb-row-update` | `rowUpdate` | `key`, `values` |
1301
+ | Request name | Key under `crud` | `run` receives |
1302
+ | --------------- | ---------------- | --------------- |
1303
+ | `lb-row-insert` | `rowInsert` | `values` |
1304
+ | `lb-row-update` | `rowUpdate` | `key`, `values` |
1305
+ | `lb-row-delete` | `rowDelete` | `key` |
1109
1306
 
1110
1307
  A `rowUpdate` sets the columns `values` names and leaves the rest as they
1111
- are, as an SQL `UPDATE` does. A form sends the cells it holds and a widget
1112
- cell sends itself, so one handler answers both.
1308
+ are, as an SQL `UPDATE` does. A form sends the controls it holds and a
1309
+ control carrying `lb-request` sends itself, so one handler answers both.
1113
1310
 
1114
- ## Widgets
1311
+ ### The wire
1115
1312
 
1116
1313
  ---- UNEDITED ----
1117
1314
 
1118
- A widget is an HTML custom element following these fixed rules:
1119
- - Element name must contain a hyphen, as per hTML rules, like <my-custom-element>
1120
- - HTML code, if present, is `my-custom-element.html`
1121
- - TS code, if present, is in `my-custom-element.browser.ts`. The
1122
- 'browser' segment is a safety feature, requiring the file to be
1123
- explicitly named as a browser file, to help prevent unfortunate
1124
- naming collisions where a server file happens to have the name of
1125
- a widget and gets built into the browser bundle.
1315
+ One path, one method. The hub sends every round trip as
1316
+ `POST /lb/<stub>?<query string>`, with the query string the browser is
1317
+ showing. An empty body asks for the page load. Any other body is a
1318
+ request:
1126
1319
 
1127
- A widget must have either one or the other of HTML and Typescript, and
1128
- it may have both. If it has neither, the builder reports an error.
1320
+ ```ts
1321
+ interface HubRequest {
1322
+ name: string;
1323
+ query?: string;
1324
+ key?: string;
1325
+ values?: Record<string, string>;
1326
+ }
1327
+ ```
1129
1328
 
1130
- To use a widget from a library, add the library to `imports.ts` anywhere
1131
- in the builders `--src`:
1329
+ Every response is an array of response items:
1132
1330
 
1133
1331
  ```ts
1134
- export default ["@scope/library-name"];
1332
+ interface ResponseItem {
1333
+ query: string;
1334
+ kind: "row" | "rows";
1335
+ key: string;
1336
+ row?: Row;
1337
+ rows?: Row[];
1338
+ patch?: { rows?: Row[]; drop?: unknown[] };
1339
+ }
1135
1340
  ```
1136
1341
 
1137
- ### Widget authoring
1342
+ A response item carries exactly one of `row`, `rows` or `patch`. A patch
1343
+ answers a `rows` query only: rows named in `rows` are added or updated, keys
1344
+ in `drop` are removed, and every other row is left alone.
1345
+
1346
+ `createHub(pages)` builds the `Hub`, which has two calls, both answering
1347
+ with response items: `dataForPage(page, ctx)` for a page load, and
1348
+ `runRequest(page, request, ctx)` for a request.
1349
+
1350
+ ## Custom elements
1138
1351
 
1139
1352
  ---- UNEDITED ----
1140
1353
 
1141
- The expansion grammar, `exp-`, `lb-slot`, `lb-template`, the reserved `LB-`
1142
- namespace, then `lbPlaceRow` and `lbRowsLanded`. One bucket spanning build
1143
- time and run time, because a widget is a definition and a script together.
1354
+ A custom element is an element defined by `customElements.define`, and
1355
+ follows these fixed rules in a Loadbare/app application:
1356
+ - Its name contains a hyphen, as HTML requires, like `<my-custom-element>`.
1357
+ - Its element file, if present, is `my-custom-element.html`.
1358
+ - Its script, if present, is `my-custom-element.browser.ts`. The
1359
+ `browser` segment is a safety feature, requiring the file to be
1360
+ explicitly named as a browser file, to help prevent unfortunate
1361
+ naming collisions where a server file happens to have the name of
1362
+ a custom element and gets built into the browser bundle.
1144
1363
 
1145
- #### Request state
1364
+ A custom element must have one or the other of an element file and a
1365
+ script, and it may have both. If it has neither, the builder reports an
1366
+ error.
1367
+
1368
+ To use custom elements from a widget library, add the library to
1369
+ `imports.ts` anywhere in the builder's `--src`:
1370
+
1371
+ ```ts
1372
+ export default ["@scope/library-name"];
1373
+ ```
1374
+
1375
+ Import every attribute name from `@loadbare/app/constants` — see
1376
+ [Constants](#constants). Never write one as a string literal.
1146
1377
 
1147
- The hub stamps these on the element that dispatched a request, which is the
1148
- widget itself when a widget fired it. A widget observes them and reacts; it
1149
- must name them in `observedAttributes` to see them change.
1378
+ ### Controls
1150
1379
 
1151
- | Attribute | Written on | Holds |
1152
- | -------------- | ----------------------- | --------------------------------------------------------------------------------------- |
1153
- | `lb-pending` | The dispatching element | The round trip is in flight |
1154
- | `lb-error` | The dispatching element | The last round trip failed, cleared on the next — see [The round trip](#the-round-trip) |
1155
- | `lb-row-count` | A list scope | How many rows the scope is showing |
1380
+ A form-associated custom element with a `value` property that fires
1381
+ `change` is a control. The hub sets its `value` when a column lands, and
1382
+ gathers its `value` when a request is issued, exactly as for a native
1383
+ `<input>`. It commits on `change`, and belongs to its form owner, which the
1384
+ browser tracks and the `form` attribute names.
1156
1385
 
1157
- They are also stamped on plain HTML, where a stylesheet is the only
1158
- consumer: dim a pending button, mark a failed one, and style an empty list
1159
- against `lb-row-count` rather than carrying an empty-state element.
1386
+ ```ts
1387
+ class NoteField extends HTMLElement {
1388
+ static formAssociated = true;
1389
+ // a `value` property, and a `change` event when the edit commits
1390
+ }
1391
+ ```
1160
1392
 
1161
- The hub reads one of them. A native `lb-action` element, a button or a
1162
- form, that is performed again while it carries `lb-pending` is ignored: the
1163
- click or submit is cancelled and nothing is sent. A pending native action is
1164
- therefore disabled in fact, and a stylesheet only has to show it. A widget is
1165
- not held back this way, because a widget that sends on change must have its
1166
- latest value sent rather than dropped.
1393
+ A custom element that is not a control receives `lb-column-value` and
1394
+ renders it; its content is never replaced.
1167
1395
 
1168
- Alongside `lb-pending` the hub sets `aria-busy="true"` on the same element and
1169
- removes it when the round trip settles, so assistive technology hears the
1170
- state a stylesheet shows.
1396
+ ### Row hooks
1171
1397
 
1172
- #### Row hooks
1398
+ | Method | Implemented By | The hub calls it |
1399
+ | ------------------------------- | -------------- | -------------------------------------------------- |
1400
+ | `lbPlaceRow(el, row, template)` | Developer | To place a live row, detached, on its first arrival and whenever all rows decide the order |
1401
+ | `lbRowsLanded()` | Developer | After the rows land |
1173
1402
 
1174
- | Name | Implemented By | Behavior |
1175
- | ------------------------------- | -------------- | ----------------------------------------------------------- |
1176
- | `applyRow(root, row)` | Loadbare | Fills one scope from one row |
1177
- | `lbPlaceRow(el, row, template)` | Developer | Optional on a list scope: where a row goes |
1178
- | `lbRowsLanded()` | Developer | Optional on a list scope: scaffolding derived from the rows |
1403
+ The hub calls both on a custom element with `lb-query` and a row template.
1404
+ Both are optional. The `RowsHost` interface in `@loadbare/app/types`
1405
+ declares them.
1406
+
1407
+ `applyRow(root, row)`, exported by `@loadbare/app`, fills `root` from one
1408
+ row, the same operation that fills a live row.
1179
1409
 
1180
1410
  ## Internal linkage
1181
1411
 
@@ -1209,16 +1439,16 @@ Loadbare/app owns every file name and pattern in this table. A file so
1209
1439
  named carries its meaning wherever it sits in the `--src` tree, so an
1210
1440
  application must not use one of these names for anything else.
1211
1441
 
1212
- | File | How many | Holds | Defined in |
1213
- | ----------------------- | ---------------------- | -------------------------------------------------------- | ------------------------------------------------------- |
1214
- | `chrome.html` | Exactly one | The application's one HTML document | [Chrome](#chrome) |
1215
- | `imports.ts` | Zero or one | Default-exports an array of widget library package names | [Imported widget libraries](#imported-widget-libraries) |
1216
- | `<stub>.page.html` | One per page | One page's markup, as a fragment | [Pages](#pages) |
1217
- | `<stub>.queries.ts` | Zero or one per page | That page's queries | [Page Queries](#page-queries) |
1218
- | `<stub>.requests.ts` | Zero or one per page | That page's requests | [Page Requests](#page-requests) |
1219
- | `<tag-name>.html` | Zero or one per widget | One widget definition, as a fragment | [Widgets](#widgets) |
1220
- | `<tag-name>.browser.ts` | Zero or one per widget | One widget's script, `.js` also accepted | [Widgets](#widgets) |
1221
- | `*.css` | Any number | A stylesheet, concatenated into `app.css` | [Running the builder](#running-the-builder) |
1442
+ | File | How many | Holds | Defined in |
1443
+ | ----------------------- | ------------------------------ | -------------------------------------------------------- | ------------------------------------------------------- |
1444
+ | `chrome.html` | Exactly one | The application's one HTML document | [Chrome](#chrome) |
1445
+ | `imports.ts` | Zero or one | Default-exports an array of widget library package names | [Imported widget libraries](#imported-widget-libraries) |
1446
+ | `<stub>.page.html` | One per page | One page's markup, as a fragment | [Pages](#pages) |
1447
+ | `<stub>.queries.ts` | Zero or one per page | That page's queries | [Page Queries](#page-queries) |
1448
+ | `<stub>.requests.ts` | Zero or one per page | That page's requests | [Page Requests](#page-requests) |
1449
+ | `<tag-name>.html` | Zero or one per custom element | One element file, as a fragment | [Custom elements](#custom-elements) |
1450
+ | `<tag-name>.browser.ts` | Zero or one per custom element | One custom element's script, `.js` also accepted | [Custom elements](#custom-elements) |
1451
+ | `*.css` | Any number | A stylesheet, concatenated into `app.css` | [Running the builder](#running-the-builder) |
1222
1452
 
1223
1453
  ### Build outputs
1224
1454
 
@@ -1230,7 +1460,7 @@ only when some origin has a stylesheet, `pages.ts` only when some page has a
1230
1460
  | Path | Written | Holds | Read by | Defined in |
1231
1461
  | ------------------ | ----------------- | ------------------------------------------------------------------ | ---------- | ------------------------------------------- |
1232
1462
  | `/app.html` | Always | The chrome, built | The server | [Chrome](#chrome) |
1233
- | `/client.js` | Always | `<lb-hub>` and every widget the application uses | The chrome | [Chrome](#chrome) |
1463
+ | `/client.js` | Always | `<lb-hub>` and every custom element the application uses | The chrome | [Chrome](#chrome) |
1234
1464
  | `/client-entry.ts` | Always | The generated entry `client.js` is bundled from | Nothing | [Running the builder](#running-the-builder) |
1235
1465
  | `/app.css` | With a stylesheet | Every stylesheet in the cascade, concatenated | The chrome | [Chrome](#chrome) |
1236
1466
  | `/pages.ts` | With page data | The one `createHub()` call, over every page's queries and requests | The server | [The Express server](#the-express-server) |
@@ -1240,57 +1470,64 @@ only when some origin has a stylesheet, `pages.ts` only when some page has a
1240
1470
  Loadbare/app owns every key in this table. A widget library declares them.
1241
1471
  An application never does.
1242
1472
 
1243
- | Key | Value | Means | Defined in |
1244
- | ------------------ | ------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------- |
1245
- | `loadbare.widgets` | A path inside the package | Where this package's widgets are. Absent means the whole installed directory | [Imported widget libraries](#imported-widget-libraries) |
1473
+ | Key | Value | Means | Defined in |
1474
+ | ------------------ | ------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------- |
1475
+ | `loadbare.widgets` | A path inside the package | Where this package's custom elements are. Absent means the whole installed directory | [Imported widget libraries](#imported-widget-libraries) |
1246
1476
 
1247
1477
  ### Reserved namespaces
1248
1478
 
1249
1479
  Loadbare/app owns every namespace in this table. Ownership of a name and
1250
1480
  ownership of its behavior are stated separately, because they differ.
1251
1481
 
1252
- | Namespace | Applies to | Loadbare owns | Defined in |
1253
- | --------- | -------------------------- | -------------------------------------------- | ----------------------------------------------- |
1254
- | `lb-*` | HTML attributes | The names and their behavior | [The lb-* namespace](#the-lb--namespace) |
1255
- | `exp-*` | HTML attributes | The behavior; widget authors pick the values | [Build time parameters](#build-time-parameters) |
1256
- | `lb*` | Methods on custom elements | The names and their behavior | [Row hooks](#row-hooks) |
1482
+ | Namespace | Applies to | Loadbare owns | Defined in |
1483
+ | --------- | ----------------------------------------------------------- | --------------------------------------------------- | ----------------------------------------------- |
1484
+ | `lb-*` | Attributes, their values, queries, columns of `lb-` queries, requests, custom elements, events | The names and their behavior | [The lb-* namespace](#the-lb--namespace) |
1485
+ | `exp-*` | Attributes on a custom element | The behavior; element file authors pick the names | [Build time parameters](#build-time-parameters) |
1486
+ | `lb*` | Methods on custom elements | The names and their behavior | [Row hooks](#row-hooks) |
1257
1487
 
1258
1488
  #### The lb-* namespace
1259
1489
 
1260
- Every HTML attribute beginning with `lb-` belongs to Loadbare/app. An
1261
- application writes the ones this table names and invents none of its own,
1262
- because a name Loadbare has not defined today it may define tomorrow.
1263
-
1264
- The prefix reaches past HTML. A query name cannot begin with `lb-` either,
1265
- which is what keeps the three operations apart from an application's own
1266
- actions on the wire — see [Page Queries](#page-queries).
1267
-
1268
- Each attribute is defined in one section, and this table says which.
1269
-
1270
- | Attribute | Written by | Defined in |
1271
- | ----------------- | ------------- | --------------------------------------------------- |
1272
- | `lb-list` | Developer | [Binding](#binding) |
1273
- | `lb-row` | Developer | [Binding](#binding) |
1274
- | `lb-cell` | Developer | [Binding](#binding) |
1275
- | `lb-show` | Developer | [Displaying by condition](#displaying-by-condition) |
1276
- | `lb-key` | Developer | [Binding](#binding) |
1277
- | `lb-key-value` | Hub | [Binding](#binding) |
1278
- | `lb-value` | Hub | [How a value lands](#how-a-value-lands) |
1279
- | `lb-action` | Developer | [Requests](#requests) |
1280
- | `lb-nav-link` | Developer | [Links](#links) |
1281
- | `lb-query-parm` | Developer | [Query parms](#query-parms) |
1282
- | `lb-query-parm-push` | Developer | [Query parms](#query-parms) |
1283
- | `lb-pending` | Hub | [Request state](#request-state) |
1284
- | `lb-error` | Hub | [Request state](#request-state) |
1285
- | `lb-row-count` | Hub | [Request state](#request-state) |
1286
- | `lb-unknown-page` | Developer | [Chrome](#chrome) |
1287
- | `lb-slot` | Widget author | [Slots and templates](#slots-and-templates) |
1288
- | `lb-template` | Widget author | [Slots and templates](#slots-and-templates) |
1289
- | `lb-page` | Builder | [Pages](#pages) |
1290
-
1291
- An attribute the builder does not recognize is left alone today. Refusing
1292
- one is on the list of validations still to land — see
1293
- [The builder](#the-builder).
1490
+ Every name beginning with `lb-` belongs to Loadbare/app — see
1491
+ [Names](#names). An application writes the attributes this section names,
1492
+ and invents none of its own, because a name Loadbare has not defined today
1493
+ it may define tomorrow.
1494
+
1495
+ | Attribute | Written by | On | Defined in |
1496
+ | ----------------- | ------------- | ----------------------------------- | --------------------------------------------- |
1497
+ | `lb-query` | Developer | Any element | [Landing](#landing) |
1498
+ | `lb-column` | Developer | Any element | [Landing](#landing) |
1499
+ | `lb-show` | Developer | Any element but a `<template>` | [Displaying by condition](#displaying-by-condition) |
1500
+ | `lb-request` | Developer | Any element | [Requests](#requests) |
1501
+ | `lb-url-link` | Developer | An `<a>` | [Links](#links) |
1502
+ | `lb-url-push` | Developer | An element with `lb-request` | [Query parms](#query-parms) |
1503
+ | `lb-url-unknown` | Developer | A `<dialog>` inside `<lb-hub>` | [An unknown page](#an-unknown-page) |
1504
+ | `lb-exp-slot` | Developer | One element in an element file | [Slots and templates](#slots-and-templates) |
1505
+ | `lb-exp-template` | Developer | An element file, and a `<template>` | [Slots and templates](#slots-and-templates) |
1506
+ | `lb-column-value` | Hub | Every element set from a column | [How a column lands](#how-a-column-lands) |
1507
+ | `lb-key-value` | Hub | A live row, and an element a `row` lands on | [Rows](#rows) |
1508
+ | `lb-query-row-count` | Hub | An element with a row template | [Rows](#rows) |
1509
+ | `lb-request-pending` | Hub | The element that issued a request | [Request state](#request-state) |
1510
+ | `lb-request-error` | Hub | The element that issued a request | [Request state](#request-state) |
1511
+ | `lb-show` on a `<template>` | Hub and builder | Where an absent element stands | [Displaying by condition](#displaying-by-condition) |
1512
+ | `lb-page` | Builder | A page's `<template>` | [Page HTML](#page-html) |
1513
+ | `lb-page-title` | Builder | A page's `<template>` | [Page HTML](#page-html) |
1514
+
1515
+ The builder refuses any other `lb-` attribute in markup — see
1516
+ [The markup checks](#the-markup-checks).
1517
+
1518
+ ### Reserved queries
1519
+
1520
+ | Query | Kind | Key | Columns | Defined in |
1521
+ | -------- | ----- | --------- | --------------------------------------------------------------- | ------------------- |
1522
+ | `lb-url` | `row` | `lb-path` | `lb-path`, `lb-page-label`, `lb-page-unknown`, every query parm | [The URL](#the-url) |
1523
+
1524
+ ### Reserved request names
1525
+
1526
+ | Request name | Runs under `crud` | Defined in |
1527
+ | --------------- | ----------------- | ----------------------------------- |
1528
+ | `lb-row-insert` | `rowInsert` | [The request names](#the-request-names) |
1529
+ | `lb-row-update` | `rowUpdate` | [The request names](#the-request-names) |
1530
+ | `lb-row-delete` | `rowDelete` | [The request names](#the-request-names) |
1294
1531
 
1295
1532
  ### Reserved tags
1296
1533
 
@@ -1306,16 +1543,53 @@ never defines them.
1306
1543
  Loadbare/app owns every DOM event in this table, both the name and what its
1307
1544
  `detail` carries.
1308
1545
 
1309
- | Event | Dispatched from | Bubbles | Cancelable | Defined in |
1310
- | ------------ | ---------------------- | ------- | ---------- | --------------------------------------- |
1311
- | `lb-request` | The element that acted | Yes | No | [The request event](#the-request-event) |
1312
-
1313
- ### Reserved attributes
1314
-
1315
- Loadbare/app owns every attribute in this table, both the name and its
1316
- behavior.
1317
-
1318
- | Attribute | Written on | Takes a value | Defined in |
1319
- | ----------------- | ----------------------------------- | ------------- | ----------------- |
1320
- | `lb-unknown-page` | A `<dialog>` in chrome | No | [Chrome](#chrome) |
1321
- | `lb-page` | A `<template>`, by the builder only | Yes | [Pages](#pages) |
1546
+ | Event | Dispatched from | Bubbles | Cancelable | Defined in |
1547
+ | ------------ | -------------------------------- | ------- | ---------- | --------------------------------------- |
1548
+ | `lb-request` | The element that committed | Yes | No | [The request event](#the-request-event) |
1549
+
1550
+ ### Reserved methods
1551
+
1552
+ | Method | On | Defined in |
1553
+ | -------------- | -------------------------------------------------- | ----------------------- |
1554
+ | `lbPlaceRow` | A custom element with `lb-query` and a row template | [Row hooks](#row-hooks) |
1555
+ | `lbRowsLanded` | A custom element with `lb-query` and a row template | [Row hooks](#row-hooks) |
1556
+
1557
+ ### Constants
1558
+
1559
+ `@loadbare/app/constants` exports every name above. Custom element code
1560
+ imports them rather than writing a string.
1561
+
1562
+ | Constant | Value |
1563
+ | ------------------------- | --------------------------------------- |
1564
+ | `ATTR_QUERY` | `lb-query` |
1565
+ | `ATTR_COLUMN` | `lb-column` |
1566
+ | `ATTR_SHOW` | `lb-show` |
1567
+ | `ATTR_COLUMN_VALUE` | `lb-column-value` |
1568
+ | `ATTR_KEY_VALUE` | `lb-key-value` |
1569
+ | `ATTR_QUERY_ROW_COUNT` | `lb-query-row-count` |
1570
+ | `ATTR_REQUEST` | `lb-request` |
1571
+ | `ATTR_REQUEST_PENDING` | `lb-request-pending` |
1572
+ | `ATTR_REQUEST_ERROR` | `lb-request-error` |
1573
+ | `ATTR_URL_LINK` | `lb-url-link` |
1574
+ | `ATTR_URL_PUSH` | `lb-url-push` |
1575
+ | `ATTR_URL_UNKNOWN` | `lb-url-unknown` |
1576
+ | `ATTR_EXP_SLOT` | `lb-exp-slot` |
1577
+ | `ATTR_EXP_TEMPLATE` | `lb-exp-template` |
1578
+ | `ATTR_PAGE` | `lb-page` |
1579
+ | `ATTR_PAGE_TITLE` | `lb-page-title` |
1580
+ | `DEVELOPER_ATTRIBUTES` | The seven attributes a developer writes |
1581
+ | `LB_EVENT_NAME` | `lb-request` |
1582
+ | `LB_RESERVED_PREFIX` | `lb-` |
1583
+ | `REQUEST_ROW_INSERT` | `lb-row-insert` |
1584
+ | `REQUEST_ROW_UPDATE` | `lb-row-update` |
1585
+ | `REQUEST_ROW_DELETE` | `lb-row-delete` |
1586
+ | `LB_ROW_REQUESTS` | All three request names |
1587
+ | `KIND_ROW` | `row` |
1588
+ | `KIND_ROWS` | `rows` |
1589
+ | `URL_QUERY` | `lb-url` |
1590
+ | `URL_COLUMN_PATH` | `lb-path` |
1591
+ | `URL_COLUMN_PAGE_LABEL` | `lb-page-label` |
1592
+ | `URL_COLUMN_PAGE_UNKNOWN` | `lb-page-unknown` |
1593
+ | `HUB_TAG_NAME` | `lb-hub` |
1594
+ | `LB_ENDPOINT` | `/lb` |
1595
+ | `LB_REQUEST_TIMEOUT_MS` | `10000` |