@loadbare/app 0.26.0 → 0.27.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 (40) hide show
  1. package/dist/build/assemble.d.ts.map +1 -1
  2. package/dist/build/assemble.js +37 -1
  3. package/dist/build/assemble.js.map +1 -1
  4. package/dist/core/lb-constants.d.ts +14 -2
  5. package/dist/core/lb-constants.d.ts.map +1 -1
  6. package/dist/core/lb-constants.js +42 -6
  7. package/dist/core/lb-constants.js.map +1 -1
  8. package/dist/hub/lb-apply.d.ts +5 -2
  9. package/dist/hub/lb-apply.d.ts.map +1 -1
  10. package/dist/hub/lb-apply.js +402 -78
  11. package/dist/hub/lb-apply.js.map +1 -1
  12. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  13. package/dist/hub/lb-hub.browser.js +1 -2
  14. package/dist/hub/lb-hub.browser.js.map +1 -1
  15. package/dist/hub/lb-lifecycle.d.ts +2 -2
  16. package/dist/hub/lb-lifecycle.d.ts.map +1 -1
  17. package/dist/hub/lb-lifecycle.js +6 -5
  18. package/dist/hub/lb-lifecycle.js.map +1 -1
  19. package/docs/comparison.md +29 -22
  20. package/docs/miswarts.md +2 -2
  21. package/docs/opinions.md +2 -2
  22. package/docs/reference/chrome.md +8 -10
  23. package/docs/reference/css.md +20 -8
  24. package/docs/reference/custom-elements.md +19 -14
  25. package/docs/reference/data-binding.md +163 -61
  26. package/docs/reference/names.md +23 -8
  27. package/docs/reference/page-files.md +25 -16
  28. package/docs/reference/server.md +3 -3
  29. package/docs/roadmap.md +2 -2
  30. package/docs/testing.md +8 -6
  31. package/package.json +1 -1
  32. package/skills/loadbare-app/SKILL.md +16 -7
  33. package/skills/loadbare-app/references/chrome.md +8 -10
  34. package/skills/loadbare-app/references/css.md +20 -8
  35. package/skills/loadbare-app/references/custom-elements.md +19 -14
  36. package/skills/loadbare-app/references/data-binding.md +163 -61
  37. package/skills/loadbare-app/references/miswarts.md +2 -2
  38. package/skills/loadbare-app/references/names.md +23 -8
  39. package/skills/loadbare-app/references/page-files.md +25 -16
  40. package/skills/loadbare-app/references/server.md +3 -3
@@ -183,7 +183,7 @@ from it.
183
183
 
184
184
  **A column never holds rows.** Master-detail is a `row` query and a `rows`
185
185
  query under two names. Many masters with their details is one `rows` query
186
- of joined rows, ordered by the master first. A
186
+ of joined rows, ordered by the master first and shown in groups. A
187
187
  query nested inside another query's row template receives the same rows in
188
188
  every outer row; use it for a picker, never for per-row detail. Inside a
189
189
  form, the live rows of a query nested in the form's own are gathered as
@@ -243,10 +243,20 @@ stylesheet, `lb-hub[lb-load-state="failed"] .load-failed`, rather than
243
243
  leaving an empty page; see
244
244
  [`references/chrome.md`](references/chrome.md#the-hubs-state).
245
245
 
246
+ **Groups are templates.** A `<template lb-group>` holds a heading and one
247
+ nested `<template>`, the next level or the row template; the nth breaks on
248
+ the order's nth term, to any depth. Contents land before the nested
249
+ template, so put it inside the heading's element for a container and beside
250
+ the heading for a heading row. `<tbody>` and `<optgroup>` do not nest. The
251
+ heading is filled from the group's first row. `lb-aggregate` with `count`,
252
+ `sum`, `avg`, `min` or `max`, and `lb-column` naming the column, totals the
253
+ rows showing in the nearest group, or in the whole list outside any.
254
+ `count` with no column counts the rows.
255
+
246
256
  **Animate with CSS.** The hub stamps `lb-row-created`, `lb-row-changed`,
247
257
  `lb-row-moved` (its values changed and it now stands elsewhere), and
248
258
  `lb-row-requested` for this page's request, and marks a
249
- leaving row `lb-row-leaving`, removing it once
259
+ leaving row or group `lb-row-leaving` or `lb-group-leaving`, removing it once
250
260
  its animations end. Scrolling to a row is script, in `lbRowsLanded`.
251
261
 
252
262
  **Every request has one shape.** `lb-request` names the request, and the
@@ -376,8 +386,7 @@ the element sits.
376
386
  shows, a filter the server applies, a date range: each is a query parm,
377
387
  `/accounts?acct=23&from=2026-09-09`, so a reload, a bookmark or a mailed link
378
388
  shows the same thing. The hub serves the query `lb-url`, of kind `row`,
379
- keyed by `lb-path`, with `lb-page-label`, `lb-page-unknown`, and one column
380
- per query parm. A control inside `lb-query="lb-url"` carrying
389
+ keyed by `lb-path`, with `lb-page-label` and one column per query parm. A control inside `lb-query="lb-url"` carrying
381
390
  `lb-request="lb-row-update"` writes its parm; the hub replaces the history
382
391
  entry, or pushes one when the control carries `lb-url-push`, and reloads the
383
392
  page's queries with no request to the server. A parm the URL repeats,
@@ -421,9 +430,9 @@ whole data channel.
421
430
 
422
431
  **The developer writes `lb-*` attributes in markup, and script never
423
432
  assigns them.** The builder refuses any `lb-` attribute other than the
424
- ten in [Attributes](references/names.md#attributes): `lb-query`,
425
- `lb-column`, `lb-show`, `lb-filter`, `lb-manual`,
426
- `lb-request`,
433
+ twelve in [Attributes](references/names.md#attributes): `lb-query`,
434
+ `lb-column`, `lb-show`, `lb-filter`, `lb-manual`, `lb-group`,
435
+ `lb-aggregate`, `lb-request`,
427
436
  `lb-url-link`, `lb-url-parm`, `lb-url-push` and `lb-url-unknown`. `lb-exp-slot` and
428
437
  `lb-exp-template` are expansion's, and never reach the page. Everything
429
438
  else with the prefix is a stamp the hub or the builder writes.
@@ -101,7 +101,6 @@ server's row lands, so a chrome shows where the page is with no code:
101
101
  |-------------------|---------------------------------------------------------|
102
102
  | `lb-path` | The path, such as `/members`. The row's key |
103
103
  | `lb-page-label` | The page's title, or its stub when it has none |
104
- | `lb-page-unknown` | `true` when no page's stub matches the path |
105
104
  | Any other name | The query parm of that name, or empty when it is absent |
106
105
 
107
106
  The path names a page, never a resource: `/accounts/42` is not a way to
@@ -116,9 +115,9 @@ page file carries; see [page files](./page-files.md#the-page-title). The hub
116
115
  sets the document title to `lb-page-label` whenever it is not null, so the
117
116
  tab and the browser history show the page.
118
117
 
119
- `lb-url` is one of two queries a page may use that the server does not
120
- declare. The other is [`lb-choices`](./data-binding.md#choices). A page
121
- names either the same way, anywhere inside the hub.
118
+ `lb-url` is one of the queries a page may use that the server does not
119
+ declare. The others are `lb-sort`, `lb-filters` and `lb-hub-status`. A page
120
+ names any the same way, anywhere inside the hub.
122
121
 
123
122
  ### Where focus starts
124
123
 
@@ -223,8 +222,8 @@ pushes one instead when the element carrying `lb-request` also carries
223
222
  The hub answers the request itself, with no round trip, then reloads the
224
223
  page's queries at the new URL. The page stays in place, and rows that come
225
224
  back keep their place. A request that leaves the URL as it was does
226
- nothing. `lb-page-label` and `lb-page-unknown` are the hub's, and a request
227
- never sets them.
225
+ nothing. `lb-page-label` is the hub's, and a request
226
+ never sets it.
228
227
 
229
228
  A request that gives `lb-path` a new value enters that page, pushes a
230
229
  history entry, and the new URL carries only the query parms the request
@@ -288,8 +287,8 @@ it; a per-session database role means an id outside the caller's reach
288
287
  finds nothing.
289
288
 
290
289
  An order or a filter the hub applies to rows the page already holds is not
291
- a query parm. It is a [choice](./data-binding.md#choices), which holds until
292
- the page changes.
290
+ a query parm. It is a [sort or a filter](./data-binding.md#sort-and-filters),
291
+ which holds until the page changes.
293
292
 
294
293
  The hub sends the query parms with every round trip, and the server hands
295
294
  them to `contextFor`; see [the Express server](./server.md#database-layer).
@@ -308,8 +307,7 @@ show the path:
308
307
  </dialog>
309
308
  ```
310
309
 
311
- When no page's stub matches the path, `lb-page-unknown` is `true`,
312
- `lb-page-label` is null, `<main>` keeps the page it had, the hub calls
310
+ When the path names a page that does not exist, `lb-page-label` is null, `<main>` keeps the page it had, the hub calls
313
311
  `showModal()` on the dialog, and reports the path to the console. A chrome
314
312
  with no dialog shows nothing. `lb-url` lands before the page is looked up,
315
313
  so the dialog shows the path by naming it like any other element.
@@ -91,6 +91,17 @@ is not one of the list's rows.
91
91
  See [Rows](./data-binding.md#rows)
92
92
  and [Editing a row in one form](./data-binding.md#editing-a-row-in-one-form).
93
93
 
94
+ ### A group
95
+
96
+ Each top-level element of a group carries `lb-group-live`, the column it
97
+ breaks on as `lb-group-column`, and its value there as `lb-group-value`:
98
+
99
+ ```css
100
+ [lb-group-column="category"][lb-group-value="Income"] { color: var(--income); }
101
+ ```
102
+
103
+ See [Groups](./data-binding.md#groups).
104
+
94
105
  ### What just happened to a row
95
106
 
96
107
  The hub stamps what its last landing did, so a stylesheet animates with
@@ -102,7 +113,7 @@ nothing else:
102
113
  | `[lb-row-changed]` | A row whose values changed |
103
114
  | `[lb-row-moved]` | A row whose values changed and now stands elsewhere; it carries `lb-row-changed` too |
104
115
  | `[lb-row-requested]` | Any of the three, done by this page's request, as `created`, `changed` or `moved` |
105
- | `[lb-row-leaving]` | A row on its way out; the hub removes it when its animations end |
116
+ | `[lb-row-leaving]`, `[lb-group-leaving]` | A row or group on its way out; the hub removes it when its animations end |
106
117
 
107
118
  ```css
108
119
  [lb-row-created] { animation: arrive 200ms; }
@@ -166,18 +177,19 @@ form:not([lb-key-value=""]) .save-new { display: none; }
166
177
 
167
178
  See [Editing a row in one form](./data-binding.md#editing-a-row-in-one-form).
168
179
 
169
- ### The user's choices
180
+ ### The user's order and filters
170
181
 
171
- An order or a filter the user chose is a column of `lb-choices`, so the
172
- element showing it carries it as `lb-column-value`. Style anything else by
173
- it with `:has()`:
182
+ The order the user chose is the `order` column of `lb-sort`, and a filter
183
+ is a column of `lb-filters`, so the element showing either carries it as
184
+ `lb-column-value`. Style anything else by it with `:has()`:
174
185
 
175
186
  ```css
176
- table:has([lb-column="accounts_order"][lb-column-value="-balance"])
177
- th.balance::after { content: "▼"; }
187
+ :has([lb-column="name"][lb-column-value="^=Amazon"]) th.name::after {
188
+ content: "🔍";
189
+ }
178
190
  ```
179
191
 
180
- See [Choices](./data-binding.md#choices).
192
+ See [Sort and filters](./data-binding.md#sort-and-filters).
181
193
 
182
194
  ### Content shown by condition
183
195
 
@@ -268,7 +268,7 @@ Each direction has one mechanism for each kind of message:
268
268
 
269
269
  | Direction | What | How | Today |
270
270
  |----------------|--------------------------------------------|-----------------------------|-------|
271
- | Hub to element | State that lasts | An attribute the hub stamps | `lb-column-value`, `lb-key-value`, `lb-query-row-count`, `lb-query-unplaced-count`, `lb-query-order`, `lb-row-*`, `lb-request-pending`, `lb-request-error`, `lb-request-refused` |
271
+ | Hub to element | State that lasts | An attribute the hub stamps | `lb-column-value`, `lb-key-value`, `lb-query-row-count`, `lb-query-unplaced-count`, `lb-query-order`, `lb-group-*`, `lb-row-*`, `lb-request-pending`, `lb-request-error`, `lb-request-refused` |
272
272
  | Hub to element | State the platform already names | A property | A control's `value` |
273
273
  | Hub to element | What the hub did | An optional `lb` method | `lbAttached`, `lbRowsLanded`, `lbRequestDone`, `lbDetached` |
274
274
  | Hub to element | A moment an element started | A bubbling event | `lb-request-done` |
@@ -315,11 +315,11 @@ customElements.define("note-popup", NotePopup);
315
315
  | `lbRequestDone(detail, origin)` | After a request committed by the element, or inside it, has finished |
316
316
  | `lbDetached()` | Once, after it removes the element |
317
317
 
318
- The hub places an element when it starts up the chrome, shows a page, or
319
- creates a live row. It places the chrome and a page as
318
+ The hub places an element when it starts up the chrome, shows a page,
319
+ creates a live row, or builds a group. It places the chrome and a page as
320
320
  the page's first answer lands, or once that round trip fails, so an element
321
321
  in either hears `lbAttached` with the page's data in place. It removes an
322
- element when a live row has finished leaving, and when it shows
322
+ element when a live row or a group has finished leaving, and when it shows
323
323
  another page in place of the one holding it. Write setup in `lbAttached` and its undoing in
324
324
  `lbDetached`; the two pair exactly, so neither guards against running
325
325
  twice.
@@ -572,18 +572,21 @@ missing a field its name needs, and stamps the dispatching element with
572
572
 
573
573
  Let the event bubble, so an ancestor can stop it before the hub sends it.
574
574
 
575
- A request with `lb-row-update` for `lb-url` or `lb-choices` is keyed by the
576
- path, so an element anywhere on the page may set a query parm or make a
577
- [choice](./data-binding.md#choices) without sitting inside the query:
575
+ A request with `lb-row-update` for `lb-url` is keyed by the path; one for
576
+ `lb-sort` or `lb-filters` is keyed by the query's name. An element anywhere
577
+ on the page may set a query parm or make a
578
+ [sort or a filter](./data-binding.md#sort-and-filters) without sitting
579
+ inside the query:
578
580
 
579
581
  ```ts
580
- import { CHOICES_QUERY, LB_EVENT_NAME, REQUEST_ROW_UPDATE } from "@loadbare/app/constants";
582
+ import { SORT_QUERY, LB_EVENT_NAME, REQUEST_ROW_UPDATE } from "@loadbare/app/constants";
581
583
  import type { HubRequest } from "@loadbare/app/types";
582
584
 
583
585
  const detail: HubRequest = {
584
586
  name: REQUEST_ROW_UPDATE,
585
- query: CHOICES_QUERY,
586
- values: { ledger_order: "-amount" },
587
+ query: SORT_QUERY,
588
+ key: "ledger",
589
+ values: { order: "-amount" },
587
590
  };
588
591
  this.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
589
592
  ```
@@ -616,8 +619,8 @@ listener. Import its name as `LB_DONE_EVENT_NAME`.
616
619
  `items` is every response item that landed because of the request, in
617
620
  order. `refusal` is set beside `error` when the handler refused the
618
621
  request, holding its `message` and `columns`. An answer that moved the URL contributes its `lb-url` item, which
619
- carries a key only the server knew, and the page load. A choice contributes
620
- the row of `lb-choices` it made. `error` is set when
622
+ carries a key only the server knew, and the page load. A sort or a filter contributes
623
+ the row of `lb-sort` or `lb-filters` it made. `error` is set when
621
624
  the round trip failed, and then nothing landed. A new row's key is in the
622
625
  answer only when the handler returned it in a patch: a refresh sends all
623
626
  rows, and nothing marks which one is new.
@@ -633,7 +636,8 @@ had there hears the event, and no custom element the hub has removed hears
633
636
 
634
637
  The hub lands every row template itself, places every row by the query's
635
638
  order (see [Placing by hand](./data-binding.md#placing-by-hand)
636
- for when it does not). A
639
+ for when it does not), and builds every group the markup's group templates describe; see
640
+ [Order](./data-binding.md#order) and [Groups](./data-binding.md#groups). A
637
641
  plain element carrying `lb-query` with a row template inside it needs no
638
642
  code, so a custom element holds rows only to do something with them once
639
643
  they land.
@@ -661,7 +665,8 @@ Everything else is the hub's:
661
665
  | Cloning | Clones the row template once per new key |
662
666
  | Matching | Fills the live row already showing that key, when its values changed |
663
667
  | Placing | Places every live row by the user's order or the query's, and leaves them where they stand for an answer under `lb-manual` |
664
- | Filtering | Hides a row the user's filter does not pass |
668
+ | Filtering | Hides a row the user's filter does not pass, and a group whose rows are all hidden |
669
+ | Grouping | Builds a group where the order breaks, keeps it while its run goes on, and removes it with its last row |
665
670
  | Removing | Marks the live rows the response says are gone as leaving, and removes them |
666
671
  | Stamping | Stamps each live row with `lb-row-live`, `lb-key-value`, and what happened to it |
667
672
  | Counting | Stamps `lb-query-row-count` with the number of live rows showing, `lb-query-unplaced-count` with the number out of place, and `lb-query-order` with the order placing them |
@@ -2,7 +2,7 @@
2
2
 
3
3
  A page shows server data by naming queries and columns in its HTML, and asks
4
4
  the server to change that data by naming requests. The developer writes
5
- fourteen `lb-` attributes. The server declares every query a page may show and
5
+ ten `lb-` attributes. The server declares every query a page may show and
6
6
  every request it may send, and refuses anything it has not declared.
7
7
 
8
8
  Everything is a query. A query is a name for rows, of kind `row` or `rows`,
@@ -46,8 +46,9 @@ each with its key, and permits the three requests on `roster`; see
46
46
  | `lb-query` | Any element | The query whose rows land in it |
47
47
  | `lb-column` | Any element | The column it shows, and the column it gathers |
48
48
  | `lb-show` | Any element but `<template>` | The column that decides whether it is present |
49
- | `lb-order` | An element with `lb-query` | The choice holding the user's order; see [Order](#order) |
50
- | `lb-filter` | An element with `lb-query` | The columns a choice filters; see [Filters](#filters) |
49
+ | `lb-filter` | An element with `lb-query` | The columns the user may filter; see [Filters](#filters) |
50
+ | `lb-group` | A `<template>` | Nothing: it marks a group template; see [Groups](#groups) |
51
+ | `lb-aggregate` | Any element | The function it applies to `lb-column`; see [Aggregates](#aggregates) |
51
52
  | `lb-request` | Any element | The request it sends when it commits |
52
53
  | `lb-url-link` | An `<a>` | A link between pages; see [chrome](./chrome.md#links) |
53
54
  | `lb-url-parm` | An `<a lb-url-link>` in a row | Columns of the row its `href` carries as parms; see [chrome](./chrome.md#a-link-from-a-row) |
@@ -65,8 +66,9 @@ builder writes a stamp, and a stylesheet or a custom element reads it.
65
66
  | `lb-query-row-count` | The number of live rows an element shows |
66
67
  | `lb-query-unplaced-count` | The number of them out of place; see [Placing by hand](#placing-by-hand) |
67
68
  | `lb-query-order` | The order placing them; see [Order](#order) |
69
+ | `lb-group-live`, `lb-group-column`, `lb-group-value` | A group, the column it breaks on, and its value; see [Groups](#groups) |
68
70
  | `lb-row-created`, `lb-row-changed`, `lb-row-moved`, `lb-row-requested`, `lb-row-unplaced` | What happened to a row; see [What happened to a row](#what-happened-to-a-row) |
69
- | `lb-row-leaving` | A row on its way out |
71
+ | `lb-row-leaving`, `lb-group-leaving` | A row or group on its way out |
70
72
  | `lb-row-editing` | The row a form is pointed at; see [Editing a row in one form](#editing-a-row-in-one-form) |
71
73
  | `lb-request-pending` | The element's request is in flight |
72
74
  | `lb-request-error` | The element's last request failed |
@@ -86,7 +88,8 @@ elements as the page needs.
86
88
 
87
89
  What lands depends on the query's kind and on whether the element holds a
88
90
  row template, which is the first `<template>` among its descendants outside
89
- any nested `lb-query`:
91
+ any nested `lb-query`, or, when that template is a [group
92
+ template](#groups), the innermost template inside it:
90
93
 
91
94
  | Kind | Row template | The hub |
92
95
  |--------|--------------|----------------------------------------|
@@ -178,7 +181,7 @@ or `""`:
178
181
 
179
182
  A checkbox lands what it sends, so it shows the same state after a round
180
183
  trip through a query whose values are strings, such as `lb-url` or
181
- `lb-choices`. A query parm the URL does not carry lands as `""`, so the
184
+ `lb-filters`. A query parm the URL does not carry lands as `""`, so the
182
185
  checkbox is unchecked. Unchecked sends `"false"` rather than nothing, so a
183
186
  flag can be set back. A checkbox carrying a `value` attribute is not a flag
184
187
  but one of a set, holding a query parm the URL repeats; see
@@ -264,25 +267,22 @@ roster: rows("id", (ctx) => ctx.db.members(), { order: "team,name" }),
264
267
  With no order, rows show in the order they arrived, and a patch's new rows
265
268
  go last.
266
269
 
267
- The user's order replaces the server's. Write `lb-order` on the element
268
- carrying `lb-query`, naming a column of [`lb-choices`](#choices). That
269
- column holds an order written the same way, and while it is empty the
270
- server's order places the rows:
270
+ The user's order replaces the server's. The hub serves the query
271
+ [`lb-sort`](#sort-and-filters), one row per `rows` query, keyed by the
272
+ query's name, whose `order` column holds an order written the same way.
273
+ While it is empty, the server's order places the rows:
271
274
 
272
275
  ```html
273
- <div lb-query="lb-choices">
274
- <select lb-column="roster_order" lb-request="lb-row-update">
275
- <option value="">By team</option>
276
- <option value="name">By name</option>
277
- <option value="-joined">Newest first</option>
278
- </select>
279
- </div>
280
-
281
- <ul lb-query="roster" lb-order="roster_order">
276
+ <ul lb-query="roster">
282
277
  <template><li lb-column="name"></li></template>
283
278
  </ul>
284
279
  ```
285
280
 
281
+ A control makes a choice by sending `lb-row-update` for `lb-sort`, keyed by
282
+ the query's name; the `<lb-table>` widget's sortable headings do this for
283
+ each column. The hub places the rows again from the ones it already holds,
284
+ and tells the server through `onChoices`.
285
+
286
286
  | Values | Compare |
287
287
  |----------------------|----------------------------------------------|
288
288
  | Two numbers | Numerically |
@@ -314,7 +314,7 @@ user works through them. It names `order`, `filter` or both, separated by a
314
314
  space:
315
315
 
316
316
  ```html
317
- <lb-table lb-query="entries" lb-order="entries_order"
317
+ <lb-table lb-query="entries"
318
318
  lb-filter="description, is_unmatched" lb-manual="order filter">
319
319
  ```
320
320
 
@@ -325,7 +325,8 @@ form `--lb-edit` pointed at a row, or a form elsewhere on the page.
325
325
  | The answer | Under `lb-manual`, the row | Side |
326
326
  |----------------------------------------|---------------------------------------------------|----------|
327
327
  | Changes a column the order reads | Stays where it stands | `order` |
328
- | Creates a row | Goes last | `order` |
328
+ | Creates a row | Goes after the last row of its group, or last of all | `order` |
329
+ | Changes the column its group breaks on | Stays under its group's heading | `order` |
329
330
  | Changes a row so the filter hides it | Stays showing | `filter` |
330
331
  | Drops a row | Leaves, under either side | |
331
332
 
@@ -374,21 +375,22 @@ the move.
374
375
 
375
376
  Write `lb-filter` on the element carrying `lb-query`, naming the columns of
376
377
  its rows the user may filter, separated by commas. The column of
377
- [`lb-choices`](#choices) of the same name holds the filter for each:
378
+ [`lb-filters`](#sort-and-filters) of the same name holds the filter for
379
+ each:
378
380
 
379
381
  ```html
380
- <div lb-query="lb-choices">
381
- <input lb-column="name" lb-request="lb-row-update" placeholder="Name" />
382
- </div>
383
-
384
382
  <ul lb-query="roster" lb-filter="name, team">
385
383
  <template><li lb-column="name"></li></template>
386
384
  </ul>
387
385
  ```
388
386
 
387
+ A filter is made by sending `lb-row-update` for `lb-filters`, keyed by the
388
+ query's name; the `<lb-table>` widget's selection does this.
389
+
389
390
  A row that does not pass every filter is `hidden`. Under `lb-manual="filter"`, a row an answer changed stays
390
391
  showing until the rows are placed again; see
391
- [Placing by hand](#placing-by-hand). An empty
392
+ [Placing by hand](#placing-by-hand). A group whose rows are all hidden is hidden too
393
+ ([miswart](./miswarts.md#rows-hidden-by-a-filter)). An empty
392
394
  choice filters nothing.
393
395
 
394
396
  A filter is one or more terms, each an operator followed by a value:
@@ -421,6 +423,97 @@ column that is null or missing, pass only `!=`.
421
423
  `parseFilter`, `passes` and `passesTerm` in `@loadbare/app/types` are the
422
424
  rule the hub applies.
423
425
 
426
+ ### Groups
427
+
428
+ A group is a run of rows sharing the order's leading term. Write a
429
+ `<template lb-group>` where the row template goes, holding the group's
430
+ heading and one nested `<template>`: the next group template, or the row
431
+ template. The nth group template breaks on the order's nth term.
432
+
433
+ ```html
434
+ <ul lb-query="roster">
435
+ <template lb-group>
436
+ <li>
437
+ <h3 lb-column="team"></h3>
438
+ <ul>
439
+ <template><li lb-column="name"></li></template>
440
+ </ul>
441
+ </li>
442
+ </template>
443
+ </ul>
444
+ ```
445
+
446
+ The hub clones the group template where the order breaks, and the group's
447
+ contents land immediately before the clone of its nested template, so the
448
+ markup's nesting decides the shape:
449
+
450
+ | The nested template is | The group's contents land | Example |
451
+ |------------------------------|------------------------------------|-----------------------------------|
452
+ | Inside the heading's element | Inside it | `<tbody>`, `<li>`, `<details>` |
453
+ | Beside the heading | After the heading, as its siblings | A heading `<tr>` in one `<tbody>` |
454
+
455
+ `<tbody>` and `<optgroup>` do not nest, so a table's levels below the first
456
+ are heading rows beside their rows:
457
+
458
+ ```html
459
+ <table lb-query="accounts">
460
+ <thead><tr><th>Account</th></tr></thead>
461
+ <template lb-group>
462
+ <tbody>
463
+ <tr><th scope="rowgroup" lb-column="category"></th></tr>
464
+ <template><tr><td lb-column="account_name"></td></tr></template>
465
+ </tbody>
466
+ </template>
467
+ </table>
468
+ ```
469
+
470
+ Content after the nested template lands after the group's rows, which is
471
+ where a group's footer goes.
472
+
473
+ A heading is filled from its group's first row as an element in a row is,
474
+ `href`s from `lb-url-parm` included, so it may show a column other than the
475
+ one the group breaks on. Under `lb-manual="order"`, a row an answer
476
+ moved to another group stays under its old heading, and the heading shows
477
+ the row as it stood, until the rows are placed again. The hub stamps each top-level
478
+ element of a group with `lb-group-live`, `lb-group-column` and
479
+ `lb-group-value`. A row creates its group, and a group leaves with its last
480
+ row. When the order changes, a group whose run goes on is kept and moved,
481
+ and a group whose run ended leaves.
482
+
483
+ A group template with no nested template is reported to the console. More
484
+ group templates than the order has terms show each extra level as one
485
+ group, and are reported once.
486
+
487
+ ### Aggregates
488
+
489
+ `lb-aggregate` sets an element from the rows of the nearest group around
490
+ it, or of the whole list outside any group. It names the function, and
491
+ `lb-column` names the column:
492
+
493
+ ```html
494
+ <tr><td><span lb-aggregate="count"></span> people</td><td lb-aggregate="sum" lb-column="salary"></td></tr>
495
+ ```
496
+
497
+ | `lb-aggregate` | Sets the element to |
498
+ |----------------|-----------------------------------------------------------|
499
+ | `count` | The number of rows; with `lb-column`, the values in it that are not null |
500
+ | `sum` | The sum of the column |
501
+ | `avg` | The average of the column |
502
+ | `min` | The least value of the column, by the [order](#order)'s comparison |
503
+ | `max` | The greatest, likewise |
504
+
505
+ An aggregate lands as a column does, with `lb-column-value`; its
506
+ `lb-column` names the column it totals, and no row lands on it. A sum and an
507
+ average are exact decimals, read from JSON numbers and from strings holding
508
+ a plain decimal, as Postgres sends `numeric`. A sum has as many places as
509
+ the most precise value, and an average up to two more. `null` and empty
510
+ values are left out, as SQL leaves them out, and any other value is left
511
+ out and reported once. The result lands unformatted.
512
+
513
+ An aggregate covers the rows showing, so a row a filter hides is left out.
514
+ Where the true total matters beyond what the page shows, have the server
515
+ answer it as a row.
516
+
424
517
  ### What happened to a row
425
518
 
426
519
  Every change to a list is one a stylesheet can see:
@@ -433,6 +526,7 @@ Every change to a list is one a stylesheet can see:
433
526
  | `lb-row-requested` | `created`, `changed` or `moved` by this page's request |
434
527
  | `lb-row-unplaced` | Stands where the order or the filter would not leave it |
435
528
  | `lb-row-leaving` | A row on its way out |
529
+ | `lb-group-leaving` | A group on its way out |
436
530
 
437
531
  The hub touches a row when an answer names it. `lb-row-created`,
438
532
  `lb-row-changed` and `lb-row-moved` are cleared when it next does, and one
@@ -443,8 +537,8 @@ query, and set only by a landing that answers a request from the page.
443
537
  placed again; see
444
538
  [Placing by hand](#placing-by-hand).
445
539
 
446
- A row that leaves loses `lb-row-live`, takes
447
- `lb-row-leaving` and `inert`
540
+ A row or a group that leaves loses `lb-row-live` or `lb-group-live`, takes
541
+ `lb-row-leaving` or `lb-group-leaving` and `inert`
448
542
  ([miswart](./miswarts.md#leaving-rows-are-inert)), and is removed once its
449
543
  animations finish, at once when there are none.
450
544
 
@@ -468,7 +562,7 @@ The hub keeps each query's last answer until the page changes, with any
468
562
  patch since applied to it. An element that names a query and arrives after
469
563
  that query's answer is filled from what was kept, at the end of the landing
470
564
  that follows its arrival. Such an element is a picker in a new live row, or
471
- scaffolding around rows, such as a ghost row. So
565
+ scaffolding around rows, such as a group's heading or a ghost row. So
472
566
  a query nested in another query's rows need not land again, or land after
473
567
  the outer query, for a new outer row to show its choices.
474
568
 
@@ -477,8 +571,8 @@ and its `lbRowsLanded` runs as its rows land. See
477
571
  [Holding rows](./custom-elements.md#holding-rows).
478
572
 
479
573
  A request always goes to the server, and a query re-runs only when a
480
- response names it. A [choice](#choices) is the one thing landed from what
481
- the hub kept. A patch that
574
+ response names it. A [sort or a filter](#sort-and-filters) is the one thing
575
+ landed from what the hub kept. A patch that
482
576
  arrives before all of a query's rows is not kept, since it is not the whole
483
577
  of anything.
484
578
 
@@ -536,8 +630,8 @@ answered side by side. A column never holds rows.
536
630
  ```
537
631
 
538
632
  Many masters, each with its own detail, is one `rows` query of joined rows:
539
- each detail row carries its master's columns, ordered by the master's key
540
- first.
633
+ each detail row carries its master's columns. A [group template](#groups)
634
+ shows each master as a group, ordered by the master's key first.
541
635
 
542
636
  A query nested in another query's row template receives the same rows in
543
637
  every live row. That serves a picker offering the same choices on every
@@ -827,7 +921,7 @@ it, and sends nothing:
827
921
  Its controls start at their HTML defaults, and its `lb-show` branches are
828
922
  as for a row whose every column is null.
829
923
  - It stands after every row of the list, in the order added, and is not
830
- ordered or filtered, since it holds no data. It counts in
924
+ ordered, filtered or aggregated, since it holds no data. It counts in
831
925
  `lb-query-row-count`.
832
926
  - The form's submit carries it in `rows`, with `key: ""`, beside the rows on
833
927
  record: an insert while the master is not on record, an update once it
@@ -907,10 +1001,10 @@ The hub sends one of these only when it has what the table lists, and
907
1001
  otherwise reports to the console. An insert or an update that gathers
908
1002
  nothing is not sent.
909
1003
 
910
- For a query the hub serves, `lb-url` or `lb-choices`, the hub answers
911
- `lb-row-update` itself, with no round trip; see
912
- [Query parms](./chrome.md#query-parms) and [Choices](#choices). The third,
913
- `lb-hub-status`, takes no request.
1004
+ For a query the hub serves, `lb-url`, `lb-sort` or `lb-filters`, the hub
1005
+ answers `lb-row-update` itself, with no round trip; see
1006
+ [Query parms](./chrome.md#query-parms) and
1007
+ [Sort and filters](#sort-and-filters). `lb-hub-status` takes no request.
914
1008
 
915
1009
  A key comes from a live row, or from an element a `row` landed on, so all
916
1010
  three work against either kind:
@@ -1225,28 +1319,28 @@ answers it with no round trip:
1225
1319
  See [The URL](./chrome.md#the-url) for its columns, links, history, and the
1226
1320
  unknown-page dialog.
1227
1321
 
1228
- ## Choices
1322
+ ## Sort and filters
1229
1323
 
1230
- The hub serves a second query, `lb-choices`: one row keyed by `lb-path`,
1231
- with a column for each choice the user made for how rows show, an
1232
- [order](#order) or a [filter](#filters). The application names the
1233
- columns. A control inside `lb-query="lb-choices"` that sends
1234
- `lb-row-update` makes a choice, and an element anywhere may make one by
1235
- dispatching the `lb-request` event with `lb-row-update` for `lb-choices`,
1236
- which the hub keys by the path; see
1324
+ The hub serves two queries for how rows show: `lb-sort` and `lb-filters`,
1325
+ one row per `rows` query, keyed by the query's name. `lb-sort` holds each
1326
+ query's order as the server resolved it; `lb-filters` holds each query's
1327
+ filter values. An element makes a sort or a filter by dispatching
1328
+ `lb-request` with `lb-row-update` for `lb-sort` or `lb-filters`, keyed by
1329
+ the query's name; the `<lb-table>` widget's sortable headings and its
1330
+ selection do this; see
1237
1331
  [Sending a request](./custom-elements.md#sending-a-request).
1238
1332
 
1239
- A choice joins the ones already made, and shows with no round trip: every
1240
- element whose `lb-order` or `lb-filter` reads a column that changed lands
1241
- its query's last answer again, as a round trip carrying the choice would
1242
- have. Then the hub sends the server every choice made on the page, as
1243
- strings, in `lb-row-update` for `lb-choices`. No element waits on it, and
1244
- the page's [`onChoices`](./page-files.md#onchoices) runs, when it has one.
1333
+ A sort or a filter shows with no round trip: the hub places and filters the
1334
+ rows it already holds again, as a round trip carrying the choice would
1335
+ have. Then it sends the server the choice, in `lb-row-update` for `lb-sort`
1336
+ or `lb-filters`. No element waits on it, and the page's
1337
+ [`onChoices`](./page-files.md#onchoices) runs, when it has one, with the
1338
+ query and its new `sort` or `filters`.
1245
1339
 
1246
1340
  Choices hold until the page changes, and a change of query parm keeps them.
1247
- On entering a page, every column named under `lb-query="lb-choices"` lands
1248
- empty, unless the page's `onPageEnter` restores them. Choices are not query
1249
- parms, and the URL does not carry them.
1341
+ On entering a page, `lb-sort` and `lb-filters` land empty, unless the
1342
+ page's `onPageEnter` restores them with `sort()` and `filters()`. Choices
1343
+ are not query parms, and the URL does not carry them.
1250
1344
 
1251
1345
  ## The markup checks
1252
1346
 
@@ -1254,16 +1348,18 @@ The developer writes `lb-` attributes in markup, and script never assigns
1254
1348
  them. The builder checks the markup of the chrome and of every page, after
1255
1349
  expansion, and refuses to build:
1256
1350
 
1257
- - an `lb-` attribute that is not one of the fourteen the developer writes;
1351
+ - an `lb-` attribute that is not one of the ten the developer writes;
1258
1352
  `lb-exp-slot` and `lb-exp-template` are consumed by expansion and never
1259
1353
  reach the check
1260
1354
  - `lb-request` naming an `lb-` request other than the three Loadbare provides
1261
1355
  - `lb-query` naming a query the page does not declare; every page and the
1262
- chrome may name `lb-url` and `lb-choices`, and the chrome names no other
1356
+ chrome may name `lb-url`, `lb-sort`, `lb-filters` and `lb-hub-status`,
1357
+ and the chrome names no other
1263
1358
  - `lb-request` naming a request the page does not declare under `handlers`;
1264
1359
  the chrome sends none
1265
1360
  - one of the three requests for a query whose `crud` entry does not declare
1266
- it, or any but `lb-row-update` for `lb-url` or `lb-choices`; the query is
1361
+ it, or any but `lb-row-update` for `lb-url`, `lb-sort` or `lb-filters`;
1362
+ the query is
1267
1363
  a form's own when the form carries `lb-query`, and otherwise the nearest
1268
1364
  `lb-query` around the element
1269
1365
  - `lb-url-link` on anything but an `<a>`
@@ -1278,10 +1374,16 @@ expansion, and refuses to build:
1278
1374
  - `lb-show` on a `<template>`, or on a row template's root
1279
1375
  - `lb-show` with no `lb-query` around it
1280
1376
  - `lb-show="!"` or `lb-show="!!column"`, which reverse no column
1281
- - `lb-order` or `lb-filter` on an element not carrying `lb-query`, or naming
1282
- no column
1377
+ - `lb-filter` on an element not carrying `lb-query`, or naming no column
1283
1378
  - `lb-manual` on an element not carrying `lb-query`, or naming anything but
1284
1379
  `order`, `filter` or both
1380
+ - `lb-group` on anything but a `<template>`, or on one holding no
1381
+ `<template>` for its contents
1382
+ - `lb-aggregate` on a `<template>`, naming a function other than `count`,
1383
+ `sum`, `avg`, `min` or `max`, or, for any but `count`, beside no
1384
+ `lb-column`
1385
+ - `lb-count`, `lb-sum`, `lb-avg`, `lb-min` or `lb-max`, with what to write
1386
+ instead
1285
1387
  - a `command` beginning `--lb-` that is not one of the four commands
1286
1388
  - one of the four commands with no `commandfor`
1287
1389
  - `--lb-new` aimed at an element the markup holds that is neither a