@loadbare/app 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/build/assemble.d.ts.map +1 -1
  2. package/dist/build/assemble.js +61 -1
  3. package/dist/build/assemble.js.map +1 -1
  4. package/dist/build/cli.js +2 -1
  5. package/dist/build/cli.js.map +1 -1
  6. package/dist/build/elements.d.ts +9 -1
  7. package/dist/build/elements.d.ts.map +1 -1
  8. package/dist/build/elements.js +50 -0
  9. package/dist/build/elements.js.map +1 -1
  10. package/dist/build/origins.d.ts +2 -0
  11. package/dist/build/origins.d.ts.map +1 -1
  12. package/dist/build/origins.js +1 -1
  13. package/dist/build/origins.js.map +1 -1
  14. package/dist/core/lb-constants.d.ts +26 -1
  15. package/dist/core/lb-constants.d.ts.map +1 -1
  16. package/dist/core/lb-constants.js +79 -0
  17. package/dist/core/lb-constants.js.map +1 -1
  18. package/dist/core/lb-types.d.ts +58 -18
  19. package/dist/core/lb-types.d.ts.map +1 -1
  20. package/dist/core/lb-types.js +74 -11
  21. package/dist/core/lb-types.js.map +1 -1
  22. package/dist/hub/lb-apply.d.ts +44 -16
  23. package/dist/hub/lb-apply.d.ts.map +1 -1
  24. package/dist/hub/lb-apply.js +694 -49
  25. package/dist/hub/lb-apply.js.map +1 -1
  26. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  27. package/dist/hub/lb-hub.browser.js +168 -36
  28. package/dist/hub/lb-hub.browser.js.map +1 -1
  29. package/dist/server/lb-express.d.ts +6 -4
  30. package/dist/server/lb-express.d.ts.map +1 -1
  31. package/dist/server/lb-express.js +32 -14
  32. package/dist/server/lb-express.js.map +1 -1
  33. package/dist/server/lb-server.d.ts +49 -10
  34. package/dist/server/lb-server.d.ts.map +1 -1
  35. package/dist/server/lb-server.js +87 -28
  36. package/dist/server/lb-server.js.map +1 -1
  37. package/docs/TECHREF-1.0.md +364 -72
  38. package/docs/comparison.md +16 -10
  39. package/docs/possible-ideas.md +188 -0
  40. package/docs/reference/chrome.md +22 -1
  41. package/docs/reference/custom-elements.md +90 -29
  42. package/docs/reference/data-binding.md +132 -7
  43. package/docs/reference/page-files.md +48 -11
  44. package/docs/reference/server.md +4 -0
  45. package/docs/reference/widgets.md +94 -24
  46. package/docs/roadmap.md +10 -6
  47. package/docs/testing.md +21 -10
  48. package/package.json +2 -3
  49. package/skills/loadbare-app/SKILL.md +85 -12
  50. package/skills/loadbare-app/references/TECHREF-1.0.md +364 -72
  51. package/skills/loadbare-app/references/chrome.md +22 -1
  52. package/skills/loadbare-app/references/custom-elements.md +90 -29
  53. package/skills/loadbare-app/references/data-binding.md +132 -7
  54. package/skills/loadbare-app/references/page-files.md +48 -11
  55. package/skills/loadbare-app/references/server.md +4 -0
  56. package/skills/loadbare-app/references/widgets.md +94 -24
@@ -128,7 +128,7 @@ shape of the data an application works with.
128
128
  ### Loadbare/app
129
129
 
130
130
  The hub holds no application data. There is no store, no signal, no
131
- observable, and no client cache of query results.
131
+ observable, and no client cache that answers a request.
132
132
 
133
133
  - A value that has landed exists in the DOM: as `textContent` or `value`, and
134
134
  as the `lb-column-value` stamp.
@@ -138,6 +138,10 @@ observable, and no client cache of query results.
138
138
  the path, the page label and the query parms.
139
139
  - An element hidden by `lb-show` exists inside a `<template>` standing where
140
140
  it stood.
141
+ - The hub keeps each query's last answer until the page changes, only to
142
+ fill an element that names the query and arrives after it, such as a
143
+ picker in a new live row. Nothing is read from it in place of asking the
144
+ server.
141
145
  - The hub keeps two pieces of state of its own: the name of the current page,
142
146
  and, until an insert completes, the form it gathered from.
143
147
 
@@ -316,9 +320,9 @@ without `.browser` is not bundled, and the build error names it.
316
320
  `CustomEvent`. The hub completes it with `query`, `key` and `values` from
317
321
  where the element sits. An ancestor may stop the event.
318
322
  - A custom element carrying `lb-query` and a row template may implement
319
- `lbPlaceRow(el, row, template)` to decide where a row goes, and
320
- `lbRowsLanded()` to build scaffolding such as group headings. The hub does
321
- the cloning, matching, removal and counting.
323
+ `lbRowsLanded()` to act on the rows once they land, such as scrolling to
324
+ the one a request created. The hub does the cloning, matching, ordering,
325
+ grouping, removal and counting.
322
326
  - Method names beginning with `lb` on a custom element are reserved.
323
327
 
324
328
  Widget libraries are npm packages listed in `imports.ts`. The builder scans
@@ -392,7 +396,7 @@ query, the kind and key the server declared, and one of these:
392
396
  A column holds one value. A set of values is a query of its own. A row
393
397
  never contains rows. Master-detail is a row and rows answered under two
394
398
  names. Many masters with their details is one `rows` query of joined rows,
395
- grouped for display by a custom element's `lbPlaceRow`.
399
+ shown in groups by a group template.
396
400
 
397
401
  The application designs no endpoints, routes or serialization format. The
398
402
  names in the markup are the keys in the page's `.queries.ts` and
@@ -781,11 +785,12 @@ TECHREF-1.0 lists every name Loadbare/app owns in one cross-reference.
781
785
 
782
786
  | Owned | Count | Names |
783
787
  |-------------------------------------|-------|---------------------------------------------------------------|
784
- | `lb-*` attributes a developer writes | 7 | `lb-query`, `lb-column`, `lb-show`, `lb-request`, `lb-url-link`, `lb-url-push`, `lb-url-unknown` |
785
- | `lb-*` stamps | 8 | `lb-column-value`, `lb-key-value`, `lb-row-live`, `lb-query-row-count`, `lb-request-pending`, `lb-request-error`, `lb-page`, `lb-page-title` |
788
+ | `lb-*` attributes a developer writes | 13 | `lb-query`, `lb-column`, `lb-show`, `lb-group`, `lb-count`, `lb-sum`, `lb-avg`, `lb-min`, `lb-max`, `lb-request`, `lb-url-link`, `lb-url-push`, `lb-url-unknown` |
789
+ | `lb-*` stamps | 17 | `lb-column-value`, `lb-key-value`, `lb-row-live`, `lb-query-row-count`, `lb-group-live`, `lb-group-column`, `lb-group-value`, `lb-row-created`, `lb-row-changed`, `lb-row-moved`, `lb-row-requested`, `lb-row-leaving`, `lb-group-leaving`, `lb-request-pending`, `lb-request-error`, `lb-page`, `lb-page-title` |
786
790
  | `lb-*` build-time attributes | 2 | `lb-exp-slot`, `lb-exp-template` |
787
791
  | Requests Loadbare provides | 3 | `lb-row-insert`, `lb-row-update`, `lb-row-delete` |
788
- | `lb*` methods on custom elements | 2 | `lbPlaceRow`, `lbRowsLanded` |
792
+ | `lb*` methods on custom elements | 1 | `lbRowsLanded` |
793
+ | Query parms Loadbare reads | 1 | `lb-order-<query>` |
789
794
  | Attribute namespace for expansion | 1 | `exp-*` |
790
795
  | Reserved tags | 1 | `<lb-hub>` |
791
796
  | Reserved events | 1 | `lb-request` |
@@ -922,7 +927,7 @@ Roadmap.
922
927
  | Offline or local-first use | None. |
923
928
  | Server-side rendering for crawlers | None. Every path answers the same document with status 200. |
924
929
  | Subpath hosting | Not possible. The hub uses absolute paths. |
925
- | Chrome-level queries | A widget in the chrome bound to a query requires every page to declare it. Open blocker. |
930
+ | Chrome-level queries | A widget in the chrome bound to a query requires every page to declare it. On the roadmap. |
926
931
  | A widget receiving a whole row | Not available. Open blocker. |
927
932
  | A nested `rows` query per outer row | A nested query receives the same rows in every outer row. A new outer row's nested query stays empty until its name lands again. Open blocker. |
928
933
  | Configurable request deadline | Fixed at ten seconds. |
@@ -1177,7 +1182,8 @@ Each equivalent is approximate.
1177
1182
  | Expansion | Server-side include; partial; build-time component render (Astro) |
1178
1183
  | `exp-` parameter | A prop fixed at build time |
1179
1184
  | `lb-exp-slot`, `lb-exp-template` | Default slot and named slots; `ng-content` with `select` |
1180
- | `lbPlaceRow`, `lbRowsLanded` | Custom list rendering; a render prop |
1185
+ | `lbRowsLanded` | An effect after a list renders |
1186
+ | `order`, `lb-group` | `ORDER BY` with report-writer control breaks (`BREAK ON`, group bands) |
1181
1187
  | `ctx` / `HubContext` | `event.locals` (SvelteKit); loader `context` (React Router 7); request-scoped dependency injection |
1182
1188
  | `imports.ts` | Installing and registering a component library |
1183
1189
  | `pages.ts` | Generated route manifest |
@@ -0,0 +1,188 @@
1
+ # Possible Ideas
2
+
3
+ > **LLM-authored, not yet revised by a person.** Drafted by Claude on
4
+ > 2026-10-02 against `@loadbare/app` 0.12.0, from a session in ef. Nothing
5
+ > here is decided or planned.
6
+
7
+ ## Code on elements the parser constrains
8
+
9
+ Loadbare's data vocabulary is attributes, and an attribute goes on any
10
+ element: `<tr lb-query>`, `<option lb-column>`, `<tbody lb-show>`. Behavior
11
+ is different. The platform attaches code to markup through a custom element
12
+ tag, and a tag must survive the parser. In a table, a `<select>`, or a `<p>`
13
+ it does not: an unexpected tag is dropped, moved out of the table, or closes
14
+ the paragraph.
15
+
16
+ So code that concerns one row lives on a wrapper around the whole table.
17
+ `lb-table` owns the `<table>`, and the page writes its contents as named
18
+ templates (`head`, `foot`, `ghost`) that the builder joins back into a
19
+ table. Each new need is another named destination, and the page's source
20
+ reads as `lb-table`'s shape rather than a table's. The shipped HTML is
21
+ honest; the source is a dialect.
22
+
23
+ Two ways to put code on the element itself follow.
24
+
25
+ ### Customized built-ins
26
+
27
+ ```html
28
+ <tr is="lb-ghost-row">…</tr>
29
+ ```
30
+
31
+ The platform's own answer: a class extending `HTMLTableRowElement`,
32
+ registered with `customElements.define(name, cls, { extends: "tr" })`. The
33
+ parser sees a `<tr>`, so the element sits wherever a `<tr>` may, keeps every
34
+ native behavior of one, and gets the custom element lifecycle.
35
+
36
+ - Chrome and Firefox ship it. WebKit has declined to, on principle
37
+ (subclassing a built-in breaks when its internals change), so Safari needs
38
+ a polyfill for as long as the feature exists.
39
+ - Every browser on iOS and iPadOS is WebKit, Chrome included. The polyfill
40
+ is therefore not a Safari fallback but the code path on every iPhone and
41
+ iPad, permanently. The usual one, `@ungap/custom-elements`, watches the
42
+ document with a `MutationObserver`, so on those devices the lifecycle runs
43
+ because something noticed.
44
+ - `is` must be present when the element is created; setting it later does
45
+ nothing. Cloning a template preserves it, so a row template carrying
46
+ `is` yields upgraded rows.
47
+ - A customized built-in cannot be form-associated (`ElementInternals` is for
48
+ autonomous custom elements), which matters little: the built-in elements
49
+ that controls are already take part in forms.
50
+
51
+ ### Behavior by attribute
52
+
53
+ ```html
54
+ <tr lb-behavior="ghost-row">…</tr>
55
+ ```
56
+
57
+ Code bound to any element by an attribute, as Stimulus does with
58
+ `data-controller`. It fits the parser everywhere and needs no polyfill.
59
+
60
+ - What usually drives it is a `MutationObserver`, which is incidental: the
61
+ code runs because something noticed. The hub, though, already knows every
62
+ time it puts an element in the page: it shows a page, clones a row, builds
63
+ a group, and moves an element in and out of an `lb-show` template. A
64
+ behavior attached and detached at those moments would be explicit.
65
+ - Elements the hub does not add (a widget's own scaffolding, anything a
66
+ script creates) would get no behavior, which may be the right rule rather
67
+ than a limitation.
68
+ - No form association of its own, and mostly none needed: a behavior on a
69
+ native control leaves it a native control, which joins its form, takes
70
+ focus and honors `autofocus` as it always did. Only a control whose UI is
71
+ built from scratch needs `ElementInternals`, and stays an autonomous
72
+ custom element.
73
+ - The platform has discussed a registry for custom attributes, which would
74
+ be this idea built in. It has not shipped. Without it the browser has no
75
+ part in a behavior: everything below is the hub's.
76
+
77
+ #### A lifecycle in the hub's terms
78
+
79
+ A custom element's lifecycle is the browser's, and it speaks of insertion:
80
+ connected, disconnected, adopted. The hub's work rarely lines up with it. A
81
+ moved row is disconnected and connected, the copy a move leaves behind is
82
+ constructed, an `lb-show` toggle adopts twice, which is why a custom element
83
+ must write `connectedCallback` to run again.
84
+
85
+ A behavior's lifecycle would be calls the hub makes from its own code, at
86
+ moments it already knows, named for what the hub did:
87
+
88
+ | The hub | A behavior hears |
89
+ | -------------------------------------------------------------- | ----------------------------------------------------------- |
90
+ | Shows a page, places a cloned row, builds a group, first shows an element shipped absent | `attached()`, once, after the element is placed and filled |
91
+ | Lands new values on the element's row | `landed(row)` |
92
+ | Moves a row the order put elsewhere | `moved()`; the instance is kept |
93
+ | Leaves a copy of a moved row behind | Nothing; the copy gets no behavior |
94
+ | Turns `lb-show` off, then on | `hidden()`, then `shown()`; the instance is kept |
95
+ | Removes a row, once its animations end | `detached()`, once |
96
+ | Finishes a request issued from inside the element | `requestDone(detail)` |
97
+
98
+ - `attached` and `detached` pair exactly, so setup and teardown need no
99
+ guard against running twice.
100
+ - The calls are the code-side twin of the stamps a stylesheet already sees
101
+ (`lb-row-created`, `lb-row-changed`, `lb-row-moved`, `lb-row-leaving`):
102
+ one vocabulary for CSS and code, in the hub's relational terms.
103
+ - The hub keeps one instance per element and behavior name. An element may
104
+ carry several, `lb-behavior="ghost-row autosave"`, which a tag cannot.
105
+ - A behavior is a class in a file the builder finds by name, as it finds
106
+ `<tag>.browser.ts`, and a name with no file is a build error.
107
+
108
+ What the hub would have to define and keep: whether `attached` comes before
109
+ or after the rows inside a host land, and the order of calls among nested
110
+ behaviors. A custom element gets its ordering from the platform; here it is
111
+ loadbare's to state.
112
+
113
+ A rule for choosing: a custom element makes a new kind of element (its own
114
+ markup, expansion, or a control built from scratch); a behavior adds code to
115
+ an element HTML already has.
116
+
117
+ ### Where each would land
118
+
119
+ Either one lets a page write a real `<table lb-query>` with code on the rows
120
+ that need it, and leaves `lb-table` to supply only what is truly the
121
+ table's. Customized built-ins borrow the platform's lifecycle at the cost of
122
+ a permanent polyfill; behavior by attribute keeps the parser and the browser
123
+ matrix out of it, at the cost of a lifecycle the hub would define.
124
+
125
+ ## Expansion that replaces the tag
126
+
127
+ Today a custom element's tag does two jobs: at build time it is expanded,
128
+ and at run time it carries code. Because the code needs the tag, the tag
129
+ ships as a wrapper around what it expanded, and the wrapper is the source of
130
+ most widget friction: focus searched for inside it, values forwarded through
131
+ it, `autofocus` lost, and no place in a table.
132
+
133
+ With behaviors carrying the code, expansion is free to stop shipping the
134
+ tag.
135
+
136
+ ```html
137
+ <!-- page -->
138
+ <lb-field exp-label="Name" lb-column="name"></lb-field>
139
+
140
+ <!-- what ships -->
141
+ <!-- lb-field exp-label="Name" lb-column="name" -->
142
+ <label lb-behavior="lb-field">Name <input lb-column="name" /></label>
143
+ ```
144
+
145
+ - **The tag becomes a comment.** The shipped DOM keeps a trail back to the
146
+ source at no run-time cost. A comment is legal anywhere, tables included,
147
+ and is invisible to CSS, `:nth-child` and the hub's counting.
148
+ - **The builder puts the behavior on the root.** When
149
+ `<name>.behavior.ts` exists, the expanded root gets `lb-behavior="<name>"`;
150
+ the author never writes it. `[lb-behavior~="lb-field"]` then serves CSS
151
+ and `querySelector` as the tag selector `lb-field` does now.
152
+ - **Every widget inherits one base class.** A `Behavior` holds `this.el`,
153
+ has empty methods for every call in the hub's lifecycle, and types
154
+ `landed(row)` and `requestDone(detail)`. A widget overrides only what it
155
+ uses, as it extends `HTMLElement` or `WrappedControl` today.
156
+
157
+ The file convention decides which path an element takes:
158
+
159
+ | The element has | The builder | At run time |
160
+ | ------------------------------------------ | -------------------------------------------------- | ---------------------- |
161
+ | An element file, no script | Replaces the tag, leaves a comment | Plain HTML |
162
+ | An element file and `<name>.behavior.ts` | Replaces the tag, puts `lb-behavior` on the root | The hub's lifecycle |
163
+ | `<name>.browser.ts` | Keeps the tag, as today | The browser's lifecycle |
164
+
165
+ The third row is for controls built from scratch, the only elements that
166
+ still need `ElementInternals`.
167
+
168
+ ### A flag to keep the tag
169
+
170
+ Considered and set aside. It gives every widget two DOM shapes, so its CSS
171
+ and code must work in both. It cannot apply where replacing pays off most,
172
+ since a kept tag still has no place in a table. And what people would keep
173
+ the tag for, a name to find and style the widget by and a trail to its
174
+ source, the `lb-behavior` attribute and the comment already give.
175
+
176
+ ### To settle
177
+
178
+ 1. **One root.** A behavior attaches to one element, so an element file with
179
+ a behavior has exactly one top-level element, and anything more is a
180
+ build error.
181
+ 2. **Where the tag's attributes go.** The wrapper used to keep them; once
182
+ the tag is gone each needs a destination. The natural default is the
183
+ root, and `lb-field` breaks it: its root is the `<label>`, but
184
+ `lb-column` belongs on the `<input>` inside while `lb-show` belongs on the
185
+ label. Either the element file marks a target for data attributes, or
186
+ the page passes them as parameters (`exp-column`, written into the file as
187
+ `lb-column="{{column}}"`), which works today but reads less naturally
188
+ than `lb-column`. This wants worked examples before a rule.
@@ -72,6 +72,10 @@ Put everything the user interacts with inside `<lb-hub>`. The hub normally
72
72
  sits directly inside `<body>`, with banner, nav, footer and `<main>` inside
73
73
  it, so Loadbare can act on all of them.
74
74
 
75
+ Write exactly one `<lb-hub>` and exactly one `<main>`, with the `<main>`
76
+ inside the hub and empty but for whitespace. The builder rejects a chrome
77
+ that does otherwise.
78
+
75
79
  The chrome's `<title>` is the document title until the first page shows.
76
80
  From then on the hub sets the document title to the page's label; see
77
81
  [The URL](#the-url).
@@ -108,6 +112,21 @@ tab and the browser history show the page.
108
112
  `lb-url` is the one query a page may use that the server does not declare. A
109
113
  page names it the same way, anywhere inside the hub.
110
114
 
115
+ ### Where focus starts
116
+
117
+ On entering a page, once its queries have landed, the hub focuses the first
118
+ element in `<main>` the user can operate, so a keyboard user starts in the
119
+ page rather than on the document. The chrome comes first in the document,
120
+ and is passed over. So is anything disabled, in a closed `<dialog>`,
121
+ `inert`, `hidden`, in an absent `lb-show` branch, or that does not take
122
+ the focus when asked. A widget's native control counts, so write no
123
+ `autofocus` to restate this.
124
+
125
+ A page is entered on a cold load, a new `lb-path` from a link or from a
126
+ handler's `url()`, and Back or Forward to another page. A change of query
127
+ parm enters no page: the user who chose a record in a picker stays in the
128
+ picker. Focus the user has already put in `<main>` is left there.
129
+
111
130
  ### Links
112
131
 
113
132
  Write `lb-url-link` on an `<a>` to move between pages:
@@ -150,7 +169,9 @@ pushes one instead when the element carrying `lb-request` also carries
150
169
 
151
170
  The hub answers the request itself, with no round trip, then reloads the
152
171
  page's queries at the new URL. The page stays in place, and rows that come
153
- back keep their place.
172
+ back keep their place. A request that changes only `lb-order-<query>`, the
173
+ user's [order](./data-binding.md#order) for a query the hub sorts itself,
174
+ re-sorts the rows already on the page and loads nothing.
154
175
 
155
176
  A control whose column the URL does not carry lands empty, so every control
156
177
  inside `lb-query="lb-url"` shows what the address bar says, after a reload
@@ -268,6 +268,8 @@ one as a string literal:
268
268
  | `REQUEST_ROW_INSERT`, `REQUEST_ROW_UPDATE`, `REQUEST_ROW_DELETE` | The requests Loadbare provides |
269
269
  | `LB_ROW_REQUESTS` | All three, in one array |
270
270
  | `LB_EVENT_NAME` | `lb-request`, the event every request travels as |
271
+ | `LB_DONE_EVENT_NAME` | `lb-request-done`, the event that says how it turned out |
272
+ | `SHOW_NOT` | `!`, written before a column in `lb-show` |
271
273
  | `URL_QUERY` | `lb-url` |
272
274
  | `URL_COLUMN_PATH`, `URL_COLUMN_PAGE_LABEL`, `URL_COLUMN_PAGE_UNKNOWN` | `lb-path`, `lb-page-label`, `lb-page-unknown` |
273
275
  | `LB_RESERVED_PREFIX` | `lb-` |
@@ -276,7 +278,28 @@ A custom element reads `lb-` attributes and never assigns one. The developer
276
278
  writes them in markup, and the hub and the builder write their stamps.
277
279
 
278
280
  Loadbare reserves method names beginning with `lb` on a custom element, for
279
- the methods it calls; see [Holding rows](#holding-rows).
281
+ the methods it calls; see [Holding rows](#holding-rows). The build refuses
282
+ a script that declares a method, accessor or field whose name is `lb`, a
283
+ capital, and anything else Loadbare does not call, so a misspelled
284
+ `lbRowLanded` fails the build rather than never being called.
285
+
286
+ ### What the hub and an element say to each other
287
+
288
+ Each direction has one mechanism for each kind of message:
289
+
290
+ | Direction | What | How | Today |
291
+ |----------------|--------------------------------------------|-----------------------------|-------|
292
+ | 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` |
293
+ | Hub to element | State the platform already names | A property | A control's `value` |
294
+ | Hub to element | Work the hub needs done now, while landing | An optional `lb` method | `lbRowsLanded` |
295
+ | Hub to element | A moment an element started | A bubbling event | `lb-request-done` |
296
+ | Element to hub | A moment | A bubbling event | `lb-request`, and `change`, `click` and `submit` |
297
+
298
+ State is an attribute: a stylesheet can select on it, and an element that
299
+ upgrades late still finds it. A method is for work the hub cannot go on
300
+ without, done by one element at one point in landing. A moment is an event,
301
+ because the element that cares is often an ancestor of the one it concerns,
302
+ and an event reaches it knowing neither.
280
303
 
281
304
  ### When its code runs
282
305
 
@@ -309,7 +332,7 @@ So each kind of work has one place:
309
332
  reads no attribute and no child: an element made with
310
333
  `document.createElement` has neither when it runs.
311
334
  2. **A child is looked up when it is needed**, in a handler, a getter,
312
- `lbPlaceRow` or `lbRowsLanded`, and never held from setup.
335
+ or `lbRowsLanded`, and never held from setup.
313
336
  3. **`connectedCallback` runs on every move**, so it is written to run
314
337
  again. It rearranges the element's own children into a state it checks
315
338
  for first, or adds a listener to `document` or `window`, which
@@ -450,36 +473,67 @@ missing a field its name needs, and stamps the dispatching element with
450
473
 
451
474
  Let the event bubble, so an ancestor can stop it before the hub sends it.
452
475
 
476
+ ### Hearing how it turned out
477
+
478
+ Once what a request brought back has landed, or its round trip has failed,
479
+ the hub dispatches `lb-request-done` from the element that committed. It
480
+ bubbles, so the element that cares need not be the one that committed: a
481
+ dialog hears it from the form inside it.
482
+
483
+ ```ts
484
+ import { LB_DONE_EVENT_NAME } from "@loadbare/app/constants";
485
+ import type { HubRequestDone } from "@loadbare/app/types";
486
+
487
+ this.addEventListener(LB_DONE_EVENT_NAME, (e) => {
488
+ const { request, items, error } = (e as CustomEvent<HubRequestDone>).detail;
489
+ if (error) return;
490
+ const item = items.find((i) => i.query === request.query);
491
+ const created = item?.patch?.rows?.[0];
492
+ if (created) this.choose(String(created[item!.key]));
493
+ });
494
+ ```
495
+
496
+ `items` is every response item that landed because of the request, in
497
+ order. An answer that moved the URL contributes its `lb-url` item, which
498
+ carries a key only the server knew, and the page load. `error` is set when
499
+ the round trip failed, and then nothing landed. A new row's key is in the
500
+ answer only when the handler returned it in a patch: a refresh sends all
501
+ rows, and nothing marks which one is new.
502
+
503
+ By the time the event is dispatched, `lb-request-pending` is gone and the
504
+ page is as the answer left it. A request that was never sent, because the
505
+ hub or an ancestor stopped it, gets no `lb-request-done`. An element the
506
+ answer removed, such as the row a delete took away, dispatches the event
507
+ outside the document, and no ancestor it had there hears it.
508
+
453
509
  ### Holding rows
454
510
 
455
- The hub lands every row template itself. A plain element carrying `lb-query`
456
- with a row template inside it needs no code, so a custom element holds rows
457
- only when they need placement or scaffolding that only it can decide.
511
+ The hub lands every row template itself, places every row by the query's
512
+ order, and builds every group the markup's group templates describe; see
513
+ [Order](./data-binding.md#order) and [Groups](./data-binding.md#groups). A
514
+ plain element carrying `lb-query` with a row template inside it needs no
515
+ code, so a custom element holds rows only to do something with them once
516
+ they land.
458
517
 
459
- A custom element carrying `lb-query` and a row template may implement two
460
- optional methods, which the hub calls:
518
+ A custom element carrying `lb-query` and a row template may implement one
519
+ optional method, which the hub calls:
461
520
 
462
521
  ```ts
463
- import type { RowsHost, Row } from "@loadbare/app/types";
464
-
465
- class SortedList extends HTMLElement implements RowsHost {
466
- lbPlaceRow(el: Element, row: Row, template: HTMLTemplateElement) {
467
- // Where this live row goes. Called with the live row detached, on its
468
- // first appearance and again whenever all rows decide the order.
469
- }
522
+ import type { RowsHost } from "@loadbare/app/types";
523
+ import { ATTR_ROW_REQUESTED, REQUESTED_CREATED } from "@loadbare/app/constants";
470
524
 
525
+ class ScrollingList extends HTMLElement implements RowsHost {
471
526
  lbRowsLanded() {
472
- // Once, after the rows have landed. For scaffolding derived from the
473
- // rows: a section heading, an <optgroup>, anything that goes when its
474
- // last row does.
527
+ // Once, after the rows have landed, every row placed and stamped.
528
+ this.querySelector(`[${ATTR_ROW_REQUESTED}="${REQUESTED_CREATED}"]`)
529
+ ?.scrollIntoView({ block: "nearest" });
475
530
  }
476
531
  }
477
532
  ```
478
533
 
479
- | Method | The hub calls it |
480
- |----------------|---------------------|
481
- | `lbPlaceRow` | To place a live row |
482
- | `lbRowsLanded` | After the rows land |
534
+ | Method | The hub calls it |
535
+ |----------------|-------------------------------------------------------|
536
+ | `lbRowsLanded` | After the rows land, with every row placed and stamped |
483
537
 
484
538
  Everything else is the hub's:
485
539
 
@@ -487,16 +541,23 @@ Everything else is the hub's:
487
541
  |-------------|----------------------------------------------------------|
488
542
  | Cloning | Clones the row template once per new key |
489
543
  | Matching | Fills the live row already showing that key |
490
- | Removing | Removes the live rows the response says are gone |
491
- | Stamping | Stamps each live row with `lb-row-live` and `lb-key-value` |
544
+ | Placing | Places every live row by the query's order |
545
+ | Grouping | Builds a group where the order breaks, and removes it with its last row |
546
+ | Removing | Marks the live rows the response says are gone as leaving, and removes them |
547
+ | Stamping | Stamps each live row with `lb-row-live`, `lb-key-value`, and what happened to it |
492
548
  | Counting | Stamps `lb-query-row-count` with the number of live rows |
493
549
 
494
- `lbPlaceRow` receives the live row already filled and not yet in the
495
- document, so a custom element that reads a column to decide where the row
496
- goes can. Without it, the hub places a live row immediately before the row
497
- template, in arrival order. `lb-options.browser.ts` and
498
- `lb-table.browser.ts` in [`@loadbare/widgets`](./widgets.md) are two
499
- different placements over the same landing.
550
+ `lb-options.browser.ts` and `lb-table.browser.ts` in
551
+ [`@loadbare/widgets`](./widgets.md) are two uses of `lbRowsLanded`: one
552
+ gives each option and `<optgroup>` what landing does not, a value and a
553
+ label, and the other scrolls to the row the page's request created or
554
+ moved.
555
+
556
+ A custom element the builder shipped absent under `lb-show` has not
557
+ upgraded while its column is off, and the hub places the rows that land on
558
+ it then as it places any. When the column first turns on and the element
559
+ upgrades, the hub lands the query's last answer on it again, as all rows,
560
+ so `lbRowsLanded` runs.
500
561
 
501
562
  A custom element that walks its own rows finds them with `LIVE_ROW` from
502
563
  `@loadbare/app/constants`, never by `lb-key-value`: an element a `row` lands
@@ -153,9 +153,9 @@ places it immediately before the template:
153
153
  ```
154
154
 
155
155
  The hub matches each row to a live row by its key. All rows decide
156
- membership and order: a live row whose key did not arrive is removed. A
157
- patch changes only the rows it names, and every other live row keeps its
158
- content and its place.
156
+ membership: a live row whose key did not arrive leaves. A patch changes only
157
+ the rows it names, and every other live row keeps its content. The hub
158
+ places every live row by the query's order; see [Order](#order).
159
159
 
160
160
  A key is unique within a query. Two rows with one key in the same answer,
161
161
  or two live rows showing one key, are reported on the console.
@@ -163,10 +163,121 @@ or two live rows showing one key, are reported on the console.
163
163
  A live row's root counts as a column when it carries `lb-column`, which is
164
164
  how an `<option>`, whose content is text, shows the column it is.
165
165
 
166
- A custom element that carries `lb-query` and a row template may decide where
167
- a row goes and add scaffolding around the rows; see
168
- [Holding rows](./custom-elements.md#holding-rows) and
169
- [The Basic Widget Library](./widgets.md).
166
+ A custom element that carries `lb-query` and a row template reacts to the
167
+ rows once they land; see [Holding rows](./custom-elements.md#holding-rows)
168
+ and [The Basic Widget Library](./widgets.md).
169
+
170
+ ### Order
171
+
172
+ A `rows` query declares its order with the query, as columns separated by
173
+ commas, each descending when written after `-`:
174
+
175
+ ```ts
176
+ roster: rows("id", (ctx) => ctx.db.members(), { order: "team,name" }),
177
+ ```
178
+
179
+ The query parm `lb-order-<query>` replaces it for that query, so a control
180
+ inside `lb-query="lb-url"` writing that parm re-sorts the rows the hub
181
+ already has, with no round trip. With no order, rows show in the order they
182
+ arrived, and a patch's new rows go last.
183
+
184
+ Values compare by their JSON type: numbers numerically, strings as the
185
+ user's language orders them, `false` before `true`, and `null` last. A
186
+ number sent as a string sorts as a string. Rows that compare equal keep the
187
+ order they arrived in. A row moves only when the order puts it somewhere
188
+ else.
189
+
190
+ A query that pages or limits its rows reads the parm itself and declares
191
+ `serverSortedByUrl: true`, so a change of order loads the page again.
192
+
193
+ ### Groups
194
+
195
+ A group is a run of rows sharing the order's leading term. Write a
196
+ `<template lb-group>` where the row template goes, holding the group's
197
+ heading and one nested `<template>`: the next group template, or the row
198
+ template. The nth group template breaks on the order's nth term.
199
+
200
+ ```html
201
+ <ul lb-query="roster">
202
+ <template lb-group>
203
+ <li>
204
+ <h3 lb-column="team"></h3>
205
+ <ul>
206
+ <template><li lb-column="name"></li></template>
207
+ </ul>
208
+ </li>
209
+ </template>
210
+ </ul>
211
+ ```
212
+
213
+ The group's contents land immediately before its nested template. When that
214
+ template is inside the heading's element, the contents land inside it; when
215
+ it is beside the heading, they land after it. `<tbody>` and `<optgroup>` do
216
+ not nest, so a table's deeper levels are heading rows. Content after the
217
+ nested template is the group's footer.
218
+
219
+ A heading is filled from its group's first row, so it may show a column
220
+ other than the one the group breaks on. The hub stamps each top-level
221
+ element of a group with `lb-group-live`, `lb-group-column` and
222
+ `lb-group-value`. A row creates its group, and a group leaves with its last
223
+ row.
224
+
225
+ ### Aggregates
226
+
227
+ `lb-count`, `lb-sum="col"`, `lb-avg="col"`, `lb-min="col"` and
228
+ `lb-max="col"` set an element from the rows of the nearest group around it,
229
+ or of the whole list outside any group:
230
+
231
+ ```html
232
+ <tr><td><span lb-count></span> people</td><td lb-sum="salary"></td></tr>
233
+ ```
234
+
235
+ A sum and an average are exact, read from JSON numbers and from strings
236
+ holding a plain decimal. `null` and empty values are left out. The result
237
+ lands unformatted, and covers the rows the browser holds.
238
+
239
+ ### What happened to a row
240
+
241
+ Every change to a list is one a stylesheet can see:
242
+
243
+ | Attribute | Means |
244
+ | ------------------ | ------------------------------------------------------ |
245
+ | `lb-row-created` | Created when the hub last touched the row |
246
+ | `lb-row-changed` | Its values changed when the hub last touched it |
247
+ | `lb-row-moved` | The order put it somewhere else when last touched |
248
+ | `lb-row-requested` | `created`, `changed` or `moved` by this page's request |
249
+ | `lb-row-leaving` | A row on its way out |
250
+ | `lb-group-leaving` | A group on its way out |
251
+
252
+ A row that leaves loses `lb-row-live`, takes `inert`, and is removed once
253
+ its animations finish, at once when there are none. A moved row moves, and
254
+ a copy stays behind where it was, leaving. `lb-row-requested` is cleared at
255
+ every landing; the others when the hub next touches the row.
256
+
257
+ ```css
258
+ [lb-row-created] { animation: arrive 200ms; }
259
+ [lb-row-leaving] { animation: depart 200ms forwards; }
260
+ ```
261
+
262
+ ### What arrives later
263
+
264
+ The hub keeps each query's last answer until the page changes, with any
265
+ patch since applied to it. An element that names a query and arrives after
266
+ that query's answer is filled from what was kept, at the end of the landing
267
+ that follows its arrival. Such an element is a picker in a new live row, or
268
+ scaffolding around rows, such as a group's heading or a ghost row. So
269
+ a query nested in another query's rows need not land again, or land after
270
+ the outer query, for a new outer row to show its choices.
271
+
272
+ A custom element the builder shipped absent has not upgraded while its
273
+ `lb-show` column is off. When the column first turns on and the element
274
+ upgrades, the hub lands the kept answer on it again, so its `lbRowsLanded`
275
+ runs. See [Holding rows](./custom-elements.md#holding-rows).
276
+
277
+ Nothing is answered from what the hub kept: a request always goes to the
278
+ server, and a query re-runs only when a response names it. A patch that
279
+ arrives before all of a query's rows is not kept, since it is not the whole
280
+ of anything.
170
281
 
171
282
  A `rows` query on an element with no row template lands nothing. That is the
172
283
  insert form above: it sits inside `lb-query="roster"` so its request is for
@@ -225,6 +336,14 @@ have the query answer with a boolean or a null. A row that does not carry
225
336
  the column leaves the element as it is, so a query answers with the column
226
337
  in every row.
227
338
 
339
+ Write `!` before the column to reverse it. One column then decides between
340
+ two elements, rather than a column and its opposite, which could disagree:
341
+
342
+ ```html
343
+ <h2 lb-show="chosen" lb-column="title"></h2>
344
+ <p lb-show="!chosen">Choose a batch.</p>
345
+ ```
346
+
228
347
  `lb-show` reads from the nearest ancestor row, as `lb-column` does. On an
229
348
  element that also carries `lb-query`, the column belongs to the row around
230
349
  it, so this picker takes its choices from `groups` and whether it is present
@@ -463,6 +582,11 @@ An ancestor may stop the event, and the request is not sent. A custom
463
582
  element may dispatch the event itself; see
464
583
  [Sending a request](./custom-elements.md#sending-a-request).
465
584
 
585
+ Once the answer has landed, or the round trip has failed, the hub dispatches
586
+ a bubbling `lb-request-done` event from the same element, carrying the
587
+ request and what landed because of it, or the error; see
588
+ [Hearing how it turned out](./custom-elements.md#hearing-how-it-turned-out).
589
+
466
590
  ## The URL
467
591
 
468
592
  The hub serves one query of its own, `lb-url`: one row holding the path, the
@@ -495,3 +619,4 @@ expansion, and refuses to build:
495
619
  - `lb-url-unknown` on anything but a `<dialog>`, or outside `<lb-hub>`
496
620
  - `lb-show` on a `<template>`, or on a row template's root
497
621
  - `lb-show` with no `lb-query` around it
622
+ - `lb-show="!"` or `lb-show="!!column"`, which reverse no column