@loadbare/app 0.12.0 → 0.13.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 +57 -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 +24 -1
  15. package/dist/core/lb-constants.d.ts.map +1 -1
  16. package/dist/core/lb-constants.js +71 -0
  17. package/dist/core/lb-constants.js.map +1 -1
  18. package/dist/core/lb-types.d.ts +47 -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 +37 -15
  23. package/dist/hub/lb-apply.d.ts.map +1 -1
  24. package/dist/hub/lb-apply.js +598 -56
  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 +61 -14
  28. package/dist/hub/lb-hub.browser.js.map +1 -1
  29. package/dist/server/lb-express.d.ts +6 -4
  30. package/dist/server/lb-express.d.ts.map +1 -1
  31. package/dist/server/lb-express.js +32 -14
  32. package/dist/server/lb-express.js.map +1 -1
  33. package/dist/server/lb-server.d.ts +49 -10
  34. package/dist/server/lb-server.d.ts.map +1 -1
  35. package/dist/server/lb-server.js +76 -34
  36. package/dist/server/lb-server.js.map +1 -1
  37. package/docs/TECHREF-1.0.md +292 -92
  38. package/docs/comparison.md +11 -9
  39. package/docs/possible-ideas.md +188 -0
  40. package/docs/reference/chrome.md +7 -1
  41. package/docs/reference/custom-elements.md +37 -38
  42. package/docs/reference/data-binding.md +102 -13
  43. package/docs/reference/page-files.md +25 -16
  44. package/docs/reference/server.md +4 -0
  45. package/docs/reference/widgets.md +94 -24
  46. package/docs/roadmap.md +10 -6
  47. package/docs/testing.md +21 -10
  48. package/package.json +2 -3
  49. package/skills/loadbare-app/SKILL.md +43 -24
  50. package/skills/loadbare-app/references/TECHREF-1.0.md +292 -92
  51. package/skills/loadbare-app/references/chrome.md +7 -1
  52. package/skills/loadbare-app/references/custom-elements.md +37 -38
  53. package/skills/loadbare-app/references/data-binding.md +102 -13
  54. package/skills/loadbare-app/references/page-files.md +25 -16
  55. package/skills/loadbare-app/references/server.md +4 -0
  56. package/skills/loadbare-app/references/widgets.md +94 -24
@@ -22,6 +22,10 @@ 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
 
@@ -32,28 +36,15 @@ handle.
32
36
  what counts as checked, which waits on [Data types](#data-types), and a
33
37
  radio group is several elements answering to one column.
34
38
 
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.
44
-
45
39
  ### Custom element hooks
46
40
 
47
41
  Loadbare/app owns every method name beginning with `lb` on a custom element.
48
42
  That decision is firm; what the set contains is not.
49
43
 
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
44
  - **Decide whether a custom element may receive a whole row.** A column
54
45
  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.
46
+ `lbAcceptRow` would sit beside `lbRowsLanded`, and this is the most likely
47
+ first request from a custom element author.
57
48
 
58
49
  ### Nested queries
59
50
 
@@ -77,38 +68,13 @@ But the allowed values may depend on other values in the row.
77
68
  </tbody>
78
69
  ```
79
70
 
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
71
  ### The server API
99
72
 
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
73
  - **Decide the options-argument shape once.** Global hooks, transaction
108
74
  wrapping, error handling and a CSRF token all want the same trailing
109
75
  parameter on `createHub` and `hubRoutes`. Build none of them, but pick
110
76
  where they go.
111
- - **Land `staticRoutes`.** See NEXT.md. It closes the last place where an
77
+ - **Land `staticRoutes`.** It closes the last place where an
112
78
  application hand-writes facts the builder owns.
113
79
 
114
80
  ### The builder
@@ -123,9 +89,6 @@ builds each section's heading. Neither side owns creation or order.
123
89
  definition would let the builder ship only what survives expansion.
124
90
  Recommend deferring the mechanism and reserving the configuration key, so
125
91
  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
92
  - **Decide whether the build validates HTML.** A page can be well formed and
130
93
  still break HTML's rules for what an element may hold, such as a `<div>`
131
94
  inside a `<p>`. The parser rearranges it silently, the same way at build
@@ -141,9 +104,6 @@ builds each section's heading. Neither side owns creation or order.
141
104
  None of this is surface. It is listed here because trimming it is free
142
105
  today and breaking after 1.0.
143
106
 
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
107
  - **Audit what the hub does.** It appears to have routines that go beyond
148
108
  what the `lb-*` attribute namespace specifies.
149
109
  - **Keep the rest as constants.** The endpoint, `index` and the timeout are
@@ -455,9 +415,10 @@ The markup names a query and the columns it shows. The kind and the key are
455
415
  the server's, and the markup states neither.
456
416
 
457
417
  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`.
418
+ element with `lb-query`, outside any nested `lb-query`, or, when that
419
+ template is a [group template](#groups), the innermost template inside it.
420
+ A live row is an element the hub cloned from a row template for one row,
421
+ and carries `lb-row-live`.
461
422
 
462
423
  | Kind | Row template | The hub |
463
424
  | ------ | ------------ | ----------------------------------- |
@@ -538,14 +499,15 @@ with `[lb-row-live]`, and never with `[lb-key-value]`.
538
499
  A key is unique within a query. Two rows with one key in the same answer,
539
500
  or two live rows showing one key, are reported on the console.
540
501
 
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.
502
+ All rows decide membership: a live row whose key did not arrive leaves —
503
+ see [What happened to a row](#what-happened-to-a-row). A patch touches only
504
+ the rows it names, and leaves every other live row's contents alone.
545
505
 
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).
506
+ The hub places every live row by the query's [order](#order), immediately
507
+ before the row template, or before its clone in the row's
508
+ [group](#groups). A row moves only when the order puts it somewhere else,
509
+ so all rows landing again unchanged move nothing. A live row already in the
510
+ markup when the first answer lands is one of the query's rows.
549
511
 
550
512
  After every landing the hub stamps the element with `lb-query-row-count`,
551
513
  the number of live rows it holds. The server answers with rows and says
@@ -560,6 +522,162 @@ sent. It makes an empty query a stylesheet rule:
560
522
 
561
523
  A row missing its key column is reported to the console and not landed.
562
524
 
525
+ #### Order
526
+
527
+ A `rows` query's order is a rule over its columns. The server declares it
528
+ with the query, and the URL may carry the user's own:
529
+
530
+ ```ts
531
+ accounts: rows("id", (ctx) => ctx.db.accounts(), {
532
+ order: "category_order,account_name",
533
+ }),
534
+ ```
535
+
536
+ An order is columns separated by commas. A column written after `-` sorts
537
+ descending. The query parm `lb-order-<query>` replaces the declared order
538
+ for that query: `/coa?lb-order-accounts=-account_name`. With neither, rows
539
+ show in the order the server sent them, and a patch's new rows go last.
540
+
541
+ | Values | Compare |
542
+ | -------------------- | ------------------------------------------ |
543
+ | Two numbers | Numerically |
544
+ | Two strings | As the user's language orders them |
545
+ | Two booleans | `false` before `true` |
546
+ | Two different types | Booleans, then numbers, then strings |
547
+ | `null` or missing | After everything ascending, first descending |
548
+
549
+ A number sent as a string sorts as a string, so a column the order reads
550
+ numerically arrives as a JSON number. Rows that compare equal keep the
551
+ order they arrived in, and a patch's new rows come after the rows already
552
+ showing.
553
+
554
+ The hub sorts the rows it has, so a control that writes the order parm
555
+ re-sorts the page with no round trip:
556
+
557
+ ```html
558
+ <div lb-query="lb-url">
559
+ <select lb-column="lb-order-accounts" lb-request="lb-row-update">
560
+ <option value="">By category</option>
561
+ <option value="account_name">By name</option>
562
+ </select>
563
+ </div>
564
+ ```
565
+
566
+ A query that pages or limits its rows decides membership by its order, so
567
+ it reads the parm itself, from the parms `contextFor` receives, and declares
568
+ `serverSortedByUrl: true` beside `order`. A change of the user's order then
569
+ loads the page again. The hub still places the rows it receives by the same
570
+ order, so a patch lands in its place.
571
+
572
+ #### Groups
573
+
574
+ A group is a run of rows sharing the order's leading term. A group
575
+ template is a `<template lb-group>` in the row template's place, holding the
576
+ group's heading and exactly one nested `<template>`, which is where the
577
+ group's contents go: the next group template, or the row template. The nth
578
+ group template breaks on the order's nth term, so groups nest to any depth.
579
+
580
+ ```html
581
+ <table lb-query="accounts">
582
+ <thead><tr><th>Account</th></tr></thead>
583
+ <template lb-group>
584
+ <tbody>
585
+ <tr><th scope="rowgroup" lb-column="category"></th></tr>
586
+ <template><tr><td lb-column="account_name"></td></tr></template>
587
+ </tbody>
588
+ </template>
589
+ </table>
590
+ ```
591
+
592
+ The hub clones the group template where the order breaks and puts the
593
+ group's contents immediately before the clone of its nested template. The
594
+ markup's nesting decides the shape:
595
+
596
+ | The nested template is | The group's contents land | Example |
597
+ | --------------------------------- | ----------------------------------- | -------------------------------- |
598
+ | Inside the heading's element | Inside it | `<tbody>`, `<li>`, `<details>` |
599
+ | Beside the heading | After the heading, as its siblings | A heading `<tr>` in one `<tbody>` |
600
+
601
+ `<tbody>` and `<optgroup>` do not nest, so a table's levels below the first
602
+ are heading rows beside their rows. Content after the nested template lands
603
+ after the group's rows, which is where a group's footer goes.
604
+
605
+ A group's heading is filled from its first row, the same way a live row is
606
+ filled, so it shows any column of the row. The example breaks on
607
+ `category_order` and shows `category`. The hub fills it from no other row.
608
+
609
+ The hub stamps each top-level element of a group's clone with
610
+ `lb-group-live`, `lb-group-column`, the column it breaks on, and
611
+ `lb-group-value`, its value there. A row creates its group, and a group
612
+ leaves with its last row. Groups never move: when a change of order puts
613
+ the groups at a level in another order, each of them is made again.
614
+
615
+ A group template with no nested template is reported to the console. More
616
+ group templates than the order has terms show each extra level as one
617
+ group, and are reported once.
618
+
619
+ #### Aggregates
620
+
621
+ An aggregate attribute sets an element from the rows of the nearest group
622
+ around it, or of the whole list outside any group.
623
+
624
+ | Attribute | Sets the element to |
625
+ | --------------- | --------------------------------------------- |
626
+ | `lb-count` | The number of rows |
627
+ | `lb-sum="col"` | The sum of `col` |
628
+ | `lb-avg="col"` | The average of `col` |
629
+ | `lb-min="col"` | The least value of `col`, by the [order](#order)'s comparison |
630
+ | `lb-max="col"` | The greatest, likewise |
631
+
632
+ ```html
633
+ <tr><td><span lb-count></span> accounts</td><td lb-sum="balance"></td></tr>
634
+ ```
635
+
636
+ It lands as a column does, with `lb-column-value`. A sum and an average
637
+ are exact decimals, read from JSON numbers and from strings holding a plain
638
+ decimal, as Postgres sends `numeric`. A sum has as many places as the most
639
+ precise value, and an average up to two more. `null` and empty values are
640
+ left out, as SQL leaves them out. Any other value is left out and reported
641
+ once. The hub does not format the result.
642
+
643
+ An aggregate covers the rows the browser holds. Where the true total
644
+ matters beyond what the page shows, it is a row the server answers.
645
+
646
+ #### What happened to a row
647
+
648
+ Every change the hub makes to a list is one a stylesheet can see, so an
649
+ application animates with CSS alone and the hub starts no animation.
650
+
651
+ | Attribute | On | Means |
652
+ | ------------------ | -------------------- | -------------------------------------------------- |
653
+ | `lb-row-created` | A live row | Created when the hub last touched it |
654
+ | `lb-row-changed` | A live row | Its values changed when the hub last touched it |
655
+ | `lb-row-moved` | A live row | The order put it somewhere else when last touched |
656
+ | `lb-row-requested` | A live row | `created`, `changed` or `moved` by this page's request |
657
+ | `lb-row-leaving` | A row on its way out | No longer live |
658
+ | `lb-group-leaving` | A group on its way out | No longer live |
659
+
660
+ The hub touches a row when an answer names it. `lb-row-created`,
661
+ `lb-row-changed` and `lb-row-moved` are cleared when it next does, and one
662
+ applied again is removed and added as two changes, so an animation keyed to
663
+ it plays again. `lb-row-requested` is cleared at every landing of the
664
+ row's query, and set only by a landing that answers a request from the page.
665
+
666
+ A row or a group that leaves loses `lb-row-live` or `lb-group-live`, takes
667
+ `lb-row-leaving` or `lb-group-leaving` and `inert`, and is removed once the
668
+ animations running on it have finished, at once when there are none. A
669
+ moved row stays live and moves. A copy of it stays behind where it was,
670
+ leaving.
671
+
672
+ ```css
673
+ [lb-row-created] { animation: arrive 200ms; }
674
+ [lb-row-leaving] { animation: depart 200ms forwards; }
675
+ ```
676
+
677
+ A row never travels between the two places, since that takes measuring, and
678
+ measuring takes script. Scrolling to a row is script too: a custom element
679
+ reads `lb-row-requested` in [`lbRowsLanded`](#row-hooks).
680
+
563
681
  #### Displaying by condition
564
682
 
565
683
  `lb-show` names the column that decides whether an element is present.
@@ -624,9 +742,9 @@ answered side by side. A column never holds rows.
624
742
  ```
625
743
 
626
744
  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.
745
+ each detail row carries its master's columns. A [group
746
+ template](#groups) shows each master as a group, ordered by the master's
747
+ key first.
630
748
 
631
749
  A query nested in another query's row template receives the same rows in
632
750
  every live row. That serves a picker offering the same choices on every
@@ -993,8 +1111,10 @@ names each one in the row it lands.
993
1111
 
994
1112
  The hub sends the query parms with every round trip, a page load and a
995
1113
  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.
1114
+ [The Express server](#the-express-server). Loadbare assigns one parm a
1115
+ meaning, `lb-order-<query>`, the user's [order](#order) for a query, and it
1116
+ reaches `contextFor` like any other. A request that changes only order
1117
+ parms of queries the hub sorts itself makes no round trip.
998
1118
 
999
1119
  #### An unknown page
1000
1120
 
@@ -1058,6 +1178,29 @@ rowInsert: {
1058
1178
  - A user who has moved by the time the response arrives, to another page or
1059
1179
  to other query parms, keeps the URL they are on.
1060
1180
 
1181
+ `onPageEnter` may return `url()` the same way, to move the page before
1182
+ anything shows, as an application restoring the filters and order the user
1183
+ last had does. The page loads at the new URL in the same round trip, and
1184
+ that load does not follow `onPageEnter` again. A `url()` leading to the
1185
+ page and parms already in hand is ignored, so the page loads once. When to
1186
+ restore is the application's rule, such as only when the URL carries no
1187
+ parms of its own.
1188
+
1189
+ ```ts
1190
+ // The application put the parms on ctx in contextFor.
1191
+ onPageEnter: async (ctx) => {
1192
+ if (ctx.parms.size > 0) return;
1193
+ const saved = await ctx.db.savedView("accounts");
1194
+ if (saved) return url(saved);
1195
+ },
1196
+ ```
1197
+
1198
+ Saving what the user has on screen is a declared request with
1199
+ `refresh: []`, whose handler each page passes to a routine the
1200
+ application's pages share. A custom element naming `lb-url` sees every
1201
+ change of the URL and issues it, a change of order that made no round trip
1202
+ included.
1203
+
1061
1204
  The page is loaded with a second context, built by `contextFor` from the new
1062
1205
  parms — see [The Express server](#the-express-server).
1063
1206
 
@@ -1067,7 +1210,8 @@ The builder checks the chrome and every page, after expansion, and refuses
1067
1210
  to build on any of these:
1068
1211
 
1069
1212
  - An `lb-` attribute that is not a developer attribute: `lb-query`,
1070
- `lb-column`, `lb-show`, `lb-request`, `lb-url-link`, `lb-url-push`,
1213
+ `lb-column`, `lb-show`, `lb-group`, `lb-count`, `lb-sum`, `lb-avg`,
1214
+ `lb-min`, `lb-max`, `lb-request`, `lb-url-link`, `lb-url-push`,
1071
1215
  `lb-url-unknown`. Every other `lb-` attribute is a stamp.
1072
1216
  `lb-exp-slot` and `lb-exp-template` are consumed by expansion and never
1073
1217
  reach the check.
@@ -1080,6 +1224,18 @@ to build on any of these:
1080
1224
  - `lb-show` on a `<template>`, on a row template's root, or with no
1081
1225
  `lb-query` around it.
1082
1226
  - `lb-show="!"` or `lb-show="!!column"`, which reverse no column.
1227
+ - `lb-group` on anything but a `<template>`, or on one holding no
1228
+ `<template>` for its contents.
1229
+ - An aggregate on a `<template>`, and any but `lb-count` naming no column.
1230
+ - A chrome without exactly one `<lb-hub>` and exactly one `<main>`, a
1231
+ `<main>` outside `<lb-hub>`, or a `<main>` with anything in it but
1232
+ whitespace.
1233
+ - A page carrying an `<lb-hub>` or a `<main>`.
1234
+
1235
+ The builder also reads the script of every custom element it ships, other
1236
+ than `<lb-hub>`, and refuses one that declares a method, accessor or field
1237
+ whose name begins with `lb` and a capital and is not in
1238
+ [Reserved methods](#reserved-methods).
1083
1239
 
1084
1240
  `lb-column` with no ancestor row is allowed: the hub gathers from it.
1085
1241
 
@@ -1099,7 +1255,8 @@ where the standard landmarks go: header, nav, footer, main.
1099
1255
  The file is an HTML document, which must contain:
1100
1256
  - `<lb-hub>` inside `<body>`. The hub can only act on its children,
1101
1257
  that is why it is usually nested right below `<body>`.
1102
- - One empty `<main>` inside `<lb-hub>`.
1258
+ - One empty `<main>` inside `<lb-hub>`. The builder refuses a chrome
1259
+ without exactly one of each, and a page carrying either.
1103
1260
  - The script tag for `/client.js`, the javascript bundle.
1104
1261
 
1105
1262
  It may also contain:
@@ -1209,6 +1366,8 @@ The builder writes `pages.ts` into `--out` whenever any page has a
1209
1366
  `.queries.ts` or a `.requests.ts` file. It imports each of them, calls
1210
1367
  `createHub()` once over the lot, and exports the result as `hub`. The
1211
1368
  server imports that rather than maintaining the registry by hand.
1369
+ `createHub` is the only implementation of the `Hub` interface, which may
1370
+ gain members in any release; an application never implements it.
1212
1371
 
1213
1372
  ```ts
1214
1373
  // server.ts
@@ -1273,7 +1432,8 @@ the whole path space through unchanged.
1273
1432
  Each key is a query name, which the markup names with `lb-query`.
1274
1433
 
1275
1434
  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
1435
+ `row(key, run)` or `rows(key, run, options)`, naming the key column first.
1436
+ A `rows` query's `options` declare its [order](#order). A query
1277
1437
  cannot be built without one of these functions. Every query has a key; an
1278
1438
  aggregate row answers with a constant one.
1279
1439
 
@@ -1291,14 +1451,15 @@ export const queries: Queries = {
1291
1451
  ```
1292
1452
 
1293
1453
  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
1454
+ objects, which the page shows in that order when the query declares none. An answer that disagrees with
1295
1455
  its declared kind is left out of the response, with a warning naming the
1296
1456
  query.
1297
1457
 
1298
1458
  Query names cannot begin with `lb-`, that namespace is reserved for
1299
1459
  Loadbare/app queries the hub serves in the browser, such as
1300
1460
  [`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.
1461
+ such a name as a query, a handler or a `crud` entry, and a query whose order
1462
+ has a term naming no column.
1302
1463
 
1303
1464
  Every query and every handler receives `ctx`, the application's own request
1304
1465
  context. The application builds it once per request and hands it to the hub,
@@ -1318,7 +1479,7 @@ It has three optional keys.
1318
1479
  | ------------- | --------------------- | -------------------------------------------------- |
1319
1480
  | `handlers` | The request name | The page's declared requests |
1320
1481
  | `crud` | A query name | The three requests Loadbare provides |
1321
- | `onPageEnter` | Nothing | Runs once on entering the page, before its queries |
1482
+ | `onPageEnter` | Nothing | Runs once on entering the page, before its queries, and may move the URL |
1322
1483
 
1323
1484
  The server resolves a declared request against `handlers`, and a request
1324
1485
  Loadbare provides against `crud` under the request's query. A name with no
@@ -1340,14 +1501,13 @@ and `values`, as present.
1340
1501
  the refreshed ones. That is how a delta reaches the browser: wrap it in
1341
1502
  `patch()`, naming the rows that arrived or changed and the keys that went.
1342
1503
 
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:
1504
+ A refreshed `rows` query sends every row to say what changed. An update or
1505
+ a delete names its row by key, so its handler always knows what changed and
1506
+ answers with a patch, which the hub places by the query's [order](#order):
1347
1507
  `createHub` refuses at startup a `crud` `rowUpdate` or `rowDelete` on a
1348
1508
  `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).
1509
+ row and may refresh itself. An insert may refresh its own query, since its
1510
+ key exists only once the row does.
1351
1511
 
1352
1512
  `run` may instead return `url()`, a new row for `lb-url`, naming query parms
1353
1513
  only the write can know, such as the key of a row it inserted. The page
@@ -1430,9 +1590,14 @@ A response item carries exactly one of `row`, `rows` or `patch`. A patch
1430
1590
  answers a `rows` query only: rows named in `rows` are added or updated, keys
1431
1591
  in `drop` are removed, and every other row is left alone.
1432
1592
 
1593
+ A `rows` item also carries `order` and `serverSortedByUrl` when the query
1594
+ declares them.
1595
+
1433
1596
  `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.
1597
+ with response items: `dataForPage(page, ctx, options)` for a page load, and
1598
+ `runRequest(page, request, ctx)` for a request. `options.parms` are the
1599
+ parms the page is loaded at, and `options.follow` false ignores a `url()`
1600
+ from `onPageEnter`; `hubRoutes` passes both.
1436
1601
 
1437
1602
  ## Custom elements
1438
1603
 
@@ -1468,9 +1633,9 @@ Each direction has one mechanism for each kind of message:
1468
1633
 
1469
1634
  | Direction | What | How | Today |
1470
1635
  | -------------- | ------------------------------------------- | ---------------------------- | ------------------------------------------------ |
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` |
1636
+ | 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
1637
  | 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` |
1638
+ | Hub to element | Work the hub needs done now, while landing | An optional `lb` method | `lbRowsLanded` |
1474
1639
  | Hub to element | A moment an element started | A bubbling event | `lb-request-done` |
1475
1640
  | Element to hub | A moment | A bubbling event | `lb-request`, and `change`, `click` and `submit` |
1476
1641
 
@@ -1512,8 +1677,9 @@ renders it; its content is never replaced.
1512
1677
  | Shows a page | `constructor`, `connectedCallback` |
1513
1678
  | Creates a live row, before placing it | `constructor` |
1514
1679
  | 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` |
1680
+ | Moves a live row the order put elsewhere | `connectedMoveCallback` where the browser moves in place, otherwise `disconnectedCallback`, `connectedCallback` |
1681
+ | Leaves a copy of a moved row behind | The copy's `constructor`, `connectedCallback`, and later `disconnectedCallback` |
1682
+ | Removes a live row | `disconnectedCallback`, once its animations end |
1517
1683
  | Turns `lb-show` off | `disconnectedCallback`, `adoptedCallback` |
1518
1684
  | Turns `lb-show` on | `adoptedCallback`, `connectedCallback` |
1519
1685
  | Turns on an element shipped absent | `constructor`, `connectedCallback` |
@@ -1525,19 +1691,27 @@ done once per instance goes behind a flag.
1525
1691
 
1526
1692
  ### Row hooks
1527
1693
 
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 |
1694
+ | Method | Implemented By | The hub calls it |
1695
+ | ---------------- | -------------- | ------------------------------------------------------ |
1696
+ | `lbRowsLanded()` | Developer | After the rows land, with every row placed and stamped |
1532
1697
 
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.
1698
+ The hub calls it on a custom element with `lb-query` and a row template.
1699
+ It is optional. The `RowsHost` interface in `@loadbare/app/types` declares
1700
+ it. The hub places every row and builds every group itself, so a custom
1701
+ element reacts to rows and never places them. Scrolling to the row the
1702
+ page's request created is the case:
1703
+
1704
+ ```ts
1705
+ lbRowsLanded() {
1706
+ this.querySelector(`[${ATTR_ROW_REQUESTED}="${REQUESTED_CREATED}"]`)
1707
+ ?.scrollIntoView({ block: "nearest" });
1708
+ }
1709
+ ```
1536
1710
 
1537
1711
  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.
1712
+ upgraded while its column is off. When the column first turns on and it
1713
+ upgrades, the hub lands the query's last answer on it again, as all rows,
1714
+ so the hook runs.
1541
1715
 
1542
1716
  `applyRow(root, row)`, exported by `@loadbare/app`, fills `root` from one
1543
1717
  row, the same operation that fills a live row.
@@ -1636,12 +1810,17 @@ it may define tomorrow.
1636
1810
  | `lb-url-link` | Developer | An `<a>` | [Links](#links) |
1637
1811
  | `lb-url-push` | Developer | An element with `lb-request` | [Query parms](#query-parms) |
1638
1812
  | `lb-url-unknown` | Developer | A `<dialog>` inside `<lb-hub>` | [An unknown page](#an-unknown-page) |
1813
+ | `lb-group` | Developer | A `<template>` holding a `<template>` | [Groups](#groups) |
1814
+ | `lb-count`, `lb-sum`, `lb-avg`, `lb-min`, `lb-max` | Developer | Any element but a `<template>` | [Aggregates](#aggregates) |
1639
1815
  | `lb-exp-slot` | Developer | One element in an element file | [Slots and templates](#slots-and-templates) |
1640
1816
  | `lb-exp-template` | Developer | An element file, and a `<template>` | [Slots and templates](#slots-and-templates) |
1641
1817
  | `lb-column-value` | Hub | Every element set from a column | [How a column lands](#how-a-column-lands) |
1642
1818
  | `lb-key-value` | Hub | A live row, and an element a `row` lands on | [Rows](#rows) |
1643
1819
  | `lb-row-live` | Hub | A live row | [Rows](#rows) |
1644
1820
  | `lb-query-row-count` | Hub | An element with a row template | [Rows](#rows) |
1821
+ | `lb-group-live`, `lb-group-column`, `lb-group-value` | Hub | Each top-level element of a group | [Groups](#groups) |
1822
+ | `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) |
1823
+ | `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
1824
  | `lb-request-pending` | Hub | The element that issued a request | [Request state](#request-state) |
1646
1825
  | `lb-request-error` | Hub | The element that issued a request | [Request state](#request-state) |
1647
1826
  | `lb-show` on a `<template>` | Hub and builder | Where an absent element stands | [Displaying by condition](#displaying-by-condition) |
@@ -1657,6 +1836,12 @@ The builder refuses any other `lb-` attribute in markup — see
1657
1836
  | -------- | ----- | --------- | --------------------------------------------------------------- | ------------------- |
1658
1837
  | `lb-url` | `row` | `lb-path` | `lb-path`, `lb-page-label`, `lb-page-unknown`, every query parm | [The URL](#the-url) |
1659
1838
 
1839
+ ### Reserved query parms
1840
+
1841
+ | Parm | Means | Defined in |
1842
+ | ------------------ | --------------------------- | --------------- |
1843
+ | `lb-order-<query>` | The user's order for a query | [Order](#order) |
1844
+
1660
1845
  ### Reserved request names
1661
1846
 
1662
1847
  | Request name | Runs under `crud` | Defined in |
@@ -1688,7 +1873,6 @@ Loadbare/app owns every DOM event in this table, both the name and what its
1688
1873
 
1689
1874
  | Method | On | Defined in |
1690
1875
  | -------------- | -------------------------------------------------- | ----------------------- |
1691
- | `lbPlaceRow` | A custom element with `lb-query` and a row template | [Row hooks](#row-hooks) |
1692
1876
  | `lbRowsLanded` | A custom element with `lb-query` and a row template | [Row hooks](#row-hooks) |
1693
1877
 
1694
1878
  ### Constants
@@ -1706,6 +1890,22 @@ imports them rather than writing a string.
1706
1890
  | `ATTR_ROW_LIVE` | `lb-row-live` |
1707
1891
  | `LIVE_ROW` | `[lb-row-live]`, the selector for a live row |
1708
1892
  | `ATTR_QUERY_ROW_COUNT` | `lb-query-row-count` |
1893
+ | `ATTR_GROUP` | `lb-group` |
1894
+ | `ATTR_GROUP_LIVE` | `lb-group-live` |
1895
+ | `ATTR_GROUP_COLUMN` | `lb-group-column` |
1896
+ | `ATTR_GROUP_VALUE` | `lb-group-value` |
1897
+ | `LIVE_GROUP` | `[lb-group-live]`, the selector for a group's element |
1898
+ | `ATTR_COUNT`, `ATTR_SUM`, `ATTR_AVG`, `ATTR_MIN`, `ATTR_MAX` | `lb-count`, `lb-sum`, `lb-avg`, `lb-min`, `lb-max` |
1899
+ | `AGGREGATES` | All five aggregate attributes |
1900
+ | `ORDER_PARM_PREFIX` | `lb-order-` |
1901
+ | `ORDER_DESCENDING` | `-`, written before a column in an order |
1902
+ | `ATTR_ROW_CREATED` | `lb-row-created` |
1903
+ | `ATTR_ROW_CHANGED` | `lb-row-changed` |
1904
+ | `ATTR_ROW_MOVED` | `lb-row-moved` |
1905
+ | `ATTR_ROW_REQUESTED` | `lb-row-requested` |
1906
+ | `REQUESTED_CREATED`, `REQUESTED_CHANGED`, `REQUESTED_MOVED` | `created`, `changed`, `moved` |
1907
+ | `ATTR_ROW_LEAVING` | `lb-row-leaving` |
1908
+ | `ATTR_GROUP_LEAVING` | `lb-group-leaving` |
1709
1909
  | `ATTR_REQUEST` | `lb-request` |
1710
1910
  | `ATTR_REQUEST_PENDING` | `lb-request-pending` |
1711
1911
  | `ATTR_REQUEST_ERROR` | `lb-request-error` |
@@ -1716,7 +1916,7 @@ imports them rather than writing a string.
1716
1916
  | `ATTR_EXP_TEMPLATE` | `lb-exp-template` |
1717
1917
  | `ATTR_PAGE` | `lb-page` |
1718
1918
  | `ATTR_PAGE_TITLE` | `lb-page-title` |
1719
- | `DEVELOPER_ATTRIBUTES` | The seven attributes a developer writes |
1919
+ | `DEVELOPER_ATTRIBUTES` | The thirteen attributes a developer writes |
1720
1920
  | `LB_EVENT_NAME` | `lb-request` |
1721
1921
  | `LB_DONE_EVENT_NAME` | `lb-request-done` |
1722
1922
  | `SHOW_NOT` | `!`, written before a column in `lb-show` |
@@ -72,6 +72,10 @@ Put everything the user interacts with inside `<lb-hub>`. The hub normally
72
72
  sits directly inside `<body>`, with banner, nav, footer and `<main>` inside
73
73
  it, so Loadbare can act on all of them.
74
74
 
75
+ Write exactly one `<lb-hub>` and exactly one `<main>`, with the `<main>`
76
+ inside the hub and empty but for whitespace. The builder rejects a chrome
77
+ that does otherwise.
78
+
75
79
  The chrome's `<title>` is the document title until the first page shows.
76
80
  From then on the hub sets the document title to the page's label; see
77
81
  [The URL](#the-url).
@@ -165,7 +169,9 @@ pushes one instead when the element carrying `lb-request` also carries
165
169
 
166
170
  The hub answers the request itself, with no round trip, then reloads the
167
171
  page's queries at the new URL. The page stays in place, and rows that come
168
- back keep their place.
172
+ back keep their place. A request that changes only `lb-order-<query>`, the
173
+ user's [order](./data-binding.md#order) for a query the hub sorts itself,
174
+ re-sorts the rows already on the page and loads nothing.
169
175
 
170
176
  A control whose column the URL does not carry lands empty, so every control
171
177
  inside `lb-query="lb-url"` shows what the address bar says, after a reload