@loadbare/app 0.13.0 → 0.15.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 (51) hide show
  1. package/dist/build/assemble.d.ts.map +1 -1
  2. package/dist/build/assemble.js +31 -1
  3. package/dist/build/assemble.js.map +1 -1
  4. package/dist/build/elements.d.ts +12 -2
  5. package/dist/build/elements.d.ts.map +1 -1
  6. package/dist/build/elements.js +44 -10
  7. package/dist/build/elements.js.map +1 -1
  8. package/dist/core/lb-constants.d.ts +13 -2
  9. package/dist/core/lb-constants.d.ts.map +1 -1
  10. package/dist/core/lb-constants.js +50 -3
  11. package/dist/core/lb-constants.js.map +1 -1
  12. package/dist/core/lb-types.d.ts +52 -8
  13. package/dist/core/lb-types.d.ts.map +1 -1
  14. package/dist/core/lb-types.js.map +1 -1
  15. package/dist/hub/lb-apply.d.ts +22 -3
  16. package/dist/hub/lb-apply.d.ts.map +1 -1
  17. package/dist/hub/lb-apply.js +82 -37
  18. package/dist/hub/lb-apply.js.map +1 -1
  19. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  20. package/dist/hub/lb-hub.browser.js +382 -26
  21. package/dist/hub/lb-hub.browser.js.map +1 -1
  22. package/dist/hub/lb-lifecycle.d.ts +40 -0
  23. package/dist/hub/lb-lifecycle.d.ts.map +1 -0
  24. package/dist/hub/lb-lifecycle.js +140 -0
  25. package/dist/hub/lb-lifecycle.js.map +1 -0
  26. package/dist/server/lb-express.d.ts +13 -3
  27. package/dist/server/lb-express.d.ts.map +1 -1
  28. package/dist/server/lb-express.js +30 -12
  29. package/dist/server/lb-express.js.map +1 -1
  30. package/dist/server/lb-server.d.ts +31 -1
  31. package/dist/server/lb-server.d.ts.map +1 -1
  32. package/dist/server/lb-server.js +112 -2
  33. package/dist/server/lb-server.js.map +1 -1
  34. package/docs/TECHREF-1.0.md +343 -68
  35. package/docs/comparison.md +13 -9
  36. package/docs/possible-ideas.md +152 -26
  37. package/docs/reference/custom-elements.md +203 -88
  38. package/docs/reference/data-binding.md +310 -11
  39. package/docs/reference/page-files.md +76 -3
  40. package/docs/reference/server.md +20 -0
  41. package/docs/reference/widgets.md +35 -12
  42. package/docs/roadmap.md +9 -2
  43. package/docs/testing.md +2 -2
  44. package/package.json +1 -1
  45. package/skills/loadbare-app/SKILL.md +70 -23
  46. package/skills/loadbare-app/references/TECHREF-1.0.md +343 -68
  47. package/skills/loadbare-app/references/custom-elements.md +203 -88
  48. package/skills/loadbare-app/references/data-binding.md +310 -11
  49. package/skills/loadbare-app/references/page-files.md +76 -3
  50. package/skills/loadbare-app/references/server.md +20 -0
  51. package/skills/loadbare-app/references/widgets.md +35 -12
@@ -31,10 +31,9 @@ handle.
31
31
 
32
32
  ### Form controls
33
33
 
34
- - **Checkboxes and radio buttons are not implemented.** A value does not
35
- land on one and the hub does not gather one. Landing needs a decision on
36
- what counts as checked, which waits on [Data types](#data-types), and a
37
- radio group is several elements answering to one column.
34
+ - **Radio buttons are not implemented.** A value does not land on one and
35
+ the hub does not gather one. A radio group is several elements answering
36
+ to one column.
38
37
 
39
38
  ### Custom element hooks
40
39
 
@@ -70,10 +69,6 @@ But the allowed values may depend on other values in the row.
70
69
 
71
70
  ### The server API
72
71
 
73
- - **Decide the options-argument shape once.** Global hooks, transaction
74
- wrapping, error handling and a CSRF token all want the same trailing
75
- parameter on `createHub` and `hubRoutes`. Build none of them, but pick
76
- where they go.
77
72
  - **Land `staticRoutes`.** It closes the last place where an
78
73
  application hand-writes facts the builder owns.
79
74
 
@@ -457,7 +452,8 @@ A query that arrives with nowhere to land is reported to the console.
457
452
  | Any other element | Its text content |
458
453
 
459
454
  A control is an `<input>`, `<select>` or `<textarea>`, or a form-associated
460
- custom element with a `value` property that fires `change`. A
455
+ custom element with a `value` property that fires `change`. A checkbox is a
456
+ control too, by the rule below. A
461
457
  form-associated custom element declares `static formAssociated = true`.
462
458
 
463
459
  The hub stamps `lb-column-value` with the value on every element it sets.
@@ -468,9 +464,21 @@ renders `lb-column-value` itself.
468
464
  The hub gathers from the same controls it lands on, so a value read back is
469
465
  the one that landed.
470
466
 
471
- An `<input>` of type `checkbox`, `radio`, or `file` receives nothing, not
472
- even `lb-column-value`, and the hub reports it to the console. The hub does
473
- not gather one either.
467
+ A checkbox's state is `checked`, not `value`: HTML's quirk, and so the
468
+ hub's, so that every other part of the vocabulary treats it as any control.
469
+ The hub checks it by the rule `lb-show` is on by: `null` and `false` are
470
+ unchecked, and every other value is checked, with no string read. It
471
+ gathers `"true"` or `"false"`, so an unchecked box still says so, where a
472
+ native form submission leaves it out. The checkbox's own `value`
473
+ attribute means nothing to the hub.
474
+
475
+ ```html
476
+ <input type="checkbox" lb-column="active" lb-request="lb-row-update" />
477
+ ```
478
+
479
+ An `<input>` of type `radio` or `file` receives nothing, not even
480
+ `lb-column-value`, and the hub reports it to the console. The hub does not
481
+ gather one either.
474
482
 
475
483
  A `<select>` with no option for the value shows no selection.
476
484
 
@@ -707,10 +715,9 @@ standing where it stood, and moved back out when the column turns on. It is
707
715
  not rendered, focused, clicked, announced or gathered. It is moved and
708
716
  never rebuilt, so a custom element keeps its instance and a control keeps
709
717
  what was typed into it. Landing reaches into that template, so the element
710
- and everything inside it return current. A custom element sees
711
- `disconnectedCallback` then `adoptedCallback` going in, and
712
- `adoptedCallback` then `connectedCallback` coming out, and still receives
713
- `lb-column-value` while it is away.
718
+ and everything inside it return current. A custom element hears nothing
719
+ going in or coming out, and still receives `lb-column-value` while it is
720
+ away.
714
721
 
715
722
  The builder ships every `lb-show` element already inside its template, so
716
723
  nothing conditional shows until its row lands. An absent element's template
@@ -782,9 +789,13 @@ Every request has one shape, on the event and on the wire:
782
789
  { name: "lb-row-update", query: "roster", key: "42", values: { name: "Ann" } }
783
790
  { name: "lb-row-delete", query: "roster", key: "42" }
784
791
  { name: "mailRoster", query: "roster", key: "42" }
792
+ { name: "lb-row-update", query: "roster", keys: ["42", "43"], values: { role: "lead" } }
793
+ { name: "lb-row-insert", query: "invoice", values: { memo: "May" }, rows: { lines: [{ key: "1", values: { qty: "3" } }] } }
785
794
  ```
786
795
 
787
- The hub sends `query`, `key` and `values` as present.
796
+ The hub sends `query`, `key` and `values` as present, and `keys` in place of
797
+ `key` for a request acting on the rows selected — see
798
+ [Selecting rows](#selecting-rows).
788
799
 
789
800
  #### Committing
790
801
 
@@ -852,7 +863,8 @@ controls belong to a form that cannot wrap a `<tr>`:
852
863
 
853
864
  The hub gathers from controls only. It skips a control whose nearest
854
865
  ancestor `lb-query` is a descendant of the live row or form, since that
855
- control belongs to a nested query. An element carrying `lb-query` and
866
+ control belongs to a nested query, except that a form gathers the rows of a
867
+ query nested in its own — see [A form over a set of rows](#a-form-over-a-set-of-rows). An element carrying `lb-query` and
856
868
  `lb-column` both, such as a picker, belongs to the row around it and is
857
869
  gathered.
858
870
 
@@ -866,6 +878,61 @@ ancestor `lb-query` of the element carrying `lb-request`, and takes `key`
866
878
  from that element's nearest ancestor row when that row is of the same
867
879
  query.
868
880
 
881
+ #### A form over a set of rows
882
+
883
+ A form gathers the live rows of a query nested directly in its own query,
884
+ besides its own controls. Each goes as its key and the values of the
885
+ controls in it, under the nested query's name:
886
+
887
+ ```
888
+ { name: "lb-row-insert", query: "paychecks", values: { pay_date: "2026-10-02" },
889
+ rows: { deduction_types: [{ key: "401k", values: { amount: "50" } },
890
+ { key: "dental", values: { amount: "" } }] } }
891
+ ```
892
+
893
+ The form's own query is the one it carries, or else the nearest around it.
894
+ A nested query is gathered when its nearest ancestor `lb-query` is the
895
+ form's own, so one level only: a query nested deeper shows the same rows in
896
+ every row around it, and its keys would repeat. Every live row of it goes,
897
+ whatever its values, and only controls inside a live row of it.
898
+
899
+ An insert, an update and a declared request carry `rows`; a delete does
900
+ not. A form whose only controls are in nested rows sends for its own query,
901
+ keyed by the row landed on it or the row it is pointed at, with empty
902
+ `values`. A live row gathers nothing nested.
903
+
904
+ The handler for the form's query receives `rows` beside `values`, so the
905
+ master and its details are written together. The server checks that
906
+ `rows` holds string keys and string values by query, and ignores a request
907
+ whose `rows` does not.
908
+
909
+ A nested query answers once for the page, as anywhere — see
910
+ [Master-detail](#master-detail). The form enters a new master with its
911
+ details, or edits the master the page shows by a query parm, whose details
912
+ the nested query reads from the same parm. A pointed form fills its own
913
+ controls from the row it points at, and its nested rows stay the nested
914
+ query's answer.
915
+
916
+ #### A row not yet entered
917
+
918
+ A row whose key is the empty string is a new one, not yet entered. It
919
+ arises two ways: a `row` query answers with an empty key, as one reading
920
+ the master's key from an empty query parm does, or `--lb-new` points a form
921
+ (see [A form pointed at a row](#a-form-pointed-at-a-row)). Either way the
922
+ element carries `lb-key-value=""`.
923
+
924
+ The hub sends no `key` for it, whether the key came from where the element
925
+ sits or from a request a custom element dispatched. An `lb-row-insert` and
926
+ a declared request are sent without one. An `lb-row-update` and an
927
+ `lb-row-delete` are not sent, because nothing is on record to change, and
928
+ nothing is reported: it is the row's state, not a mistake. So a control
929
+ saving its own column on `change` saves on a row on record, and on a new
930
+ row only holds what a submit will gather. No ancestor sees a request not
931
+ sent, and it gets no `lb-request-done`.
932
+
933
+ An update or a delete with no row around it at all has no key either, and
934
+ is reported to the console, as any request missing what it needs is.
935
+
869
936
  A field the request already carries is its issuer's and is kept. A custom
870
937
  element that dispatches its own request with `values` is not gathered over.
871
938
 
@@ -877,7 +944,8 @@ element that dispatches its own request with `values` is not gathered over.
877
944
  | `lb-row-update` | `query`, `key`, `values` | `rowUpdate` |
878
945
  | `lb-row-delete` | `query`, `key` | `rowDelete` |
879
946
 
880
- The hub issues one of these only when it has what the table lists. An
947
+ `keys` stands in for `key`. The hub issues one of these only when it has
948
+ what the table lists. An
881
949
  `lb-row-insert` or `lb-row-update` that gathers nothing is not issued. A
882
950
  request that is not issued is reported to the console.
883
951
 
@@ -892,6 +960,125 @@ For a query the hub serves, the hub answers the request itself, with no
892
960
  round trip. The hub serves `lb-url`, and answers `lb-row-update` for it —
893
961
  see [Query parms](#query-parms).
894
962
 
963
+ #### A form pointed at a row
964
+
965
+ A set of rows the user edits or adds to has one form on the page, naming
966
+ the same query, outside every row. A button points it with HTML's
967
+ `commandfor`, naming the form's `id`, and `command`:
968
+
969
+ | Command | On a button | The hub |
970
+ | -------------- | --------------------------------- | ----------------------------------------- |
971
+ | `--lb-edit` | In a live row of the form's query | Fills the form from that row, keyed by it |
972
+ | `--lb-new` | Anywhere | Starts the form empty, for an insert |
973
+ | `--lb-release` | Anywhere | Lets the form go without saving |
974
+
975
+ HTML requires a custom command to begin with `--`, and fires it as a
976
+ `command` event on the form, which the hub hears.
977
+
978
+ ```html
979
+ <tbody lb-query="paychecks">
980
+ <template><tr>
981
+ <td lb-column="pay_date"></td>
982
+ <td><button type="button" commandfor="paycheck" command="--lb-edit">Edit</button></td>
983
+ </tr></template>
984
+ </tbody>
985
+ <button type="button" commandfor="paycheck" command="--lb-new">New</button>
986
+
987
+ <dialog>
988
+ <form id="paycheck" lb-query="paychecks" lb-request="lb-row-update">
989
+ <input lb-column="pay_date" type="date" />
990
+ <button type="submit">Save</button>
991
+ <button type="submit" lb-request="lb-row-insert">Save new</button>
992
+ <button type="button" commandfor="paycheck" command="--lb-release">Cancel</button>
993
+ </form>
994
+ </dialog>
995
+ ```
996
+
997
+ Which row a form edits is interaction state, not a view, so it is not a
998
+ query parm. The user edits only a row they can see, so Edit fills the form
999
+ from the row as the hub holds it, every column the query answered, with no
1000
+ round trip. A row that must be fetched to be edited is another page, with
1001
+ that row as its master.
1002
+
1003
+ Pointing a form first empties it: every column it names lands as null, and
1004
+ the form resets, so each control returns to its default. Edit then lands
1005
+ the row on the form as on any element a `row` lands on, stamps the form
1006
+ `lb-key-value` with the row's key, and stamps the row `lb-row-editing`.
1007
+ Gathering then takes the key from the form. New stamps an empty key, making
1008
+ the form [a row not yet entered](#a-row-not-yet-entered). Both focus the
1009
+ form's first control.
1010
+
1011
+ | The form's `lb-key-value` | The form is |
1012
+ | ------------------------- | ------------------ |
1013
+ | A key | Editing that row |
1014
+ | Empty | Entering a new row |
1015
+ | Absent | Not in use |
1016
+
1017
+ A stylesheet shows the form's buttons by it, such as Save while it has a
1018
+ key and Save new while the key is empty.
1019
+
1020
+ A request from the form that succeeds lets it go, as `--lb-release` does:
1021
+ the form empties, and both stamps go. One that fails leaves the form
1022
+ pointed, as the user had it.
1023
+
1024
+ When the form is inside a `<dialog>`, Edit and New call `showModal()` on it,
1025
+ and letting go closes it. The dialog closing by any of HTML's means,
1026
+ Escape, `command="close"` or `method="dialog"`, lets the form go. A form in
1027
+ no dialog opens nothing; it is always showing, or a stylesheet shows it
1028
+ while it carries `lb-key-value`.
1029
+
1030
+ The form names a `rows` query and holds no row template, so that query's
1031
+ answers land nothing on it, and what the user typed survives a refresh.
1032
+
1033
+ A command the hub cannot carry out is reported to the console: Edit from
1034
+ outside a live row of the form's query, Edit for a key the hub holds no row
1035
+ for, and any command aimed at an element that is not a form carrying
1036
+ `lb-query`.
1037
+
1038
+ #### Selecting rows
1039
+
1040
+ A checkbox carrying `lb-select` in a row template selects the live row it
1041
+ sits in, and the hub stamps the row `lb-row-selected` while it is ticked.
1042
+ It is not a column: it carries no `lb-column` or `lb-request`, lands
1043
+ nothing, and is not gathered.
1044
+
1045
+ A request for the query that names no one row, neither from inside one of
1046
+ its rows nor from a form pointed at one, acts on the rows selected. When
1047
+ any `lb-select` checkbox of that query is showing, the hub adds the keys of
1048
+ those ticked as `keys`:
1049
+
1050
+ ```
1051
+ { name: "lb-row-update", query: "transactions", keys: ["4", "9"], values: { acct: "12" } }
1052
+ ```
1053
+
1054
+ - `lb-row-update` and `lb-row-delete` are sent with `keys`, and with none
1055
+ ticked are not sent, silently, as for [a row not yet
1056
+ entered](#a-row-not-yet-entered).
1057
+ - A declared request is sent with `keys`, empty when none is ticked, and the
1058
+ handler decides.
1059
+ - `lb-row-insert` adds a row, and the selection is not its.
1060
+ - With no `lb-select` checkbox of the query showing, the request is an
1061
+ ordinary one, and carries no `keys`.
1062
+
1063
+ The server runs the page's `rowUpdate` or `rowDelete` once for each key,
1064
+ with that key, as for each row alone, so a handler written for one row
1065
+ serves several. It lays the patches the runs return together, and runs the
1066
+ refresh set once. A run returning `url()` is reported and ignored, since
1067
+ several rows cannot each choose one view. A declared request receives
1068
+ `keys` as sent.
1069
+
1070
+ After the request succeeds, the hub clears every `lb-select` checkbox of the
1071
+ query and the stamps with them. A failed request leaves the selection.
1072
+
1073
+ Only a checkbox that is showing counts: one parked by `lb-show` is in
1074
+ template content, out of reach. Parking one also clears its tick, so a
1075
+ selection never waits out of sight to return stale.
1076
+
1077
+ Which rows are selected is interaction state, as which row a form edits is.
1078
+ A selecting mode the page keeps in the URL reaches the rows as a column the
1079
+ query computes from the parm, which `lb-show` reads, since `lb-show` reads
1080
+ from the row it sits in.
1081
+
895
1082
  #### The request event
896
1083
 
897
1084
  The hub dispatches every request as the bubbling `lb-request` event before
@@ -910,8 +1097,10 @@ makes a confirmation wrapper possible without the wrapped element knowing
910
1097
  about it.
911
1098
 
912
1099
  Once what the request brought back has landed, or its round trip has failed,
913
- the hub dispatches the bubbling `lb-request-done` event from the same
914
- element. Its `detail`, `HubRequestDone` in `@loadbare/app/types`, holds
1100
+ the hub calls `lbRequestDone(detail, origin)` on the committing element and
1101
+ every custom element around it, innermost first (see
1102
+ [Lifecycle](#lifecycle)), then dispatches the bubbling `lb-request-done`
1103
+ event from the same element. Its `detail`, `HubRequestDone` in `@loadbare/app/types`, holds
915
1104
  the request as the hub sent it and `items`, every response item that landed
916
1105
  because of it, in order. An answer that moved the URL contributes its
917
1106
  `lb-url` item and the page load. `error` is set instead when the round trip
@@ -935,6 +1124,46 @@ failed one.
935
1124
  The hub ignores a commit on an element carrying `lb-request-pending`, so a
936
1125
  pending element is disabled in fact and a stylesheet only has to show it.
937
1126
 
1127
+ #### Refusals
1128
+
1129
+ A handler refuses a request by throwing `refusal()`, exported by
1130
+ `@loadbare/app/server`, with words written for the user: a message for the
1131
+ request as a whole, a message per column, or both.
1132
+
1133
+ ```ts
1134
+ throw refusal("That series already has paychecks entered.");
1135
+ throw refusal({ amount: "Must be more than zero" });
1136
+ throw refusal("Two fields need attention.", { amount: "…", pay_date: "…" });
1137
+ ```
1138
+
1139
+ It is thrown rather than returned, so it may come from anything the handler
1140
+ calls, and rolls back whatever the application has open as any throw does.
1141
+ `hubRoutes` answers it with status 422 and `{ refusal: { message?, columns? } }`.
1142
+ Anything else thrown is a failure, answered 500 with no body, so no stack
1143
+ or database message reaches the browser.
1144
+
1145
+ The hub shows a refusal where it belongs:
1146
+
1147
+ - It stamps the element that committed `lb-request-refused`, whose value is
1148
+ the whole-request message, empty when there is none. A stylesheet shows
1149
+ it with `attr()`.
1150
+ - For each column, it calls `setCustomValidity(message)` on the control the
1151
+ request gathered that column from, and `reportValidity()` on the first,
1152
+ so the browser shows the message there and moves focus to it. A column
1153
+ the request gathered no such control for is reported to the console.
1154
+ - `lb-request-done` carries the refusal as `refusal`, beside `error`.
1155
+
1156
+ A refusal is not a failure: `lb-request-error` is not stamped and nothing is
1157
+ reported. It stands until the user edits any control the request gathered,
1158
+ a form holding them resets, or the element that committed sends again;
1159
+ then every message it put up is withdrawn at once. While a control carries
1160
+ one, the browser holds its form's submit, as for any invalid control.
1161
+
1162
+ A form-associated custom element takes a column's message when it defines
1163
+ `setCustomValidity` and `reportValidity`; one wrapping a native control
1164
+ passes both to it. A request for several rows takes the whole-request
1165
+ message only.
1166
+
938
1167
  #### The round trip
939
1168
 
940
1169
  A request the hub sends has ten seconds to come back. Past that the hub
@@ -943,7 +1172,8 @@ its own. An application cannot change the deadline.
943
1172
 
944
1173
  A round trip fails on a server error, on a network failure, or on that
945
1174
  deadline, and all three stamp `lb-request-error` on the element that issued
946
- the request — see [Request state](#request-state).
1175
+ the request — see [Request state](#request-state). A refusal is not a
1176
+ failure — see [Refusals](#refusals).
947
1177
 
948
1178
  After a successful `lb-row-insert` gathered from a form, the hub resets the
949
1179
  form, after it lands the response. A failed insert resets nothing, so the
@@ -1211,8 +1441,8 @@ to build on any of these:
1211
1441
 
1212
1442
  - An `lb-` attribute that is not a developer attribute: `lb-query`,
1213
1443
  `lb-column`, `lb-show`, `lb-group`, `lb-count`, `lb-sum`, `lb-avg`,
1214
- `lb-min`, `lb-max`, `lb-request`, `lb-url-link`, `lb-url-push`,
1215
- `lb-url-unknown`. Every other `lb-` attribute is a stamp.
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.
1216
1446
  `lb-exp-slot` and `lb-exp-template` are consumed by expansion and never
1217
1447
  reach the check.
1218
1448
  - `lb-request` naming a value that begins with `lb-` and is not one of the
@@ -1224,6 +1454,11 @@ to build on any of these:
1224
1454
  - `lb-show` on a `<template>`, on a row template's root, or with no
1225
1455
  `lb-query` around it.
1226
1456
  - `lb-show="!"` or `lb-show="!!column"`, which reverse no column.
1457
+ - `lb-select` on anything but an `<input type="checkbox">`, outside a row
1458
+ template, or beside `lb-column` or `lb-request`.
1459
+ - A `command` beginning `--lb-` that is not one of the three
1460
+ [commands](#a-form-pointed-at-a-row), and one of them with no
1461
+ `commandfor`.
1227
1462
  - `lb-group` on anything but a `<template>`, or on one holding no
1228
1463
  `<template>` for its contents.
1229
1464
  - An aggregate on a `<template>`, and any but `lb-count` naming no column.
@@ -1403,6 +1638,16 @@ Explaining Express is beyond the scope of this technical reference. The
1403
1638
  only real requirement is that the catch-all for app.html is at the end,
1404
1639
  so it does not catch any other files.
1405
1640
 
1641
+ `hubRoutes` mounts `express.json()` on its own routes, and takes that
1642
+ function's options under `json` in an optional third argument. The default
1643
+ limit of 100 KB refuses an oversized body with a 413 before any handler
1644
+ runs, and the server log names the option. A page that sends a file's text
1645
+ as a value needs more:
1646
+
1647
+ ```ts
1648
+ app.use(hubRoutes(hub, contextFor, { json: { limit: "5mb" } }));
1649
+ ```
1650
+
1406
1651
  `hubRoutes` answers `POST /lb/<stub>`, with the query string the browser is
1407
1652
  showing after it, verbatim. It hands `contextFor` that query string's parms
1408
1653
  as a second argument, and `contextFor` puts on the context whatever a query
@@ -1569,11 +1814,16 @@ interface HubRequest {
1569
1814
  name: string;
1570
1815
  query?: string;
1571
1816
  key?: string;
1817
+ keys?: string[];
1572
1818
  values?: Record<string, string>;
1819
+ rows?: Record<string, { key: string; values: Record<string, string> }[]>;
1573
1820
  }
1574
1821
  ```
1575
1822
 
1576
- Every response is an array of response items:
1823
+ A refused request is answered with status 422 and
1824
+ `{ refusal: { message?, columns? } }`, and any other failure with 500 and
1825
+ no body — see [Refusals](#refusals). Every other response is an array of
1826
+ response items:
1577
1827
 
1578
1828
  ```ts
1579
1829
  interface ResponseItem {
@@ -1635,14 +1885,12 @@ Each direction has one mechanism for each kind of message:
1635
1885
  | -------------- | ------------------------------------------- | ---------------------------- | ------------------------------------------------ |
1636
1886
  | Hub to element | State that lasts | An attribute the hub stamps | `lb-column-value`, `lb-key-value`, `lb-query-row-count`, `lb-group-*`, `lb-row-*`, `lb-request-pending`, `lb-request-error` |
1637
1887
  | Hub to element | State the platform already names | A property | A control's `value` |
1638
- | Hub to element | Work the hub needs done now, while landing | An optional `lb` method | `lbRowsLanded` |
1888
+ | Hub to element | What the hub did | An optional `lb` method | `lbAttached`, `lbRowsLanded`, `lbRequestDone`, `lbDetached` |
1639
1889
  | Hub to element | A moment an element started | A bubbling event | `lb-request-done` |
1640
1890
  | Element to hub | A moment | A bubbling event | `lb-request`, and `change`, `click` and `submit` |
1641
1891
 
1642
- State is an attribute, because a stylesheet can select on it and an element
1643
- that upgrades late still finds it. A method is for work the hub cannot go
1644
- on without, since it acts on one element at one point in landing and nothing
1645
- else can do it. A moment is an event, because the element that cares is
1892
+ State is an attribute, because a stylesheet can select on it. A method is
1893
+ how the hub tells one element what it just did to that element. A moment is an event, because the element that cares is
1646
1894
  often an ancestor of the one it concerns, and an event reaches it with no
1647
1895
  knowledge of either.
1648
1896
 
@@ -1661,45 +1909,47 @@ class NoteField extends HTMLElement {
1661
1909
  }
1662
1910
  ```
1663
1911
 
1664
- A control that is not yet upgraded when its column lands receives
1665
- `lb-column-value` alone, since the hub sets `value` only on a control. That
1666
- happens to an element the builder shipped absent, and to one whose
1667
- definition loads after the hub lands. A control takes the stamp up the
1668
- first time it connects, and never again.
1669
-
1670
1912
  A custom element that is not a control receives `lb-column-value` and
1671
1913
  renders it; its content is never replaced.
1672
1914
 
1673
1915
  ### Lifecycle
1674
1916
 
1675
- | The hub | A custom element sees |
1676
- | ---------------------------------------- | -------------------------------------------- |
1677
- | Shows a page | `constructor`, `connectedCallback` |
1678
- | Creates a live row, before placing it | `constructor` |
1679
- | Places a live row the first time | `connectedCallback` |
1680
- | Moves a live row the order put elsewhere | `connectedMoveCallback` where the browser moves in place, otherwise `disconnectedCallback`, `connectedCallback` |
1681
- | Leaves a copy of a moved row behind | The copy's `constructor`, `connectedCallback`, and later `disconnectedCallback` |
1682
- | Removes a live row | `disconnectedCallback`, once its animations end |
1683
- | Turns `lb-show` off | `disconnectedCallback`, `adoptedCallback` |
1684
- | Turns `lb-show` on | `adoptedCallback`, `connectedCallback` |
1685
- | Turns on an element shipped absent | `constructor`, `connectedCallback` |
1917
+ A custom element hears the hub's lifecycle, never the browser's. The hub
1918
+ calls four optional methods, which the `HubLifecycle` interface in
1919
+ `@loadbare/app/types` declares:
1920
+
1921
+ | The hub | A custom element hears |
1922
+ | ------------------------------------------------ | ------------------------------- |
1923
+ | Places it: chrome at startup, a page, a new live row, a new group | `lbAttached()`, once, after every column has landed on it |
1924
+ | Lands rows on it | `lbRowsLanded()` |
1925
+ | Finishes a request committed by it or inside it | `lbRequestDone(detail, origin)` |
1926
+ | Removes it: a row or group done leaving, the page replaced | `lbDetached()`, once |
1927
+ | Moves a live row the order put elsewhere | Nothing |
1928
+ | Turns `lb-show` off or on | Nothing |
1929
+ | Leaves a copy of a moved row behind | Nothing, on the copy |
1930
+
1931
+ The chrome and a page are placed as the page's first answer lands, or once
1932
+ that round trip fails. An element under an `lb-show` that is off is placed
1933
+ inside its template, so `lbAttached` runs whether it shows or not. `lbAttached` reaches an outer
1934
+ custom element before the ones inside it, and `lbDetached` the inner ones
1935
+ first. `lbRequestDone` reaches the committing element and every custom
1936
+ element around it, innermost first, before the `lb-request-done` event.
1937
+ An element a script creates hears none of them.
1686
1938
 
1687
1939
  A listener on the element itself goes in the constructor, which reads no
1688
- attribute and no child. A child is looked up when it is needed.
1689
- `connectedCallback` runs on every move and is written to run again; work
1690
- done once per instance goes behind a flag.
1940
+ attribute and no child. A child is looked up when it is needed. Work done
1941
+ once per instance goes in `lbAttached`, and its undoing in `lbDetached`.
1691
1942
 
1692
- ### Row hooks
1943
+ The build refuses a script that declares `connectedCallback`,
1944
+ `disconnectedCallback`, `adoptedCallback` or `connectedMoveCallback`.
1693
1945
 
1694
- | Method | Implemented By | The hub calls it |
1695
- | ---------------- | -------------- | ------------------------------------------------------ |
1696
- | `lbRowsLanded()` | Developer | After the rows land, with every row placed and stamped |
1946
+ ### Row hooks
1697
1947
 
1698
- The hub calls it on a custom element with `lb-query` and a row template.
1699
- It is optional. The `RowsHost` interface in `@loadbare/app/types` declares
1700
- it. The hub places every row and builds every group itself, so a custom
1701
- element reacts to rows and never places them. Scrolling to the row the
1702
- page's request created is the case:
1948
+ The hub calls `lbRowsLanded()` on a custom element with `lb-query` and a row
1949
+ template, after the rows land, with every row placed and stamped. The hub
1950
+ places every row and builds every group itself, so a custom element reacts
1951
+ to rows and never places them. Scrolling to the row the page's request
1952
+ created is the case:
1703
1953
 
1704
1954
  ```ts
1705
1955
  lbRowsLanded() {
@@ -1708,10 +1958,8 @@ lbRowsLanded() {
1708
1958
  }
1709
1959
  ```
1710
1960
 
1711
- A custom element the builder shipped absent under `lb-show` has not
1712
- upgraded while its column is off. When the column first turns on and it
1713
- upgrades, the hub lands the query's last answer on it again, as all rows,
1714
- so the hook runs.
1961
+ A custom element under an `lb-show` that is off holds its rows all the
1962
+ same, and the hook runs as they land.
1715
1963
 
1716
1964
  `applyRow(root, row)`, exported by `@loadbare/app`, fills `root` from one
1717
1965
  row, the same operation that fills a live row.
@@ -1791,6 +2039,7 @@ ownership of its behavior are stated separately, because they differ.
1791
2039
  | Namespace | Applies to | Loadbare owns | Defined in |
1792
2040
  | --------- | ----------------------------------------------------------- | --------------------------------------------------- | ----------------------------------------------- |
1793
2041
  | `lb-*` | Attributes, their values, queries, columns of `lb-` queries, requests, custom elements, events | The names and their behavior | [The lb-* namespace](#the-lb--namespace) |
2042
+ | `--lb-*` | Values of HTML's `command` attribute | The names and their behavior | [Reserved commands](#reserved-commands) |
1794
2043
  | `exp-*` | Attributes on a custom element | The behavior; element file authors pick the names | [Build time parameters](#build-time-parameters) |
1795
2044
  | `lb*` | Methods on custom elements | The names and their behavior | [Row hooks](#row-hooks) |
1796
2045
 
@@ -1807,6 +2056,7 @@ it may define tomorrow.
1807
2056
  | `lb-column` | Developer | Any element | [Landing](#landing) |
1808
2057
  | `lb-show` | Developer | Any element but a `<template>` | [Displaying by condition](#displaying-by-condition) |
1809
2058
  | `lb-request` | Developer | Any element | [Requests](#requests) |
2059
+ | `lb-select` | Developer | A checkbox in a row template | [Selecting rows](#selecting-rows) |
1810
2060
  | `lb-url-link` | Developer | An `<a>` | [Links](#links) |
1811
2061
  | `lb-url-push` | Developer | An element with `lb-request` | [Query parms](#query-parms) |
1812
2062
  | `lb-url-unknown` | Developer | A `<dialog>` inside `<lb-hub>` | [An unknown page](#an-unknown-page) |
@@ -1815,7 +2065,9 @@ it may define tomorrow.
1815
2065
  | `lb-exp-slot` | Developer | One element in an element file | [Slots and templates](#slots-and-templates) |
1816
2066
  | `lb-exp-template` | Developer | An element file, and a `<template>` | [Slots and templates](#slots-and-templates) |
1817
2067
  | `lb-column-value` | Hub | Every element set from a column | [How a column lands](#how-a-column-lands) |
1818
- | `lb-key-value` | Hub | A live row, and an element a `row` lands on | [Rows](#rows) |
2068
+ | `lb-key-value` | Hub | A live row, an element a `row` lands on, and a pointed form | [Rows](#rows), [A form pointed at a row](#a-form-pointed-at-a-row) |
2069
+ | `lb-row-editing` | Hub | The live row a form is pointed at | [A form pointed at a row](#a-form-pointed-at-a-row) |
2070
+ | `lb-row-selected` | Hub | A live row its `lb-select` checkbox ticks | [Selecting rows](#selecting-rows) |
1819
2071
  | `lb-row-live` | Hub | A live row | [Rows](#rows) |
1820
2072
  | `lb-query-row-count` | Hub | An element with a row template | [Rows](#rows) |
1821
2073
  | `lb-group-live`, `lb-group-column`, `lb-group-value` | Hub | Each top-level element of a group | [Groups](#groups) |
@@ -1823,6 +2075,7 @@ it may define tomorrow.
1823
2075
  | `lb-row-leaving`, `lb-group-leaving` | Hub | A row or a group on its way out | [What happened to a row](#what-happened-to-a-row) |
1824
2076
  | `lb-request-pending` | Hub | The element that issued a request | [Request state](#request-state) |
1825
2077
  | `lb-request-error` | Hub | The element that issued a request | [Request state](#request-state) |
2078
+ | `lb-request-refused` | Hub | The element whose request was refused | [Refusals](#refusals) |
1826
2079
  | `lb-show` on a `<template>` | Hub and builder | Where an absent element stands | [Displaying by condition](#displaying-by-condition) |
1827
2080
  | `lb-page` | Builder | A page's `<template>` | [Page HTML](#page-html) |
1828
2081
  | `lb-page-title` | Builder | A page's `<template>` | [Page HTML](#page-html) |
@@ -1850,6 +2103,17 @@ The builder refuses any other `lb-` attribute in markup — see
1850
2103
  | `lb-row-update` | `rowUpdate` | [The request names](#the-request-names) |
1851
2104
  | `lb-row-delete` | `rowDelete` | [The request names](#the-request-names) |
1852
2105
 
2106
+ ### Reserved commands
2107
+
2108
+ Loadbare/app owns every value of HTML's `command` attribute beginning with
2109
+ `--lb-`.
2110
+
2111
+ | Command | Defined in |
2112
+ | -------------- | --------------------------------------------------- |
2113
+ | `--lb-edit` | [A form pointed at a row](#a-form-pointed-at-a-row) |
2114
+ | `--lb-new` | [A form pointed at a row](#a-form-pointed-at-a-row) |
2115
+ | `--lb-release` | [A form pointed at a row](#a-form-pointed-at-a-row) |
2116
+
1853
2117
  ### Reserved tags
1854
2118
 
1855
2119
  Loadbare/app owns every tag in this table. An application writes them and
@@ -1871,9 +2135,12 @@ Loadbare/app owns every DOM event in this table, both the name and what its
1871
2135
 
1872
2136
  ### Reserved methods
1873
2137
 
1874
- | Method | On | Defined in |
1875
- | -------------- | -------------------------------------------------- | ----------------------- |
1876
- | `lbRowsLanded` | A custom element with `lb-query` and a row template | [Row hooks](#row-hooks) |
2138
+ | Method | On | Defined in |
2139
+ | --------------- | --------------------------------------------------- | ----------------------- |
2140
+ | `lbAttached` | Any custom element the hub places | [Lifecycle](#lifecycle) |
2141
+ | `lbRowsLanded` | A custom element with `lb-query` and a row template | [Row hooks](#row-hooks) |
2142
+ | `lbRequestDone` | A custom element that committed, or holds one that did | [Lifecycle](#lifecycle) |
2143
+ | `lbDetached` | Any custom element the hub removes | [Lifecycle](#lifecycle) |
1877
2144
 
1878
2145
  ### Constants
1879
2146
 
@@ -1906,9 +2173,17 @@ imports them rather than writing a string.
1906
2173
  | `REQUESTED_CREATED`, `REQUESTED_CHANGED`, `REQUESTED_MOVED` | `created`, `changed`, `moved` |
1907
2174
  | `ATTR_ROW_LEAVING` | `lb-row-leaving` |
1908
2175
  | `ATTR_GROUP_LEAVING` | `lb-group-leaving` |
2176
+ | `COMMAND_EDIT`, `COMMAND_NEW`, `COMMAND_RELEASE` | `--lb-edit`, `--lb-new`, `--lb-release` |
2177
+ | `LB_COMMANDS` | All three commands |
2178
+ | `LB_COMMAND_PREFIX` | `--lb-` |
2179
+ | `ATTR_ROW_EDITING` | `lb-row-editing` |
2180
+ | `ATTR_SELECT` | `lb-select` |
2181
+ | `ATTR_ROW_SELECTED` | `lb-row-selected` |
1909
2182
  | `ATTR_REQUEST` | `lb-request` |
1910
2183
  | `ATTR_REQUEST_PENDING` | `lb-request-pending` |
1911
2184
  | `ATTR_REQUEST_ERROR` | `lb-request-error` |
2185
+ | `ATTR_REQUEST_REFUSED` | `lb-request-refused` |
2186
+ | `LB_REFUSAL_STATUS` | `422` |
1912
2187
  | `ATTR_URL_LINK` | `lb-url-link` |
1913
2188
  | `ATTR_URL_PUSH` | `lb-url-push` |
1914
2189
  | `ATTR_URL_UNKNOWN` | `lb-url-unknown` |
@@ -1916,7 +2191,7 @@ imports them rather than writing a string.
1916
2191
  | `ATTR_EXP_TEMPLATE` | `lb-exp-template` |
1917
2192
  | `ATTR_PAGE` | `lb-page` |
1918
2193
  | `ATTR_PAGE_TITLE` | `lb-page-title` |
1919
- | `DEVELOPER_ATTRIBUTES` | The thirteen attributes a developer writes |
2194
+ | `DEVELOPER_ATTRIBUTES` | The fourteen attributes a developer writes |
1920
2195
  | `LB_EVENT_NAME` | `lb-request` |
1921
2196
  | `LB_DONE_EVENT_NAME` | `lb-request-done` |
1922
2197
  | `SHOW_NOT` | `!`, written before a column in `lb-show` |