@loadbare/app 0.5.6 → 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 +19 -32
  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 +15 -13
  19. package/dist/core/lb-constants.d.ts.map +1 -1
  20. package/dist/core/lb-constants.js +103 -53
  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 +131 -84
  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 +6 -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 +10 -9
  57. package/docs/reference/custom-elements.md +51 -36
  58. package/docs/reference/data-binding.md +141 -88
  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 +22 -30
  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 +8 -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 +1 -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,7 +24,7 @@ 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 lb-query="lb-navigation">
27
+ <header lb-row="lb-navigation">
28
28
  <h1>Membership Roster</h1>
29
29
  <h2 lb-cell="page-label"></h2>
30
30
  </header>
@@ -34,7 +34,7 @@ Here is a minimal but fully complaint chrome for a typical app:
34
34
  <a href="https://example.org/">Our website</a>
35
35
  </nav>
36
36
  <main></main>
37
- <dialog lb-unknown-page lb-query="lb-navigation">
37
+ <dialog lb-unknown-page lb-row="lb-navigation">
38
38
  The URL <span lb-cell="page-uri"></span> is not in this app.
39
39
  </dialog>
40
40
  </lb-hub>
@@ -60,7 +60,7 @@ Everything else is optional:
60
60
  |-------------------------------------------|---------------------------------------------|
61
61
  | `<link rel="stylesheet" href="/app.css">` | The bundled stylesheet |
62
62
  | `<a lb-nav-link>` | Navigation between pages |
63
- | `lb-query="lb-navigation"` | Where the page is — see below |
63
+ | `lb-row="lb-navigation"` | Where the page is — see below |
64
64
  | `<dialog lb-unknown-page>` | A message when a URL matches no page |
65
65
  | Custom elements | The chrome, decomposed into widget files |
66
66
  | `<body hidden>` + a `<noscript>` fallback | Avoids a first-load blink — see below |
@@ -77,8 +77,9 @@ A navigation anchor's `href` is a path, and the path names a page:
77
77
  landing page is the one named `index.page.html`. An anchor without
78
78
  `lb-nav-link` is left alone and behaves like any other link.
79
79
 
80
- The `lb-unknown-page` attribute, if used, must appear on a `<dialog>`. The
81
- hub opens it when a path names no page. What it says is up to the chrome:
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:
82
83
  the dialog is a subtree like any other, and it displays where the page is by
83
84
  naming the hub's own query, described next.
84
85
 
@@ -86,11 +87,11 @@ naming the hub's own query, described next.
86
87
 
87
88
  On every navigation the hub lands a query of its own, `lb-navigation`, on
88
89
  any subtree inside the hub that names it. It arrives the way a server's
89
- query arrives — `lb-query` on the subtree, `lb-cell` on each element that
90
+ row arrives — `lb-row` on the subtree, `lb-cell` on each element that
90
91
  shows a value — so a chrome displays the current page with no code at all:
91
92
 
92
93
  ```html
93
- <header lb-query="lb-navigation">
94
+ <header lb-row="lb-navigation">
94
95
  <h1>Membership Roster</h1>
95
96
  <h2 lb-cell="page-label"></h2>
96
97
  </header>
@@ -112,12 +113,12 @@ namespace: no server answers a query so named. It is landed only where a
112
113
  subtree names it, so a chrome that displays no navigation is not warned
113
114
  about a query with no scope.
114
115
 
115
- The same tuple is what an unknown-page dialog has to work with. It lands
116
+ The same row is what an unknown-page dialog has to work with. It lands
116
117
  before the page host is looked up, so a miss has it too. Name the query on
117
118
  the dialog and show whichever cell fits:
118
119
 
119
120
  ```html
120
- <dialog lb-unknown-page lb-query="lb-navigation">
121
+ <dialog lb-unknown-page lb-row="lb-navigation">
121
122
  The URL <span lb-cell="page-uri"></span> is not in this app.
122
123
  </dialog>
123
124
  ```
@@ -223,12 +223,15 @@ as a string literal.
223
223
  |------------------|------------|
224
224
  | `ATTR_VALUE` | `lb-value` |
225
225
  | `ATTR_CELL` | `lb-cell` |
226
- | `ATTR_QUERY` | `lb-query` |
226
+ | `ATTR_LIST` | `lb-list` |
227
+ | `ATTR_ROW` | `lb-row` |
227
228
  | `ATTR_KEY` | `lb-key` |
228
- | `ATTR_GROUP` | `lb-group` |
229
- | `ATTR_SORT` | `lb-sort` |
229
+ | `ATTR_KEY_VALUE` | `lb-key-value` |
230
230
  | `ATTR_ACTION` | `lb-action`|
231
- | `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`|
232
235
  | `LB_EVENT_NAME` | `lb-request` |
233
236
 
234
237
  ### Receiving a value
@@ -266,20 +269,20 @@ click — builds its own request and dispatches it as a bubbling
266
269
  as its `detail`:
267
270
 
268
271
  ```ts
269
- 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";
270
273
  import type { HubRequest } from "@loadbare/app/types";
271
274
 
272
275
  const detail: HubRequest = {
273
- op: "cell-change",
274
- query: this.closest(`[${ATTR_QUERY}]`)!.getAttribute(ATTR_QUERY)!,
275
- 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)!,
276
279
  cell: this.getAttribute(ATTR_CELL)!,
277
280
  value: input.value,
278
281
  };
279
282
  this.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
280
283
  ```
281
284
 
282
- 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
283
286
  finds the binding for a native element. See
284
287
  [Data Binding](./data-binding.md#requests) for the request vocabulary and
285
288
  what each operation carries.
@@ -288,54 +291,66 @@ Let the event bubble, so an ancestor widget can intercept and stop it before
288
291
  the hub sees it. A hand-written widget and a native element carrying
289
292
  `lb-action` produce the same event.
290
293
 
291
- ### Accepting rows
294
+ ### Decorating a list
292
295
 
293
- A query answering with many rows is delivered to whichever element carries
294
- an `acceptRows` method. It is a protocol, not a tag name — the hub tests for
295
- the method and never for the element:
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.
300
+
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:
296
304
 
297
305
  ```ts
298
- import { applyRows } from "@loadbare/app/rows";
299
- import type { Projection, HubRowHost } from "@loadbare/app/types";
306
+ import type { ListHost, Row } from "@loadbare/app/types";
307
+
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
+ }
300
313
 
301
- class UpdateList extends HTMLElement implements HubRowHost {
302
- acceptRows(result: Projection) {
303
- applyRows(this, result);
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.
304
318
  }
305
319
  }
306
320
  ```
307
321
 
308
- Call `applyRows(scope, result, place?)` rather than reimplementing rows.
309
- 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:
310
323
 
311
- | Concern | What `applyRows` does |
324
+ | Concern | What the hub does |
312
325
  |-------------|---------------------------------------------------------|
313
- | Cloning | Clones the `<template lb-key="...">` in the widget |
326
+ | Cloning | Clones the `<template lb-key="...">` in the scope |
314
327
  | Matching | Updates the row already showing that key, or clones one |
315
328
  | Reconciling | Removes the rows the response says are gone |
316
- | Counting | Stamps `data-rows` with the number of rows showing |
329
+ | Counting | Stamps `lb-row-count` with the number of rows showing |
317
330
 
318
- `rows` is the whole set, so it decides membership and order, and a key
319
- 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
320
333
  every other row's contents and position alone.
321
334
 
322
- Supply a `place` function to decide where a row goes — `(row, tuple,
323
- template) => void`, called with a fresh or reordered row. The default
324
- inserts immediately before the template, so rows accumulate in arrival
325
- order. A widget that groups or sorts supplies its own `place` instead of
326
- reimplementing matching and cloning around it; `lb-options.browser.ts` and
327
- `lb-table.browser.ts` in [`@loadbare/widgets`](./widgets.md) are two different
328
- `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.
329
344
 
330
- 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
331
346
  conditional in the widget — see
332
347
  [Conditional rendering](./data-binding.md#conditional-rendering).
333
348
 
334
349
  ### Filling a scope by hand
335
350
 
336
- `@loadbare/app/rows` also exports `applyTuple(root, cells)`, the same
337
- tuple-landing operation a page host uses. Call it in a widget that builds
338
- 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`
339
354
  itself when `root` carries a matching `lb-cell`, and every matching
340
355
  descendant.
341
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,33 +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
- One query is the hub's rather than the server's: `lb-navigation`, landed on
60
- every navigation with the cells `page-label` and `page-uri`. It binds the
61
- same way, and the `lb-` prefix on its name is what marks it as Loadbare's —
62
- see [Where the page is](./chrome.md#where-the-page-is).
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).
63
79
 
64
- `lb-key` holds one name in two positions. On a row template it names the
65
- cell that identifies a row; on a row that is showing, it carries that row's
66
- key value. A template is never a row, so the two never collide.
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.
67
85
 
68
86
  ### Where a bound value lands
69
87
 
@@ -82,40 +100,71 @@ observing `lb-value`.
82
100
  Nothing an application writes ever sets `lb-value`. It is written by
83
101
  Loadbare and read by the widget it is written on.
84
102
 
85
- ### Rows
86
-
87
- A query that answers with many rows is delivered to a list widget, which
88
- clones its row template once per row and binds each clone to one row. A
89
- native element has one destination for a value and cannot acquire children,
90
- so a query that answers with rows must be bound to a widget that accepts
91
- them.
103
+ ### Lists
92
104
 
93
- Bind the widget to the query with `lb-query`, and name the key cell on the
94
- `<template>` inside it with `lb-key`. See
95
- [The Basic Widget Library](./widgets.md) for the widgets that accept
96
- rows, and for `lb-group` and `lb-sort`, which a list widget reads to decide
97
- 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.
98
109
 
99
- ## 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.
100
113
 
101
- 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).
102
120
 
103
- | Written | Asks for | Carries |
104
- |---------------------------|------------------|------------------------------|
105
- | `lb-action="name"` | The named action | `name`, and what is in scope |
106
- | `lb-delete` | `tupleDelete` | `query`, `key` |
107
- | `lb-insert` on a `<form>` | `tupleInsert` | `query`, `values` |
108
- | `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.
109
124
 
110
- The shipped `<lb-input>` widget asks for a fifth, `cellChange`, carrying
111
- `query`, `key`, `cell`, and `value`. It sends one when the page applies
112
- `data-fire-on-change` to it, and stays quiet otherwise — an input inside an
113
- `lb-insert` or `lb-update` form is read again by the form on submit, so a
114
- widget that sent on its own would write the same edit twice.
125
+ ## Requests
115
126
 
116
- Declare every one of these on the server. A name the page has not declared,
117
- and an operation a query does not permit, are refused; see
118
- [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).
119
168
 
120
169
  ### Actions
121
170
 
@@ -126,10 +175,10 @@ one of the four CRUD operations:
126
175
  <button lb-action="mailRoster">Mail the roster</button>
127
176
  ```
128
177
 
129
- An action carries whatever binding is in scope at the element — `query`,
130
- `key`, `cell` — and nothing else. There is no argument list. A button
131
- carries no value, so the server computes the whole of the new state and the
132
- 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.
133
182
 
134
183
  Write `lb-action` on a widget to have the widget decide what performing the
135
184
  action means. Loadbare turns a click into a request for a native element
@@ -139,30 +188,31 @@ on change, not on click.
139
188
  ### Deleting a row
140
189
 
141
190
  ```html
142
- <button lb-delete>Remove</button>
191
+ <button lb-action="lb-row-delete">Remove</button>
143
192
  ```
144
193
 
145
- `lb-delete` needs no name. The row's `lb-query` and `lb-key` are already in
146
- scope, and they are all the server needs to know which row is meant and
147
- 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.
148
197
 
149
- Write `lb-delete` on a native element, the same as `lb-action`. A widget
150
- sends its own request.
198
+ The hub sends it from a native element. A widget sends its own request.
151
199
 
152
200
  ### Forms
153
201
 
154
- `lb-insert` and `lb-update` both gather every `lb-cell` inside the form into
155
- one values map, read from the control each cell is or wraps. They differ in
156
- one thing: `lb-update` also carries the key of the row it is inside, and
157
- `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.
158
208
 
159
- 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
160
210
  from the same ancestor a delete button reads:
161
211
 
162
212
  ```html
163
213
  <template lb-key="id">
164
214
  <li>
165
- <form lb-update>
215
+ <form lb-action="lb-row-update">
166
216
  <input lb-cell="name" />
167
217
  <button type="submit">Save</button>
168
218
  </form>
@@ -193,31 +243,34 @@ HTML.
193
243
 
194
244
  ### An empty list
195
245
 
196
- 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
197
247
  showing. It is the one conditional a page cannot be sent, because the server
198
- answers with rows and says nothing about how many survived. It makes an
199
- 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:
200
250
 
201
251
  ```html
202
- <lb-list lb-query="roster">
252
+ <div lb-list="roster">
203
253
  <ul>
204
254
  <template lb-key="id">
205
255
  <li lb-cell="name"></li>
206
256
  </template>
207
257
  </ul>
208
258
  <p class="roster-empty">No members yet.</p>
209
- </lb-list>
259
+ </div>
210
260
  ```
211
261
 
212
262
  ```css
213
263
  .roster-empty {
214
264
  display: none;
215
265
  }
216
- lb-list[data-rows="0"] .roster-empty {
266
+ [lb-row-count="0"] .roster-empty {
217
267
  display: revert;
218
268
  }
219
269
  ```
220
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
+
221
274
  ## Request state
222
275
 
223
276
  Loadbare stamps two attributes on the element a request came from — the
@@ -225,11 +278,11 @@ button, the form, or the widget itself:
225
278
 
226
279
  | Attribute | Means |
227
280
  |-------------------|-------------------------------------------|
228
- | `data-lb-pending` | The request is in flight |
229
- | `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 |
230
283
 
231
- `data-lb-pending` is set when the request goes out and removed when it
232
- 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
233
286
  a timeout alike, and cleared when that element sends its next request.
234
287
 
235
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