@loadbare/app 0.19.0 → 0.20.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 (42) hide show
  1. package/dist/build/assemble.d.ts.map +1 -1
  2. package/dist/build/assemble.js +28 -2
  3. package/dist/build/assemble.js.map +1 -1
  4. package/dist/core/lb-constants.d.ts +7 -2
  5. package/dist/core/lb-constants.d.ts.map +1 -1
  6. package/dist/core/lb-constants.js +30 -4
  7. package/dist/core/lb-constants.js.map +1 -1
  8. package/dist/hub/lb-apply.d.ts +21 -0
  9. package/dist/hub/lb-apply.d.ts.map +1 -1
  10. package/dist/hub/lb-apply.js +221 -30
  11. package/dist/hub/lb-apply.js.map +1 -1
  12. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  13. package/dist/hub/lb-hub.browser.js +26 -6
  14. package/dist/hub/lb-hub.browser.js.map +1 -1
  15. package/docs/comparison.md +23 -13
  16. package/docs/opinions.md +2 -2
  17. package/docs/reference/builder.md +11 -2
  18. package/docs/reference/chrome.md +83 -9
  19. package/docs/reference/css.md +0 -1
  20. package/docs/reference/custom-elements.md +7 -28
  21. package/docs/reference/data-binding.md +320 -50
  22. package/docs/reference/names.md +258 -0
  23. package/docs/reference/overview.md +6 -0
  24. package/docs/reference/page-files.md +47 -10
  25. package/docs/reference/server.md +54 -1
  26. package/docs/reference/widgets.md +10 -1
  27. package/docs/testing.md +5 -1
  28. package/docs/tutorials/040-displaying-data.md +1 -1
  29. package/package.json +1 -1
  30. package/skills/loadbare-app/SKILL.md +26 -14
  31. package/skills/loadbare-app/references/builder.md +11 -2
  32. package/skills/loadbare-app/references/chrome.md +83 -9
  33. package/skills/loadbare-app/references/css.md +0 -1
  34. package/skills/loadbare-app/references/custom-elements.md +7 -28
  35. package/skills/loadbare-app/references/data-binding.md +320 -50
  36. package/skills/loadbare-app/references/names.md +258 -0
  37. package/skills/loadbare-app/references/overview.md +6 -0
  38. package/skills/loadbare-app/references/page-files.md +47 -10
  39. package/skills/loadbare-app/references/server.md +54 -1
  40. package/skills/loadbare-app/references/widgets.md +10 -1
  41. package/docs/TECHREF-1.0.md +0 -2267
  42. package/skills/loadbare-app/references/TECHREF-1.0.md +0 -2267
@@ -2,7 +2,7 @@
2
2
 
3
3
  A page shows server data by naming queries and columns in its HTML, and asks
4
4
  the server to change that data by naming requests. The developer writes
5
- sixteen `lb-` attributes. The server declares every query a page may show and
5
+ seventeen `lb-` attributes. The server declares every query a page may show and
6
6
  every request it may send, and refuses anything it has not declared.
7
7
 
8
8
  Everything is a query. A query is a name for rows, of kind `row` or `rows`,
@@ -54,6 +54,7 @@ each with its key, and permits the three requests on `roster`; see
54
54
  | `lb-select` | A checkbox in a row template | Nothing: it selects its live row; see [Selecting rows](#selecting-rows) |
55
55
  | `lb-request` | Any element | The request it sends when it commits |
56
56
  | `lb-url-link` | An `<a>` | A link between pages; see [chrome](./chrome.md#links) |
57
+ | `lb-url-parm` | An `<a lb-url-link>` in a row | Columns of the row its `href` carries as parms; see [chrome](./chrome.md#a-link-from-a-row) |
57
58
  | `lb-url-push` | An element with `lb-request` | A URL change that pushes a history entry |
58
59
  | `lb-url-unknown` | A `<dialog>` in the chrome | The dialog for a URL that names no page |
59
60
 
@@ -66,8 +67,10 @@ builder writes a stamp, and a stylesheet or a custom element reads it.
66
67
  | `lb-key-value` | The key of a live row, or of a `row` landed on an element |
67
68
  | `lb-row-live` | The element is a live row |
68
69
  | `lb-query-row-count` | The number of live rows an element shows |
70
+ | `lb-query-unsorted-count` | The number of them out of order; see [Rows the user is working in](#rows-the-user-is-working-in) |
71
+ | `lb-query-order` | The order placing them; see [Order](#order) |
69
72
  | `lb-group-live`, `lb-group-column`, `lb-group-value` | A group, the column it breaks on, and its value; see [Groups](#groups) |
70
- | `lb-row-created`, `lb-row-changed`, `lb-row-moved`, `lb-row-requested` | What happened to a row; see [What happened to a row](#what-happened-to-a-row) |
73
+ | `lb-row-created`, `lb-row-changed`, `lb-row-moved`, `lb-row-requested`, `lb-row-unsorted` | What happened to a row; see [What happened to a row](#what-happened-to-a-row) |
71
74
  | `lb-row-leaving`, `lb-group-leaving` | A row or group on its way out |
72
75
  | `lb-row-editing` | The row a form is pointed at; see [Editing a row in one form](#editing-a-row-in-one-form) |
73
76
  | `lb-row-selected` | A row whose `lb-select` is ticked |
@@ -89,7 +92,8 @@ elements as the page needs.
89
92
 
90
93
  What lands depends on the query's kind and on whether the element holds a
91
94
  row template, which is the first `<template>` among its descendants outside
92
- any nested `lb-query`:
95
+ any nested `lb-query`, or, when that template is a [group
96
+ template](#groups), the innermost template inside it:
93
97
 
94
98
  | Kind | Row template | The hub |
95
99
  |--------|--------------|----------------------------------------|
@@ -128,6 +132,9 @@ row's `account`:
128
132
  `lb-column` on an element with `lb-query` is undefined unless the element is
129
133
  a control.
130
134
 
135
+ A query that arrives with nowhere to land is reported to the console,
136
+ unless its name begins with `lb-`.
137
+
131
138
  An element with `lb-column` and no ancestor row receives nothing. The hub
132
139
  still gathers from it, so an insert form writes `lb-column` on controls that
133
140
  no row fills.
@@ -143,6 +150,10 @@ no row fills.
143
150
  | Any other custom element | `lb-column-value` only |
144
151
  | Any other element | Its text content, and `lb-column-value` |
145
152
 
153
+ A column lands in one attribute besides: the `href` of a link carrying
154
+ `lb-url-parm` that names it; see
155
+ [A link from a row](./chrome.md#a-link-from-a-row).
156
+
146
157
  A control is an `<input>`, `<select>` or `<textarea>`, or a form-associated
147
158
  custom element with a `value` property that fires `change`. The hub sets a
148
159
  control's `value`, and gathers it back under the same column. A custom
@@ -150,7 +161,9 @@ element that is not a control keeps the content the builder placed in it
150
161
  from its element file, and renders `lb-column-value` itself; see
151
162
  [Custom Elements](./custom-elements.md#receiving-a-value).
152
163
 
153
- A file input receives nothing, and the hub reports it to the console.
164
+ A file input receives nothing, not even `lb-column-value`, and the hub
165
+ reports it to the console and gathers nothing from it. A `<select>` with no
166
+ option for the value shows no selection.
154
167
 
155
168
  The hub hands every value to the browser as the server sent it. What a
156
169
  number, a date or a null looks like is decided in the query, or by a custom
@@ -212,7 +225,9 @@ places it immediately before the template:
212
225
  The hub matches each row to a live row by its key. All rows decide
213
226
  membership: a live row whose key did not arrive leaves. A patch changes only
214
227
  the rows it names, and every other live row keeps its content. The hub
215
- places every live row by the query's order; see [Order](#order). A row that
228
+ places live rows by the query's order, except that an edit from inside the
229
+ list leaves them where they stand; see [Order](#order) and
230
+ [Rows the user is working in](#rows-the-user-is-working-in). A row that
216
231
  does not pass the user's filter is `hidden`; see [Filters](#filters).
217
232
 
218
233
  A live row whose values arrive as it already shows them is not filled
@@ -221,7 +236,11 @@ what was typed, through a refresh and through a change of order. A row
221
236
  whose values changed is filled whole, and its controls show what arrived.
222
237
 
223
238
  A key is unique within a query. Two rows with one key in the same answer,
224
- or two live rows showing one key, are reported on the console.
239
+ or two live rows showing one key, are reported on the console. A row
240
+ missing its key column is reported and not landed.
241
+
242
+ A live row already in the markup when the first answer lands is one of the
243
+ query's rows.
225
244
 
226
245
  When the focus is in a live row that placing moves, the hub puts it back on
227
246
  the same element and scrolls it into view. When that row leaves, or a
@@ -266,14 +285,81 @@ server's order places the rows:
266
285
  </ul>
267
286
  ```
268
287
 
269
- Values compare by their JSON type: numbers numerically, strings as the
270
- user's language orders them, `false` before `true`, and `null` last. A
271
- number sent as a string sorts as a string, unless the query declares the
288
+ | Values | Compare |
289
+ |----------------------|----------------------------------------------|
290
+ | Two numbers | Numerically |
291
+ | Two strings | As the user's language orders them |
292
+ | Two booleans | `false` before `true` |
293
+ | Two different types | Booleans, then numbers, then strings |
294
+ | `null` or missing | After everything ascending, first descending |
295
+
296
+ A number sent as a string sorts as a string, unless the query declares the
272
297
  column's [type](./page-files.md#queries): a column declared `number` sorts
273
298
  numerically whatever JSON type its values arrive as, and a value that is no
274
- number sorts with the nulls. Rows that compare equal keep the order they
275
- arrived in. A row moves only when the order puts it somewhere
276
- else.
299
+ value of its declared type compares as `null` does. `readAs` in
300
+ `@loadbare/app/types` is the rule the hub applies.
301
+
302
+ The element carrying `lb-query` carries `lb-query-order`, the order
303
+ placing its rows as an order's text, the user's when one is chosen and
304
+ the server's otherwise. Rows with no order carry none.
305
+
306
+ Rows that compare equal keep the order they arrived in, a patch's new rows
307
+ after the rows already showing. A row moves only when the order
308
+ puts it somewhere else, so all rows landing again unchanged move nothing.
309
+
310
+ ### Rows the user is working in
311
+
312
+ A landing that answers a request sent from inside a list moves none of that
313
+ list's rows. The request is from inside the list when the list is the
314
+ nearest element carrying `lb-query` around the element that sent it: a
315
+ control in a live row, or a ghost row. A form elsewhere on the page is not
316
+ inside the list. A user who changes a value sees the value change, and the
317
+ row stays where they were working:
318
+
319
+ | The answer | The row |
320
+ |------------------------------------------|---------------------------------------------------|
321
+ | Changes a column the order reads | Stays where it stands |
322
+ | Creates a row | Goes after the last row of its group, or last of all |
323
+ | Drops a row | Leaves |
324
+ | Changes a row so the filter hides it | Stays showing |
325
+ | Changes the column its group breaks on | Stays under its group's heading |
326
+
327
+ A patch and all rows answering the same edit land the same way. A row left
328
+ where the order or the filter would not leave it is stamped
329
+ `lb-row-unsorted`, and the element carrying `lb-query` carries
330
+ `lb-query-unsorted-count`, the number of such rows. Another element naming
331
+ the same query places its rows by the order: the user was not working
332
+ there.
333
+
334
+ The rows are placed by the order again, and every `lb-row-unsorted` is
335
+ cleared, when the user asks: when the page loads, when the user's order or
336
+ filter changes, or on `--lb-sort`. A button names the list with HTML's
337
+ `commandfor`, and a stylesheet shows it only while it has something to do:
338
+
339
+ ```html
340
+ <div id="roster" lb-query="roster">
341
+ <button type="button" class="resort" commandfor="roster" command="--lb-sort">Re-sort</button>
342
+ <ul>
343
+ <template><li><input lb-column="name" lb-request="lb-row-update" /></li></template>
344
+ </ul>
345
+ </div>
346
+ ```
347
+
348
+ ```css
349
+ .resort {
350
+ display: none;
351
+ }
352
+ [lb-query-unsorted-count]:not([lb-query-unsorted-count="0"]) .resort {
353
+ display: revert;
354
+ }
355
+ ```
356
+
357
+ `--lb-sort` places the rows the hub holds, with no round trip. Aimed at an
358
+ element that carries no `lb-query`, or shows no rows the hub holds, it is
359
+ reported to the console. A custom element that reorders rows by a request of
360
+ its own dispatches the same command on the list once the answer lands, with
361
+ `new CommandEvent("command", { command: "--lb-sort" })`: the user asked for
362
+ the move.
277
363
 
278
364
  ### Filters
279
365
 
@@ -292,7 +378,9 @@ its rows the user may filter, separated by commas. The column of
292
378
  ```
293
379
 
294
380
  A row that does not pass every filter is `hidden`, and a ticked `lb-select`
295
- in it is cleared. A group whose rows are all hidden is hidden too
381
+ in it is cleared. A row an edit from inside the list changed stays showing
382
+ until the rows are placed again; see
383
+ [Rows the user is working in](#rows-the-user-is-working-in). A group whose rows are all hidden is hidden too
296
384
  ([miswart](./miswarts.md#rows-hidden-by-a-filter)). An empty
297
385
  choice filters nothing.
298
386
 
@@ -323,6 +411,9 @@ The column's type is the one its query declares, if it declares one, and
323
411
  its JSON type otherwise. A value that is not of the column's type, and a
324
412
  column that is null or missing, pass only `!=`.
325
413
 
414
+ `parseFilter`, `passes` and `passesTerm` in `@loadbare/app/types` are the
415
+ rule the hub applies.
416
+
326
417
  ### Groups
327
418
 
328
419
  A group is a run of rows sharing the order's leading term. Write a
@@ -343,19 +434,46 @@ template. The nth group template breaks on the order's nth term.
343
434
  </ul>
344
435
  ```
345
436
 
346
- The group's contents land immediately before its nested template. When that
347
- template is inside the heading's element, the contents land inside it; when
348
- it is beside the heading, they land after it. `<tbody>` and `<optgroup>` do
349
- not nest, so a table's deeper levels are heading rows. Content after the
350
- nested template is the group's footer.
437
+ The hub clones the group template where the order breaks, and the group's
438
+ contents land immediately before the clone of its nested template, so the
439
+ markup's nesting decides the shape:
440
+
441
+ | The nested template is | The group's contents land | Example |
442
+ |------------------------------|------------------------------------|-----------------------------------|
443
+ | Inside the heading's element | Inside it | `<tbody>`, `<li>`, `<details>` |
444
+ | Beside the heading | After the heading, as its siblings | A heading `<tr>` in one `<tbody>` |
445
+
446
+ `<tbody>` and `<optgroup>` do not nest, so a table's levels below the first
447
+ are heading rows beside their rows:
448
+
449
+ ```html
450
+ <table lb-query="accounts">
451
+ <thead><tr><th>Account</th></tr></thead>
452
+ <template lb-group>
453
+ <tbody>
454
+ <tr><th scope="rowgroup" lb-column="category"></th></tr>
455
+ <template><tr><td lb-column="account_name"></td></tr></template>
456
+ </tbody>
457
+ </template>
458
+ </table>
459
+ ```
460
+
461
+ Content after the nested template lands after the group's rows, which is
462
+ where a group's footer goes.
351
463
 
352
464
  A heading is filled from its group's first row, so it may show a column
353
- other than the one the group breaks on. The hub stamps each top-level
465
+ other than the one the group breaks on. A row an edit from inside the list
466
+ moved to another group stays under its old heading, and the heading shows
467
+ the row as it stood, until the rows are placed again. The hub stamps each top-level
354
468
  element of a group with `lb-group-live`, `lb-group-column` and
355
469
  `lb-group-value`. A row creates its group, and a group leaves with its last
356
470
  row. When the order changes, a group whose run goes on is kept and moved,
357
471
  and a group whose run ended leaves.
358
472
 
473
+ A group template with no nested template is reported to the console. More
474
+ group templates than the order has terms show each extra level as one
475
+ group, and are reported once.
476
+
359
477
  ### Aggregates
360
478
 
361
479
  `lb-count`, `lb-sum="col"`, `lb-avg="col"`, `lb-min="col"` and
@@ -366,10 +484,24 @@ or of the whole list outside any group:
366
484
  <tr><td><span lb-count></span> people</td><td lb-sum="salary"></td></tr>
367
485
  ```
368
486
 
369
- A sum and an average are exact, read from JSON numbers and from strings
370
- holding a plain decimal. `null` and empty values are left out. The result
371
- lands unformatted, and covers the rows showing: a row a filter hides is
372
- left out.
487
+ | Attribute | Sets the element to |
488
+ |----------------|-----------------------------------------------------------|
489
+ | `lb-count` | The number of rows |
490
+ | `lb-sum="col"` | The sum of `col` |
491
+ | `lb-avg="col"` | The average of `col` |
492
+ | `lb-min="col"` | The least value of `col`, by the [order](#order)'s comparison |
493
+ | `lb-max="col"` | The greatest, likewise |
494
+
495
+ An aggregate lands as a column does, with `lb-column-value`. A sum and an
496
+ average are exact decimals, read from JSON numbers and from strings holding
497
+ a plain decimal, as Postgres sends `numeric`. A sum has as many places as
498
+ the most precise value, and an average up to two more. `null` and empty
499
+ values are left out, as SQL leaves them out, and any other value is left
500
+ out and reported once. The result lands unformatted.
501
+
502
+ An aggregate covers the rows showing, so a row a filter hides is left out.
503
+ Where the true total matters beyond what the page shows, have the server
504
+ answer it as a row.
373
505
 
374
506
  ### What happened to a row
375
507
 
@@ -381,13 +513,23 @@ Every change to a list is one a stylesheet can see:
381
513
  | `lb-row-changed` | Its values changed when the hub last touched it |
382
514
  | `lb-row-moved` | Its values changed, and it now stands elsewhere |
383
515
  | `lb-row-requested` | `created`, `changed` or `moved` by this page's request |
516
+ | `lb-row-unsorted` | Stands where the order would not put it |
384
517
  | `lb-row-leaving` | A row on its way out |
385
518
  | `lb-group-leaving` | A group on its way out |
386
519
 
387
- A row that leaves loses `lb-row-live`, takes `inert`
520
+ The hub touches a row when an answer names it. `lb-row-created`,
521
+ `lb-row-changed` and `lb-row-moved` are cleared when it next does, and one
522
+ applied again is removed and added as two changes, so an animation keyed to
523
+ it plays again. `lb-row-requested` is cleared at every landing of the row's
524
+ query, and set only by a landing that answers a request from the page.
525
+ `lb-row-unsorted` is a state, not an event: it stays until the rows are
526
+ placed by the order; see
527
+ [Rows the user is working in](#rows-the-user-is-working-in).
528
+
529
+ A row or a group that leaves loses `lb-row-live` or `lb-group-live`, takes
530
+ `lb-row-leaving` or `lb-group-leaving` and `inert`
388
531
  ([miswart](./miswarts.md#leaving-rows-are-inert)), and is removed once its
389
- animations finish, at once when there are none. `lb-row-requested` is
390
- cleared at every landing; the others when the hub next touches the row.
532
+ animations finish, at once when there are none.
391
533
 
392
534
  A row is moved when its values changed and the rows whose values did not
393
535
  change now stand around it differently. A row it passed is not stamped,
@@ -398,6 +540,11 @@ and a change of the user's order, which changes no values, stamps none.
398
540
  [lb-row-leaving] { animation: depart 200ms forwards; }
399
541
  ```
400
542
 
543
+ A row never travels between its two places, since that takes measuring, and
544
+ measuring takes script. Scrolling to a row is script too: a custom element
545
+ reads `lb-row-requested` in
546
+ [`lbRowsLanded`](./custom-elements.md#holding-rows).
547
+
401
548
  ### What arrives later
402
549
 
403
550
  The hub keeps each query's last answer until the page changes, with any
@@ -426,7 +573,9 @@ insert form above: it sits inside `lb-query="roster"` so its request is for
426
573
 
427
574
  The hub stamps every element that holds a row template with
428
575
  `lb-query-row-count`, the number of live rows it shows. A row a filter
429
- hides is not counted. An empty query is a stylesheet rule:
576
+ hides is not counted. Beside it, `lb-query-unsorted-count` is the number of
577
+ rows out of order; see
578
+ [Rows the user is working in](#rows-the-user-is-working-in). An empty query is a stylesheet rule:
430
579
 
431
580
  ```html
432
581
  <div lb-query="roster">
@@ -451,6 +600,39 @@ hides is not counted. An empty query is a stylesheet rule:
451
600
  The element carrying `lb-query` is a `<div>` here rather than the `<ul>`, so
452
601
  that the message is inside it and one rule reaches both.
453
602
 
603
+ ### Master-detail
604
+
605
+ A master is a `row` query and its detail is a `rows` query, as two names
606
+ answered side by side. A column never holds rows.
607
+
608
+ ```html
609
+ <section lb-query="invoice">
610
+ <h2 lb-column="number"></h2>
611
+ <span lb-column="customer"></span>
612
+ </section>
613
+
614
+ <table>
615
+ <tbody lb-query="invoiceLines">
616
+ <template><tr><td lb-column="item"></td><td lb-column="amount"></td></tr></template>
617
+ </tbody>
618
+ </table>
619
+ ```
620
+
621
+ Many masters, each with its own detail, is one `rows` query of joined rows:
622
+ each detail row carries its master's columns. A [group template](#groups)
623
+ shows each master as a group, ordered by the master's key first.
624
+
625
+ A query nested in another query's row template receives the same rows in
626
+ every live row. That serves a picker offering the same choices on every
627
+ row, and is not a way to show a different detail per row; see
628
+ [What arrives later](#what-arrives-later).
629
+
630
+ Which master a page shows is a query parm, which a query reads off `ctx`,
631
+ since a query takes no argument from the browser; see
632
+ [Query parms](./chrome.md#query-parms). When a write creates or removes the
633
+ master, the handler moves the URL; see
634
+ [Moving the URL](./page-files.md#moving-the-url).
635
+
454
636
  ## Conditional rendering
455
637
 
456
638
  A page holds every element it can show. Write `lb-show` on an element,
@@ -565,10 +747,14 @@ answers with response items, which land like any others.
565
747
  | A control | `change` |
566
748
  | Any other element | `click` |
567
749
 
568
- A click inside an element carrying `lb-request` commits it unless the click
569
- lands on interactive content between them: a link, a button, a control or a
570
- label, as HTML defines interactive content. A click into an input inside a
571
- deletable row focuses the input.
750
+ A click inside an element carrying `lb-request` commits it, as long as the
751
+ element is inside `<lb-hub>` and no interactive content lies between them.
752
+ Interactive content is HTML's: `<a href>`, `<button>`, `<input>` other than
753
+ hidden, `<select>`, `<textarea>`, `<label>`, `<details>`, `<iframe>`,
754
+ `<embed>`, `<audio controls>`, `<video controls>` and `<img usemap>`, plus
755
+ any form-associated custom element. A click on one of them belongs to it: a
756
+ click into an input inside a deletable row focuses the input and deletes
757
+ nothing.
572
758
 
573
759
  A submit button with a form owner commits with its form. On `submit`, the
574
760
  hub sends the submitter's `lb-request` when the submitter carries one, and
@@ -602,7 +788,10 @@ carrying `lb-request`:
602
788
 
603
789
  The hub gathers from controls only, and skips a control that belongs to a
604
790
  query nested inside the group, except that a form gathers the rows of a
605
- query nested in its own; see [A form over a set of rows](#a-form-over-a-set-of-rows). A form owner includes a control outside the
791
+ query nested in its own; see [A form over a set of rows](#a-form-over-a-set-of-rows).
792
+ An element carrying `lb-query` and `lb-column` both, such as a picker,
793
+ belongs to the row around it, and is gathered with it and sends for that
794
+ row's query. A form owner includes a control outside the
606
795
  form that names it with the HTML `form` attribute, which is how a table row,
607
796
  where a form cannot go, sends one:
608
797
 
@@ -674,8 +863,10 @@ query's name, in `rows`:
674
863
  The list of deduction types lives only in the database: the nested query
675
864
  answers it, and the form shows one input per row.
676
865
 
677
- - Every live row of the nested query goes, whatever its values. What an
678
- empty value means, such as no deduction of that type, is the handler's.
866
+ - Every live row of the nested query that holds a control goes, whatever
867
+ its values. What an empty value means, such as no deduction of that type,
868
+ is the handler's. A picker's options hold no control, so a picker in the
869
+ form sends its own value and none of its rows.
679
870
  - One level only. A query nested inside a nested row shows the same rows in
680
871
  every row around it, so it is not gathered.
681
872
  - An insert, an update and a declared request carry `rows`. A delete does
@@ -685,7 +876,9 @@ answers it, and the form shows one input per row.
685
876
  - A live row gathers nothing nested, as before.
686
877
 
687
878
  The handler for the form's query receives `rows` beside `values`, and writes
688
- the master and its details together; see [crud](./page-files.md#crud).
879
+ the master and its details together; see [crud](./page-files.md#crud). The
880
+ server checks that `rows` holds string keys and string values by query, and
881
+ ignores a request whose `rows` does not.
689
882
 
690
883
  A nested query answers once for the page, so its rows are the same wherever
691
884
  they show. The form enters a new master with its details, or edits the
@@ -722,9 +915,14 @@ holds what a submit will gather:
722
915
  </section>
723
916
  ```
724
917
 
725
- The same holds for a request a custom element sends carrying `key: ""`.
726
- An update or delete with no row around it at all has no key either, and
727
- that is reported to the console.
918
+ The same holds for a request a custom element sends carrying `key: ""`. No
919
+ ancestor sees a request not sent, and it gets no `lb-request-done`. An
920
+ update or delete with no row around it at all has no key either, and that
921
+ is reported to the console.
922
+
923
+ A field the request already carries is its issuer's and is kept, so a
924
+ custom element that dispatches its own request with `values` is not
925
+ gathered over.
728
926
 
729
927
  ### Inserting, updating and deleting rows
730
928
 
@@ -741,6 +939,10 @@ The hub sends one of these only when it has what the table lists, and
741
939
  otherwise reports to the console. An insert or an update that gathers
742
940
  nothing is not sent.
743
941
 
942
+ For a query the hub serves, `lb-url` or `lb-choices`, the hub answers
943
+ `lb-row-update` itself, with no round trip; see
944
+ [Query parms](./chrome.md#query-parms) and [Choices](#choices).
945
+
744
946
  A key comes from a live row, or from an element a `row` landed on, so all
745
947
  three work against either kind:
746
948
 
@@ -804,11 +1006,13 @@ round trip, including columns no element in the table shows. The user
804
1006
  edits a row they can see, so the row is on the page. A row that has to be
805
1007
  fetched to be edited is another page, with that row as its master.
806
1008
 
807
- The hub stamps the form with the row's key, so the form's request carries
808
- it, and stamps the row `lb-row-editing`. New stamps an empty key, making the
809
- form [a row not yet entered](#a-row-not-yet-entered), and resets every
810
- control to its default. The form's state is in its `lb-key-value`
811
- stamp:
1009
+ Pointing a form first empties it: every column it names lands as null, and
1010
+ the form resets, so each control returns to its default. Edit then lands
1011
+ the row on the form, stamps the form with the row's key, so the form's
1012
+ request carries it, and stamps the row `lb-row-editing`. New stamps an
1013
+ empty key, making the form [a row not yet entered](#a-row-not-yet-entered).
1014
+ Both focus the form's first control. The form's state is in its
1015
+ `lb-key-value` stamp:
812
1016
 
813
1017
  | `lb-key-value` | The form is |
814
1018
  |----------------|-----------------------------|
@@ -851,6 +1055,11 @@ landing again lands nothing on it, and what the user has typed stays.
851
1055
  A button inside a form that carries a command is `type="button"`, or it
852
1056
  submits the form instead.
853
1057
 
1058
+ A command the hub cannot carry out is reported to the console: Edit from
1059
+ outside a live row of the form's query, Edit for a key the hub holds no row
1060
+ for, and any of these three commands aimed at an element that is not a form
1061
+ carrying `lb-query`.
1062
+
854
1063
  ### Selecting rows
855
1064
 
856
1065
  A checkbox carrying `lb-select` in a row template selects the live row it
@@ -927,7 +1136,7 @@ A button carries no value, so a declared request from one sends the values
927
1136
  of the live row or form it sits in, and the server computes the rest.
928
1137
 
929
1138
  A request name beginning with `lb-` that is not one of the three is refused
930
- by the builder.
1139
+ by the builder in markup, and by the hub when a script dispatches it.
931
1140
 
932
1141
  ### Request state
933
1142
 
@@ -992,14 +1201,23 @@ submit, as for any invalid control, so the user edits before trying again.
992
1201
 
993
1202
  A form-associated custom element takes a column's message when it has
994
1203
  `setCustomValidity` and `reportValidity`; see
995
- [Being a control](./custom-elements.md#being-a-control).
1204
+ [Being a control](./custom-elements.md#being-a-control). A request for
1205
+ several rows takes the whole-request message only. A column the request
1206
+ gathered no control for is reported to the console.
1207
+
1208
+ On the wire a refusal is status 422 with `{ refusal: { message?, columns? } }`;
1209
+ see [The data channel](./server.md#the-data-channel).
996
1210
 
997
1211
  ### The request event
998
1212
 
999
1213
  The hub dispatches every request as a bubbling `lb-request` event from the
1000
1214
  element that committed, with the request as its `detail`, before sending it.
1001
- An ancestor may stop the event, and the request is not sent. A custom
1002
- element may dispatch the event itself; see
1215
+ The hub completes the request on the event's way down, so every ancestor
1216
+ sees it whole on the way up. An ancestor may stop it, and the request is
1217
+ not sent: the event is not cancelable, so an interceptor calls
1218
+ `stopPropagation`, not `preventDefault`. That is how a confirmation wrapper
1219
+ works without the wrapped element knowing about it. A custom element may
1220
+ dispatch the event itself; see
1003
1221
  [Sending a request](./custom-elements.md#sending-a-request).
1004
1222
 
1005
1223
  Once the answer has landed, or the round trip has failed, the hub calls
@@ -1008,6 +1226,37 @@ then dispatches a bubbling `lb-request-done` event from the same element.
1008
1226
  Both carry the request and what landed because of it, or the error; see
1009
1227
  [Hearing how it turned out](./custom-elements.md#hearing-how-it-turned-out).
1010
1228
 
1229
+ ### The round trip
1230
+
1231
+ A request the hub sends has ten seconds to come back. Past that the hub
1232
+ aborts it and treats it as a failure, since `fetch` imposes no deadline of
1233
+ its own. An application cannot change the deadline.
1234
+
1235
+ A round trip fails on a server error, on a network failure, or on that
1236
+ deadline, and all three stamp `lb-request-error` on the element that sent
1237
+ the request. A refusal is not a failure.
1238
+
1239
+ After a successful `lb-row-insert` gathered from a form, the hub resets the
1240
+ form, after it lands the response. A failed insert resets nothing, so the
1241
+ entry can be corrected. An `lb-row-update` resets nothing: the row it sent
1242
+ lands back on its controls.
1243
+
1244
+ A reset of any form, whether the hub's, a reset button's or a script's,
1245
+ returns its controls to their defaults, and the hub removes
1246
+ `lb-column-value` from each of them. A cancelled reset keeps both.
1247
+
1248
+ A page load that fails lands nothing and is reported to the console. A load
1249
+ started by a request for `lb-url` stamps the element that sent it, as any
1250
+ request stamps its element.
1251
+
1252
+ A response answers for the URL its request was sent from, path and query
1253
+ string both. One that returns after the URL has moved lands nothing, since
1254
+ it describes what the user is no longer looking at: a request's answer,
1255
+ including any `url()` it carries, and a page load overtaken by a newer one.
1256
+ A write whose answer is dropped this way has still happened. Its
1257
+ `lb-request-done` carries no items and no error, and an insert still resets
1258
+ its form.
1259
+
1011
1260
  ## The URL
1012
1261
 
1013
1262
  The hub serves one query of its own, `lb-url`: one row holding the path, the
@@ -1056,7 +1305,9 @@ The developer writes `lb-` attributes in markup, and script never assigns
1056
1305
  them. The builder checks the markup of the chrome and of every page, after
1057
1306
  expansion, and refuses to build:
1058
1307
 
1059
- - an `lb-` attribute that is not one of the sixteen the developer writes
1308
+ - an `lb-` attribute that is not one of the seventeen the developer writes;
1309
+ `lb-exp-slot` and `lb-exp-template` are consumed by expansion and never
1310
+ reach the check
1060
1311
  - `lb-request` naming an `lb-` request other than the three Loadbare provides
1061
1312
  - `lb-query` naming a query the page does not declare; every page and the
1062
1313
  chrome may name `lb-url` and `lb-choices`, and the chrome names no other
@@ -1067,6 +1318,9 @@ expansion, and refuses to build:
1067
1318
  a form's own when the form carries `lb-query`, and otherwise the nearest
1068
1319
  `lb-query` around the element
1069
1320
  - `lb-url-link` on anything but an `<a>`
1321
+ - `lb-url-parm` on anything but an `<a>` carrying `lb-url-link`, naming no
1322
+ column, naming columns not separated by commas or one beginning `lb-`, or
1323
+ with no row around it
1070
1324
  - `lb-url-push` on an element without `lb-request`
1071
1325
  - `lb-url-unknown` on anything but a `<dialog>`, or outside `<lb-hub>`
1072
1326
  - `lb-show` on a `<template>`, or on a row template's root
@@ -1076,8 +1330,24 @@ expansion, and refuses to build:
1076
1330
  no column
1077
1331
  - `lb-select` on anything but `<input type="checkbox">`, outside a row
1078
1332
  template, or beside `lb-column` or `lb-request`
1079
- - a `command` beginning `--lb-` that is not one of the three commands
1080
- - one of the three commands with no `commandfor`
1333
+ - a `command` beginning `--lb-` that is not one of the four commands
1334
+ - one of the four commands with no `commandfor`
1335
+ - `lb-group` on anything but a `<template>`, or on one holding no
1336
+ `<template>` for its contents
1337
+ - an aggregate on a `<template>`, or any but `lb-count` naming no column
1338
+ - a chrome without exactly one `<lb-hub>` and exactly one `<main>`, a
1339
+ `<main>` outside `<lb-hub>`, or a `<main>` holding anything but whitespace
1340
+ - a page carrying an `<lb-hub>` or a `<main>`
1341
+
1342
+ The builder also reads the script of every custom element it ships, other
1343
+ than `<lb-hub>`, and refuses one that declares a method, accessor or field
1344
+ whose name is `lb`, a capital, and anything Loadbare does not call; see
1345
+ [Reserved methods](./names.md#reserved-methods).
1346
+
1347
+ `lb-column` with no ancestor row is allowed: the hub gathers from it.
1348
+
1349
+ Script never assigns an `lb-` attribute, so the markup states every one
1350
+ that exists, and checking the markup is sound.
1081
1351
 
1082
1352
  To know what each page declares, the builder loads its `.queries.ts` and
1083
1353
  `.requests.ts` modules, so their import-time code runs during the build, as