@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
@@ -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
@@ -65,7 +65,7 @@ A widget package declares these. An application never does.
65
65
 
66
66
  ## Attributes
67
67
 
68
- The developer writes fourteen attributes in pages and the chrome, and the
68
+ The developer writes ten attributes in pages and the chrome, and the
69
69
  builder refuses any other `lb-` attribute there. `lb-exp-slot` and
70
70
  `lb-exp-template` are written for expansion, which removes them before the
71
71
  check. The hub and the builder write the rest, as stamps, and script never
@@ -76,9 +76,10 @@ assigns one.
76
76
  | `lb-query` | Developer | [Landing](./data-binding.md#landing) |
77
77
  | `lb-column` | Developer | [Landing](./data-binding.md#landing) |
78
78
  | `lb-show` | Developer | [Conditional rendering](./data-binding.md#conditional-rendering) |
79
- | `lb-order` | Developer | [Order](./data-binding.md#order) |
80
79
  | `lb-filter` | Developer | [Filters](./data-binding.md#filters) |
81
80
  | `lb-manual` | Developer | [Placing by hand](./data-binding.md#placing-by-hand) |
81
+ | `lb-group` | Developer | [Groups](./data-binding.md#groups) |
82
+ | `lb-aggregate` | Developer | [Aggregates](./data-binding.md#aggregates) |
82
83
  | `lb-request` | Developer | [Requests](./data-binding.md#requests) |
83
84
  | `lb-url-link` | Developer | [Links](./chrome.md#links) |
84
85
  | `lb-url-parm` | Developer | [A link from a row](./chrome.md#a-link-from-a-row) |
@@ -92,8 +93,9 @@ assigns one.
92
93
  | `lb-query-row-count` | Hub | [Counting rows](./data-binding.md#counting-rows) |
93
94
  | `lb-query-unplaced-count`, `lb-row-unplaced` | Hub | [Placing by hand](./data-binding.md#placing-by-hand) |
94
95
  | `lb-query-order` | Hub | [Order](./data-binding.md#order) |
96
+ | `lb-group-live`, `lb-group-column`, `lb-group-value` | Hub | [Groups](./data-binding.md#groups) |
95
97
  | `lb-row-created`, `lb-row-changed`, `lb-row-moved`, `lb-row-requested` | Hub | [What happened to a row](./data-binding.md#what-happened-to-a-row) |
96
- | `lb-row-leaving` | Hub | [What happened to a row](./data-binding.md#what-happened-to-a-row) |
98
+ | `lb-row-leaving`, `lb-group-leaving` | Hub | [What happened to a row](./data-binding.md#what-happened-to-a-row) |
97
99
  | `lb-row-editing` | Hub | [Editing a row in one form](./data-binding.md#editing-a-row-in-one-form) |
98
100
  | `lb-request-pending` | Hub | [Request state](./data-binding.md#request-state) |
99
101
  | `lb-request-error` | Hub | [Request state](./data-binding.md#request-state) |
@@ -111,7 +113,8 @@ them, and a page declares none.
111
113
  | Query | Kind | Key | Columns | Defined in |
112
114
  | ------------ | ----- | --------- | --------------------------------------------------------------- | ------------------------------------- |
113
115
  | `lb-url` | `row` | `lb-path` | `lb-path`, `lb-page-label`, every query parm | [The URL](./chrome.md#the-url) |
114
- | `lb-choices` | `row` | `lb-path` | `lb-path`, every choice | [Choices](./data-binding.md#choices) |
116
+ | `lb-sort` | `rows` | `query` | `query`, `order` | [Sort and filters](./data-binding.md#sort-and-filters) |
117
+ | `lb-filters` | `rows` | `query` | `query`, every filter column | [Sort and filters](./data-binding.md#sort-and-filters) |
115
118
  | `lb-hub-status` | `row` | `lb-path` | `lb-path`, `lb-load-state`, `lb-server-state`, `lb-error`, `lb-pending`, `lb-as-of` | [The hub's state](./chrome.md#the-hubs-state) |
116
119
 
117
120
  No query parm is reserved: a column of `lb-url` whose name begins with
@@ -170,7 +173,7 @@ accessor or field named `lb` and a capital.
170
173
 
171
174
  | Entry point | Exports | Defined in |
172
175
  | -------------------------- | ---------------------------------------------------------------- | ---------------------------------------------- |
173
- | `@loadbare/app/server` | `row`, `rows`, `patch`, `url`, `choices`, `refusal`, `createHub`, and the types `Queries`, `Requests`, `HubContext`, `Hub` | [Page Files](./page-files.md), [The data channel](./server.md#the-data-channel) |
176
+ | `@loadbare/app/server` | `row`, `rows`, `patch`, `url`, `sort`, `filters`, `refusal`, `createHub`, and the types `Queries`, `Requests`, `HubContext`, `Hub` | [Page Files](./page-files.md), [The data channel](./server.md#the-data-channel) |
174
177
  | `@loadbare/app/express` | `hubRoutes` | [The Express Server](./server.md) |
175
178
  | `@loadbare/app/dev` | `devServer`, `appServer`, `gate`, `devProxy`, `DOWN_MESSAGE` | [The Dev Server](./dev-server.md) |
176
179
  | `@loadbare/app/build` | `buildApp`, `watchApp` | [Assembling your own](./dev-server.md#assembling-your-own) |
@@ -196,10 +199,21 @@ imports them rather than writing a string.
196
199
  | `ATTR_QUERY_ROW_COUNT` | `lb-query-row-count` |
197
200
  | `ATTR_QUERY_UNPLACED_COUNT` | `lb-query-unplaced-count` |
198
201
  | `ATTR_QUERY_ORDER` | `lb-query-order` |
202
+ | `ATTR_GROUP` | `lb-group` |
203
+ | `ATTR_GROUP_LIVE` | `lb-group-live` |
204
+ | `ATTR_GROUP_COLUMN` | `lb-group-column` |
205
+ | `ATTR_GROUP_VALUE` | `lb-group-value` |
206
+ | `LIVE_GROUP` | `[lb-group-live]`, the selector for a group's element |
207
+ | `ATTR_AGGREGATE` | `lb-aggregate` |
208
+ | `AGGREGATE_COUNT`, `AGGREGATE_SUM`, `AGGREGATE_AVG`, `AGGREGATE_MIN`, `AGGREGATE_MAX` | `count`, `sum`, `avg`, `min`, `max` |
209
+ | `AGGREGATES` | All five aggregate functions |
199
210
  | `ORDER_DESCENDING` | `-`, written before a column in an order |
200
211
  | `COLUMN_TYPES` | `number`, `boolean`, `text` |
201
- | `CHOICES_QUERY` | `lb-choices` |
202
- | `ATTR_ORDER` | `lb-order` |
212
+ | `SORT_QUERY` | `lb-sort` |
213
+ | `SORT_COLUMN_QUERY` | `query` |
214
+ | `SORT_COLUMN_ORDER` | `order` |
215
+ | `FILTERS_QUERY` | `lb-filters` |
216
+ | `FILTERS_COLUMN_QUERY` | `query` |
203
217
  | `ATTR_FILTER` | `lb-filter` |
204
218
  | `ATTR_MANUAL` | `lb-manual` |
205
219
  | `MANUAL_ORDER`, `MANUAL_FILTER` | `order`, `filter`: the sides `lb-manual` names |
@@ -211,6 +225,7 @@ imports them rather than writing a string.
211
225
  | `REQUESTED_CREATED`, `REQUESTED_CHANGED`, `REQUESTED_MOVED` | `created`, `changed`, `moved` |
212
226
  | `ATTR_ROW_UNPLACED` | `lb-row-unplaced` |
213
227
  | `ATTR_ROW_LEAVING` | `lb-row-leaving` |
228
+ | `ATTR_GROUP_LEAVING` | `lb-group-leaving` |
214
229
  | `ATTR_REQUEST` | `lb-request` |
215
230
  | `LB_EVENT_NAME` | `lb-request`, the event every request travels as |
216
231
  | `LB_DONE_EVENT_NAME` | `lb-request-done` |
@@ -252,7 +267,7 @@ imports them rather than writing a string.
252
267
  | `ATTR_EXP_TEMPLATE` | `lb-exp-template` |
253
268
  | `ATTR_PAGE` | `lb-page` |
254
269
  | `ATTR_PAGE_TITLE` | `lb-page-title` |
255
- | `DEVELOPER_ATTRIBUTES` | The fourteen attributes a developer writes |
270
+ | `DEVELOPER_ATTRIBUTES` | The ten attributes a developer writes |
256
271
  | `HUB_TAG_NAME` | `lb-hub` |
257
272
  | `LB_ENDPOINT` | `/lb` |
258
273
  | `LB_REQUEST_TIMEOUT_MS` | `10000` |
@@ -152,8 +152,8 @@ optional:
152
152
 
153
153
  | Key | Runs |
154
154
  |---------------|--------------------------------------------------------|
155
- | `onPageEnter` | Before the page's queries, when the page loads; may move the URL or restore choices |
156
- | `onChoices` | Each time the user makes a choice |
155
+ | `onPageEnter` | Before the page's queries, when the page loads; may move the URL or restore the user's order and filters |
156
+ | `onChoices` | Each time the user changes the order or a filter |
157
157
  | `handlers` | The page's declared requests, by request name |
158
158
  | `crud` | The requests Loadbare provides, by query name |
159
159
 
@@ -190,34 +190,41 @@ loads there in the same round trip, and that load does not follow
190
190
  `onPageEnter` again. A `url()` leading to the parms already in hand is
191
191
  ignored. See [Moving the URL](#moving-the-url).
192
192
 
193
- It may return `choices()`, alone or beside `url()`, to restore the user's
194
- [choices](./data-binding.md#choices) for the page:
193
+ It may return `sort()` and `filters()`, alone or beside `url()`, to restore
194
+ the user's [order and filters](./data-binding.md#sort-and-filters) for the
195
+ page:
195
196
 
196
197
  ```ts
197
198
  // src/pages/directory.requests.ts
198
- import { choices, url, type Requests } from "@loadbare/app/server";
199
+ import { sort, filters, url, type Requests } from "@loadbare/app/server";
199
200
 
200
201
  export const requests: Requests = {
201
202
  onPageEnter: async (ctx) => {
202
203
  const saved = await ctx.db.savedView("directory");
203
- if (saved) return { ...url(saved.parms), ...choices(saved.choices) };
204
+ if (saved) {
205
+ return {
206
+ ...url(saved.parms),
207
+ ...sort(saved.order),
208
+ ...filters(saved.filters),
209
+ };
210
+ }
204
211
  },
205
- onChoices: (ctx, chosen) => ctx.db.saveChoices("directory", chosen),
212
+ onChoices: (ctx, change) => ctx.db.saveChoices(change),
206
213
  };
207
214
  ```
208
215
 
209
- The page load answers `lb-choices` first, so the rows land by the choices.
210
- When `url()` moves the page, the choices arrive with the load at the new
211
- URL. `choices()`, like `url()`, takes columns whose values are strings, at
212
- least one of them; anything else is reported on the server console and
213
- ignored.
216
+ The page load answers `lb-sort` and `lb-filters` first, so the rows land by
217
+ the order and the filters. When `url()` moves the page, the choices arrive
218
+ with the load at the new URL. `sort()` and `filters()`, like `url()`, take
219
+ columns whose values are strings, at least one of them; anything else is
220
+ reported on the server console and ignored.
214
221
 
215
222
  ### onChoices
216
223
 
217
- `onChoices` runs each time the user makes a choice, with every choice made
218
- on the page, by column, as strings. It is how an application keeps them for
219
- `onPageEnter` to restore, and answers nothing, by convention. Whatever it
220
- returns lands. A page without it is not told.
224
+ `onChoices` runs each time the user changes the order or a filter, with the
225
+ query and its new `sort` or `filters`, as strings. It is how an application
226
+ keeps them for `onPageEnter` to restore, and answers nothing, by
227
+ convention. Whatever it returns lands. A page without it is not told.
221
228
 
222
229
  ### handlers
223
230
 
@@ -377,6 +384,8 @@ A change that seems to need a refresh usually has a patch:
377
384
  [order](./data-binding.md#order), or leaves it where it stands under the
378
385
  list's [`lb-manual`](./data-binding.md#placing-by-hand); a refresh would do
379
386
  the same.
387
+ - A group that appears or goes: the hub makes a group with its first row
388
+ and removes it with its last, so no row stands in for an empty one.
380
389
  - A row whose other columns change with the edit: re-read the row and patch
381
390
  it.
382
391
  - Rows the database removes with a deleted one: select their keys before the
@@ -228,9 +228,9 @@ in `drop` are removed, and every other row is left alone. A `rows` item on
228
228
  the answer that loads the page also carries the query's declared `order`
229
229
  and `types`, which the hub keeps for every answer after it.
230
230
 
231
- A choice is sent as `lb-row-update` for `lb-choices`, keyed by the path,
232
- with every choice made on the page as `values`; see
233
- [Choices](./data-binding.md#choices).
231
+ A sort or a filter is sent as `lb-row-update` for `lb-sort` or `lb-filters`,
232
+ keyed by the query's name, with the new order or filter as `values`; see
233
+ [Sort and filters](./data-binding.md#sort-and-filters).
234
234
 
235
235
  `createHub(pages)` builds the `Hub`, which has two calls, both answering
236
236
  with response items: `dataForPage(page, ctx, options)` for a page load, and
package/docs/roadmap.md CHANGED
@@ -148,8 +148,8 @@ breaks nothing.
148
148
 
149
149
  The chrome belongs to no page, so the builder refuses an application query
150
150
  named there, and a custom element in the chrome cannot show application
151
- data. `lb-url` and `lb-choices` show the pattern from the framework side,
152
- and an application has no equivalent.
151
+ data. `lb-url`, `lb-sort` and `lb-filters` show the pattern from the
152
+ framework side, and an application has no equivalent.
153
153
 
154
154
  A chrome-level query set on `createHub` would close this, and it is additive.
155
155
  It takes its place in the [options on `createHub`](#options-on-createhub).
package/docs/testing.md CHANGED
@@ -172,8 +172,8 @@ watch, and surviving a failed build.
172
172
  that refreshes its own `rows` query are refused
173
173
  - an update or delete for several rows runs once per key, lays the patches
174
174
  together, and refreshes once
175
- - `onPageEnter` may return `url()` or `choices()`, and `onChoices` hears
176
- every choice the user makes
175
+ - `onPageEnter` may return `url()`, `sort()` or `filters()`, and
176
+ `onChoices` hears every order and filter the user makes
177
177
  - a refusal carries a message, columns or both, and passes through the
178
178
  engine untouched
179
179
 
@@ -210,11 +210,12 @@ evidence here.
210
210
  is not counted
211
211
  - the order and the filter place all rows and a patch alike, except where
212
212
  the list's `lb-manual` leaves a side to the user, which keeps its rows
213
- standing and stamped until they are placed again, and every change
213
+ standing and stamped until they are placed again; groups come and go with
214
+ their rows to any depth, aggregates follow every landing, and every change
214
215
  leaves a stamp a stylesheet can see (`lb-order.test.ts`)
215
216
  - a filter's text reads and passes values as documented, and a choice of
216
217
  order or filter places the rows already landed again, hiding rows and
217
- leaving them out of counts (`lb-choices.test.ts`)
218
+ groups and leaving them out of counts and aggregates (`lb-choices.test.ts`)
218
219
  - `lb-show` moves an element into a template and back, and landing reaches
219
220
  inside it
220
221
  - a row whose values arrive as it already shows them is not filled again,
@@ -275,8 +276,9 @@ What the hub is tested for here:
275
276
  deadline, under mocked timers, and fails the way any round trip fails
276
277
  - a successful insert gathered from a form resets the form, unless the
277
278
  answer landed the new row on it
278
- - a path with no page reports, lands `lb-page-unknown`, and opens the
279
- `lb-url-unknown` dialog; two pages for one stub report and take the first
279
+ - a path naming a page that does not exist reports, lands `lb-page-label`
280
+ as null, and opens the `lb-url-unknown` dialog; two pages for one stub
281
+ report and take the first
280
282
  - `lb-url` lands with the path, the page's `lb-page-title` or stub as
281
283
  `lb-page-label`, and a column per query parm, and sets the document title
282
284
  - `lb-url-link` pushes state and swaps the page on a plain primary click,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@loadbare/app",
3
3
  "description": "High performance web app framework for server-bound applications",
4
- "version": "0.26.0",
4
+ "version": "0.27.0",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",