@loadbare/app 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/build/assemble.d.ts.map +1 -1
  2. package/dist/build/assemble.js +87 -1
  3. package/dist/build/assemble.js.map +1 -1
  4. package/dist/build/cli.js +2 -1
  5. package/dist/build/cli.js.map +1 -1
  6. package/dist/build/elements.d.ts +9 -1
  7. package/dist/build/elements.d.ts.map +1 -1
  8. package/dist/build/elements.js +50 -0
  9. package/dist/build/elements.js.map +1 -1
  10. package/dist/build/origins.d.ts +2 -0
  11. package/dist/build/origins.d.ts.map +1 -1
  12. package/dist/build/origins.js +1 -1
  13. package/dist/build/origins.js.map +1 -1
  14. package/dist/core/lb-constants.d.ts +34 -1
  15. package/dist/core/lb-constants.d.ts.map +1 -1
  16. package/dist/core/lb-constants.js +105 -0
  17. package/dist/core/lb-constants.js.map +1 -1
  18. package/dist/core/lb-types.d.ts +74 -18
  19. package/dist/core/lb-types.d.ts.map +1 -1
  20. package/dist/core/lb-types.js +74 -11
  21. package/dist/core/lb-types.js.map +1 -1
  22. package/dist/hub/lb-apply.d.ts +57 -18
  23. package/dist/hub/lb-apply.d.ts.map +1 -1
  24. package/dist/hub/lb-apply.js +661 -63
  25. package/dist/hub/lb-apply.js.map +1 -1
  26. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  27. package/dist/hub/lb-hub.browser.js +400 -33
  28. package/dist/hub/lb-hub.browser.js.map +1 -1
  29. package/dist/server/lb-express.d.ts +19 -7
  30. package/dist/server/lb-express.d.ts.map +1 -1
  31. package/dist/server/lb-express.js +62 -26
  32. package/dist/server/lb-express.js.map +1 -1
  33. package/dist/server/lb-server.d.ts +80 -11
  34. package/dist/server/lb-server.d.ts.map +1 -1
  35. package/dist/server/lb-server.js +185 -33
  36. package/dist/server/lb-server.js.map +1 -1
  37. package/docs/TECHREF-1.0.md +584 -111
  38. package/docs/comparison.md +17 -13
  39. package/docs/possible-ideas.md +308 -0
  40. package/docs/reference/chrome.md +7 -1
  41. package/docs/reference/custom-elements.md +134 -39
  42. package/docs/reference/data-binding.md +405 -17
  43. package/docs/reference/page-files.md +101 -19
  44. package/docs/reference/server.md +24 -0
  45. package/docs/reference/widgets.md +118 -25
  46. package/docs/roadmap.md +17 -6
  47. package/docs/testing.md +21 -10
  48. package/package.json +2 -3
  49. package/skills/loadbare-app/SKILL.md +92 -30
  50. package/skills/loadbare-app/references/TECHREF-1.0.md +584 -111
  51. package/skills/loadbare-app/references/chrome.md +7 -1
  52. package/skills/loadbare-app/references/custom-elements.md +134 -39
  53. package/skills/loadbare-app/references/data-binding.md +405 -17
  54. package/skills/loadbare-app/references/page-files.md +101 -19
  55. package/skills/loadbare-app/references/server.md +24 -0
  56. package/skills/loadbare-app/references/widgets.md +118 -25
@@ -22,38 +22,28 @@ a value is rendered by browser coercion when it lands on an HTML text
22
22
  element or a control, and by whatever the custom element does with
23
23
  `lb-column-value` when it lands on one that is not a control.
24
24
 
25
+ Two places read a value's type today. The [order](#order) compares values
26
+ by their JSON type, and an [aggregate](#aggregates) reads a sum or an
27
+ average from a JSON number or a string holding a plain decimal.
28
+
25
29
  We will then see if a useful solution emerges that Loadbare/app should
26
30
  handle.
27
31
 
28
32
  ### Form controls
29
33
 
30
- - **Checkboxes and radio buttons are not implemented.** A value does not
31
- land on one and the hub does not gather one. Landing needs a decision on
32
- what counts as checked, which waits on [Data types](#data-types), and a
33
- radio group is several elements answering to one column.
34
-
35
- ### Run-time stamps
36
-
37
- The hub stamps `lb-query-row-count`, `lb-request-pending` and
38
- `lb-request-error` for a stylesheet to read. Their consumer is CSS rather
39
- than code.
40
-
41
- - **Decide whether an unarrived value needs a signal.** An element whose
42
- column has not landed carries no `lb-column-value`. Recommend confirming
43
- that as the signal and adding nothing.
34
+ - **Radio buttons are not implemented.** A value does not land on one and
35
+ the hub does not gather one. A radio group is several elements answering
36
+ to one column.
44
37
 
45
38
  ### Custom element hooks
46
39
 
47
40
  Loadbare/app owns every method name beginning with `lb` on a custom element.
48
41
  That decision is firm; what the set contains is not.
49
42
 
50
- - **Decide what the build does with an unknown `lb*` method.** An element
51
- carrying a method beginning with `lb` that Loadbare/app does not define may
52
- be an error, a warning, or ignored.
53
43
  - **Decide whether a custom element may receive a whole row.** A column
54
44
  lands one element at a time and offers no escape hatch. An optional
55
- `lbAcceptRow` would sit beside `lbPlaceRow` and `lbRowsLanded`, and this is
56
- the most likely first request from a custom element author.
45
+ `lbAcceptRow` would sit beside `lbRowsLanded`, and this is the most likely
46
+ first request from a custom element author.
57
47
 
58
48
  ### Nested queries
59
49
 
@@ -77,38 +67,9 @@ But the allowed values may depend on other values in the row.
77
67
  </tbody>
78
68
  ```
79
69
 
80
- ### Who creates and orders rows
81
-
82
- The hub creates every live row from a row template and places it in the
83
- query's order, unless the element holding the rows has `lbPlaceRow`, in
84
- which case the custom element decides where each row goes. Custom elements
85
- also create rows of their own: `lb-table` clones a ghost row per section and
86
- builds each section's heading. Neither side owns creation or order.
87
-
88
- - **Decide who creates rows and who orders them.** A patch cannot say where
89
- a new row goes, so it lands where the host puts it: in order under
90
- `lb-table` with `data-sort`, last in plain markup and in `lb-options`.
91
- Whether an insert may answer with a patch therefore depends on markup the
92
- server cannot see, and the server cannot refuse an insert that refreshes
93
- its own query the way it refuses an update or a delete.
94
- - **Decide whether landing all rows moves rows already in place.** It moves
95
- every row today, which takes focus from the control the user is in, and
96
- `lbPlaceRow` does the same.
97
-
98
70
  ### The server API
99
71
 
100
- - **Give the chrome a way to state its own queries.** A custom element in
101
- the chrome naming a query forces every page to declare that query, since
102
- the page load covers one page's set. `lb-url` shows the pattern from the
103
- framework side, and an application has no equivalent. Recommend a
104
- chrome-level query set on `createHub`.
105
- - **State that only `createHub` implements `Hub`.** Otherwise a third call
106
- breaks anyone who wrote the interface by hand.
107
- - **Decide the options-argument shape once.** Global hooks, transaction
108
- wrapping, error handling and a CSRF token all want the same trailing
109
- parameter on `createHub` and `hubRoutes`. Build none of them, but pick
110
- where they go.
111
- - **Land `staticRoutes`.** See NEXT.md. It closes the last place where an
72
+ - **Land `staticRoutes`.** It closes the last place where an
112
73
  application hand-writes facts the builder owns.
113
74
 
114
75
  ### The builder
@@ -123,9 +84,6 @@ builds each section's heading. Neither side owns creation or order.
123
84
  definition would let the builder ship only what survives expansion.
124
85
  Recommend deferring the mechanism and reserving the configuration key, so
125
86
  that it lands as an opt-in rather than as a silent drop.
126
- - **Land the remaining validations.** One `lb-hub` and one empty `<main>`.
127
- Recommend landing these now, because refusing markup that used to build is
128
- the kind of change 1.0 gives up.
129
87
  - **Decide whether the build validates HTML.** A page can be well formed and
130
88
  still break HTML's rules for what an element may hold, such as a `<div>`
131
89
  inside a `<p>`. The parser rearranges it silently, the same way at build
@@ -141,9 +99,6 @@ builds each section's heading. Neither side owns creation or order.
141
99
  None of this is surface. It is listed here because trimming it is free
142
100
  today and breaking after 1.0.
143
101
 
144
- - **Drop `./build` from the exports map.** Nothing in the repository imports
145
- it, and nothing outside the CLI should call `resolveElements` or
146
- `clientEntrySource`.
147
102
  - **Audit what the hub does.** It appears to have routines that go beyond
148
103
  what the `lb-*` attribute namespace specifies.
149
104
  - **Keep the rest as constants.** The endpoint, `index` and the timeout are
@@ -455,9 +410,10 @@ The markup names a query and the columns it shows. The kind and the key are
455
410
  the server's, and the markup states neither.
456
411
 
457
412
  The row template is the first `<template>` among the descendants of an
458
- element with `lb-query`, outside any nested `lb-query`. A live row is an
459
- element the hub cloned from a row template for one row, and carries
460
- `lb-row-live`.
413
+ element with `lb-query`, outside any nested `lb-query`, or, when that
414
+ template is a [group template](#groups), the innermost template inside it.
415
+ A live row is an element the hub cloned from a row template for one row,
416
+ and carries `lb-row-live`.
461
417
 
462
418
  | Kind | Row template | The hub |
463
419
  | ------ | ------------ | ----------------------------------- |
@@ -496,7 +452,8 @@ A query that arrives with nowhere to land is reported to the console.
496
452
  | Any other element | Its text content |
497
453
 
498
454
  A control is an `<input>`, `<select>` or `<textarea>`, or a form-associated
499
- custom element with a `value` property that fires `change`. A
455
+ custom element with a `value` property that fires `change`. A checkbox is a
456
+ control too, by the rule below. A
500
457
  form-associated custom element declares `static formAssociated = true`.
501
458
 
502
459
  The hub stamps `lb-column-value` with the value on every element it sets.
@@ -507,9 +464,21 @@ renders `lb-column-value` itself.
507
464
  The hub gathers from the same controls it lands on, so a value read back is
508
465
  the one that landed.
509
466
 
510
- An `<input>` of type `checkbox`, `radio`, or `file` receives nothing, not
511
- even `lb-column-value`, and the hub reports it to the console. The hub does
512
- not gather one either.
467
+ A checkbox's state is `checked`, not `value`: HTML's quirk, and so the
468
+ hub's, so that every other part of the vocabulary treats it as any control.
469
+ The hub checks it by the rule `lb-show` is on by: `null` and `false` are
470
+ unchecked, and every other value is checked, with no string read. It
471
+ gathers `"true"` or `"false"`, so an unchecked box still says so, where a
472
+ native form submission leaves it out. The checkbox's own `value`
473
+ attribute means nothing to the hub.
474
+
475
+ ```html
476
+ <input type="checkbox" lb-column="active" lb-request="lb-row-update" />
477
+ ```
478
+
479
+ An `<input>` of type `radio` or `file` receives nothing, not even
480
+ `lb-column-value`, and the hub reports it to the console. The hub does not
481
+ gather one either.
513
482
 
514
483
  A `<select>` with no option for the value shows no selection.
515
484
 
@@ -538,14 +507,15 @@ with `[lb-row-live]`, and never with `[lb-key-value]`.
538
507
  A key is unique within a query. Two rows with one key in the same answer,
539
508
  or two live rows showing one key, are reported on the console.
540
509
 
541
- All rows decide membership and order: every row is placed in the order
542
- given, and a live row whose key did not arrive is removed. A patch touches
543
- only the rows it names and leaves every other live row's contents and
544
- position alone.
510
+ All rows decide membership: a live row whose key did not arrive leaves —
511
+ see [What happened to a row](#what-happened-to-a-row). A patch touches only
512
+ the rows it names, and leaves every other live row's contents alone.
545
513
 
546
- A new live row lands immediately before the row template, so rows
547
- accumulate in the order they arrive. A custom element carrying `lb-query`
548
- and a row template may place them itself — see [Row hooks](#row-hooks).
514
+ The hub places every live row by the query's [order](#order), immediately
515
+ before the row template, or before its clone in the row's
516
+ [group](#groups). A row moves only when the order puts it somewhere else,
517
+ so all rows landing again unchanged move nothing. A live row already in the
518
+ markup when the first answer lands is one of the query's rows.
549
519
 
550
520
  After every landing the hub stamps the element with `lb-query-row-count`,
551
521
  the number of live rows it holds. The server answers with rows and says
@@ -560,6 +530,162 @@ sent. It makes an empty query a stylesheet rule:
560
530
 
561
531
  A row missing its key column is reported to the console and not landed.
562
532
 
533
+ #### Order
534
+
535
+ A `rows` query's order is a rule over its columns. The server declares it
536
+ with the query, and the URL may carry the user's own:
537
+
538
+ ```ts
539
+ accounts: rows("id", (ctx) => ctx.db.accounts(), {
540
+ order: "category_order,account_name",
541
+ }),
542
+ ```
543
+
544
+ An order is columns separated by commas. A column written after `-` sorts
545
+ descending. The query parm `lb-order-<query>` replaces the declared order
546
+ for that query: `/coa?lb-order-accounts=-account_name`. With neither, rows
547
+ show in the order the server sent them, and a patch's new rows go last.
548
+
549
+ | Values | Compare |
550
+ | -------------------- | ------------------------------------------ |
551
+ | Two numbers | Numerically |
552
+ | Two strings | As the user's language orders them |
553
+ | Two booleans | `false` before `true` |
554
+ | Two different types | Booleans, then numbers, then strings |
555
+ | `null` or missing | After everything ascending, first descending |
556
+
557
+ A number sent as a string sorts as a string, so a column the order reads
558
+ numerically arrives as a JSON number. Rows that compare equal keep the
559
+ order they arrived in, and a patch's new rows come after the rows already
560
+ showing.
561
+
562
+ The hub sorts the rows it has, so a control that writes the order parm
563
+ re-sorts the page with no round trip:
564
+
565
+ ```html
566
+ <div lb-query="lb-url">
567
+ <select lb-column="lb-order-accounts" lb-request="lb-row-update">
568
+ <option value="">By category</option>
569
+ <option value="account_name">By name</option>
570
+ </select>
571
+ </div>
572
+ ```
573
+
574
+ A query that pages or limits its rows decides membership by its order, so
575
+ it reads the parm itself, from the parms `contextFor` receives, and declares
576
+ `serverSortedByUrl: true` beside `order`. A change of the user's order then
577
+ loads the page again. The hub still places the rows it receives by the same
578
+ order, so a patch lands in its place.
579
+
580
+ #### Groups
581
+
582
+ A group is a run of rows sharing the order's leading term. A group
583
+ template is a `<template lb-group>` in the row template's place, holding the
584
+ group's heading and exactly one nested `<template>`, which is where the
585
+ group's contents go: the next group template, or the row template. The nth
586
+ group template breaks on the order's nth term, so groups nest to any depth.
587
+
588
+ ```html
589
+ <table lb-query="accounts">
590
+ <thead><tr><th>Account</th></tr></thead>
591
+ <template lb-group>
592
+ <tbody>
593
+ <tr><th scope="rowgroup" lb-column="category"></th></tr>
594
+ <template><tr><td lb-column="account_name"></td></tr></template>
595
+ </tbody>
596
+ </template>
597
+ </table>
598
+ ```
599
+
600
+ The hub clones the group template where the order breaks and puts the
601
+ group's contents immediately before the clone of its nested template. The
602
+ markup's nesting decides the shape:
603
+
604
+ | The nested template is | The group's contents land | Example |
605
+ | --------------------------------- | ----------------------------------- | -------------------------------- |
606
+ | Inside the heading's element | Inside it | `<tbody>`, `<li>`, `<details>` |
607
+ | Beside the heading | After the heading, as its siblings | A heading `<tr>` in one `<tbody>` |
608
+
609
+ `<tbody>` and `<optgroup>` do not nest, so a table's levels below the first
610
+ are heading rows beside their rows. Content after the nested template lands
611
+ after the group's rows, which is where a group's footer goes.
612
+
613
+ A group's heading is filled from its first row, the same way a live row is
614
+ filled, so it shows any column of the row. The example breaks on
615
+ `category_order` and shows `category`. The hub fills it from no other row.
616
+
617
+ The hub stamps each top-level element of a group's clone with
618
+ `lb-group-live`, `lb-group-column`, the column it breaks on, and
619
+ `lb-group-value`, its value there. A row creates its group, and a group
620
+ leaves with its last row. Groups never move: when a change of order puts
621
+ the groups at a level in another order, each of them is made again.
622
+
623
+ A group template with no nested template is reported to the console. More
624
+ group templates than the order has terms show each extra level as one
625
+ group, and are reported once.
626
+
627
+ #### Aggregates
628
+
629
+ An aggregate attribute sets an element from the rows of the nearest group
630
+ around it, or of the whole list outside any group.
631
+
632
+ | Attribute | Sets the element to |
633
+ | --------------- | --------------------------------------------- |
634
+ | `lb-count` | The number of rows |
635
+ | `lb-sum="col"` | The sum of `col` |
636
+ | `lb-avg="col"` | The average of `col` |
637
+ | `lb-min="col"` | The least value of `col`, by the [order](#order)'s comparison |
638
+ | `lb-max="col"` | The greatest, likewise |
639
+
640
+ ```html
641
+ <tr><td><span lb-count></span> accounts</td><td lb-sum="balance"></td></tr>
642
+ ```
643
+
644
+ It lands as a column does, with `lb-column-value`. A sum and an average
645
+ are exact decimals, read from JSON numbers and from strings holding a plain
646
+ decimal, as Postgres sends `numeric`. A sum has as many places as the most
647
+ precise value, and an average up to two more. `null` and empty values are
648
+ left out, as SQL leaves them out. Any other value is left out and reported
649
+ once. The hub does not format the result.
650
+
651
+ An aggregate covers the rows the browser holds. Where the true total
652
+ matters beyond what the page shows, it is a row the server answers.
653
+
654
+ #### What happened to a row
655
+
656
+ Every change the hub makes to a list is one a stylesheet can see, so an
657
+ application animates with CSS alone and the hub starts no animation.
658
+
659
+ | Attribute | On | Means |
660
+ | ------------------ | -------------------- | -------------------------------------------------- |
661
+ | `lb-row-created` | A live row | Created when the hub last touched it |
662
+ | `lb-row-changed` | A live row | Its values changed when the hub last touched it |
663
+ | `lb-row-moved` | A live row | The order put it somewhere else when last touched |
664
+ | `lb-row-requested` | A live row | `created`, `changed` or `moved` by this page's request |
665
+ | `lb-row-leaving` | A row on its way out | No longer live |
666
+ | `lb-group-leaving` | A group on its way out | No longer live |
667
+
668
+ The hub touches a row when an answer names it. `lb-row-created`,
669
+ `lb-row-changed` and `lb-row-moved` are cleared when it next does, and one
670
+ applied again is removed and added as two changes, so an animation keyed to
671
+ it plays again. `lb-row-requested` is cleared at every landing of the
672
+ row's query, and set only by a landing that answers a request from the page.
673
+
674
+ A row or a group that leaves loses `lb-row-live` or `lb-group-live`, takes
675
+ `lb-row-leaving` or `lb-group-leaving` and `inert`, and is removed once the
676
+ animations running on it have finished, at once when there are none. A
677
+ moved row stays live and moves. A copy of it stays behind where it was,
678
+ leaving.
679
+
680
+ ```css
681
+ [lb-row-created] { animation: arrive 200ms; }
682
+ [lb-row-leaving] { animation: depart 200ms forwards; }
683
+ ```
684
+
685
+ A row never travels between the two places, since that takes measuring, and
686
+ measuring takes script. Scrolling to a row is script too: a custom element
687
+ reads `lb-row-requested` in [`lbRowsLanded`](#row-hooks).
688
+
563
689
  #### Displaying by condition
564
690
 
565
691
  `lb-show` names the column that decides whether an element is present.
@@ -624,9 +750,9 @@ answered side by side. A column never holds rows.
624
750
  ```
625
751
 
626
752
  Many masters, each with its own detail, is one `rows` query of joined rows:
627
- each detail row carries its master's columns. A custom element's
628
- `lbPlaceRow` groups them for display, as `lb-table` builds sections and
629
- `lb-options` builds `<optgroup>`s.
753
+ each detail row carries its master's columns. A [group
754
+ template](#groups) shows each master as a group, ordered by the master's
755
+ key first.
630
756
 
631
757
  A query nested in another query's row template receives the same rows in
632
758
  every live row. That serves a picker offering the same choices on every
@@ -664,9 +790,13 @@ Every request has one shape, on the event and on the wire:
664
790
  { name: "lb-row-update", query: "roster", key: "42", values: { name: "Ann" } }
665
791
  { name: "lb-row-delete", query: "roster", key: "42" }
666
792
  { name: "mailRoster", query: "roster", key: "42" }
793
+ { name: "lb-row-update", query: "roster", keys: ["42", "43"], values: { role: "lead" } }
794
+ { name: "lb-row-insert", query: "invoice", values: { memo: "May" }, rows: { lines: [{ key: "1", values: { qty: "3" } }] } }
667
795
  ```
668
796
 
669
- The hub sends `query`, `key` and `values` as present.
797
+ The hub sends `query`, `key` and `values` as present, and `keys` in place of
798
+ `key` for a request acting on the rows selected — see
799
+ [Selecting rows](#selecting-rows).
670
800
 
671
801
  #### Committing
672
802
 
@@ -734,7 +864,8 @@ controls belong to a form that cannot wrap a `<tr>`:
734
864
 
735
865
  The hub gathers from controls only. It skips a control whose nearest
736
866
  ancestor `lb-query` is a descendant of the live row or form, since that
737
- control belongs to a nested query. An element carrying `lb-query` and
867
+ control belongs to a nested query, except that a form gathers the rows of a
868
+ query nested in its own — see [A form over a set of rows](#a-form-over-a-set-of-rows). An element carrying `lb-query` and
738
869
  `lb-column` both, such as a picker, belongs to the row around it and is
739
870
  gathered.
740
871
 
@@ -748,6 +879,61 @@ ancestor `lb-query` of the element carrying `lb-request`, and takes `key`
748
879
  from that element's nearest ancestor row when that row is of the same
749
880
  query.
750
881
 
882
+ #### A form over a set of rows
883
+
884
+ A form gathers the live rows of a query nested directly in its own query,
885
+ besides its own controls. Each goes as its key and the values of the
886
+ controls in it, under the nested query's name:
887
+
888
+ ```
889
+ { name: "lb-row-insert", query: "paychecks", values: { pay_date: "2026-10-02" },
890
+ rows: { deduction_types: [{ key: "401k", values: { amount: "50" } },
891
+ { key: "dental", values: { amount: "" } }] } }
892
+ ```
893
+
894
+ The form's own query is the one it carries, or else the nearest around it.
895
+ A nested query is gathered when its nearest ancestor `lb-query` is the
896
+ form's own, so one level only: a query nested deeper shows the same rows in
897
+ every row around it, and its keys would repeat. Every live row of it goes,
898
+ whatever its values, and only controls inside a live row of it.
899
+
900
+ An insert, an update and a declared request carry `rows`; a delete does
901
+ not. A form whose only controls are in nested rows sends for its own query,
902
+ keyed by the row landed on it or the row it is pointed at, with empty
903
+ `values`. A live row gathers nothing nested.
904
+
905
+ The handler for the form's query receives `rows` beside `values`, so the
906
+ master and its details are written together. The server checks that
907
+ `rows` holds string keys and string values by query, and ignores a request
908
+ whose `rows` does not.
909
+
910
+ A nested query answers once for the page, as anywhere — see
911
+ [Master-detail](#master-detail). The form enters a new master with its
912
+ details, or edits the master the page shows by a query parm, whose details
913
+ the nested query reads from the same parm. A pointed form fills its own
914
+ controls from the row it points at, and its nested rows stay the nested
915
+ query's answer.
916
+
917
+ #### A row not yet entered
918
+
919
+ A row whose key is the empty string is a new one, not yet entered. It
920
+ arises two ways: a `row` query answers with an empty key, as one reading
921
+ the master's key from an empty query parm does, or `--lb-new` points a form
922
+ (see [A form pointed at a row](#a-form-pointed-at-a-row)). Either way the
923
+ element carries `lb-key-value=""`.
924
+
925
+ The hub sends no `key` for it, whether the key came from where the element
926
+ sits or from a request a custom element dispatched. An `lb-row-insert` and
927
+ a declared request are sent without one. An `lb-row-update` and an
928
+ `lb-row-delete` are not sent, because nothing is on record to change, and
929
+ nothing is reported: it is the row's state, not a mistake. So a control
930
+ saving its own column on `change` saves on a row on record, and on a new
931
+ row only holds what a submit will gather. No ancestor sees a request not
932
+ sent, and it gets no `lb-request-done`.
933
+
934
+ An update or a delete with no row around it at all has no key either, and
935
+ is reported to the console, as any request missing what it needs is.
936
+
751
937
  A field the request already carries is its issuer's and is kept. A custom
752
938
  element that dispatches its own request with `values` is not gathered over.
753
939
 
@@ -759,7 +945,8 @@ element that dispatches its own request with `values` is not gathered over.
759
945
  | `lb-row-update` | `query`, `key`, `values` | `rowUpdate` |
760
946
  | `lb-row-delete` | `query`, `key` | `rowDelete` |
761
947
 
762
- The hub issues one of these only when it has what the table lists. An
948
+ `keys` stands in for `key`. The hub issues one of these only when it has
949
+ what the table lists. An
763
950
  `lb-row-insert` or `lb-row-update` that gathers nothing is not issued. A
764
951
  request that is not issued is reported to the console.
765
952
 
@@ -774,6 +961,125 @@ For a query the hub serves, the hub answers the request itself, with no
774
961
  round trip. The hub serves `lb-url`, and answers `lb-row-update` for it —
775
962
  see [Query parms](#query-parms).
776
963
 
964
+ #### A form pointed at a row
965
+
966
+ A set of rows the user edits or adds to has one form on the page, naming
967
+ the same query, outside every row. A button points it with HTML's
968
+ `commandfor`, naming the form's `id`, and `command`:
969
+
970
+ | Command | On a button | The hub |
971
+ | -------------- | --------------------------------- | ----------------------------------------- |
972
+ | `--lb-edit` | In a live row of the form's query | Fills the form from that row, keyed by it |
973
+ | `--lb-new` | Anywhere | Starts the form empty, for an insert |
974
+ | `--lb-release` | Anywhere | Lets the form go without saving |
975
+
976
+ HTML requires a custom command to begin with `--`, and fires it as a
977
+ `command` event on the form, which the hub hears.
978
+
979
+ ```html
980
+ <tbody lb-query="paychecks">
981
+ <template><tr>
982
+ <td lb-column="pay_date"></td>
983
+ <td><button type="button" commandfor="paycheck" command="--lb-edit">Edit</button></td>
984
+ </tr></template>
985
+ </tbody>
986
+ <button type="button" commandfor="paycheck" command="--lb-new">New</button>
987
+
988
+ <dialog>
989
+ <form id="paycheck" lb-query="paychecks" lb-request="lb-row-update">
990
+ <input lb-column="pay_date" type="date" />
991
+ <button type="submit">Save</button>
992
+ <button type="submit" lb-request="lb-row-insert">Save new</button>
993
+ <button type="button" commandfor="paycheck" command="--lb-release">Cancel</button>
994
+ </form>
995
+ </dialog>
996
+ ```
997
+
998
+ Which row a form edits is interaction state, not a view, so it is not a
999
+ query parm. The user edits only a row they can see, so Edit fills the form
1000
+ from the row as the hub holds it, every column the query answered, with no
1001
+ round trip. A row that must be fetched to be edited is another page, with
1002
+ that row as its master.
1003
+
1004
+ Pointing a form first empties it: every column it names lands as null, and
1005
+ the form resets, so each control returns to its default. Edit then lands
1006
+ the row on the form as on any element a `row` lands on, stamps the form
1007
+ `lb-key-value` with the row's key, and stamps the row `lb-row-editing`.
1008
+ Gathering then takes the key from the form. New stamps an empty key, making
1009
+ the form [a row not yet entered](#a-row-not-yet-entered). Both focus the
1010
+ form's first control.
1011
+
1012
+ | The form's `lb-key-value` | The form is |
1013
+ | ------------------------- | ------------------ |
1014
+ | A key | Editing that row |
1015
+ | Empty | Entering a new row |
1016
+ | Absent | Not in use |
1017
+
1018
+ A stylesheet shows the form's buttons by it, such as Save while it has a
1019
+ key and Save new while the key is empty.
1020
+
1021
+ A request from the form that succeeds lets it go, as `--lb-release` does:
1022
+ the form empties, and both stamps go. One that fails leaves the form
1023
+ pointed, as the user had it.
1024
+
1025
+ When the form is inside a `<dialog>`, Edit and New call `showModal()` on it,
1026
+ and letting go closes it. The dialog closing by any of HTML's means,
1027
+ Escape, `command="close"` or `method="dialog"`, lets the form go. A form in
1028
+ no dialog opens nothing; it is always showing, or a stylesheet shows it
1029
+ while it carries `lb-key-value`.
1030
+
1031
+ The form names a `rows` query and holds no row template, so that query's
1032
+ answers land nothing on it, and what the user typed survives a refresh.
1033
+
1034
+ A command the hub cannot carry out is reported to the console: Edit from
1035
+ outside a live row of the form's query, Edit for a key the hub holds no row
1036
+ for, and any command aimed at an element that is not a form carrying
1037
+ `lb-query`.
1038
+
1039
+ #### Selecting rows
1040
+
1041
+ A checkbox carrying `lb-select` in a row template selects the live row it
1042
+ sits in, and the hub stamps the row `lb-row-selected` while it is ticked.
1043
+ It is not a column: it carries no `lb-column` or `lb-request`, lands
1044
+ nothing, and is not gathered.
1045
+
1046
+ A request for the query that names no one row, neither from inside one of
1047
+ its rows nor from a form pointed at one, acts on the rows selected. When
1048
+ any `lb-select` checkbox of that query is showing, the hub adds the keys of
1049
+ those ticked as `keys`:
1050
+
1051
+ ```
1052
+ { name: "lb-row-update", query: "transactions", keys: ["4", "9"], values: { acct: "12" } }
1053
+ ```
1054
+
1055
+ - `lb-row-update` and `lb-row-delete` are sent with `keys`, and with none
1056
+ ticked are not sent, silently, as for [a row not yet
1057
+ entered](#a-row-not-yet-entered).
1058
+ - A declared request is sent with `keys`, empty when none is ticked, and the
1059
+ handler decides.
1060
+ - `lb-row-insert` adds a row, and the selection is not its.
1061
+ - With no `lb-select` checkbox of the query showing, the request is an
1062
+ ordinary one, and carries no `keys`.
1063
+
1064
+ The server runs the page's `rowUpdate` or `rowDelete` once for each key,
1065
+ with that key, as for each row alone, so a handler written for one row
1066
+ serves several. It lays the patches the runs return together, and runs the
1067
+ refresh set once. A run returning `url()` is reported and ignored, since
1068
+ several rows cannot each choose one view. A declared request receives
1069
+ `keys` as sent.
1070
+
1071
+ After the request succeeds, the hub clears every `lb-select` checkbox of the
1072
+ query and the stamps with them. A failed request leaves the selection.
1073
+
1074
+ Only a checkbox that is showing counts: one parked by `lb-show` is in
1075
+ template content, out of reach. Parking one also clears its tick, so a
1076
+ selection never waits out of sight to return stale.
1077
+
1078
+ Which rows are selected is interaction state, as which row a form edits is.
1079
+ A selecting mode the page keeps in the URL reaches the rows as a column the
1080
+ query computes from the parm, which `lb-show` reads, since `lb-show` reads
1081
+ from the row it sits in.
1082
+
777
1083
  #### The request event
778
1084
 
779
1085
  The hub dispatches every request as the bubbling `lb-request` event before
@@ -817,6 +1123,46 @@ failed one.
817
1123
  The hub ignores a commit on an element carrying `lb-request-pending`, so a
818
1124
  pending element is disabled in fact and a stylesheet only has to show it.
819
1125
 
1126
+ #### Refusals
1127
+
1128
+ A handler refuses a request by throwing `refusal()`, exported by
1129
+ `@loadbare/app/server`, with words written for the user: a message for the
1130
+ request as a whole, a message per column, or both.
1131
+
1132
+ ```ts
1133
+ throw refusal("That series already has paychecks entered.");
1134
+ throw refusal({ amount: "Must be more than zero" });
1135
+ throw refusal("Two fields need attention.", { amount: "…", pay_date: "…" });
1136
+ ```
1137
+
1138
+ It is thrown rather than returned, so it may come from anything the handler
1139
+ calls, and rolls back whatever the application has open as any throw does.
1140
+ `hubRoutes` answers it with status 422 and `{ refusal: { message?, columns? } }`.
1141
+ Anything else thrown is a failure, answered 500 with no body, so no stack
1142
+ or database message reaches the browser.
1143
+
1144
+ The hub shows a refusal where it belongs:
1145
+
1146
+ - It stamps the element that committed `lb-request-refused`, whose value is
1147
+ the whole-request message, empty when there is none. A stylesheet shows
1148
+ it with `attr()`.
1149
+ - For each column, it calls `setCustomValidity(message)` on the control the
1150
+ request gathered that column from, and `reportValidity()` on the first,
1151
+ so the browser shows the message there and moves focus to it. A column
1152
+ the request gathered no such control for is reported to the console.
1153
+ - `lb-request-done` carries the refusal as `refusal`, beside `error`.
1154
+
1155
+ A refusal is not a failure: `lb-request-error` is not stamped and nothing is
1156
+ reported. It stands until the user edits any control the request gathered,
1157
+ a form holding them resets, or the element that committed sends again;
1158
+ then every message it put up is withdrawn at once. While a control carries
1159
+ one, the browser holds its form's submit, as for any invalid control.
1160
+
1161
+ A form-associated custom element takes a column's message when it defines
1162
+ `setCustomValidity` and `reportValidity`; one wrapping a native control
1163
+ passes both to it. A request for several rows takes the whole-request
1164
+ message only.
1165
+
820
1166
  #### The round trip
821
1167
 
822
1168
  A request the hub sends has ten seconds to come back. Past that the hub
@@ -825,7 +1171,8 @@ its own. An application cannot change the deadline.
825
1171
 
826
1172
  A round trip fails on a server error, on a network failure, or on that
827
1173
  deadline, and all three stamp `lb-request-error` on the element that issued
828
- the request — see [Request state](#request-state).
1174
+ the request — see [Request state](#request-state). A refusal is not a
1175
+ failure — see [Refusals](#refusals).
829
1176
 
830
1177
  After a successful `lb-row-insert` gathered from a form, the hub resets the
831
1178
  form, after it lands the response. A failed insert resets nothing, so the
@@ -993,8 +1340,10 @@ names each one in the row it lands.
993
1340
 
994
1341
  The hub sends the query parms with every round trip, a page load and a
995
1342
  request alike. The server passes them to `contextFor` — see
996
- [The Express server](#the-express-server). Nothing in Loadbare assigns a
997
- parm a meaning.
1343
+ [The Express server](#the-express-server). Loadbare assigns one parm a
1344
+ meaning, `lb-order-<query>`, the user's [order](#order) for a query, and it
1345
+ reaches `contextFor` like any other. A request that changes only order
1346
+ parms of queries the hub sorts itself makes no round trip.
998
1347
 
999
1348
  #### An unknown page
1000
1349
 
@@ -1058,6 +1407,29 @@ rowInsert: {
1058
1407
  - A user who has moved by the time the response arrives, to another page or
1059
1408
  to other query parms, keeps the URL they are on.
1060
1409
 
1410
+ `onPageEnter` may return `url()` the same way, to move the page before
1411
+ anything shows, as an application restoring the filters and order the user
1412
+ last had does. The page loads at the new URL in the same round trip, and
1413
+ that load does not follow `onPageEnter` again. A `url()` leading to the
1414
+ page and parms already in hand is ignored, so the page loads once. When to
1415
+ restore is the application's rule, such as only when the URL carries no
1416
+ parms of its own.
1417
+
1418
+ ```ts
1419
+ // The application put the parms on ctx in contextFor.
1420
+ onPageEnter: async (ctx) => {
1421
+ if (ctx.parms.size > 0) return;
1422
+ const saved = await ctx.db.savedView("accounts");
1423
+ if (saved) return url(saved);
1424
+ },
1425
+ ```
1426
+
1427
+ Saving what the user has on screen is a declared request with
1428
+ `refresh: []`, whose handler each page passes to a routine the
1429
+ application's pages share. A custom element naming `lb-url` sees every
1430
+ change of the URL and issues it, a change of order that made no round trip
1431
+ included.
1432
+
1061
1433
  The page is loaded with a second context, built by `contextFor` from the new
1062
1434
  parms — see [The Express server](#the-express-server).
1063
1435
 
@@ -1067,8 +1439,9 @@ The builder checks the chrome and every page, after expansion, and refuses
1067
1439
  to build on any of these:
1068
1440
 
1069
1441
  - An `lb-` attribute that is not a developer attribute: `lb-query`,
1070
- `lb-column`, `lb-show`, `lb-request`, `lb-url-link`, `lb-url-push`,
1071
- `lb-url-unknown`. Every other `lb-` attribute is a stamp.
1442
+ `lb-column`, `lb-show`, `lb-group`, `lb-count`, `lb-sum`, `lb-avg`,
1443
+ `lb-min`, `lb-max`, `lb-request`, `lb-select`, `lb-url-link`,
1444
+ `lb-url-push`, `lb-url-unknown`. Every other `lb-` attribute is a stamp.
1072
1445
  `lb-exp-slot` and `lb-exp-template` are consumed by expansion and never
1073
1446
  reach the check.
1074
1447
  - `lb-request` naming a value that begins with `lb-` and is not one of the
@@ -1080,6 +1453,23 @@ to build on any of these:
1080
1453
  - `lb-show` on a `<template>`, on a row template's root, or with no
1081
1454
  `lb-query` around it.
1082
1455
  - `lb-show="!"` or `lb-show="!!column"`, which reverse no column.
1456
+ - `lb-select` on anything but an `<input type="checkbox">`, outside a row
1457
+ template, or beside `lb-column` or `lb-request`.
1458
+ - A `command` beginning `--lb-` that is not one of the three
1459
+ [commands](#a-form-pointed-at-a-row), and one of them with no
1460
+ `commandfor`.
1461
+ - `lb-group` on anything but a `<template>`, or on one holding no
1462
+ `<template>` for its contents.
1463
+ - An aggregate on a `<template>`, and any but `lb-count` naming no column.
1464
+ - A chrome without exactly one `<lb-hub>` and exactly one `<main>`, a
1465
+ `<main>` outside `<lb-hub>`, or a `<main>` with anything in it but
1466
+ whitespace.
1467
+ - A page carrying an `<lb-hub>` or a `<main>`.
1468
+
1469
+ The builder also reads the script of every custom element it ships, other
1470
+ than `<lb-hub>`, and refuses one that declares a method, accessor or field
1471
+ whose name begins with `lb` and a capital and is not in
1472
+ [Reserved methods](#reserved-methods).
1083
1473
 
1084
1474
  `lb-column` with no ancestor row is allowed: the hub gathers from it.
1085
1475
 
@@ -1099,7 +1489,8 @@ where the standard landmarks go: header, nav, footer, main.
1099
1489
  The file is an HTML document, which must contain:
1100
1490
  - `<lb-hub>` inside `<body>`. The hub can only act on its children,
1101
1491
  that is why it is usually nested right below `<body>`.
1102
- - One empty `<main>` inside `<lb-hub>`.
1492
+ - One empty `<main>` inside `<lb-hub>`. The builder refuses a chrome
1493
+ without exactly one of each, and a page carrying either.
1103
1494
  - The script tag for `/client.js`, the javascript bundle.
1104
1495
 
1105
1496
  It may also contain:
@@ -1209,6 +1600,8 @@ The builder writes `pages.ts` into `--out` whenever any page has a
1209
1600
  `.queries.ts` or a `.requests.ts` file. It imports each of them, calls
1210
1601
  `createHub()` once over the lot, and exports the result as `hub`. The
1211
1602
  server imports that rather than maintaining the registry by hand.
1603
+ `createHub` is the only implementation of the `Hub` interface, which may
1604
+ gain members in any release; an application never implements it.
1212
1605
 
1213
1606
  ```ts
1214
1607
  // server.ts
@@ -1244,6 +1637,16 @@ Explaining Express is beyond the scope of this technical reference. The
1244
1637
  only real requirement is that the catch-all for app.html is at the end,
1245
1638
  so it does not catch any other files.
1246
1639
 
1640
+ `hubRoutes` mounts `express.json()` on its own routes, and takes that
1641
+ function's options under `json` in an optional third argument. The default
1642
+ limit of 100 KB refuses an oversized body with a 413 before any handler
1643
+ runs, and the server log names the option. A page that sends a file's text
1644
+ as a value needs more:
1645
+
1646
+ ```ts
1647
+ app.use(hubRoutes(hub, contextFor, { json: { limit: "5mb" } }));
1648
+ ```
1649
+
1247
1650
  `hubRoutes` answers `POST /lb/<stub>`, with the query string the browser is
1248
1651
  showing after it, verbatim. It hands `contextFor` that query string's parms
1249
1652
  as a second argument, and `contextFor` puts on the context whatever a query
@@ -1273,7 +1676,8 @@ the whole path space through unchanged.
1273
1676
  Each key is a query name, which the markup names with `lb-query`.
1274
1677
 
1275
1678
  A query's kind and key belong to its name. Declare each query with
1276
- `row(key, run)` or `rows(key, run)`, naming the key column first. A query
1679
+ `row(key, run)` or `rows(key, run, options)`, naming the key column first.
1680
+ A `rows` query's `options` declare its [order](#order). A query
1277
1681
  cannot be built without one of these functions. Every query has a key; an
1278
1682
  aggregate row answers with a constant one.
1279
1683
 
@@ -1291,14 +1695,15 @@ export const queries: Queries = {
1291
1695
  ```
1292
1696
 
1293
1697
  A `row` query returns one object, and a `rows` query returns an array of
1294
- objects in the order the page shows them. An answer that disagrees with
1698
+ objects, which the page shows in that order when the query declares none. An answer that disagrees with
1295
1699
  its declared kind is left out of the response, with a warning naming the
1296
1700
  query.
1297
1701
 
1298
1702
  Query names cannot begin with `lb-`, that namespace is reserved for
1299
1703
  Loadbare/app queries the hub serves in the browser, such as
1300
1704
  [`lb-url`](#the-url). `createHub` refuses at startup a page that declares
1301
- such a name as a query, a handler or a `crud` entry.
1705
+ such a name as a query, a handler or a `crud` entry, and a query whose order
1706
+ has a term naming no column.
1302
1707
 
1303
1708
  Every query and every handler receives `ctx`, the application's own request
1304
1709
  context. The application builds it once per request and hands it to the hub,
@@ -1318,7 +1723,7 @@ It has three optional keys.
1318
1723
  | ------------- | --------------------- | -------------------------------------------------- |
1319
1724
  | `handlers` | The request name | The page's declared requests |
1320
1725
  | `crud` | A query name | The three requests Loadbare provides |
1321
- | `onPageEnter` | Nothing | Runs once on entering the page, before its queries |
1726
+ | `onPageEnter` | Nothing | Runs once on entering the page, before its queries, and may move the URL |
1322
1727
 
1323
1728
  The server resolves a declared request against `handlers`, and a request
1324
1729
  Loadbare provides against `crud` under the request's query. A name with no
@@ -1340,14 +1745,13 @@ and `values`, as present.
1340
1745
  the refreshed ones. That is how a delta reaches the browser: wrap it in
1341
1746
  `patch()`, naming the rows that arrived or changed and the keys that went.
1342
1747
 
1343
- A refreshed `rows` query sends every row, and the hub places every row
1344
- again, which moves each element and takes focus from the control the user
1345
- is in. An update or a delete names its row by key, and removing a row
1346
- never reorders the rest, so its handler always knows what changed:
1748
+ A refreshed `rows` query sends every row to say what changed. An update or
1749
+ a delete names its row by key, so its handler always knows what changed and
1750
+ answers with a patch, which the hub places by the query's [order](#order):
1347
1751
  `createHub` refuses at startup a `crud` `rowUpdate` or `rowDelete` on a
1348
1752
  `rows` query whose `refresh` names that same query. A `row` query sends one
1349
- row and may refresh itself. An insert may refresh its own query; see
1350
- [Who creates and orders rows](#who-creates-and-orders-rows).
1753
+ row and may refresh itself. An insert may refresh its own query, since its
1754
+ key exists only once the row does.
1351
1755
 
1352
1756
  `run` may instead return `url()`, a new row for `lb-url`, naming query parms
1353
1757
  only the write can know, such as the key of a row it inserted. The page
@@ -1409,11 +1813,16 @@ interface HubRequest {
1409
1813
  name: string;
1410
1814
  query?: string;
1411
1815
  key?: string;
1816
+ keys?: string[];
1412
1817
  values?: Record<string, string>;
1818
+ rows?: Record<string, { key: string; values: Record<string, string> }[]>;
1413
1819
  }
1414
1820
  ```
1415
1821
 
1416
- Every response is an array of response items:
1822
+ A refused request is answered with status 422 and
1823
+ `{ refusal: { message?, columns? } }`, and any other failure with 500 and
1824
+ no body — see [Refusals](#refusals). Every other response is an array of
1825
+ response items:
1417
1826
 
1418
1827
  ```ts
1419
1828
  interface ResponseItem {
@@ -1430,9 +1839,14 @@ A response item carries exactly one of `row`, `rows` or `patch`. A patch
1430
1839
  answers a `rows` query only: rows named in `rows` are added or updated, keys
1431
1840
  in `drop` are removed, and every other row is left alone.
1432
1841
 
1842
+ A `rows` item also carries `order` and `serverSortedByUrl` when the query
1843
+ declares them.
1844
+
1433
1845
  `createHub(pages)` builds the `Hub`, which has two calls, both answering
1434
- with response items: `dataForPage(page, ctx)` for a page load, and
1435
- `runRequest(page, request, ctx)` for a request.
1846
+ with response items: `dataForPage(page, ctx, options)` for a page load, and
1847
+ `runRequest(page, request, ctx)` for a request. `options.parms` are the
1848
+ parms the page is loaded at, and `options.follow` false ignores a `url()`
1849
+ from `onPageEnter`; `hubRoutes` passes both.
1436
1850
 
1437
1851
  ## Custom elements
1438
1852
 
@@ -1468,9 +1882,9 @@ Each direction has one mechanism for each kind of message:
1468
1882
 
1469
1883
  | Direction | What | How | Today |
1470
1884
  | -------------- | ------------------------------------------- | ---------------------------- | ------------------------------------------------ |
1471
- | Hub to element | State that lasts | An attribute the hub stamps | `lb-column-value`, `lb-key-value`, `lb-query-row-count`, `lb-request-pending`, `lb-request-error` |
1885
+ | Hub to element | State that lasts | An attribute the hub stamps | `lb-column-value`, `lb-key-value`, `lb-query-row-count`, `lb-group-*`, `lb-row-*`, `lb-request-pending`, `lb-request-error` |
1472
1886
  | Hub to element | State the platform already names | A property | A control's `value` |
1473
- | Hub to element | Work the hub needs done now, while landing | An optional `lb` method | `lbPlaceRow`, `lbRowsLanded` |
1887
+ | Hub to element | Work the hub needs done now, while landing | An optional `lb` method | `lbRowsLanded` |
1474
1888
  | Hub to element | A moment an element started | A bubbling event | `lb-request-done` |
1475
1889
  | Element to hub | A moment | A bubbling event | `lb-request`, and `change`, `click` and `submit` |
1476
1890
 
@@ -1512,8 +1926,9 @@ renders it; its content is never replaced.
1512
1926
  | Shows a page | `constructor`, `connectedCallback` |
1513
1927
  | Creates a live row, before placing it | `constructor` |
1514
1928
  | Places a live row the first time | `connectedCallback` |
1515
- | Places it again, on every set of all rows | `disconnectedCallback`, `connectedCallback` |
1516
- | Removes a live row | `disconnectedCallback` |
1929
+ | Moves a live row the order put elsewhere | `connectedMoveCallback` where the browser moves in place, otherwise `disconnectedCallback`, `connectedCallback` |
1930
+ | Leaves a copy of a moved row behind | The copy's `constructor`, `connectedCallback`, and later `disconnectedCallback` |
1931
+ | Removes a live row | `disconnectedCallback`, once its animations end |
1517
1932
  | Turns `lb-show` off | `disconnectedCallback`, `adoptedCallback` |
1518
1933
  | Turns `lb-show` on | `adoptedCallback`, `connectedCallback` |
1519
1934
  | Turns on an element shipped absent | `constructor`, `connectedCallback` |
@@ -1525,19 +1940,27 @@ done once per instance goes behind a flag.
1525
1940
 
1526
1941
  ### Row hooks
1527
1942
 
1528
- | Method | Implemented By | The hub calls it |
1529
- | ------------------------------- | -------------- | -------------------------------------------------- |
1530
- | `lbPlaceRow(el, row, template)` | Developer | To place a live row, detached, on its first arrival and whenever all rows decide the order |
1531
- | `lbRowsLanded()` | Developer | After the rows land |
1943
+ | Method | Implemented By | The hub calls it |
1944
+ | ---------------- | -------------- | ------------------------------------------------------ |
1945
+ | `lbRowsLanded()` | Developer | After the rows land, with every row placed and stamped |
1532
1946
 
1533
- The hub calls both on a custom element with `lb-query` and a row template.
1534
- Both are optional. The `RowsHost` interface in `@loadbare/app/types`
1535
- declares them.
1947
+ The hub calls it on a custom element with `lb-query` and a row template.
1948
+ It is optional. The `RowsHost` interface in `@loadbare/app/types` declares
1949
+ it. The hub places every row and builds every group itself, so a custom
1950
+ element reacts to rows and never places them. Scrolling to the row the
1951
+ page's request created is the case:
1952
+
1953
+ ```ts
1954
+ lbRowsLanded() {
1955
+ this.querySelector(`[${ATTR_ROW_REQUESTED}="${REQUESTED_CREATED}"]`)
1956
+ ?.scrollIntoView({ block: "nearest" });
1957
+ }
1958
+ ```
1536
1959
 
1537
1960
  A custom element the builder shipped absent under `lb-show` has not
1538
- upgraded while its column is off, and rows that land on it then are placed
1539
- without it. When the column first turns on and it upgrades, the hub lands
1540
- the query's last answer on it again, as all rows, so both hooks run.
1961
+ upgraded while its column is off. When the column first turns on and it
1962
+ upgrades, the hub lands the query's last answer on it again, as all rows,
1963
+ so the hook runs.
1541
1964
 
1542
1965
  `applyRow(root, row)`, exported by `@loadbare/app`, fills `root` from one
1543
1966
  row, the same operation that fills a live row.
@@ -1617,6 +2040,7 @@ ownership of its behavior are stated separately, because they differ.
1617
2040
  | Namespace | Applies to | Loadbare owns | Defined in |
1618
2041
  | --------- | ----------------------------------------------------------- | --------------------------------------------------- | ----------------------------------------------- |
1619
2042
  | `lb-*` | Attributes, their values, queries, columns of `lb-` queries, requests, custom elements, events | The names and their behavior | [The lb-* namespace](#the-lb--namespace) |
2043
+ | `--lb-*` | Values of HTML's `command` attribute | The names and their behavior | [Reserved commands](#reserved-commands) |
1620
2044
  | `exp-*` | Attributes on a custom element | The behavior; element file authors pick the names | [Build time parameters](#build-time-parameters) |
1621
2045
  | `lb*` | Methods on custom elements | The names and their behavior | [Row hooks](#row-hooks) |
1622
2046
 
@@ -1633,17 +2057,26 @@ it may define tomorrow.
1633
2057
  | `lb-column` | Developer | Any element | [Landing](#landing) |
1634
2058
  | `lb-show` | Developer | Any element but a `<template>` | [Displaying by condition](#displaying-by-condition) |
1635
2059
  | `lb-request` | Developer | Any element | [Requests](#requests) |
2060
+ | `lb-select` | Developer | A checkbox in a row template | [Selecting rows](#selecting-rows) |
1636
2061
  | `lb-url-link` | Developer | An `<a>` | [Links](#links) |
1637
2062
  | `lb-url-push` | Developer | An element with `lb-request` | [Query parms](#query-parms) |
1638
2063
  | `lb-url-unknown` | Developer | A `<dialog>` inside `<lb-hub>` | [An unknown page](#an-unknown-page) |
2064
+ | `lb-group` | Developer | A `<template>` holding a `<template>` | [Groups](#groups) |
2065
+ | `lb-count`, `lb-sum`, `lb-avg`, `lb-min`, `lb-max` | Developer | Any element but a `<template>` | [Aggregates](#aggregates) |
1639
2066
  | `lb-exp-slot` | Developer | One element in an element file | [Slots and templates](#slots-and-templates) |
1640
2067
  | `lb-exp-template` | Developer | An element file, and a `<template>` | [Slots and templates](#slots-and-templates) |
1641
2068
  | `lb-column-value` | Hub | Every element set from a column | [How a column lands](#how-a-column-lands) |
1642
- | `lb-key-value` | Hub | A live row, and an element a `row` lands on | [Rows](#rows) |
2069
+ | `lb-key-value` | Hub | A live row, an element a `row` lands on, and a pointed form | [Rows](#rows), [A form pointed at a row](#a-form-pointed-at-a-row) |
2070
+ | `lb-row-editing` | Hub | The live row a form is pointed at | [A form pointed at a row](#a-form-pointed-at-a-row) |
2071
+ | `lb-row-selected` | Hub | A live row its `lb-select` checkbox ticks | [Selecting rows](#selecting-rows) |
1643
2072
  | `lb-row-live` | Hub | A live row | [Rows](#rows) |
1644
2073
  | `lb-query-row-count` | Hub | An element with a row template | [Rows](#rows) |
2074
+ | `lb-group-live`, `lb-group-column`, `lb-group-value` | Hub | Each top-level element of a group | [Groups](#groups) |
2075
+ | `lb-row-created`, `lb-row-changed`, `lb-row-moved`, `lb-row-requested` | Hub | A live row | [What happened to a row](#what-happened-to-a-row) |
2076
+ | `lb-row-leaving`, `lb-group-leaving` | Hub | A row or a group on its way out | [What happened to a row](#what-happened-to-a-row) |
1645
2077
  | `lb-request-pending` | Hub | The element that issued a request | [Request state](#request-state) |
1646
2078
  | `lb-request-error` | Hub | The element that issued a request | [Request state](#request-state) |
2079
+ | `lb-request-refused` | Hub | The element whose request was refused | [Refusals](#refusals) |
1647
2080
  | `lb-show` on a `<template>` | Hub and builder | Where an absent element stands | [Displaying by condition](#displaying-by-condition) |
1648
2081
  | `lb-page` | Builder | A page's `<template>` | [Page HTML](#page-html) |
1649
2082
  | `lb-page-title` | Builder | A page's `<template>` | [Page HTML](#page-html) |
@@ -1657,6 +2090,12 @@ The builder refuses any other `lb-` attribute in markup — see
1657
2090
  | -------- | ----- | --------- | --------------------------------------------------------------- | ------------------- |
1658
2091
  | `lb-url` | `row` | `lb-path` | `lb-path`, `lb-page-label`, `lb-page-unknown`, every query parm | [The URL](#the-url) |
1659
2092
 
2093
+ ### Reserved query parms
2094
+
2095
+ | Parm | Means | Defined in |
2096
+ | ------------------ | --------------------------- | --------------- |
2097
+ | `lb-order-<query>` | The user's order for a query | [Order](#order) |
2098
+
1660
2099
  ### Reserved request names
1661
2100
 
1662
2101
  | Request name | Runs under `crud` | Defined in |
@@ -1665,6 +2104,17 @@ The builder refuses any other `lb-` attribute in markup — see
1665
2104
  | `lb-row-update` | `rowUpdate` | [The request names](#the-request-names) |
1666
2105
  | `lb-row-delete` | `rowDelete` | [The request names](#the-request-names) |
1667
2106
 
2107
+ ### Reserved commands
2108
+
2109
+ Loadbare/app owns every value of HTML's `command` attribute beginning with
2110
+ `--lb-`.
2111
+
2112
+ | Command | Defined in |
2113
+ | -------------- | --------------------------------------------------- |
2114
+ | `--lb-edit` | [A form pointed at a row](#a-form-pointed-at-a-row) |
2115
+ | `--lb-new` | [A form pointed at a row](#a-form-pointed-at-a-row) |
2116
+ | `--lb-release` | [A form pointed at a row](#a-form-pointed-at-a-row) |
2117
+
1668
2118
  ### Reserved tags
1669
2119
 
1670
2120
  Loadbare/app owns every tag in this table. An application writes them and
@@ -1688,7 +2138,6 @@ Loadbare/app owns every DOM event in this table, both the name and what its
1688
2138
 
1689
2139
  | Method | On | Defined in |
1690
2140
  | -------------- | -------------------------------------------------- | ----------------------- |
1691
- | `lbPlaceRow` | A custom element with `lb-query` and a row template | [Row hooks](#row-hooks) |
1692
2141
  | `lbRowsLanded` | A custom element with `lb-query` and a row template | [Row hooks](#row-hooks) |
1693
2142
 
1694
2143
  ### Constants
@@ -1706,9 +2155,33 @@ imports them rather than writing a string.
1706
2155
  | `ATTR_ROW_LIVE` | `lb-row-live` |
1707
2156
  | `LIVE_ROW` | `[lb-row-live]`, the selector for a live row |
1708
2157
  | `ATTR_QUERY_ROW_COUNT` | `lb-query-row-count` |
2158
+ | `ATTR_GROUP` | `lb-group` |
2159
+ | `ATTR_GROUP_LIVE` | `lb-group-live` |
2160
+ | `ATTR_GROUP_COLUMN` | `lb-group-column` |
2161
+ | `ATTR_GROUP_VALUE` | `lb-group-value` |
2162
+ | `LIVE_GROUP` | `[lb-group-live]`, the selector for a group's element |
2163
+ | `ATTR_COUNT`, `ATTR_SUM`, `ATTR_AVG`, `ATTR_MIN`, `ATTR_MAX` | `lb-count`, `lb-sum`, `lb-avg`, `lb-min`, `lb-max` |
2164
+ | `AGGREGATES` | All five aggregate attributes |
2165
+ | `ORDER_PARM_PREFIX` | `lb-order-` |
2166
+ | `ORDER_DESCENDING` | `-`, written before a column in an order |
2167
+ | `ATTR_ROW_CREATED` | `lb-row-created` |
2168
+ | `ATTR_ROW_CHANGED` | `lb-row-changed` |
2169
+ | `ATTR_ROW_MOVED` | `lb-row-moved` |
2170
+ | `ATTR_ROW_REQUESTED` | `lb-row-requested` |
2171
+ | `REQUESTED_CREATED`, `REQUESTED_CHANGED`, `REQUESTED_MOVED` | `created`, `changed`, `moved` |
2172
+ | `ATTR_ROW_LEAVING` | `lb-row-leaving` |
2173
+ | `ATTR_GROUP_LEAVING` | `lb-group-leaving` |
2174
+ | `COMMAND_EDIT`, `COMMAND_NEW`, `COMMAND_RELEASE` | `--lb-edit`, `--lb-new`, `--lb-release` |
2175
+ | `LB_COMMANDS` | All three commands |
2176
+ | `LB_COMMAND_PREFIX` | `--lb-` |
2177
+ | `ATTR_ROW_EDITING` | `lb-row-editing` |
2178
+ | `ATTR_SELECT` | `lb-select` |
2179
+ | `ATTR_ROW_SELECTED` | `lb-row-selected` |
1709
2180
  | `ATTR_REQUEST` | `lb-request` |
1710
2181
  | `ATTR_REQUEST_PENDING` | `lb-request-pending` |
1711
2182
  | `ATTR_REQUEST_ERROR` | `lb-request-error` |
2183
+ | `ATTR_REQUEST_REFUSED` | `lb-request-refused` |
2184
+ | `LB_REFUSAL_STATUS` | `422` |
1712
2185
  | `ATTR_URL_LINK` | `lb-url-link` |
1713
2186
  | `ATTR_URL_PUSH` | `lb-url-push` |
1714
2187
  | `ATTR_URL_UNKNOWN` | `lb-url-unknown` |
@@ -1716,7 +2189,7 @@ imports them rather than writing a string.
1716
2189
  | `ATTR_EXP_TEMPLATE` | `lb-exp-template` |
1717
2190
  | `ATTR_PAGE` | `lb-page` |
1718
2191
  | `ATTR_PAGE_TITLE` | `lb-page-title` |
1719
- | `DEVELOPER_ATTRIBUTES` | The seven attributes a developer writes |
2192
+ | `DEVELOPER_ATTRIBUTES` | The fourteen attributes a developer writes |
1720
2193
  | `LB_EVENT_NAME` | `lb-request` |
1721
2194
  | `LB_DONE_EVENT_NAME` | `lb-request-done` |
1722
2195
  | `SHOW_NOT` | `!`, written before a column in `lb-show` |