@loadbare/app 0.5.5 → 0.6.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 (81) hide show
  1. package/README.md +3 -4
  2. package/dist/build/assemble.d.ts +1 -1
  3. package/dist/build/assemble.d.ts.map +1 -1
  4. package/dist/build/assemble.js +80 -7
  5. package/dist/build/cli.d.ts +2 -2
  6. package/dist/build/cli.js +2 -2
  7. package/dist/build/expand.d.ts.map +1 -1
  8. package/dist/build/expand.js +30 -34
  9. package/dist/build/locations.d.ts +3 -3
  10. package/dist/build/locations.d.ts.map +1 -1
  11. package/dist/build/locations.js +15 -3
  12. package/dist/build/origins.d.ts +0 -13
  13. package/dist/build/origins.d.ts.map +1 -1
  14. package/dist/build/origins.js +32 -8
  15. package/dist/build/pages.d.ts +3 -3
  16. package/dist/build/pages.d.ts.map +1 -1
  17. package/dist/build/pages.js +9 -7
  18. package/dist/core/lb-constants.d.ts +17 -12
  19. package/dist/core/lb-constants.d.ts.map +1 -1
  20. package/dist/core/lb-constants.js +106 -43
  21. package/dist/core/lb-types.d.ts +103 -62
  22. package/dist/core/lb-types.d.ts.map +1 -1
  23. package/dist/core/lb-types.js +11 -3
  24. package/dist/hub/lb-apply.d.ts +18 -4
  25. package/dist/hub/lb-apply.d.ts.map +1 -1
  26. package/dist/hub/lb-apply.js +184 -34
  27. package/dist/hub/lb-hub.browser.d.ts +1 -1
  28. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  29. package/dist/hub/lb-hub.browser.js +165 -83
  30. package/dist/server/lb-express.d.ts +7 -4
  31. package/dist/server/lb-express.d.ts.map +1 -1
  32. package/dist/server/lb-express.js +45 -33
  33. package/dist/server/lb-server.d.ts +67 -45
  34. package/dist/server/lb-server.d.ts.map +1 -1
  35. package/dist/server/lb-server.js +55 -18
  36. package/dist/tests/assemble.test.js +154 -2
  37. package/dist/tests/expand.test.js +30 -3
  38. package/dist/tests/helpers/hub.d.ts +73 -0
  39. package/dist/tests/helpers/hub.d.ts.map +1 -0
  40. package/dist/tests/helpers/hub.js +151 -0
  41. package/dist/tests/lb-apply.test.js +86 -62
  42. package/dist/tests/lb-express.test.d.ts +1 -1
  43. package/dist/tests/lb-express.test.js +43 -38
  44. package/dist/tests/lb-hub.test.d.ts +14 -0
  45. package/dist/tests/lb-hub.test.d.ts.map +1 -0
  46. package/dist/tests/lb-hub.test.js +319 -0
  47. package/dist/tests/{lb-rows.test.d.ts → lb-list.test.d.ts} +1 -1
  48. package/dist/tests/lb-list.test.d.ts.map +1 -0
  49. package/dist/tests/{lb-rows.test.js → lb-list.test.js} +109 -106
  50. package/dist/tests/lb-server.test.js +151 -100
  51. package/dist/tests/origins.test.js +19 -1
  52. package/dist/tests/pages.test.d.ts +1 -1
  53. package/dist/tests/pages.test.js +64 -14
  54. package/docs/TECHREF-1.0.md +1000 -0
  55. package/docs/reference/builder.md +4 -4
  56. package/docs/reference/chrome.md +56 -5
  57. package/docs/reference/custom-elements.md +59 -36
  58. package/docs/reference/data-binding.md +142 -84
  59. package/docs/reference/overview.md +1 -1
  60. package/docs/reference/page-files.md +64 -49
  61. package/docs/reference/server.md +7 -6
  62. package/docs/reference/widgets.md +43 -32
  63. package/docs/roadmap.md +68 -22
  64. package/docs/testing.md +47 -17
  65. package/docs/theory.md +2 -2
  66. package/docs/tutorials/010-pages-and-navigation.md +14 -8
  67. package/docs/tutorials/040-displaying-data.md +9 -9
  68. package/docs/tutorials/050-actions.md +5 -5
  69. package/docs/tutorials/060-custom-element-code.md +1 -1
  70. package/docs/tutorials/065-conditional-rendering.md +4 -4
  71. package/docs/tutorials/070-displaying-a-list.md +24 -47
  72. package/docs/tutorials/072-inserting-into-a-list.md +17 -17
  73. package/docs/tutorials/074-deleting-from-a-list.md +15 -17
  74. package/docs/tutorials/076-updating-a-list-item.md +20 -22
  75. package/docs/tutorials/080-widget-requests.md +27 -23
  76. package/docs/tutorials/090-using-widget-libraries.md +23 -1
  77. package/package.json +1 -2
  78. package/dist/hub/lb-rows.d.ts +0 -18
  79. package/dist/hub/lb-rows.d.ts.map +0 -1
  80. package/dist/hub/lb-rows.js +0 -106
  81. package/dist/tests/lb-rows.test.d.ts.map +0 -1
@@ -40,7 +40,7 @@ The builder classifies by name, not location.
40
40
  |----------------------------|---------------------------------------------------|
41
41
  | `chrome.html` | The chrome — see [`chrome.html`](./chrome.md) |
42
42
  | `*.page.html` | A page |
43
- | `*.hooks.ts` | A page's hooks, matched by base name |
43
+ | `*.requests.ts` | A page's requests, matched by base name |
44
44
  | `*.queries.ts` | A page's queries, matched by base name |
45
45
  | `imports.ts` | The packages this app takes widgets from |
46
46
  | `*.css` | A stylesheet — see [CSS](./css.md) |
@@ -48,7 +48,7 @@ The builder classifies by name, not location.
48
48
  | `<tag>.browser.ts` | A widget script, named for the tag it registers |
49
49
 
50
50
  Give the application exactly one `chrome.html` and at most one `imports.ts`.
51
- Give every `.hooks.ts` and `.queries.ts` a `.page.html` of the same base name.
51
+ Give every `.requests.ts` and `.queries.ts` a `.page.html` of the same base name.
52
52
 
53
53
  Name a widget script `<tag>.browser.ts`, not `<tag>.ts`. Only a file whose
54
54
  name carries `.browser` is bundled for the browser; every other module under
@@ -125,10 +125,10 @@ A tag with neither a script nor a definition in any origin is an error.
125
125
  The builder expands the chrome and every page against the available widget
126
126
  definitions — see [Custom Elements](./custom-elements.md#html) for the
127
127
  substitution rules — wraps each expanded page in
128
- `<template id="page-<name>">`, and splices them into the chrome's `<body>`. It formats the result with
128
+ `<template lb-page="<name>">`, and splices them into the chrome's `<body>`. It formats the result with
129
129
  Prettier when the application has it installed.
130
130
 
131
131
  The builder writes `app.css` only when it finds a stylesheet, and `pages.ts`
132
- only when some page has a `.hooks.ts` or a `.queries.ts` file. Import `hub`
132
+ only when some page has a `.requests.ts` or a `.queries.ts` file. Import `hub`
133
133
  from `pages.ts` — see [The Express Server](./server.md) for the rest of the
134
134
  wiring.
@@ -24,15 +24,18 @@ Here is a minimal but fully complaint chrome for a typical app:
24
24
  </head>
25
25
  <body hidden>
26
26
  <lb-hub>
27
- <header><h1>Membership Roster</h1></header>
27
+ <header lb-row="lb-navigation">
28
+ <h1>Membership Roster</h1>
29
+ <h2 lb-cell="page-label"></h2>
30
+ </header>
28
31
  <nav>
29
32
  <a href="/" lb-nav-link>Home</a>
30
33
  <a href="/members" lb-nav-link>Members</a>
31
34
  <a href="https://example.org/">Our website</a>
32
35
  </nav>
33
36
  <main></main>
34
- <dialog lb-unknown-page>
35
- The page <span lb-cell="page"></span> is not in this app.
37
+ <dialog lb-unknown-page lb-row="lb-navigation">
38
+ The URL <span lb-cell="page-uri"></span> is not in this app.
36
39
  </dialog>
37
40
  </lb-hub>
38
41
  </body>
@@ -57,6 +60,7 @@ Everything else is optional:
57
60
  |-------------------------------------------|---------------------------------------------|
58
61
  | `<link rel="stylesheet" href="/app.css">` | The bundled stylesheet |
59
62
  | `<a lb-nav-link>` | Navigation between pages |
63
+ | `lb-row="lb-navigation"` | Where the page is — see below |
60
64
  | `<dialog lb-unknown-page>` | A message when a URL matches no page |
61
65
  | Custom elements | The chrome, decomposed into widget files |
62
66
  | `<body hidden>` + a `<noscript>` fallback | Avoids a first-load blink — see below |
@@ -73,8 +77,55 @@ A navigation anchor's `href` is a path, and the path names a page:
73
77
  landing page is the one named `index.page.html`. An anchor without
74
78
  `lb-nav-link` is left alone and behaves like any other link.
75
79
 
76
- The `lb-unknown-page` attribute, if used, must appear on a `<dialog>`.
77
- Inside it, `lb-cell="page"` shows the name of the page that was asked for.
80
+ The `lb-unknown-page` attribute, if used, must appear on a `<dialog>` inside
81
+ `<lb-hub>`; the builder rejects it anywhere else. The hub opens it when a
82
+ path names no page. What it says is up to the chrome:
83
+ the dialog is a subtree like any other, and it displays where the page is by
84
+ naming the hub's own query, described next.
85
+
86
+ ## Where the page is
87
+
88
+ On every navigation the hub lands a query of its own, `lb-navigation`, on
89
+ any subtree inside the hub that names it. It arrives the way a server's
90
+ row arrives — `lb-row` on the subtree, `lb-cell` on each element that
91
+ shows a value — so a chrome displays the current page with no code at all:
92
+
93
+ ```html
94
+ <header lb-row="lb-navigation">
95
+ <h1>Membership Roster</h1>
96
+ <h2 lb-cell="page-label"></h2>
97
+ </header>
98
+ ```
99
+
100
+ | Cell | Holds |
101
+ |--------------|-----------------------------------------------------------|
102
+ | `page-label` | The text of the `lb-nav-link` anchor for the path, or empty if none |
103
+ | `page-uri` | The path as the browser has it, such as `/members` |
104
+
105
+ The label is the nav's. The hub takes it from the first `lb-nav-link`
106
+ anchor whose `href` names the current page, so a click, a reload and the
107
+ back button all land the same text, and a path no anchor names lands an
108
+ empty label. A chrome that shows the label somewhere fixed should expect
109
+ that case for a page reachable only by URL.
110
+
111
+ The `lb-` prefix on the query name is what keeps it out of the server's
112
+ namespace: no server answers a query so named. It is landed only where a
113
+ subtree names it, so a chrome that displays no navigation is not warned
114
+ about a query with no scope.
115
+
116
+ The same row is what an unknown-page dialog has to work with. It lands
117
+ before the page host is looked up, so a miss has it too. Name the query on
118
+ the dialog and show whichever cell fits:
119
+
120
+ ```html
121
+ <dialog lb-unknown-page lb-row="lb-navigation">
122
+ The URL <span lb-cell="page-uri"></span> is not in this app.
123
+ </dialog>
124
+ ```
125
+
126
+ `@loadbare/widgets` ships this dialog as a widget, `<lb-unknown-page>`, for a
127
+ chrome that would rather write one tag — see
128
+ [The Basic Widget Library](./widgets.md#lb-unknown-page).
78
129
 
79
130
  ## Preventing the first-load blink
80
131
 
@@ -86,6 +86,14 @@ value is a placeholder is removed rather than shipped empty, which is how
86
86
  placeholder in a text node resolves to nothing, and the whitespace around
87
87
  it survives.
88
88
 
89
+ Give a placeholder a default with a pipe: `{{button-text|OK}}` reads
90
+ `exp-button-text` when the tag supplies it and `OK` when it does not. The
91
+ default is a literal, not an expression. Whitespace around the name and the
92
+ default is not part of either, so `{{ button-text | OK }}` is the same
93
+ placeholder. An empty default, `readonly="{{readonly|}}"`, is the one that
94
+ differs from no default at all: it emits the attribute, empty, rather than
95
+ dropping it, so a definition can ship a boolean attribute switched on.
96
+
89
97
  Every attribute stays on the tag after expansion, `exp-` ones included.
90
98
 
91
99
  Parameter values reach a definition through the DOM rather than through
@@ -215,12 +223,15 @@ as a string literal.
215
223
  |------------------|------------|
216
224
  | `ATTR_VALUE` | `lb-value` |
217
225
  | `ATTR_CELL` | `lb-cell` |
218
- | `ATTR_QUERY` | `lb-query` |
226
+ | `ATTR_LIST` | `lb-list` |
227
+ | `ATTR_ROW` | `lb-row` |
219
228
  | `ATTR_KEY` | `lb-key` |
220
- | `ATTR_GROUP` | `lb-group` |
221
- | `ATTR_SORT` | `lb-sort` |
229
+ | `ATTR_KEY_VALUE` | `lb-key-value` |
222
230
  | `ATTR_ACTION` | `lb-action`|
223
- | `ATTR_ROW_COUNT` | `data-rows`|
231
+ | `ACTION_ROW_INSERT`, `ACTION_ROW_DELETE`, `ACTION_ROW_UPDATE`, `ACTION_CELL_CHANGE` | the reserved `lb-action` values |
232
+ | `LB_ACTIONS` | all four of them, in one array |
233
+ | `LB_RESERVED_PREFIX` | `lb-`, the prefix every reserved name begins with |
234
+ | `ATTR_ROW_COUNT` | `lb-row-count`|
224
235
  | `LB_EVENT_NAME` | `lb-request` |
225
236
 
226
237
  ### Receiving a value
@@ -258,20 +269,20 @@ click — builds its own request and dispatches it as a bubbling
258
269
  as its `detail`:
259
270
 
260
271
  ```ts
261
- import { ATTR_CELL, ATTR_KEY, ATTR_QUERY, LB_EVENT_NAME } from "@loadbare/app/constants";
272
+ import { ATTR_CELL, ATTR_KEY_VALUE, ATTR_LIST, LB_EVENT_NAME } from "@loadbare/app/constants";
262
273
  import type { HubRequest } from "@loadbare/app/types";
263
274
 
264
275
  const detail: HubRequest = {
265
- op: "cell-change",
266
- query: this.closest(`[${ATTR_QUERY}]`)!.getAttribute(ATTR_QUERY)!,
267
- key: this.closest(`[${ATTR_KEY}]`)!.getAttribute(ATTR_KEY)!,
276
+ action: "lb-cell-change",
277
+ list: this.closest(`[${ATTR_LIST}]`)!.getAttribute(ATTR_LIST)!,
278
+ key: this.closest(`[${ATTR_KEY_VALUE}]`)!.getAttribute(ATTR_KEY_VALUE)!,
268
279
  cell: this.getAttribute(ATTR_CELL)!,
269
280
  value: input.value,
270
281
  };
271
282
  this.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
272
283
  ```
273
284
 
274
- Find `query` and `key` by walking up with `closest()`, the same way the hub
285
+ Find the scope name and `key` by walking up with `closest()`, the same way the hub
275
286
  finds the binding for a native element. See
276
287
  [Data Binding](./data-binding.md#requests) for the request vocabulary and
277
288
  what each operation carries.
@@ -280,54 +291,66 @@ Let the event bubble, so an ancestor widget can intercept and stop it before
280
291
  the hub sees it. A hand-written widget and a native element carrying
281
292
  `lb-action` produce the same event.
282
293
 
283
- ### Accepting rows
294
+ ### Decorating a list
295
+
296
+ The hub reconciles every list scope itself. A plain element carrying
297
+ `lb-list` with a row template inside it is a whole list and needs no widget,
298
+ so a widget exists only when the rows need scaffolding or placement that
299
+ only it can decide.
284
300
 
285
- A query answering with many rows is delivered to whichever element carries
286
- an `acceptRows` method. It is a protocol, not a tag name — the hub tests for
287
- the method and never for the element:
301
+ Two optional methods say what it decides. Both are named in the `lb`
302
+ namespace, which Loadbare reserves for methods it calls on classes it does
303
+ not own, so a widget's own methods can never collide with a later one:
288
304
 
289
305
  ```ts
290
- import { applyRows } from "@loadbare/app/rows";
291
- import type { Projection, HubRowHost } from "@loadbare/app/types";
306
+ import type { ListHost, Row } from "@loadbare/app/types";
292
307
 
293
- class UpdateList extends HTMLElement implements HubRowHost {
294
- acceptRows(result: Projection) {
295
- applyRows(this, result);
308
+ class SortedList extends HTMLElement implements ListHost {
309
+ lbPlaceRow(el: Element, row: Row, template: HTMLTemplateElement) {
310
+ // Where this row goes. Called with the element detached, on its first
311
+ // appearance and again whenever a whole set decides the order.
312
+ }
313
+
314
+ lbRowsLanded() {
315
+ // Once, after the result has landed. For scaffolding derived from the
316
+ // rows: a section heading, an <optgroup>, anything that goes when its
317
+ // last row does.
296
318
  }
297
319
  }
298
320
  ```
299
321
 
300
- Call `applyRows(scope, result, place?)` rather than reimplementing rows.
301
- Every built-in list widget uses it, and it settles four things:
322
+ Everything else is the hub's, and a widget never reimplements it:
302
323
 
303
- | Concern | What `applyRows` does |
324
+ | Concern | What the hub does |
304
325
  |-------------|---------------------------------------------------------|
305
- | Cloning | Clones the `<template lb-key="...">` in the widget |
326
+ | Cloning | Clones the `<template lb-key="...">` in the scope |
306
327
  | Matching | Updates the row already showing that key, or clones one |
307
328
  | Reconciling | Removes the rows the response says are gone |
308
- | Counting | Stamps `data-rows` with the number of rows showing |
329
+ | Counting | Stamps `lb-row-count` with the number of rows showing |
309
330
 
310
- `rows` is the whole set, so it decides membership and order, and a key
311
- absent from it is removed. `patch` touches only the rows it names and leaves
331
+ An array is the whole set, so it decides membership and order, and a key
332
+ absent from it is removed. A patch touches only the rows it names and leaves
312
333
  every other row's contents and position alone.
313
334
 
314
- Supply a `place` function to decide where a row goes — `(row, tuple,
315
- template) => void`, called with a fresh or reordered row. The default
316
- inserts immediately before the template, so rows accumulate in arrival
317
- order. A widget that groups or sorts supplies its own `place` instead of
318
- reimplementing matching and cloning around it; `lb-options.browser.ts` and
319
- `lb-table.browser.ts` in [`@loadbare/widgets`](./widgets.md) are two different
320
- `place` functions over the same `applyRows`.
335
+ `lbPlaceRow` is called with the row already filled and not yet in the
336
+ document, so a widget that reads a cell to decide where the row goes can. A
337
+ scope without it lands rows immediately before the template, in arrival
338
+ order. `lb-options.browser.ts` and `lb-table.browser.ts` in
339
+ [`@loadbare/widgets`](./widgets.md) are two different placements over the
340
+ same machinery.
341
+
342
+ A list scope with no row template displays nothing, which is not an error: an
343
+ insert form names the list it adds a row to and has no rows of its own.
321
344
 
322
- Style an empty list against `data-rows` rather than carrying an empty-state
345
+ Style an empty list against `lb-row-count` rather than carrying an empty-state
323
346
  conditional in the widget — see
324
347
  [Conditional rendering](./data-binding.md#conditional-rendering).
325
348
 
326
349
  ### Filling a scope by hand
327
350
 
328
- `@loadbare/app/rows` also exports `applyTuple(root, cells)`, the same
329
- tuple-landing operation a page host uses. Call it in a widget that builds
330
- its own rows or scopes rather than relying on `acceptRows`. It fills `root`
351
+ `@loadbare/app` also exports `applyRow(root, row)`, the same row-landing
352
+ operation a page host uses. Call it in a widget that builds a scope of its
353
+ own rather than one the hub reconciles. It fills `root`
331
354
  itself when `root` carries a matching `lb-cell`, and every matching
332
355
  descendant.
333
356
  </content>
@@ -1,10 +1,14 @@
1
1
  # Data Binding
2
2
 
3
3
  A page binds its elements to server data with four attributes, and asks the
4
- server to change that data with four more. The page writes all eight into
5
- its HTML; the server declares which queries it answers and which operations
4
+ server to change that data with one more. The developer writes all five into
5
+ the HTML; the server declares which queries it answers and which operations
6
6
  it permits, and refuses anything it has not declared.
7
7
 
8
+ One vocabulary and one containment ladder: a list holds rows, a row holds
9
+ cells. A column is the second axis, and it is what the value of `lb-cell` or
10
+ `lb-key` always holds.
11
+
8
12
  ## A page that binds data
9
13
 
10
14
  Here is a page that binds a scalar, a list, and three requests:
@@ -13,23 +17,21 @@ Here is a page that binds a scalar, a list, and three requests:
13
17
  <!-- src/pages/members.page.html -->
14
18
  <h1>Members</h1>
15
19
 
16
- <p lb-query="dues">Dues collected this year: <span lb-cell="total"></span></p>
20
+ <p lb-row="dues">Dues collected this year: <span lb-cell="total"></span></p>
17
21
 
18
- <form lb-insert lb-query="roster">
22
+ <form lb-action="lb-row-insert" lb-list="roster">
19
23
  <input lb-cell="name" placeholder="Name" />
20
24
  <button type="submit">Add member</button>
21
25
  </form>
22
26
 
23
- <lb-list lb-query="roster">
24
- <ul>
25
- <template lb-key="id">
26
- <li>
27
- <lb-input lb-cell="name"></lb-input>
28
- <button lb-delete>Remove</button>
29
- </li>
30
- </template>
31
- </ul>
32
- </lb-list>
27
+ <ul lb-list="roster">
28
+ <template lb-key="id">
29
+ <li>
30
+ <lb-input lb-cell="name"></lb-input>
31
+ <button lb-action="lb-row-delete">Remove</button>
32
+ </li>
33
+ </template>
34
+ </ul>
33
35
  ```
34
36
 
35
37
  The queries named here — `dues` and `roster` — and the operations the page
@@ -37,28 +39,49 @@ asks for are declared on the server; see [page files](./page-files.md).
37
39
 
38
40
  ## Binding
39
41
 
40
- | Attribute | Names |
41
- |------------|----------------------------------------------------------|
42
- | `lb-query` | The query a subtree displays |
43
- | `lb-key` | The cell that identifies a row, and a live row's own key |
44
- | `lb-cell` | The cell an element displays |
45
- | `lb-value` | Where a widget receives its value |
46
-
47
- A binding is scoped by ancestry. An element binds to the query on its
48
- nearest ancestor carrying `lb-query`, and to the row on its nearest ancestor
49
- carrying `lb-key`. Nothing else establishes scope: an element outside every
50
- `lb-query` is bound to nothing and displays nothing.
42
+ | Attribute | Written by | Names |
43
+ |----------------|-------------|-------------------------------------------|
44
+ | `lb-list` | a developer | The set of rows a subtree displays |
45
+ | `lb-row` | a developer | The one row a subtree displays |
46
+ | `lb-key` | a developer | The column that identifies a row |
47
+ | `lb-cell` | a developer | The column an element displays |
48
+ | `lb-key-value` | the hub | A live row's own key |
49
+ | `lb-value` | the hub | Where a widget receives its value |
50
+
51
+ A binding is scoped by ancestry. Either scope attribute scopes its DOM
52
+ children, and a nested one of either kind begins a new scope, so an element
53
+ binds to the name on its nearest ancestor carrying one, and to the row on its
54
+ nearest ancestor carrying `lb-key-value`. Nothing else establishes scope: an
55
+ element outside every scope is bound to nothing and displays nothing, and a
56
+ result never crosses into a nested scope.
57
+
58
+ Which of the two a subtree writes is not a choice about display. Cardinality
59
+ is a property of the name, so one name answers with one shape, always. A page
60
+ that shows the roster both as a set and as a single row declares two queries,
61
+ `rosterList` and `rosterRow`, and binds each with the attribute that matches
62
+ what it answers with.
51
63
 
52
64
  Bind an element to a cell by putting `lb-cell` on the element that shows the
53
- value. The scope's own root counts as a cell if it carries one, which is how
54
- an `<option>` — whose content model is text — displays the value it is.
65
+ value. Its value is a column name and never a cell name: a cell has no name
66
+ of its own, because it is identified by its row and its column, and the row
67
+ arrives from scope. The scope's own root counts as a cell if it carries one,
68
+ which is how an `<option>` — whose content model is text — displays the value
69
+ it is.
55
70
 
56
71
  Name the same query on more than one subtree to display it in more than one
57
72
  place. Every subtree gets the result.
58
73
 
59
- `lb-key` holds one name in two positions. On a row template it names the
60
- cell that identifies a row; on a row that is showing, it carries that row's
61
- key value. A template is never a row, so the two never collide.
74
+ One name is the hub's rather than the server's: `lb-navigation`, one row
75
+ landed on every navigation with the columns `page-label` and `page-uri`. It binds the
76
+ same way. Its name is reserved, as every value beginning with `lb-` is in
77
+ every `lb-` attribute: the server refuses a page that declares a query so
78
+ named — see [Where the page is](./chrome.md#where-the-page-is).
79
+
80
+ A key has a name and a value, and they are two attributes. `lb-key` on a row
81
+ template names the column that identifies a row: it is a property of the
82
+ list, since a list without a fixed key column is meaningless, written where
83
+ the rows land. `lb-key-value` on a row that is showing carries that row's
84
+ value of it. A developer writes the first and never the second.
62
85
 
63
86
  ### Where a bound value lands
64
87
 
@@ -77,40 +100,71 @@ observing `lb-value`.
77
100
  Nothing an application writes ever sets `lb-value`. It is written by
78
101
  Loadbare and read by the widget it is written on.
79
102
 
80
- ### Rows
81
-
82
- A query that answers with many rows is delivered to a list widget, which
83
- clones its row template once per row and binds each clone to one row. A
84
- native element has one destination for a value and cannot acquire children,
85
- so a query that answers with rows must be bound to a widget that accepts
86
- them.
103
+ ### Lists
87
104
 
88
- Bind the widget to the query with `lb-query`, and name the key cell on the
89
- `<template>` inside it with `lb-key`. See
90
- [The Basic Widget Library](./widgets.md) for the widgets that accept
91
- rows, and for `lb-group` and `lb-sort`, which a list widget reads to decide
92
- where a row goes.
105
+ Write `lb-list` on any element, and a `<template>` inside it carrying
106
+ `lb-key`. The hub clones that template once per row, fills each clone, and
107
+ reconciles what is showing against what arrived. No widget is involved, and
108
+ none is needed.
93
109
 
94
- ## Requests
110
+ An array is the whole set, so it decides membership and order: a row whose
111
+ key did not arrive is gone. A patch touches only the rows it names and leaves
112
+ every other row's contents and position alone.
95
113
 
96
- Four attributes turn an interaction into a request:
114
+ A widget enters only where the rows need scaffolding or placement that only
115
+ it can decide — a `<select>` that builds an `<optgroup>` per distinct value, a
116
+ table that sections and sorts. Such a widget carries `lb-list` itself and
117
+ implements one or both of two optional methods; see
118
+ [Decorating a list](./custom-elements.md#decorating-a-list) and
119
+ [The Basic Widget Library](./widgets.md).
97
120
 
98
- | Written | Asks for | Carries |
99
- |---------------------------|------------------|------------------------------|
100
- | `lb-action="name"` | The named action | `name`, and what is in scope |
101
- | `lb-delete` | `tupleDelete` | `query`, `key` |
102
- | `lb-insert` on a `<form>` | `tupleInsert` | `query`, `values` |
103
- | `lb-update` on a `<form>` | `tupleUpdate` | `query`, `key`, `values` |
121
+ A scope with `lb-list` and no row template displays nothing, and that is not
122
+ an error. It is bound to the list without showing it, which is what an insert
123
+ form naming the list it adds a row to already is.
104
124
 
105
- The shipped `<lb-input>` widget asks for a fifth, `cellChange`, carrying
106
- `query`, `key`, `cell`, and `value`. It sends one when the page applies
107
- `data-fire-on-change` to it, and stays quiet otherwise — an input inside an
108
- `lb-insert` or `lb-update` form is read again by the form on submit, so a
109
- widget that sent on its own would write the same edit twice.
125
+ ## Requests
110
126
 
111
- Declare every one of these on the server. A name the page has not declared,
112
- and an operation a query does not permit, are refused; see
113
- [hooks, actions, CRUD](./page-files.md#hooks-actions-crud).
127
+ One attribute turns an interaction into a request. `lb-action` names what the
128
+ server is asked for: an action the page declared, or one of Loadbare's
129
+ reserved names, which are the CRUD operations.
130
+
131
+ One attribute name, one wire field, one set of values. The request field
132
+ `action` carries this attribute's value verbatim, so nothing is translated
133
+ between the markup and the server, and the CRUD key is the same value with
134
+ the prefix stripped and the rest camel-cased.
135
+
136
+ | Written | Asks for | Carries |
137
+ |-------------------------------------------|--------------|-------------------------------|
138
+ | `lb-action="name"` | That action | The scope, and what is in it |
139
+ | `lb-action="lb-row-delete"` | `rowDelete` | `list`, `key` |
140
+ | `lb-action="lb-row-insert"` on a `<form>` | `rowInsert` | `list`, `values` |
141
+ | `lb-action="lb-row-update"` on a `<form>` | `rowUpdate` | `list`, `key`, `values` |
142
+ | `lb-action="lb-cell-change"` on a widget | `cellChange` | `list`, `key`, `cell`, `value`|
143
+
144
+ All four operations are list operations. Each needs a key, and a key exists
145
+ only on a live row the hub stamped inside a list, so a single-row scope is
146
+ read-only and a declared action is the only thing it can send. An application
147
+ that wants a writable single row declares a list that answers with one row.
148
+
149
+ A name beginning with `lb-` is reserved, in `lb-action` and in either scope
150
+ attribute alike. The server refuses a page that declares an action or a query
151
+ so named, which is what lets a reserved name be added later without colliding
152
+ with one an application already uses. That reservation is also the whole of
153
+ the wire discriminant: a value beginning with `lb-` is an operation, and
154
+ anything else is a name the page declared.
155
+
156
+ The hub sends the first four from a native element on the element's own
157
+ event: a form on submit, anything else on click. A cell change needs a
158
+ widget to say what a change is, so only a widget sends it: the shipped
159
+ `<lb-input>` does when it carries `lb-action="lb-cell-change"`, and stays
160
+ quiet otherwise — an input inside an `lb-row-insert` or `lb-row-update` form is read
161
+ again by the form on submit, so a widget that sent on its own would write
162
+ the same edit twice.
163
+
164
+ Declare every action on the server, and permit every operation on its list. A
165
+ name the page has not declared, and an operation a list does not permit, are
166
+ refused; see
167
+ [requests, actions, CRUD](./page-files.md#requests-actions-crud).
114
168
 
115
169
  ### Actions
116
170
 
@@ -121,10 +175,10 @@ one of the four CRUD operations:
121
175
  <button lb-action="mailRoster">Mail the roster</button>
122
176
  ```
123
177
 
124
- An action carries whatever binding is in scope at the element — `query`,
125
- `key`, `cell` — and nothing else. There is no argument list. A button
126
- carries no value, so the server computes the whole of the new state and the
127
- page displays only what came back.
178
+ An action carries whatever binding is in scope at the element — `list` or
179
+ `row`, whichever scoped it, plus `key` and `cell` — and nothing else. There is
180
+ no argument list. A button carries no value, so the server computes the whole
181
+ of the new state and the page displays only what came back.
128
182
 
129
183
  Write `lb-action` on a widget to have the widget decide what performing the
130
184
  action means. Loadbare turns a click into a request for a native element
@@ -134,30 +188,31 @@ on change, not on click.
134
188
  ### Deleting a row
135
189
 
136
190
  ```html
137
- <button lb-delete>Remove</button>
191
+ <button lb-action="lb-row-delete">Remove</button>
138
192
  ```
139
193
 
140
- `lb-delete` needs no name. The row's `lb-query` and `lb-key` are already in
141
- scope, and they are all the server needs to know which row is meant and
142
- whether the query permits deleting it.
194
+ `lb-row-delete` needs nothing declared. The row's `lb-list` and `lb-key-value`
195
+ are already in scope, and they are all the server needs to know which row is
196
+ meant and whether the list permits deleting it.
143
197
 
144
- Write `lb-delete` on a native element, the same as `lb-action`. A widget
145
- sends its own request.
198
+ The hub sends it from a native element. A widget sends its own request.
146
199
 
147
200
  ### Forms
148
201
 
149
- `lb-insert` and `lb-update` both gather every `lb-cell` inside the form into
150
- one values map, read from the control each cell is or wraps. They differ in
151
- one thing: `lb-update` also carries the key of the row it is inside, and
152
- `lb-insert` carries none, because there is no row yet.
202
+ A `<form>` performs its `lb-action` on submit. `lb-row-insert` and `lb-row-update`
203
+ both gather every `lb-cell` inside the form into one values map, read from
204
+ the control each cell is or wraps. They differ in one thing: `lb-row-update`
205
+ also carries the key of the row it is inside, and `lb-row-insert` carries none,
206
+ because there is no row yet. A declared name on a form sends that action on
207
+ submit, carrying the binding and no values.
153
208
 
154
- Put an `lb-update` form inside the row it edits, so it has that row's key
209
+ Put an `lb-row-update` form inside the row it edits, so it has that row's key
155
210
  from the same ancestor a delete button reads:
156
211
 
157
212
  ```html
158
213
  <template lb-key="id">
159
214
  <li>
160
- <form lb-update>
215
+ <form lb-action="lb-row-update">
161
216
  <input lb-cell="name" />
162
217
  <button type="submit">Save</button>
163
218
  </form>
@@ -188,31 +243,34 @@ HTML.
188
243
 
189
244
  ### An empty list
190
245
 
191
- A list widget stamps itself with `data-rows`, the number of rows it is
246
+ The hub stamps every list scope with `lb-row-count`, the number of rows it is
192
247
  showing. It is the one conditional a page cannot be sent, because the server
193
- answers with rows and says nothing about how many survived. It makes an
194
- empty list a stylesheet rule rather than code in every list widget:
248
+ answers with rows and says nothing about how many survived. It makes an empty
249
+ list a stylesheet rule rather than code anywhere:
195
250
 
196
251
  ```html
197
- <lb-list lb-query="roster">
252
+ <div lb-list="roster">
198
253
  <ul>
199
254
  <template lb-key="id">
200
255
  <li lb-cell="name"></li>
201
256
  </template>
202
257
  </ul>
203
258
  <p class="roster-empty">No members yet.</p>
204
- </lb-list>
259
+ </div>
205
260
  ```
206
261
 
207
262
  ```css
208
263
  .roster-empty {
209
264
  display: none;
210
265
  }
211
- lb-list[data-rows="0"] .roster-empty {
266
+ [lb-row-count="0"] .roster-empty {
212
267
  display: revert;
213
268
  }
214
269
  ```
215
270
 
271
+ The scope is a `<div>` here rather than the `<ul>`, so that the empty message
272
+ is inside it and the same rule can reach both.
273
+
216
274
  ## Request state
217
275
 
218
276
  Loadbare stamps two attributes on the element a request came from — the
@@ -220,11 +278,11 @@ button, the form, or the widget itself:
220
278
 
221
279
  | Attribute | Means |
222
280
  |-------------------|-------------------------------------------|
223
- | `data-lb-pending` | The request is in flight |
224
- | `data-lb-error` | The last request from this element failed |
281
+ | `lb-pending` | The request is in flight |
282
+ | `lb-error` | The last request from this element failed |
225
283
 
226
- `data-lb-pending` is set when the request goes out and removed when it
227
- settles. `data-lb-error` is set on a failed response, a network failure, or
284
+ `lb-pending` is set when the request goes out and removed when it
285
+ settles. `lb-error` is set on a failed response, a network failure, or
228
286
  a timeout alike, and cleared when that element sends its next request.
229
287
 
230
288
  Neither one carries any meaning beyond the fact it states. Dim a pending
@@ -26,7 +26,7 @@ serves, and the widgets those pages are made of.
26
26
  |---------------------------------------------------------|--------------------------------|
27
27
  | [`<name>.page.html`](./page-files.md#html) | The page's HTML |
28
28
  | [`<name>.queries.ts`](./page-files.md#queries) | The data the page displays |
29
- | [`<name>.hooks.ts`](./page-files.md#hooks-actions-crud) | Data Channel handlers |
29
+ | [`<name>.requests.ts`](./page-files.md#requests-actions-crud) | Data Channel handlers |
30
30
  | [Data Binding](./data-binding.md) | Connecting HTML to server data |
31
31
 
32
32
  ## Widgets