@loadbare/app 0.16.0 → 0.17.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 (46) hide show
  1. package/README.md +10 -14
  2. package/dist/build/assemble.d.ts.map +1 -1
  3. package/dist/build/assemble.js +15 -1
  4. package/dist/build/assemble.js.map +1 -1
  5. package/dist/core/lb-constants.d.ts +6 -2
  6. package/dist/core/lb-constants.d.ts.map +1 -1
  7. package/dist/core/lb-constants.js +38 -8
  8. package/dist/core/lb-constants.js.map +1 -1
  9. package/dist/core/lb-types.d.ts +57 -20
  10. package/dist/core/lb-types.d.ts.map +1 -1
  11. package/dist/core/lb-types.js +122 -11
  12. package/dist/core/lb-types.js.map +1 -1
  13. package/dist/hub/lb-apply.d.ts +38 -26
  14. package/dist/hub/lb-apply.d.ts.map +1 -1
  15. package/dist/hub/lb-apply.js +655 -471
  16. package/dist/hub/lb-apply.js.map +1 -1
  17. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  18. package/dist/hub/lb-hub.browser.js +77 -59
  19. package/dist/hub/lb-hub.browser.js.map +1 -1
  20. package/dist/hub/lb-lifecycle.d.ts +1 -2
  21. package/dist/hub/lb-lifecycle.d.ts.map +1 -1
  22. package/dist/hub/lb-lifecycle.js +9 -3
  23. package/dist/hub/lb-lifecycle.js.map +1 -1
  24. package/dist/server/lb-server.d.ts +29 -12
  25. package/dist/server/lb-server.d.ts.map +1 -1
  26. package/dist/server/lb-server.js +72 -11
  27. package/dist/server/lb-server.js.map +1 -1
  28. package/docs/TECHREF-1.0.md +162 -48
  29. package/docs/comparison.md +9 -8
  30. package/docs/reference/chrome.md +8 -5
  31. package/docs/reference/custom-elements.md +25 -7
  32. package/docs/reference/data-binding.md +125 -22
  33. package/docs/reference/page-files.md +51 -7
  34. package/docs/reference/widgets.md +5 -1
  35. package/docs/roadmap.md +8 -0
  36. package/docs/testing.md +6 -2
  37. package/docs/theory-old.md +839 -0
  38. package/docs/theory.md +113 -839
  39. package/package.json +1 -1
  40. package/skills/loadbare-app/SKILL.md +28 -15
  41. package/skills/loadbare-app/references/TECHREF-1.0.md +162 -48
  42. package/skills/loadbare-app/references/chrome.md +8 -5
  43. package/skills/loadbare-app/references/custom-elements.md +25 -7
  44. package/skills/loadbare-app/references/data-binding.md +125 -22
  45. package/skills/loadbare-app/references/page-files.md +51 -7
  46. package/skills/loadbare-app/references/widgets.md +5 -1
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.16.0",
4
+ "version": "0.17.0",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: loadbare-app
3
- description: Build server-bound web applications with Loadbare/app. Covers the file conventions the builder finds by name (`chrome.html`, `*.page.html`, `*.queries.ts`, `*.requests.ts`, `imports.ts`, `<tag>.html`, `<tag>.browser.ts`); the `lb-*` attribute vocabulary that lands query rows on HTML and issues requests (`lb-query`, `lb-column`, `lb-show`, `lb-request`); queries declared with `row(key, run)` and `rows(key, run)`; the URL as the hub's own query `lb-url`, with `lb-url-link`, `lb-url-push` and `lb-url-unknown`; one form pointed at a row by the `--lb-edit`, `--lb-new` and `--lb-release` commands; build-time expansion with `exp-*` parameters, `lb-exp-slot` and `lb-exp-template`; custom elements as form-associated controls and row hosts; the Express wiring with `hubRoutes`; and the `@loadbare/widgets` library. Use whenever a task involves `@loadbare/app`, `@loadbare/widgets`, `loadbare-app-build`, an `lb-` attribute, or a page, query, request or custom element file in a Loadbare application. The model is not React, htmx or REST, and cannot be inferred from them.
3
+ description: Build server-bound web applications with Loadbare/app. Covers the file conventions the builder finds by name (`chrome.html`, `*.page.html`, `*.queries.ts`, `*.requests.ts`, `imports.ts`, `<tag>.html`, `<tag>.browser.ts`); the `lb-*` attribute vocabulary that lands query rows on HTML and issues requests (`lb-query`, `lb-column`, `lb-show`, `lb-request`); queries declared with `row(key, run)` and `rows(key, run)`; the URL as the hub's own query `lb-url`, with `lb-url-link`, `lb-url-push` and `lb-url-unknown`; the user's order and filters as the hub's query `lb-choices`, read by `lb-order` and `lb-filter`; one form pointed at a row by the `--lb-edit`, `--lb-new` and `--lb-release` commands; build-time expansion with `exp-*` parameters, `lb-exp-slot` and `lb-exp-template`; custom elements as form-associated controls and row hosts; the Express wiring with `hubRoutes`; and the `@loadbare/widgets` library. Use whenever a task involves `@loadbare/app`, `@loadbare/widgets`, `loadbare-app-build`, an `lb-` attribute, or a page, query, request or custom element file in a Loadbare application. The model is not React, htmx or REST, and cannot be inferred from them.
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  package: "@loadbare/app"
@@ -189,11 +189,23 @@ in.
189
189
  **A `rows` query declares its order, and the hub places every row by it.**
190
190
  `rows("id", run, { order: "category_order,name" })`; a column after `-` sorts
191
191
  descending, and values compare by JSON type, so a numeric sort column is a
192
- JSON number. The parm `lb-order-<query>` replaces the order, and a control
193
- writing it inside `lb-query="lb-url"` re-sorts with no round trip. A query
194
- that pages or limits reads the parm itself and declares
195
- `serverSortedByUrl: true`. Never sort in a widget, and never send a
196
- placeholder row to hold a place.
192
+ JSON number or is declared one: `types: { amount: "number" }` beside
193
+ `order`. Never sort in a widget, and never send a placeholder row to
194
+ hold a place.
195
+
196
+ **The user's order and filters are choices, not query parms.** The hub
197
+ serves a second query, `lb-choices`, keyed by `lb-path`, one column per
198
+ choice, named by the application. A control inside
199
+ `lb-query="lb-choices"` with `lb-request="lb-row-update"` makes a choice.
200
+ `lb-order="<choice>"` on the element carrying `lb-query` places its rows by
201
+ the order that choice holds, in place of the server's while it is not
202
+ empty. `lb-filter="col, col"` there hides each row that does not pass the
203
+ choice of the same name: `^=Amazon`, `>=2026-01-01 <=2026-03-31`, or plain
204
+ text for contains. A choice shows with no round trip, then the server's
205
+ `onChoices(ctx, choices)` hears every choice, to keep them;
206
+ `onPageEnter` returns `choices()` to restore them. Choices hold until the
207
+ page changes. A filter that decides which rows the server sends is a query
208
+ parm.
197
209
 
198
210
  **Groups are templates.** A `<template lb-group>` holds a heading and one
199
211
  nested `<template>`, the next level or the row template; the nth breaks on
@@ -201,11 +213,12 @@ the order's nth term, to any depth. Contents land before the nested
201
213
  template, so put it inside the heading's element for a container and beside
202
214
  the heading for a heading row. `<tbody>` and `<optgroup>` do not nest. The
203
215
  heading is filled from the group's first row. `lb-count`, `lb-sum="col"`,
204
- `lb-avg`, `lb-min` and `lb-max` total the nearest group, or the whole list
205
- outside any.
216
+ `lb-avg`, `lb-min` and `lb-max` total the rows showing in the nearest group,
217
+ or in the whole list outside any.
206
218
 
207
219
  **Animate with CSS.** The hub stamps `lb-row-created`, `lb-row-changed`,
208
- `lb-row-moved`, and `lb-row-requested` for this page's request, and marks a
220
+ `lb-row-moved` (its values changed and it now stands elsewhere), and
221
+ `lb-row-requested` for this page's request, and marks a
209
222
  leaving row or group `lb-row-leaving` or `lb-group-leaving`, removing it once
210
223
  its animations end. Scrolling to a row is script, in `lbRowsLanded`.
211
224
 
@@ -329,7 +342,7 @@ layer between the page and its queries.
329
342
  the element sits.
330
343
 
331
344
  **The query string is what the page has on screen.** Which record a page
332
- shows, a filter, a date range: each is a query parm,
345
+ shows, a filter the server applies, a date range: each is a query parm,
333
346
  `/accounts?acct=23&from=2026-09-09`, so a reload, a bookmark or a mailed link
334
347
  shows the same thing. The hub serves the query `lb-url`, of kind `row`,
335
348
  keyed by `lb-path`, with `lb-page-label`, `lb-page-unknown`, and one column
@@ -371,9 +384,9 @@ whole data channel.
371
384
 
372
385
  **The developer writes `lb-*` attributes in markup, and script never
373
386
  assigns them.** The builder refuses any `lb-` attribute other than
374
- `lb-query`, `lb-column`, `lb-show`, `lb-request`, `lb-url-link`,
375
- `lb-url-push` and `lb-url-unknown`. Everything else with the prefix is a
376
- stamp the hub or the builder writes.
387
+ `lb-query`, `lb-column`, `lb-show`, `lb-order`, `lb-filter`, `lb-request`,
388
+ `lb-url-link`, `lb-url-push` and `lb-url-unknown`. Everything else with the
389
+ prefix is a stamp the hub or the builder writes.
377
390
 
378
391
  **`lb-` belongs to Loadbare.** The application should never name anything
379
392
  with the `lb-` prefix anywhere: no attribute, query, column, request, custom
@@ -463,8 +476,8 @@ with the package, so no network is needed.
463
476
  gathering, requests, conditions and request state. Start here for
464
477
  anything in a page.
465
478
  - [`references/page-files.md`](references/page-files.md) — what page
466
- files are, queries, `onPageEnter`, handlers, `crud`, refresh and patch,
467
- and `url()`.
479
+ files are, queries, `onPageEnter`, `onChoices`, handlers, `crud`, refresh
480
+ and patch, `url()` and `choices()`.
468
481
  - [`references/custom-elements.md`](references/custom-elements.md) —
469
482
  expansion, parameters, slots, destinations, and custom element code and
470
483
  when it runs.
@@ -22,9 +22,13 @@ 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.
25
+ Three places read a value's type today. The [order](#order) compares
26
+ values by their JSON type, a [filter](#filters) reads its value as the
27
+ column's type, and an [aggregate](#aggregates) reads a sum or an average
28
+ from a JSON number or a string holding a plain decimal. A `rows` query may
29
+ declare a column's type, which the order, a filter, `lb-min` and `lb-max`
30
+ read the column as in place of its JSON type. No declared type changes how
31
+ a value renders.
28
32
 
29
33
  We will then see if a useful solution emerges that Loadbare/app should
30
34
  handle.
@@ -404,7 +408,7 @@ query.
404
408
  | `lb-column-value` | The value it set |
405
409
  | `lb-key-value` | The row's key |
406
410
  | `lb-row-live` | Nothing; it marks a live row |
407
- | `lb-query-row-count` | The number of live rows it holds |
411
+ | `lb-query-row-count` | The number of live rows it shows |
408
412
 
409
413
  The markup names a query and the columns it shows. The kind and the key are
410
414
  the server's, and the markup states neither.
@@ -441,7 +445,8 @@ element is a control.
441
445
  Name one query on more than one element to show it in more than one place.
442
446
  Every one of them receives it.
443
447
 
444
- A query that arrives with nowhere to land is reported to the console.
448
+ A query that arrives with nowhere to land is reported to the console,
449
+ unless its name begins with `lb-`.
445
450
 
446
451
  #### How a column lands
447
452
 
@@ -518,7 +523,8 @@ so all rows landing again unchanged move nothing. A live row already in the
518
523
  markup when the first answer lands is one of the query's rows.
519
524
 
520
525
  After every landing the hub stamps the element with `lb-query-row-count`,
521
- the number of live rows it holds. The server answers with rows and says
526
+ the number of live rows it shows. A row a [filter](#filters) hides is not
527
+ counted. The server answers with rows and says
522
528
  nothing about how many survived, so this is the one fact a page cannot be
523
529
  sent. It makes an empty query a stylesheet rule:
524
530
 
@@ -530,10 +536,15 @@ sent. It makes an empty query a stylesheet rule:
530
536
 
531
537
  A row missing its key column is reported to the console and not landed.
532
538
 
539
+ When the focus is in a live row that placing moves, the hub puts it back on
540
+ the same element and scrolls it into view. When that row leaves, or a
541
+ filter hides it, the focus goes to the first element the user can operate
542
+ in the first row showing.
543
+
533
544
  #### Order
534
545
 
535
546
  A `rows` query's order is a rule over its columns. The server declares it
536
- with the query, and the URL may carry the user's own:
547
+ with the query, and the user may choose another:
537
548
 
538
549
  ```ts
539
550
  accounts: rows("id", (ctx) => ctx.db.accounts(), {
@@ -542,9 +553,10 @@ accounts: rows("id", (ctx) => ctx.db.accounts(), {
542
553
  ```
543
554
 
544
555
  An order is columns separated by commas. A column written after `-` sorts
545
- descending. The query parm `lb-order-<query>` replaces the declared order
546
- for that query: `/coa?lb-order-accounts=-account_name`. With neither, rows
547
- show in the order the server sent them, and a patch's new rows go last.
556
+ descending. `lb-order`, on the element carrying `lb-query`, names a column
557
+ of [`lb-choices`](#choices) holding the user's order, and while that column
558
+ is not empty it replaces the declared order. With neither, rows show in the
559
+ order the server sent them, and a patch's new rows go last.
548
560
 
549
561
  | Values | Compare |
550
562
  | -------------------- | ------------------------------------------ |
@@ -555,27 +567,75 @@ show in the order the server sent them, and a patch's new rows go last.
555
567
  | `null` or missing | After everything ascending, first descending |
556
568
 
557
569
  A number sent as a string sorts as a string, so a column the order reads
558
- numerically arrives as a JSON number. Rows that compare equal keep the
559
- order they arrived in, and a patch's new rows come after the rows already
560
- showing.
570
+ numerically arrives as a JSON number, or its query declares its type.
571
+ `types` names a type for any of a `rows` query's columns, one of `number`,
572
+ `boolean` and `text`, and the column compares read as that type whatever
573
+ JSON type its values arrive as:
574
+
575
+ ```ts
576
+ accounts: rows("id", (ctx) => ctx.db.accounts(), {
577
+ order: "-balance",
578
+ types: { balance: "number" },
579
+ }),
580
+ ```
581
+
582
+ A value that is no value of its declared type compares as `null` does.
583
+ `createHub` refuses at startup a type it does not know.
584
+
585
+ The declared order and types travel once, on the answer that loads the
586
+ page, and the hub keeps them with the query's answer for every answer after
587
+ it, all rows or a patch. `readAs` in
588
+ `@loadbare/app/types` is the rule the hub applies.
589
+
590
+ Rows that compare equal keep the order they arrived in, and a patch's new
591
+ rows come after the rows already showing.
561
592
 
562
- The hub sorts the rows it has, so a control that writes the order parm
563
- re-sorts the page with no round trip:
593
+ The hub sorts the rows it has, so a choice of order places them again with
594
+ no round trip:
564
595
 
565
596
  ```html
566
- <div lb-query="lb-url">
567
- <select lb-column="lb-order-accounts" lb-request="lb-row-update">
597
+ <div lb-query="lb-choices">
598
+ <select lb-column="accounts_order" lb-request="lb-row-update">
568
599
  <option value="">By category</option>
569
600
  <option value="account_name">By name</option>
570
601
  </select>
571
602
  </div>
603
+
604
+ <table lb-query="accounts" lb-order="accounts_order">
605
+ ...
606
+ </table>
607
+ ```
608
+
609
+ #### Filters
610
+
611
+ `lb-filter`, on the element carrying `lb-query`, names the columns of its
612
+ rows a choice may filter, separated by commas. The column of
613
+ [`lb-choices`](#choices) of the same name holds the filter. A row that does
614
+ not pass every filter is `hidden`, and a ticked `lb-select` in it is
615
+ cleared. A group whose rows are all hidden is hidden. An empty choice
616
+ filters nothing.
617
+
618
+ ```html
619
+ <table lb-query="accounts" lb-filter="account_name, balance">
572
620
  ```
573
621
 
574
- A query that pages or limits its rows decides membership by its order, so
575
- it reads the parm itself, from the parms `contextFor` receives, and declares
576
- `serverSortedByUrl: true` beside `order`. A change of the user's order then
577
- loads the page again. The hub still places the rows it receives by the same
578
- order, so a patch lands in its place.
622
+ A filter is terms, each an operator and a value. A term begins wherever
623
+ whitespace comes before an operator, so a value may hold spaces. Text with
624
+ no operator is `*=`. A term with no value is left out. Every term must
625
+ pass: `>=2026-01-01 <=2026-03-31`.
626
+
627
+ | Operator | Compares |
628
+ | -------------------------- | ------------------------------------------------- |
629
+ | `^=`, `$=`, `*=` | As text, ignoring case: begins with, ends with, contains |
630
+ | `=`, `!=`, `<`, `<=`, `>`, `>=` | As the [order](#order) compares, with the value read as the column's type |
631
+
632
+ The column's type is its declared [type](#order), if its query declares
633
+ one. Otherwise it is a number for a JSON number, `true` or `false` for a
634
+ boolean, and text otherwise. A value that is not of the column's type, and
635
+ a column that is null or missing, pass only `!=`.
636
+
637
+ `parseFilter`, `passes` and `passesTerm` in `@loadbare/app/types` are the
638
+ rule the hub applies.
579
639
 
580
640
  #### Groups
581
641
 
@@ -617,8 +677,8 @@ filled, so it shows any column of the row. The example breaks on
617
677
  The hub stamps each top-level element of a group's clone with
618
678
  `lb-group-live`, `lb-group-column`, the column it breaks on, and
619
679
  `lb-group-value`, its value there. A row creates its group, and a group
620
- leaves with its last row. Groups never move: when a change of order puts
621
- the groups at a level in another order, each of them is made again.
680
+ leaves with its last row. When the order changes, a group whose run goes
681
+ on is kept and moved, and a group whose run ended leaves.
622
682
 
623
683
  A group template with no nested template is reported to the console. More
624
684
  group templates than the order has terms show each extra level as one
@@ -648,7 +708,8 @@ precise value, and an average up to two more. `null` and empty values are
648
708
  left out, as SQL leaves them out. Any other value is left out and reported
649
709
  once. The hub does not format the result.
650
710
 
651
- An aggregate covers the rows the browser holds. Where the true total
711
+ An aggregate covers the rows showing, so a row a [filter](#filters) hides
712
+ is left out. Where the true total
652
713
  matters beyond what the page shows, it is a row the server answers.
653
714
 
654
715
  #### What happened to a row
@@ -660,7 +721,7 @@ application animates with CSS alone and the hub starts no animation.
660
721
  | ------------------ | -------------------- | -------------------------------------------------- |
661
722
  | `lb-row-created` | A live row | Created when the hub last touched it |
662
723
  | `lb-row-changed` | A live row | Its values changed when the hub last touched it |
663
- | `lb-row-moved` | A live row | The order put it somewhere else when last touched |
724
+ | `lb-row-moved` | A live row | Its values changed, and it now stands elsewhere |
664
725
  | `lb-row-requested` | A live row | `created`, `changed` or `moved` by this page's request |
665
726
  | `lb-row-leaving` | A row on its way out | No longer live |
666
727
  | `lb-group-leaving` | A group on its way out | No longer live |
@@ -674,8 +735,12 @@ row's query, and set only by a landing that answers a request from the page.
674
735
  A row or a group that leaves loses `lb-row-live` or `lb-group-live`, takes
675
736
  `lb-row-leaving` or `lb-group-leaving` and `inert`, and is removed once the
676
737
  animations running on it have finished, at once when there are none. A
677
- moved row stays live and moves. A copy of it stays behind where it was,
678
- leaving.
738
+ row leaves when all rows arrive without its key, or a patch drops it.
739
+
740
+ A row is moved when its values changed and the nearest rows whose values did
741
+ not change, before and after it, are not the ones it stood between. A row
742
+ it passed is not stamped, and a change of the user's order, which changes no
743
+ values, stamps none. A moved row stays live and moves.
679
744
 
680
745
  ```css
681
746
  [lb-row-created] { animation: arrive 200ms; }
@@ -1341,10 +1406,10 @@ names each one in the row it lands.
1341
1406
 
1342
1407
  The hub sends the query parms with every round trip, a page load and a
1343
1408
  request alike. The server passes them to `contextFor` — see
1344
- [The Express server](#the-express-server). Loadbare assigns one parm a
1345
- meaning, `lb-order-<query>`, the user's [order](#order) for a query, and it
1346
- reaches `contextFor` like any other. A request that changes only order
1347
- parms of queries the hub sorts itself makes no round trip.
1409
+ [The Express server](#the-express-server). Loadbare assigns no parm a
1410
+ meaning: a column of `lb-url` whose name begins with `lb-` is never a query
1411
+ parm. An order or a filter the hub applies to rows already on the page is
1412
+ a [choice](#choices), not a parm.
1348
1413
 
1349
1414
  #### An unknown page
1350
1415
 
@@ -1409,8 +1474,8 @@ rowInsert: {
1409
1474
  to other query parms, keeps the URL they are on.
1410
1475
 
1411
1476
  `onPageEnter` may return `url()` the same way, to move the page before
1412
- anything shows, as an application restoring the filters and order the user
1413
- last had does. The page loads at the new URL in the same round trip, and
1477
+ anything shows, as an application restoring the parms the user last had
1478
+ does. The page loads at the new URL in the same round trip, and
1414
1479
  that load does not follow `onPageEnter` again. A `url()` leading to the
1415
1480
  page and parms already in hand is ignored, so the page loads once. When to
1416
1481
  restore is the application's rule, such as only when the URL carries no
@@ -1428,12 +1493,51 @@ onPageEnter: async (ctx) => {
1428
1493
  Saving what the user has on screen is a declared request with
1429
1494
  `refresh: []`, whose handler each page passes to a routine the
1430
1495
  application's pages share. A custom element naming `lb-url` sees every
1431
- change of the URL and issues it, a change of order that made no round trip
1432
- included.
1496
+ change of the URL and issues it.
1433
1497
 
1434
1498
  The page is loaded with a second context, built by `contextFor` from the new
1435
1499
  parms — see [The Express server](#the-express-server).
1436
1500
 
1501
+ ### Choices
1502
+
1503
+ The hub serves the query `lb-choices`, of kind `row`, keyed by `lb-path`,
1504
+ with a column for each choice the user made for how rows show: an
1505
+ [order](#order) or a [filter](#filters). The application names the
1506
+ columns. It lands wherever an element names it, like any server query.
1507
+
1508
+ A control inside `lb-query="lb-choices"` that sends `lb-row-update` makes a
1509
+ choice. A request with `lb-row-update` for `lb-choices` or `lb-url` from
1510
+ anywhere on the page is keyed by the path, so a custom element may
1511
+ dispatch one without sitting inside the query.
1512
+
1513
+ A choice joins the ones already made, and needs no round trip to show:
1514
+ every element whose `lb-order` or `lb-filter` reads a column that changed
1515
+ lands its query's last answer again, so it shows what a round trip carrying
1516
+ the choice would. Then the hub sends `lb-row-update` for `lb-choices`,
1517
+ keyed by the path, with every choice made on the page as `values`, and no
1518
+ element pending. The server runs the page's `onChoices` with them, when it
1519
+ has one, and otherwise answers nothing. Whatever `onChoices` returns lands,
1520
+ and by convention it returns nothing.
1521
+
1522
+ Choices hold until the page changes, and a change of query parm keeps them.
1523
+ On entering a page, every column named under `lb-query="lb-choices"` lands
1524
+ empty. `onPageEnter` restores them by returning `choices()`, alone or
1525
+ spread beside `url()`:
1526
+
1527
+ ```ts
1528
+ onPageEnter: async (ctx) => {
1529
+ const saved = await ctx.db.savedChoices("accounts");
1530
+ if (saved) return choices(saved);
1531
+ },
1532
+ onChoices: (ctx, chosen) => ctx.db.saveChoices("accounts", chosen),
1533
+ ```
1534
+
1535
+ The page load then answers the `lb-choices` item first, then the queries.
1536
+ When `url()` moves the page, the choices arrive with the load at the new
1537
+ URL. A
1538
+ `choices()` naming no column, or holding a value that is not a string, is
1539
+ reported on the server console and ignored, as for `url()`.
1540
+
1437
1541
  ### The markup checks
1438
1542
 
1439
1543
  The builder checks the chrome and every page, after expansion, and refuses
@@ -1441,8 +1545,8 @@ to build on any of these:
1441
1545
 
1442
1546
  - An `lb-` attribute that is not a developer attribute: `lb-query`,
1443
1547
  `lb-column`, `lb-show`, `lb-group`, `lb-count`, `lb-sum`, `lb-avg`,
1444
- `lb-min`, `lb-max`, `lb-request`, `lb-select`, `lb-url-link`,
1445
- `lb-url-push`, `lb-url-unknown`. Every other `lb-` attribute is a stamp.
1548
+ `lb-min`, `lb-max`, `lb-order`, `lb-filter`, `lb-request`, `lb-select`,
1549
+ `lb-url-link`, `lb-url-push`, `lb-url-unknown`. Every other `lb-` attribute is a stamp.
1446
1550
  `lb-exp-slot` and `lb-exp-template` are consumed by expansion and never
1447
1551
  reach the check.
1448
1552
  - `lb-request` naming a value that begins with `lb-` and is not one of the
@@ -1454,6 +1558,8 @@ to build on any of these:
1454
1558
  - `lb-show` on a `<template>`, on a row template's root, or with no
1455
1559
  `lb-query` around it.
1456
1560
  - `lb-show="!"` or `lb-show="!!column"`, which reverse no column.
1561
+ - `lb-order` or `lb-filter` on an element not carrying `lb-query`, or naming
1562
+ no column.
1457
1563
  - `lb-select` on anything but an `<input type="checkbox">`, outside a row
1458
1564
  template, or beside `lb-column` or `lb-request`.
1459
1565
  - A `command` beginning `--lb-` that is not one of the three
@@ -1718,13 +1824,14 @@ as explained in the next section.
1718
1824
  ### Page Requests
1719
1825
 
1720
1826
  `<stub>.requests.ts` exports one object named `requests`, typed `Requests`.
1721
- It has three optional keys.
1827
+ It has four optional keys.
1722
1828
 
1723
1829
  | Key | Keyed by | Answers |
1724
1830
  | ------------- | --------------------- | -------------------------------------------------- |
1725
1831
  | `handlers` | The request name | The page's declared requests |
1726
1832
  | `crud` | A query name | The three requests Loadbare provides |
1727
- | `onPageEnter` | Nothing | Runs once on entering the page, before its queries, and may move the URL |
1833
+ | `onPageEnter` | Nothing | Runs once on entering the page, before its queries, and may move the URL or restore [choices](#choices) |
1834
+ | `onChoices` | Nothing | Runs with every choice made on the page, each time the user makes one |
1728
1835
 
1729
1836
  The server resolves a declared request against `handlers`, and a request
1730
1837
  Loadbare provides against `crud` under the request's query. A name with no
@@ -1840,8 +1947,10 @@ A response item carries exactly one of `row`, `rows` or `patch`. A patch
1840
1947
  answers a `rows` query only: rows named in `rows` are added or updated, keys
1841
1948
  in `drop` are removed, and every other row is left alone.
1842
1949
 
1843
- A `rows` item also carries `order` and `serverSortedByUrl` when the query
1844
- declares them.
1950
+ A `rows` item also carries `order` when the query declares one.
1951
+
1952
+ A choice is sent as `lb-row-update` for `lb-choices`, keyed by the path,
1953
+ with every choice made on the page as `values`; see [Choices](#choices).
1845
1954
 
1846
1955
  `createHub(pages)` builds the `Hub`, which has two calls, both answering
1847
1956
  with response items: `dataForPage(page, ctx, options)` for a page load, and
@@ -1926,7 +2035,7 @@ calls four optional methods, which the `HubLifecycle` interface in
1926
2035
  | Removes it: a row or group done leaving, the page replaced | `lbDetached()`, once |
1927
2036
  | Moves a live row the order put elsewhere | Nothing |
1928
2037
  | Turns `lb-show` off or on | Nothing |
1929
- | Leaves a copy of a moved row behind | Nothing, on the copy |
2038
+ | Hides a row or a group for a filter | Nothing |
1930
2039
 
1931
2040
  The chrome and a page are placed as the page's first answer lands, or once
1932
2041
  that round trip fails. An element under an `lb-show` that is off is placed
@@ -2062,6 +2171,8 @@ it may define tomorrow.
2062
2171
  | `lb-url-unknown` | Developer | A `<dialog>` inside `<lb-hub>` | [An unknown page](#an-unknown-page) |
2063
2172
  | `lb-group` | Developer | A `<template>` holding a `<template>` | [Groups](#groups) |
2064
2173
  | `lb-count`, `lb-sum`, `lb-avg`, `lb-min`, `lb-max` | Developer | Any element but a `<template>` | [Aggregates](#aggregates) |
2174
+ | `lb-order` | Developer | An element with `lb-query` | [Order](#order) |
2175
+ | `lb-filter` | Developer | An element with `lb-query` | [Filters](#filters) |
2065
2176
  | `lb-exp-slot` | Developer | One element in an element file | [Slots and templates](#slots-and-templates) |
2066
2177
  | `lb-exp-template` | Developer | An element file, and a `<template>` | [Slots and templates](#slots-and-templates) |
2067
2178
  | `lb-column-value` | Hub | Every element set from a column | [How a column lands](#how-a-column-lands) |
@@ -2088,12 +2199,12 @@ The builder refuses any other `lb-` attribute in markup — see
2088
2199
  | Query | Kind | Key | Columns | Defined in |
2089
2200
  | -------- | ----- | --------- | --------------------------------------------------------------- | ------------------- |
2090
2201
  | `lb-url` | `row` | `lb-path` | `lb-path`, `lb-page-label`, `lb-page-unknown`, every query parm | [The URL](#the-url) |
2202
+ | `lb-choices` | `row` | `lb-path` | `lb-path`, every choice | [Choices](#choices) |
2091
2203
 
2092
2204
  ### Reserved query parms
2093
2205
 
2094
- | Parm | Means | Defined in |
2095
- | ------------------ | --------------------------- | --------------- |
2096
- | `lb-order-<query>` | The user's order for a query | [Order](#order) |
2206
+ None. A column of `lb-url` whose name begins with `lb-` is never a query
2207
+ parm — see [Query parms](#query-parms).
2097
2208
 
2098
2209
  ### Reserved request names
2099
2210
 
@@ -2164,8 +2275,11 @@ imports them rather than writing a string.
2164
2275
  | `LIVE_GROUP` | `[lb-group-live]`, the selector for a group's element |
2165
2276
  | `ATTR_COUNT`, `ATTR_SUM`, `ATTR_AVG`, `ATTR_MIN`, `ATTR_MAX` | `lb-count`, `lb-sum`, `lb-avg`, `lb-min`, `lb-max` |
2166
2277
  | `AGGREGATES` | All five aggregate attributes |
2167
- | `ORDER_PARM_PREFIX` | `lb-order-` |
2168
2278
  | `ORDER_DESCENDING` | `-`, written before a column in an order |
2279
+ | `CHOICES_QUERY` | `lb-choices` |
2280
+ | `ATTR_ORDER` | `lb-order` |
2281
+ | `ATTR_FILTER` | `lb-filter` |
2282
+ | `FILTER_OPERATORS` | Every filter operator, the longer first |
2169
2283
  | `ATTR_ROW_CREATED` | `lb-row-created` |
2170
2284
  | `ATTR_ROW_CHANGED` | `lb-row-changed` |
2171
2285
  | `ATTR_ROW_MOVED` | `lb-row-moved` |
@@ -2191,7 +2305,7 @@ imports them rather than writing a string.
2191
2305
  | `ATTR_EXP_TEMPLATE` | `lb-exp-template` |
2192
2306
  | `ATTR_PAGE` | `lb-page` |
2193
2307
  | `ATTR_PAGE_TITLE` | `lb-page-title` |
2194
- | `DEVELOPER_ATTRIBUTES` | The fourteen attributes a developer writes |
2308
+ | `DEVELOPER_ATTRIBUTES` | The sixteen attributes a developer writes |
2195
2309
  | `LB_EVENT_NAME` | `lb-request` |
2196
2310
  | `LB_DONE_EVENT_NAME` | `lb-request-done` |
2197
2311
  | `SHOW_NOT` | `!`, written before a column in `lb-show` |
@@ -109,8 +109,9 @@ page file carries; see [page files](./page-files.md#the-page-title). The hub
109
109
  sets the document title to `lb-page-label` whenever it is not null, so the
110
110
  tab and the browser history show the page.
111
111
 
112
- `lb-url` is the one query a page may use that the server does not declare. A
113
- page names it the same way, anywhere inside the hub.
112
+ `lb-url` is one of two queries a page may use that the server does not
113
+ declare. The other is [`lb-choices`](./data-binding.md#choices). A page
114
+ names either the same way, anywhere inside the hub.
114
115
 
115
116
  ### Where focus starts
116
117
 
@@ -169,14 +170,16 @@ pushes one instead when the element carrying `lb-request` also carries
169
170
 
170
171
  The hub answers the request itself, with no round trip, then reloads the
171
172
  page's queries at the new URL. The page stays in place, and rows that come
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.
173
+ back keep their place.
175
174
 
176
175
  A control whose column the URL does not carry lands empty, so every control
177
176
  inside `lb-query="lb-url"` shows what the address bar says, after a reload
178
177
  and after Back alike.
179
178
 
179
+ An order or a filter the hub applies to rows the page already holds is not
180
+ a query parm. It is a [choice](./data-binding.md#choices), which holds until
181
+ the page changes.
182
+
180
183
  The hub sends the query parms with every round trip, and the server hands
181
184
  them to `contextFor`; see [the Express server](./server.md#database-layer).
182
185
  A handler moves the URL by returning `url()`, when only the server knows the
@@ -271,6 +271,7 @@ one as a string literal:
271
271
  | `LB_DONE_EVENT_NAME` | `lb-request-done`, the event that says how it turned out |
272
272
  | `SHOW_NOT` | `!`, written before a column in `lb-show` |
273
273
  | `URL_QUERY` | `lb-url` |
274
+ | `CHOICES_QUERY` | `lb-choices` |
274
275
  | `URL_COLUMN_PATH`, `URL_COLUMN_PAGE_LABEL`, `URL_COLUMN_PAGE_UNKNOWN` | `lb-path`, `lb-page-label`, `lb-page-unknown` |
275
276
  | `LB_RESERVED_PREFIX` | `lb-` |
276
277
 
@@ -356,9 +357,8 @@ inside it, and `lbDetached` on the inner ones first. A table that sets the
356
357
  `form` attribute on its controls in `lbAttached` does so before each control
357
358
  hears its own.
358
359
 
359
- An element a script creates hears none of the four, and neither does the
360
- copy of a moved row the hub leaves behind while it fades. The hub calls
361
- them only on what it places.
360
+ An element a script creates hears none of the four. The hub calls them only
361
+ on what it places.
362
362
 
363
363
  Never write `connectedCallback`, `disconnectedCallback`, `adoptedCallback`
364
364
  or `connectedMoveCallback`. The browser calls those on every move, and the
@@ -589,6 +589,22 @@ missing a field its name needs, and stamps the dispatching element with
589
589
 
590
590
  Let the event bubble, so an ancestor can stop it before the hub sends it.
591
591
 
592
+ A request with `lb-row-update` for `lb-url` or `lb-choices` is keyed by the
593
+ path, so an element anywhere on the page may set a query parm or make a
594
+ [choice](./data-binding.md#choices) without sitting inside the query:
595
+
596
+ ```ts
597
+ import { CHOICES_QUERY, LB_EVENT_NAME, REQUEST_ROW_UPDATE } from "@loadbare/app/constants";
598
+ import type { HubRequest } from "@loadbare/app/types";
599
+
600
+ const detail: HubRequest = {
601
+ name: REQUEST_ROW_UPDATE,
602
+ query: CHOICES_QUERY,
603
+ values: { ledger_order: "-amount" },
604
+ };
605
+ this.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
606
+ ```
607
+
592
608
  ### Hearing how it turned out
593
609
 
594
610
  Once what a request brought back has landed, or its round trip has failed,
@@ -617,7 +633,8 @@ listener. Import its name as `LB_DONE_EVENT_NAME`.
617
633
  `items` is every response item that landed because of the request, in
618
634
  order. `refusal` is set beside `error` when the handler refused the
619
635
  request, holding its `message` and `columns`. An answer that moved the URL contributes its `lb-url` item, which
620
- carries a key only the server knew, and the page load. `error` is set when
636
+ carries a key only the server knew, and the page load. A choice contributes
637
+ the row of `lb-choices` it made. `error` is set when
621
638
  the round trip failed, and then nothing landed. A new row's key is in the
622
639
  answer only when the handler returned it in a patch: a refresh sends all
623
640
  rows, and nothing marks which one is new.
@@ -660,11 +677,12 @@ Everything else is the hub's:
660
677
  |-------------|----------------------------------------------------------|
661
678
  | Cloning | Clones the row template once per new key |
662
679
  | Matching | Fills the live row already showing that key, when its values changed |
663
- | Placing | Places every live row by the query's order |
664
- | Grouping | Builds a group where the order breaks, and removes it with its last row |
680
+ | Placing | Places every live row by the user's order or the query's |
681
+ | Filtering | Hides a row the user's filter does not pass, and a group whose rows are all hidden |
682
+ | Grouping | Builds a group where the order breaks, keeps it while its run goes on, and removes it with its last row |
665
683
  | Removing | Marks the live rows the response says are gone as leaving, and removes them |
666
684
  | Stamping | Stamps each live row with `lb-row-live`, `lb-key-value`, and what happened to it |
667
- | Counting | Stamps `lb-query-row-count` with the number of live rows |
685
+ | Counting | Stamps `lb-query-row-count` with the number of live rows showing |
668
686
 
669
687
  `lb-options.browser.ts` and `lb-table.browser.ts` in
670
688
  [`@loadbare/widgets`](./widgets.md) are two uses of `lbRowsLanded`: one