@loadbare/app 0.19.0 → 0.20.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 (42) hide show
  1. package/dist/build/assemble.d.ts.map +1 -1
  2. package/dist/build/assemble.js +28 -2
  3. package/dist/build/assemble.js.map +1 -1
  4. package/dist/core/lb-constants.d.ts +7 -2
  5. package/dist/core/lb-constants.d.ts.map +1 -1
  6. package/dist/core/lb-constants.js +30 -4
  7. package/dist/core/lb-constants.js.map +1 -1
  8. package/dist/hub/lb-apply.d.ts +21 -0
  9. package/dist/hub/lb-apply.d.ts.map +1 -1
  10. package/dist/hub/lb-apply.js +221 -30
  11. package/dist/hub/lb-apply.js.map +1 -1
  12. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  13. package/dist/hub/lb-hub.browser.js +26 -6
  14. package/dist/hub/lb-hub.browser.js.map +1 -1
  15. package/docs/comparison.md +23 -13
  16. package/docs/opinions.md +2 -2
  17. package/docs/reference/builder.md +11 -2
  18. package/docs/reference/chrome.md +83 -9
  19. package/docs/reference/css.md +0 -1
  20. package/docs/reference/custom-elements.md +7 -28
  21. package/docs/reference/data-binding.md +320 -50
  22. package/docs/reference/names.md +258 -0
  23. package/docs/reference/overview.md +6 -0
  24. package/docs/reference/page-files.md +47 -10
  25. package/docs/reference/server.md +54 -1
  26. package/docs/reference/widgets.md +10 -1
  27. package/docs/testing.md +5 -1
  28. package/docs/tutorials/040-displaying-data.md +1 -1
  29. package/package.json +1 -1
  30. package/skills/loadbare-app/SKILL.md +26 -14
  31. package/skills/loadbare-app/references/builder.md +11 -2
  32. package/skills/loadbare-app/references/chrome.md +83 -9
  33. package/skills/loadbare-app/references/css.md +0 -1
  34. package/skills/loadbare-app/references/custom-elements.md +7 -28
  35. package/skills/loadbare-app/references/data-binding.md +320 -50
  36. package/skills/loadbare-app/references/names.md +258 -0
  37. package/skills/loadbare-app/references/overview.md +6 -0
  38. package/skills/loadbare-app/references/page-files.md +47 -10
  39. package/skills/loadbare-app/references/server.md +54 -1
  40. package/skills/loadbare-app/references/widgets.md +10 -1
  41. package/docs/TECHREF-1.0.md +0 -2267
  42. package/skills/loadbare-app/references/TECHREF-1.0.md +0 -2267
@@ -1,2267 +0,0 @@
1
- # What 1.0 release means
2
-
3
- This is the complete technical reference to the upcoming Release 1.0.
4
-
5
- Method-of-work is to continually revise this document to what we want
6
- to be true, then revise the code, docs, and tests to ensure it is true.
7
-
8
- ## The build
9
-
10
- ### Running the builder
11
-
12
- ```
13
- loadbare-app-build [--src src] [--out dist] [--watch] [--minify]
14
- ```
15
-
16
- `--src` names the tree the builder scans, and is defined under
17
- [What the builder scans](#what-the-builder-scans).
18
-
19
- `--out` names the directory the builder writes into, and defaults to `dist`.
20
- Every output path in this document is written relative to it, so an
21
- application that passes `--out` reads `dist/app.html` here as
22
- `<out>/app.html`. See [Build outputs](#build-outputs).
23
-
24
- `--watch` builds once, then rebuilds on every change to an `.html`, `.ts` or
25
- `.css` file under `--src`. A build that throws is reported and the watch
26
- continues. There is no dev server and no browser reload.
27
-
28
- `--minify` minifies `client.js` and `app.css`. It leaves `app.html` alone,
29
- which stays markup an application can read.
30
-
31
- The builder bundles `client.js` from a `client-entry.ts` it generates into
32
- `--out` and leaves there. Nothing reads it after the bundle, and an
33
- application neither writes nor imports it.
34
-
35
- Every `.css` file in every origin joins `app.css`. There is no custom element, page
36
- or chrome stylesheet: the builder concatenates them all, with nothing added,
37
- removed or scoped, so what ships is what was authored. Within one origin
38
- they are ordered by filename, the full path breaking a tie, and a
39
- subdirectory never affects the order. Origins follow the cascade — this
40
- package's own custom elements, then each package named in `imports.ts`, then `--src`
41
- last — so an application's own stylesheet always lands after the ones it
42
- imported. A file named `00-global.css` sorts first by saying so.
43
-
44
- ### What the builder scans
45
-
46
- #### The current project
47
-
48
- The builder scans one directory named by its `--src` parameter,
49
- which defaults to `src/`.
50
-
51
- The sections below often state something like, "anywhere in
52
- the `--src` tree", because Loadbare/app assigns semantic meaning
53
- to file names, not subdir names.
54
-
55
- #### Imported widget libraries
56
-
57
- Any number of widget libraries can be used. They are named
58
- in the file `imports.ts`, located anywhere in builder `--src`.
59
- A project can leave out `imports.ts`, or have exactly one, but
60
- two or more is an error.
61
-
62
- ```ts
63
- // imports.ts, anywhere in --src
64
- export default ["@namespace/widget-library"];
65
- ```
66
-
67
- If the named package's `package.json` declares where the widgets
68
- are, Loadbare scans that. Otherwise it scans the entire installed
69
- directory. The declaration, if present, would look like this:
70
-
71
- ```json
72
- "loadbare": { "widgets": "./dist" }
73
- ```
74
-
75
- ### File names and the builder
76
-
77
- The builder finds a file by its name. A subdirectory carries no meaning, so
78
- the whole of `--src` is one flat namespace, as stated in
79
- [What the builder scans](#what-the-builder-scans).
80
-
81
- Three kinds of file hold HTML.
82
-
83
- | File | Holds | Found in |
84
- | ------------------ | ----------------------------------------- | ------------ |
85
- | `chrome.html` | The one HTML document | `--src` |
86
- | `<stub>.page.html` | One page's markup, as a fragment | `--src` |
87
- | `<tag-name>.html` | One custom element's markup, as a fragment | Every origin |
88
-
89
- As stated in [Chrome](#chrome), `chrome.html` is a required singleton.
90
-
91
- As stated in [Pages](#pages), the `<stub>` of a `.page.html` is the path the
92
- page maps to. The builder puts all pages into `dist/app.html` as HTML `<template>` objects
93
- that carry `lb-page="<stub>"`, and `lb-page-title` when the page file has a
94
- `<title>`.
95
-
96
- An element file is exactly `<kebab-case-name>.html`. A
97
- [custom element](#custom-elements) is suitable for organizing large HTML
98
- trees, or adding custom behavior, or both. An element file is named for the
99
- tag it expands.
100
-
101
- Any other HTML file, one that is not `chrome.html`, or `<stub>.page.html` or
102
- `<kebab-case-name>.html` will either:
103
- - be an error if it looks like an element file with capitalization
104
- - be skipped
105
-
106
-
107
- ## HTML
108
-
109
- In a Loadbare/app application, HTML is static after the build, and once
110
- it is sent to the browser on initial page load, no HTML is ever sent
111
- again.
112
-
113
- The developer writes `lb-*` attributes in markup. Script never assigns
114
- them. The hub and the builder write only their stamps, apart from the
115
- few exceptions listed in [Miswarts](./miswarts.md). See
116
- [The markup checks](#the-markup-checks).
117
-
118
- ### Expansion
119
-
120
- The builder executes a process we call "expansion". When it finds
121
- a custom element in the HTML it is processing, such as `<my-element>`,
122
- it looks for the element file `my-element.html`, and follows the expansion
123
- rules to place its contents.
124
-
125
- A custom element with no element file is left alone. A custom element
126
- with neither an element file nor a `.browser.ts` script is a build error, as
127
- stated in [Custom elements](#custom-elements).
128
-
129
- Each file is parsed on its own, and expansion joins them through the DOM,
130
- which applies none of HTML's rules for what an element may hold. The browser
131
- reads the shipped text by those rules, so a join it would rearrange, such as
132
- a definition's `<dialog>` inside a page's `<p>`, is a build error naming the
133
- file, the custom element and the path to what would move.
134
-
135
- #### Build time parameters
136
-
137
- A build time parameter is supplied as an attribute on a custom element,
138
- and is converted to a fixed value within the HTML by the builder.
139
-
140
- The attribute name carries an `exp-` prefix, as in `exp-label`, and
141
- the substitution locations use `{{label}}` (no prefix) inside the
142
- element file:
143
-
144
- ```html
145
- <!-- note-field.html, the element file -->
146
- <label>{{label}} <input readonly="{{readonly}}" /></label>
147
- ```
148
-
149
- ```html
150
- <!-- a page -->
151
- <note-field exp-label="Name"></note-field>
152
- ```
153
-
154
- ```html
155
- <!-- dist/app.html -->
156
- <note-field exp-label="Name"
157
- ><label>Name <input /></label
158
- ></note-field>
159
- ```
160
-
161
- An element file states a default with `{{name|default}}`, as in
162
- `{{button-label|OK}}`. Whitespace around the name and around the default is
163
- discarded. The default is a literal.
164
-
165
- | The attribute is assigned | Expansion defines | Result |
166
- | ------------------------- | ------------------- | -------------------------------- |
167
- | `exp-name="text"` | `{{name}}` | `text` |
168
- | `exp-name=""` | `{{name}}` | An empty string |
169
- | Nothing | `{{name\|default}}` | `default` |
170
- | Nothing | `{{name}}` | Nothing, and the attribute drops |
171
- | `exp-name` | No `{{name}}` | A build error |
172
-
173
- Attributes without the `exp-` prefix are ignored during expansion.
174
-
175
- A placeholder is the whole of an attribute value or the whole of a text node.
176
- `title="Hello {{name}}"` ships literally.
177
-
178
- A placeholder name carrying a capital is a build error.
179
-
180
- A parameter value cannot become markup.
181
-
182
- #### Slots and templates
183
-
184
- An element file may contain any number of named templates and optionally
185
- one slot.
186
-
187
- | Attribute | The builder |
188
- | ----------------- | ------------------------------------------------------- |
189
- | `lb-exp-slot` | Puts in it the rest of the custom element's content |
190
- | `lb-exp-template` | Puts in it the content of the template of the same name |
191
- | `exp-<name>` | Replaces `{{name}}` in the element file with its value |
192
-
193
- - `lb-exp-slot` goes on one element in an element file.
194
- - `lb-exp-template` goes on an element in an element file, and on a
195
- `<template>` in the custom element's content. Both name the same
196
- template.
197
- - `exp-<name>` goes on a custom element.
198
-
199
- This simplified element file for `lb-table` names two templates, `head` and
200
- `foot`, and marks `<tbody>` as the slot. A page supplies whichever templates
201
- it wants, and everything else it writes goes to the slot.
202
-
203
- ```html
204
- <!-- lb-table.html, the element file -->
205
- <table>
206
- <caption>
207
- {{caption}}
208
- </caption>
209
- <thead lb-exp-template="head"></thead>
210
- <tbody lb-exp-slot></tbody>
211
- <tfoot lb-exp-template="foot"></tfoot>
212
- </table>
213
- ```
214
-
215
- The page below supplies the `head` template and a row template. The row
216
- template names no destination, so it goes to the slot. This page writes no
217
- footer.
218
-
219
- ```html
220
- <!-- a page -->
221
- <lb-table lb-query="staff" exp-caption="Everyone, by team">
222
- <template lb-exp-template="head">
223
- <tr><th>Name</th><th>Role</th></tr>
224
- </template>
225
- <template>
226
- <tr><td lb-column="name"></td><td lb-column="role"></td></tr>
227
- </template>
228
- </lb-table>
229
- ```
230
-
231
- When a named template is expanded, the `<template>` tag is discarded and its
232
- contents are placed as children of the element that names it. Both
233
- `lb-exp-slot` and `lb-exp-template` are gone from what ships. The built
234
- document holds:
235
-
236
- ```html
237
- <!-- dist/app.html -->
238
- <lb-table lb-query="staff" exp-caption="Everyone, by team"
239
- ><table>
240
- <caption>
241
- Everyone, by team
242
- </caption>
243
- <thead>
244
- <tr>
245
- <th>Name</th>
246
- <th>Role</th>
247
- </tr>
248
- </thead>
249
- <tbody>
250
- <template>
251
- <tr>
252
- <td lb-column="name"></td>
253
- <td lb-column="role"></td>
254
- </tr>
255
- </template>
256
- </tbody>
257
- <tfoot></tfoot></table
258
- ></lb-table>
259
- ```
260
-
261
- These are build errors:
262
-
263
- - An element file with two destinations of one name
264
- - An element file with more than one `lb-exp-slot`
265
- - A page naming a template the element file does not have
266
- - A page giving two templates for one name
267
- - Content written in a custom element whose element file has no slot
268
-
269
- ### Names
270
-
271
- Loadbare owns the `lb-` prefix in several namespaces: attribute names, the
272
- values of its attributes, query names, the column names of its own queries,
273
- request names, custom element names and custom event names. It also owns
274
- custom element method names beginning with `lb`.
275
-
276
- The application should never name anything with the `lb-` prefix anywhere.
277
-
278
- `createHub` refuses, at startup, a page that declares a query or a handler
279
- whose name begins with `lb-`.
280
-
281
- ### Landing
282
-
283
- A query is a name for rows, of kind `row` or `rows`, with a key. The server
284
- declares a page's queries in `<stub>.queries.ts`, and answers every request,
285
- a page load included, with response items. A response item carries a query
286
- name, its kind, its key's column name, and one of a row, all rows, or a
287
- patch. See [The wire](#the-wire).
288
-
289
- The hub lands a response item on every element whose `lb-query` names its
290
- query.
291
-
292
- | Attribute | Names | The hub |
293
- | ----------- | -------- | -------------------------------------------------- |
294
- | `lb-query` | a query | Puts its rows in the element's content |
295
- | `lb-column` | a column | Sets the element from that column |
296
- | `lb-show` | a column | Removes it while the named column is null or false; `!column` reverses it |
297
-
298
- | Stamp | The hub stamps it with |
299
- | -------------------- | -------------------------------- |
300
- | `lb-column-value` | The value it set |
301
- | `lb-key-value` | The row's key |
302
- | `lb-row-live` | Nothing; it marks a live row |
303
- | `lb-query-row-count` | The number of live rows it shows |
304
-
305
- The markup names a query and the columns it shows. The kind and the key are
306
- the server's, and the markup states neither.
307
-
308
- The row template is the first `<template>` among the descendants of an
309
- element with `lb-query`, outside any nested `lb-query`, or, when that
310
- template is a [group template](#groups), the innermost template inside it.
311
- A live row is an element the hub cloned from a row template for one row,
312
- and carries `lb-row-live`.
313
-
314
- | Kind | Row template | The hub |
315
- | ------ | ------------ | ----------------------------------- |
316
- | `row` | no | Lands the row on the element itself |
317
- | `row` | yes | Lands one live row |
318
- | `rows` | yes | Lands one live row per row |
319
- | `rows` | no | Lands nothing |
320
-
321
- A `rows` query with no row template names the query without showing it,
322
- which is what an insert form naming the query it adds to is.
323
-
324
- The nearest ancestor row of an element is the nearest of: the element
325
- itself when it is a live row, an ancestor that is a live row, and an
326
- ancestor that carries `lb-query` of kind `row`. The hub reads `lb-column`
327
- and `lb-show` from the nearest ancestor row. A nested `lb-query` begins a
328
- new query, and what is inside it is that query's.
329
-
330
- An element's own `lb-query` names what the element holds, and its own
331
- `lb-column` reads from the row around it. So a
332
- `<select lb-query="statuses" lb-column="status">` in a live row of
333
- `accounts` holds the statuses as its options and shows that account's
334
- `status`. `lb-column` on an element with `lb-query` is undefined unless the
335
- element is a control.
336
-
337
- Name one query on more than one element to show it in more than one place.
338
- Every one of them receives it.
339
-
340
- A query that arrives with nowhere to land is reported to the console,
341
- unless its name begins with `lb-`.
342
-
343
- #### How a column lands
344
-
345
- | Element | The hub sets |
346
- | ------------------------------------------------ | ------------------------------ |
347
- | A control | Its `value` |
348
- | A custom element that is not a control | `lb-column-value` only |
349
- | Any other element | Its text content |
350
-
351
- A control is an `<input>`, `<select>` or `<textarea>`, or a form-associated
352
- custom element with a `value` property that fires `change`. A checkbox and
353
- a radio button are controls too, by the rules below. A
354
- form-associated custom element declares `static formAssociated = true`.
355
-
356
- The hub stamps `lb-column-value` with the value on every element it sets.
357
- It never replaces the content of a custom element, which the builder placed
358
- there from its element file. A custom element that is not a control
359
- renders `lb-column-value` itself.
360
-
361
- The hub gathers from the same controls it lands on, so a value read back is
362
- the one that landed.
363
-
364
- A checkbox's state is `checked`, not `value`: HTML's quirk, and so the
365
- hub's, so that every other part of the vocabulary treats it as any control.
366
- The hub checks it by the rule `lb-show` is on by: `null` and `false` are
367
- unchecked, and every other value is checked, with no string read. It
368
- gathers `"true"` or `"false"`, so an unchecked box still says so, where a
369
- native form submission leaves it out. The checkbox's own `value`
370
- attribute means nothing to the hub.
371
-
372
- ```html
373
- <input type="checkbox" lb-column="active" lb-request="lb-row-update" />
374
- ```
375
-
376
- A radio button's state is `checked` as well. The hub checks it when its
377
- `value` attribute equals the value as a string, and unchecks it otherwise,
378
- `null` included. Of the radio buttons naming one column, the hub gathers
379
- the `value` of the one checked, or `""` when none is.
380
-
381
- ```html
382
- <input type="radio" lb-column="shift" value="day" lb-request="lb-row-update" />
383
- <input type="radio" lb-column="shift" value="night" lb-request="lb-row-update" />
384
- ```
385
-
386
- An `<input>` of type `file` receives nothing, not even `lb-column-value`,
387
- and the hub reports it to the console. The hub does not gather one either.
388
-
389
- A `<select>` with no option for the value shows no selection.
390
-
391
- An element with `lb-column` and no ancestor row receives nothing. The hub
392
- still gathers from it.
393
-
394
- The value arrives as the query produced it, with no conversion, so the
395
- browser decides what a non-string looks like.
396
-
397
- A column holds one value. A set of values is a query of its own, never an
398
- array in a column.
399
-
400
- A custom element sees `attributeChangedCallback` for an `lb-column-value`
401
- already present when it upgrades, so it cannot tell a first landing from a
402
- refresh, and does not need to.
403
-
404
- #### Rows
405
-
406
- The hub matches each row to a live row by its key. It stamps
407
- `lb-key-value` on each live row, and on an element a `row` lands on itself.
408
- It stamps `lb-row-live` on each live row and nowhere else, so a `row`
409
- landed inside another query, such as a total in a table's foot, is not one
410
- of that query's rows. A stylesheet or a custom element selects live rows
411
- with `[lb-row-live]`, and never with `[lb-key-value]`.
412
-
413
- A `row` query answering `null` lands every column its element names as
414
- null, and the element loses `lb-key-value`. Under a row template, its live
415
- row leaves.
416
-
417
- A key is unique within a query. Two rows with one key in the same answer,
418
- or two live rows showing one key, are reported on the console.
419
-
420
- All rows decide membership: a live row whose key did not arrive leaves —
421
- see [What happened to a row](#what-happened-to-a-row). A patch touches only
422
- the rows it names, and leaves every other live row's contents alone.
423
-
424
- The hub places every live row by the query's [order](#order), immediately
425
- before the row template, or before its clone in the row's
426
- [group](#groups). A row moves only when the order puts it somewhere else,
427
- so all rows landing again unchanged move nothing. A live row already in the
428
- markup when the first answer lands is one of the query's rows.
429
-
430
- After every landing the hub stamps the element with `lb-query-row-count`,
431
- the number of live rows it shows. A row a [filter](#filters) hides is not
432
- counted. The server answers with rows and says
433
- nothing about how many survived, so this is the one fact a page cannot be
434
- sent. It makes an empty query a stylesheet rule:
435
-
436
- ```css
437
- [lb-query-row-count="0"] .roster-empty {
438
- display: revert;
439
- }
440
- ```
441
-
442
- A row missing its key column is reported to the console and not landed.
443
-
444
- When the focus is in a live row that placing moves, the hub puts it back on
445
- the same element and scrolls it into view. When that row leaves, or a
446
- filter hides it, the focus goes to the first element the user can operate
447
- in the first row showing.
448
-
449
- #### Order
450
-
451
- A `rows` query's order is a rule over its columns. The server declares it
452
- with the query, and the user may choose another:
453
-
454
- ```ts
455
- accounts: rows("id", (ctx) => ctx.db.accounts(), {
456
- order: "category_order,account_name",
457
- }),
458
- ```
459
-
460
- An order is columns separated by commas. A column written after `-` sorts
461
- descending. `lb-order`, on the element carrying `lb-query`, names a column
462
- of [`lb-choices`](#choices) holding the user's order, and while that column
463
- is not empty it replaces the declared order. With neither, rows show in the
464
- order the server sent them, and a patch's new rows go last.
465
-
466
- | Values | Compare |
467
- | -------------------- | ------------------------------------------ |
468
- | Two numbers | Numerically |
469
- | Two strings | As the user's language orders them |
470
- | Two booleans | `false` before `true` |
471
- | Two different types | Booleans, then numbers, then strings |
472
- | `null` or missing | After everything ascending, first descending |
473
-
474
- A number sent as a string sorts as a string, so a column the order reads
475
- numerically arrives as a JSON number, or its query declares its type.
476
- `types` names a type for any of a `rows` query's columns, one of `number`,
477
- `boolean` and `text`, and the column compares read as that type whatever
478
- JSON type its values arrive as:
479
-
480
- ```ts
481
- accounts: rows("id", (ctx) => ctx.db.accounts(), {
482
- order: "-balance",
483
- types: { balance: "number" },
484
- }),
485
- ```
486
-
487
- A value that is no value of its declared type compares as `null` does.
488
- `createHub` refuses at startup a type it does not know.
489
-
490
- The declared order and types travel once, on the answer that loads the
491
- page, and the hub keeps them with the query's answer for every answer after
492
- it, all rows or a patch. `readAs` in
493
- `@loadbare/app/types` is the rule the hub applies.
494
-
495
- Rows that compare equal keep the order they arrived in, and a patch's new
496
- rows come after the rows already showing.
497
-
498
- The hub sorts the rows it has, so a choice of order places them again with
499
- no round trip:
500
-
501
- ```html
502
- <div lb-query="lb-choices">
503
- <select lb-column="accounts_order" lb-request="lb-row-update">
504
- <option value="">By category</option>
505
- <option value="account_name">By name</option>
506
- </select>
507
- </div>
508
-
509
- <table lb-query="accounts" lb-order="accounts_order">
510
- ...
511
- </table>
512
- ```
513
-
514
- #### Filters
515
-
516
- `lb-filter`, on the element carrying `lb-query`, names the columns of its
517
- rows a choice may filter, separated by commas. The column of
518
- [`lb-choices`](#choices) of the same name holds the filter. A row that does
519
- not pass every filter is `hidden`, and a ticked `lb-select` in it is
520
- cleared. A group whose rows are all hidden is hidden. An empty choice
521
- filters nothing.
522
-
523
- ```html
524
- <table lb-query="accounts" lb-filter="account_name, balance">
525
- ```
526
-
527
- A filter is terms, each an operator and a value. A term begins wherever
528
- whitespace comes before an operator, so a value may hold spaces. Text with
529
- no operator is `*=`. A term with no value is left out. Every term must
530
- pass: `>=2026-01-01 <=2026-03-31`.
531
-
532
- | Operator | Compares |
533
- | -------------------------- | ------------------------------------------------- |
534
- | `^=`, `$=`, `*=` | As text, ignoring case: begins with, ends with, contains |
535
- | `=`, `!=`, `<`, `<=`, `>`, `>=` | As the [order](#order) compares, with the value read as the column's type |
536
-
537
- The column's type is its declared [type](#order), if its query declares
538
- one. Otherwise it is a number for a JSON number, `true` or `false` for a
539
- boolean, and text otherwise. A value that is not of the column's type, and
540
- a column that is null or missing, pass only `!=`.
541
-
542
- `parseFilter`, `passes` and `passesTerm` in `@loadbare/app/types` are the
543
- rule the hub applies.
544
-
545
- #### Groups
546
-
547
- A group is a run of rows sharing the order's leading term. A group
548
- template is a `<template lb-group>` in the row template's place, holding the
549
- group's heading and exactly one nested `<template>`, which is where the
550
- group's contents go: the next group template, or the row template. The nth
551
- group template breaks on the order's nth term, so groups nest to any depth.
552
-
553
- ```html
554
- <table lb-query="accounts">
555
- <thead><tr><th>Account</th></tr></thead>
556
- <template lb-group>
557
- <tbody>
558
- <tr><th scope="rowgroup" lb-column="category"></th></tr>
559
- <template><tr><td lb-column="account_name"></td></tr></template>
560
- </tbody>
561
- </template>
562
- </table>
563
- ```
564
-
565
- The hub clones the group template where the order breaks and puts the
566
- group's contents immediately before the clone of its nested template. The
567
- markup's nesting decides the shape:
568
-
569
- | The nested template is | The group's contents land | Example |
570
- | --------------------------------- | ----------------------------------- | -------------------------------- |
571
- | Inside the heading's element | Inside it | `<tbody>`, `<li>`, `<details>` |
572
- | Beside the heading | After the heading, as its siblings | A heading `<tr>` in one `<tbody>` |
573
-
574
- `<tbody>` and `<optgroup>` do not nest, so a table's levels below the first
575
- are heading rows beside their rows. Content after the nested template lands
576
- after the group's rows, which is where a group's footer goes.
577
-
578
- A group's heading is filled from its first row, the same way a live row is
579
- filled, so it shows any column of the row. The example breaks on
580
- `category_order` and shows `category`. The hub fills it from no other row.
581
-
582
- The hub stamps each top-level element of a group's clone with
583
- `lb-group-live`, `lb-group-column`, the column it breaks on, and
584
- `lb-group-value`, its value there. A row creates its group, and a group
585
- leaves with its last row. When the order changes, a group whose run goes
586
- on is kept and moved, and a group whose run ended leaves.
587
-
588
- A group template with no nested template is reported to the console. More
589
- group templates than the order has terms show each extra level as one
590
- group, and are reported once.
591
-
592
- #### Aggregates
593
-
594
- An aggregate attribute sets an element from the rows of the nearest group
595
- around it, or of the whole list outside any group.
596
-
597
- | Attribute | Sets the element to |
598
- | --------------- | --------------------------------------------- |
599
- | `lb-count` | The number of rows |
600
- | `lb-sum="col"` | The sum of `col` |
601
- | `lb-avg="col"` | The average of `col` |
602
- | `lb-min="col"` | The least value of `col`, by the [order](#order)'s comparison |
603
- | `lb-max="col"` | The greatest, likewise |
604
-
605
- ```html
606
- <tr><td><span lb-count></span> accounts</td><td lb-sum="balance"></td></tr>
607
- ```
608
-
609
- It lands as a column does, with `lb-column-value`. A sum and an average
610
- are exact decimals, read from JSON numbers and from strings holding a plain
611
- decimal, as Postgres sends `numeric`. A sum has as many places as the most
612
- precise value, and an average up to two more. `null` and empty values are
613
- left out, as SQL leaves them out. Any other value is left out and reported
614
- once. The hub does not format the result.
615
-
616
- An aggregate covers the rows showing, so a row a [filter](#filters) hides
617
- is left out. Where the true total
618
- matters beyond what the page shows, it is a row the server answers.
619
-
620
- #### What happened to a row
621
-
622
- Every change the hub makes to a list is one a stylesheet can see, so an
623
- application animates with CSS alone and the hub starts no animation.
624
-
625
- | Attribute | On | Means |
626
- | ------------------ | -------------------- | -------------------------------------------------- |
627
- | `lb-row-created` | A live row | Created when the hub last touched it |
628
- | `lb-row-changed` | A live row | Its values changed when the hub last touched it |
629
- | `lb-row-moved` | A live row | Its values changed, and it now stands elsewhere |
630
- | `lb-row-requested` | A live row | `created`, `changed` or `moved` by this page's request |
631
- | `lb-row-leaving` | A row on its way out | No longer live |
632
- | `lb-group-leaving` | A group on its way out | No longer live |
633
-
634
- The hub touches a row when an answer names it. `lb-row-created`,
635
- `lb-row-changed` and `lb-row-moved` are cleared when it next does, and one
636
- applied again is removed and added as two changes, so an animation keyed to
637
- it plays again. `lb-row-requested` is cleared at every landing of the
638
- row's query, and set only by a landing that answers a request from the page.
639
-
640
- A row or a group that leaves loses `lb-row-live` or `lb-group-live`, takes
641
- `lb-row-leaving` or `lb-group-leaving` and `inert`, and is removed once the
642
- animations running on it have finished, at once when there are none. A
643
- row leaves when all rows arrive without its key, or a patch drops it.
644
-
645
- A row is moved when its values changed and the nearest rows whose values did
646
- not change, before and after it, are not the ones it stood between. A row
647
- it passed is not stamped, and a change of the user's order, which changes no
648
- values, stamps none. A moved row stays live and moves.
649
-
650
- ```css
651
- [lb-row-created] { animation: arrive 200ms; }
652
- [lb-row-leaving] { animation: depart 200ms forwards; }
653
- ```
654
-
655
- A row never travels between the two places, since that takes measuring, and
656
- measuring takes script. Scrolling to a row is script too: a custom element
657
- reads `lb-row-requested` in [`lbRowsLanded`](#row-hooks).
658
-
659
- #### Displaying by condition
660
-
661
- `lb-show` names the column that decides whether an element is present.
662
- `null` and `false` are off, and every other value is on. No string is read,
663
- so `"false"` is on, and a query spells a condition as a boolean or a null.
664
-
665
- ```html
666
- <template>
667
- <tr>
668
- <td lb-column="name"></td>
669
- <td><button lb-request="lb-row-delete" lb-show="removable">Remove</button></td>
670
- </tr>
671
- </template>
672
- ```
673
-
674
- Write `!` before the column to reverse it: `lb-show="!chosen"` is present
675
- while `chosen` is null or false. One column then decides both of two
676
- elements, rather than a column and its opposite, which could disagree.
677
-
678
- It reads from the nearest ancestor row, as `lb-column` does, and on an
679
- element that carries `lb-query` the column belongs to the row around it. A
680
- row that does not carry the column leaves the element as it is.
681
- `lb-show` with no ancestor row is undefined.
682
-
683
- An element that is off is moved into a `<template lb-show="column">`
684
- standing where it stood, and moved back out when the column turns on. It is
685
- not rendered, focused, clicked, announced or gathered. It is moved and
686
- never rebuilt, so a custom element keeps its instance and a control keeps
687
- what was typed into it. Landing reaches into that template, so the element
688
- and everything inside it return current. A custom element hears nothing
689
- going in or coming out, and still receives `lb-column-value` while it is
690
- away.
691
-
692
- The builder ships every `lb-show` element already inside its template, so
693
- nothing conditional shows until its row lands. An absent element's template
694
- keeps its place among its siblings, and a position selector counts it.
695
-
696
- These are build errors: `lb-show` on a row template's root, `lb-show` with
697
- no `lb-query` around it, `lb-show` on a `<template>`, and `lb-show="!"` or
698
- `lb-show="!!column"`.
699
-
700
- Hiding is presentation, and the server still refuses what a request may not
701
- do.
702
-
703
- #### Master-detail
704
-
705
- A master is a `row` query and its detail is a `rows` query, as two names
706
- answered side by side. A column never holds rows.
707
-
708
- ```html
709
- <section lb-query="invoice">
710
- <h2 lb-column="number"></h2>
711
- <span lb-column="customer"></span>
712
- </section>
713
-
714
- <table>
715
- <tbody lb-query="invoiceLines">
716
- <template><tr><td lb-column="item"></td><td lb-column="amount"></td></tr></template>
717
- </tbody>
718
- </table>
719
- ```
720
-
721
- Many masters, each with its own detail, is one `rows` query of joined rows:
722
- each detail row carries its master's columns. A [group
723
- template](#groups) shows each master as a group, ordered by the master's
724
- key first.
725
-
726
- A query nested in another query's row template receives the same rows in
727
- every live row. That serves a picker offering the same choices on every
728
- row, and is not a way to show a different detail per row. A live row added
729
- later is filled from the nested query's last answer, so a request that adds
730
- one does not refresh the nested query to fill it.
731
-
732
- Which master a page shows is a query parm, which a query reads off `ctx`
733
- since it takes no argument from the browser — see [Query parms](#query-parms).
734
- When a write creates or removes the master, the server response moves the
735
- URL — see
736
- [When a server response moves the URL](#when-a-server-response-moves-the-url).
737
-
738
- ### Requests
739
-
740
- ---- UNEDITED ----
741
-
742
- | Attribute | Names | The hub |
743
- | ------------ | --------- | -------------------------------------------- |
744
- | `lb-request` | a request | Issues that request when the element commits |
745
-
746
- | Stamp | The hub stamps it |
747
- | -------------------- | ------------------------------------------ |
748
- | `lb-request-pending` | While the round trip is in flight |
749
- | `lb-request-error` | When the round trip fails, until the next |
750
-
751
- The request name is the attribute value of `lb-request`. It names either a
752
- request Loadbare provides or a declared request: a name the page declares
753
- under `handlers`, which runs application code.
754
-
755
- Every request has one shape, on the event and on the wire:
756
-
757
- ```
758
- { name: "lb-row-insert", query: "roster", values: { name: "Ann" } }
759
- { name: "lb-row-update", query: "roster", key: "42", values: { name: "Ann" } }
760
- { name: "lb-row-delete", query: "roster", key: "42" }
761
- { name: "mailRoster", query: "roster", key: "42" }
762
- { name: "lb-row-update", query: "roster", keys: ["42", "43"], values: { role: "lead" } }
763
- { name: "lb-row-insert", query: "invoice", values: { memo: "May" }, rows: { lines: [{ key: "1", values: { qty: "3" } }] } }
764
- ```
765
-
766
- The hub sends `query`, `key` and `values` as present, and `keys` in place of
767
- `key` for a request acting on the rows selected — see
768
- [Selecting rows](#selecting-rows).
769
-
770
- #### Committing
771
-
772
- An element commits on `submit` when it is a form, on `change` when it is a
773
- control, and on `click` otherwise.
774
-
775
- A submit button with a form owner never commits on `click`. On a form's
776
- `submit`, the hub issues the submitter's `lb-request` when it carries one,
777
- and the form's otherwise, the way `formaction` replaces `action`.
778
-
779
- A click on a descendant of an element carrying `lb-request` commits that
780
- element, as long as the element is inside `<lb-hub>` and no interactive
781
- content lies between them. Interactive content is HTML's: `<a href>`,
782
- `<button>`, `<input>` other than hidden, `<select>`, `<textarea>`, `<label>`,
783
- `<details>`, `<iframe>`, `<embed>`, `<audio controls>`, `<video controls>`
784
- and `<img usemap>`, plus any form-associated custom element. A click on
785
- one of them belongs to it: a click into an input inside a deletable row
786
- focuses the input and deletes nothing.
787
-
788
- The hub ignores a commit on an element carrying `lb-request-pending`.
789
-
790
- #### Gathering
791
-
792
- The hub gathers values when it issues a request. It gathers each control
793
- value under the column its `lb-column` names.
794
-
795
- | Attribute | The hub |
796
- | -------------- | ------------------------------------------- |
797
- | `lb-query` | Issues the request for it |
798
- | `lb-column` | Gathers the control value under that column |
799
- | `lb-key-value` | Takes `key` from it |
800
-
801
- - When the element carrying `lb-request` has `lb-column`, the hub gathers
802
- its value alone, and sends `values` with one member.
803
- - Otherwise, when the element is a `<form>`, the hub gathers every
804
- `lb-column` whose form owner is the element.
805
- - Otherwise, when the element is in a live row, the hub gathers every
806
- `lb-column` in that live row.
807
- - Otherwise the hub gathers every `lb-column` whose form owner is the
808
- element's form owner.
809
-
810
- The form owner includes a control that names the form with the HTML `form`
811
- attribute from anywhere in the document, which is how a table row's
812
- controls belong to a form that cannot wrap a `<tr>`:
813
-
814
- ```html
815
- <form id="add-member" lb-request="lb-row-insert"></form>
816
- <table lb-query="roster">
817
- <tbody>
818
- <template>
819
- <tr><td lb-column="name"></td><td lb-column="role"></td></tr>
820
- </template>
821
- </tbody>
822
- <tfoot>
823
- <tr>
824
- <td><input lb-column="name" form="add-member" /></td>
825
- <td>
826
- <input lb-column="role" form="add-member" />
827
- <button form="add-member">Add</button>
828
- </td>
829
- </tr>
830
- </tfoot>
831
- </table>
832
- ```
833
-
834
- The hub gathers from controls only. It skips a control whose nearest
835
- ancestor `lb-query` is a descendant of the live row or form, since that
836
- control belongs to a nested query, except that a form gathers the rows of a
837
- 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
838
- `lb-column` both, such as a picker, belongs to the row around it and is
839
- gathered.
840
-
841
- The hub issues the request for the nearest ancestor `lb-query` of the
842
- controls it gathered, and takes `key` from their nearest ancestor row when
843
- that row is of the same query. When the gathered controls have different
844
- nearest ancestor `lb-query`, the hub reports an error and sends nothing.
845
-
846
- When it gathers nothing, the hub issues the request for the nearest
847
- ancestor `lb-query` of the element carrying `lb-request`, and takes `key`
848
- from that element's nearest ancestor row when that row is of the same
849
- query.
850
-
851
- #### A form over a set of rows
852
-
853
- A form gathers the live rows of a query nested directly in its own query,
854
- besides its own controls. Each goes as its key and the values of the
855
- controls in it, under the nested query's name:
856
-
857
- ```
858
- { name: "lb-row-insert", query: "paychecks", values: { pay_date: "2026-10-02" },
859
- rows: { deduction_types: [{ key: "401k", values: { amount: "50" } },
860
- { key: "dental", values: { amount: "" } }] } }
861
- ```
862
-
863
- The form's own query is the one it carries, or else the nearest around it.
864
- A nested query is gathered when its nearest ancestor `lb-query` is the
865
- form's own, so one level only: a query nested deeper shows the same rows in
866
- every row around it, and its keys would repeat. Every live row of it goes,
867
- whatever its values, and only controls inside a live row of it.
868
-
869
- An insert, an update and a declared request carry `rows`; a delete does
870
- not. A form whose only controls are in nested rows sends for its own query,
871
- keyed by the row landed on it or the row it is pointed at, with empty
872
- `values`. A live row gathers nothing nested.
873
-
874
- The handler for the form's query receives `rows` beside `values`, so the
875
- master and its details are written together. The server checks that
876
- `rows` holds string keys and string values by query, and ignores a request
877
- whose `rows` does not.
878
-
879
- A nested query answers once for the page, as anywhere — see
880
- [Master-detail](#master-detail). The form enters a new master with its
881
- details, or edits the master the page shows by a query parm, whose details
882
- the nested query reads from the same parm. A pointed form fills its own
883
- controls from the row it points at, and its nested rows stay the nested
884
- query's answer.
885
-
886
- #### A row not yet entered
887
-
888
- A row whose key is the empty string is a new one, not yet entered. It
889
- arises two ways: a `row` query answers with an empty key, as one reading
890
- the master's key from an empty query parm does, or `--lb-new` points a form
891
- (see [A form pointed at a row](#a-form-pointed-at-a-row)). Either way the
892
- element carries `lb-key-value=""`.
893
-
894
- The hub sends no `key` for it, whether the key came from where the element
895
- sits or from a request a custom element dispatched. An `lb-row-insert` and
896
- a declared request are sent without one. An `lb-row-update` and an
897
- `lb-row-delete` are not sent, because nothing is on record to change, and
898
- nothing is reported: it is the row's state, not a mistake. So a control
899
- saving its own column on `change` saves on a row on record, and on a new
900
- row only holds what a submit will gather. No ancestor sees a request not
901
- sent, and it gets no `lb-request-done`.
902
-
903
- An update or a delete with no row around it at all has no key either, and
904
- is reported to the console, as any request missing what it needs is.
905
-
906
- A field the request already carries is its issuer's and is kept. A custom
907
- element that dispatches its own request with `values` is not gathered over.
908
-
909
- #### The request names
910
-
911
- | Request name | Needs | Runs under `crud` |
912
- | --------------- | ------------------------ | ----------------- |
913
- | `lb-row-insert` | `query`, `values` | `rowInsert` |
914
- | `lb-row-update` | `query`, `key`, `values` | `rowUpdate` |
915
- | `lb-row-delete` | `query`, `key` | `rowDelete` |
916
-
917
- `keys` stands in for `key`. The hub issues one of these only when it has
918
- what the table lists. An
919
- `lb-row-insert` or `lb-row-update` that gathers nothing is not issued. A
920
- request that is not issued is reported to the console.
921
-
922
- Loadbare provides the handlers for these names: each runs the page's `crud`
923
- entry for the request's query. A declared request runs the application's
924
- handler under `handlers`, and the hub issues it with whatever it found.
925
-
926
- A request name beginning with `lb-` that is none of these three is refused
927
- by the builder in markup, and by the hub when a script dispatches it.
928
-
929
- For a query the hub serves, the hub answers the request itself, with no
930
- round trip. The hub serves `lb-url`, and answers `lb-row-update` for it —
931
- see [Query parms](#query-parms).
932
-
933
- #### A form pointed at a row
934
-
935
- A set of rows the user edits or adds to has one form on the page, naming
936
- the same query, outside every row. A button points it with HTML's
937
- `commandfor`, naming the form's `id`, and `command`:
938
-
939
- | Command | On a button | The hub |
940
- | -------------- | --------------------------------- | ----------------------------------------- |
941
- | `--lb-edit` | In a live row of the form's query | Fills the form from that row, keyed by it |
942
- | `--lb-new` | Anywhere | Starts the form empty, for an insert |
943
- | `--lb-release` | Anywhere | Lets the form go without saving |
944
-
945
- HTML requires a custom command to begin with `--`, and fires it as a
946
- `command` event on the form, which the hub hears.
947
-
948
- ```html
949
- <tbody lb-query="paychecks">
950
- <template><tr>
951
- <td lb-column="pay_date"></td>
952
- <td><button type="button" commandfor="paycheck" command="--lb-edit">Edit</button></td>
953
- </tr></template>
954
- </tbody>
955
- <button type="button" commandfor="paycheck" command="--lb-new">New</button>
956
-
957
- <dialog>
958
- <form id="paycheck" lb-query="paychecks" lb-request="lb-row-update">
959
- <input lb-column="pay_date" type="date" />
960
- <button type="submit">Save</button>
961
- <button type="submit" lb-request="lb-row-insert">Save new</button>
962
- <button type="button" commandfor="paycheck" command="--lb-release">Cancel</button>
963
- </form>
964
- </dialog>
965
- ```
966
-
967
- Which row a form edits is interaction state, not a view, so it is not a
968
- query parm. The user edits only a row they can see, so Edit fills the form
969
- from the row as the hub holds it, every column the query answered, with no
970
- round trip. A row that must be fetched to be edited is another page, with
971
- that row as its master.
972
-
973
- Pointing a form first empties it: every column it names lands as null, and
974
- the form resets, so each control returns to its default. Edit then lands
975
- the row on the form as on any element a `row` lands on, stamps the form
976
- `lb-key-value` with the row's key, and stamps the row `lb-row-editing`.
977
- Gathering then takes the key from the form. New stamps an empty key, making
978
- the form [a row not yet entered](#a-row-not-yet-entered). Both focus the
979
- form's first control.
980
-
981
- | The form's `lb-key-value` | The form is |
982
- | ------------------------- | ------------------ |
983
- | A key | Editing that row |
984
- | Empty | Entering a new row |
985
- | Absent | Not in use |
986
-
987
- A stylesheet shows the form's buttons by it, such as Save while it has a
988
- key and Save new while the key is empty.
989
-
990
- A request from the form that succeeds lets it go, as `--lb-release` does:
991
- the form empties, and both stamps go. One that fails leaves the form
992
- pointed, as the user had it.
993
-
994
- When the form is inside a `<dialog>`, Edit and New call `showModal()` on it,
995
- and letting go closes it. The dialog closing by any of HTML's means,
996
- Escape, `command="close"` or `method="dialog"`, lets the form go. A form in
997
- no dialog opens nothing; it is always showing, or a stylesheet shows it
998
- while it carries `lb-key-value`.
999
-
1000
- The form names a `rows` query and holds no row template, so that query's
1001
- answers land nothing on it, and what the user typed survives a refresh.
1002
-
1003
- A command the hub cannot carry out is reported to the console: Edit from
1004
- outside a live row of the form's query, Edit for a key the hub holds no row
1005
- for, and any command aimed at an element that is not a form carrying
1006
- `lb-query`.
1007
-
1008
- #### Selecting rows
1009
-
1010
- A checkbox carrying `lb-select` in a row template selects the live row it
1011
- sits in, and the hub stamps the row `lb-row-selected` while it is ticked.
1012
- It is not a column: it carries no `lb-column` or `lb-request`, lands
1013
- nothing, and is not gathered.
1014
-
1015
- A request for the query that names no one row, neither from inside one of
1016
- its rows nor from a form pointed at one, acts on the rows selected. When
1017
- any `lb-select` checkbox of that query is showing, the hub adds the keys of
1018
- those ticked as `keys`:
1019
-
1020
- ```
1021
- { name: "lb-row-update", query: "transactions", keys: ["4", "9"], values: { acct: "12" } }
1022
- ```
1023
-
1024
- - `lb-row-update` and `lb-row-delete` are sent with `keys`, and with none
1025
- ticked are not sent, silently, as for [a row not yet
1026
- entered](#a-row-not-yet-entered).
1027
- - A declared request is sent with `keys`, empty when none is ticked, and the
1028
- handler decides.
1029
- - `lb-row-insert` adds a row, and the selection is not its.
1030
- - With no `lb-select` checkbox of the query showing, the request is an
1031
- ordinary one, and carries no `keys`.
1032
-
1033
- The server runs the page's `rowUpdate` or `rowDelete` once for each key,
1034
- with that key, as for each row alone, so a handler written for one row
1035
- serves several. It lays the patches the runs return together, and runs the
1036
- refresh set once. A run returning `url()` is reported and ignored, since
1037
- several rows cannot each choose one view. A declared request receives
1038
- `keys` as sent.
1039
-
1040
- After the request succeeds, the hub clears every `lb-select` checkbox of the
1041
- query and the stamps with them. A failed request leaves the selection.
1042
-
1043
- Only a checkbox that is showing counts: one parked by `lb-show` is in
1044
- template content, out of reach. Parking one also clears its tick, so a
1045
- selection never waits out of sight to return stale.
1046
-
1047
- Which rows are selected is interaction state, as which row a form edits is.
1048
- A selecting mode the page keeps in the URL reaches the rows as a column the
1049
- query computes from the parm, which `lb-show` reads, since `lb-show` reads
1050
- from the row it sits in.
1051
-
1052
- #### The request event
1053
-
1054
- The hub dispatches every request as the bubbling `lb-request` event before
1055
- sending it, with the request as its `detail`. The event is dispatched from
1056
- the element that committed.
1057
-
1058
- A custom element uses the same event to issue a request of its own, with at
1059
- least `name` in its `detail`. A native element and a custom element
1060
- therefore produce identical events.
1061
-
1062
- The hub completes the request on the event's way down, so every ancestor
1063
- sees it whole on the way up. A request that cannot be issued is stopped
1064
- there. An ancestor may stop it too: the event is not cancelable, so an
1065
- interceptor calls `stopPropagation`, not `preventDefault`. This is what
1066
- makes a confirmation wrapper possible without the wrapped element knowing
1067
- about it.
1068
-
1069
- Once what the request brought back has landed, or its round trip has failed,
1070
- the hub calls `lbRequestDone(detail, origin)` on the committing element and
1071
- every custom element around it, innermost first (see
1072
- [Lifecycle](#lifecycle)), then dispatches the bubbling `lb-request-done`
1073
- event from the same element. Its `detail`, `HubRequestDone` in `@loadbare/app/types`, holds
1074
- the request as the hub sent it and `items`, every response item that landed
1075
- because of it, in order. An answer that moved the URL contributes its
1076
- `lb-url` item and the page load. `error` is set instead when the round trip
1077
- failed. By then `lb-request-pending` is gone, and a listener sees the page
1078
- as the answer left it. A request the hub or an ancestor stopped was never
1079
- sent, and gets no `lb-request-done`. An element the answer removed from the
1080
- document, such as the row a delete took away, dispatches the event where it
1081
- now is, and no ancestor it had on the page hears it.
1082
-
1083
- #### Request state
1084
-
1085
- The hub stamps `lb-request-pending` on the element that issued a request
1086
- while its round trip is in flight, together with `aria-busy="true"`, and
1087
- removes both when it settles. It stamps `lb-request-error` when the round
1088
- trip fails, and clears it when that element issues its next request.
1089
-
1090
- A custom element observes them by naming them in `observedAttributes`. On
1091
- plain HTML a stylesheet is the only consumer: dim a pending button, mark a
1092
- failed one.
1093
-
1094
- The hub ignores a commit on an element carrying `lb-request-pending`, so a
1095
- pending element is disabled in fact and a stylesheet only has to show it.
1096
-
1097
- #### Refusals
1098
-
1099
- A handler refuses a request by throwing `refusal()`, exported by
1100
- `@loadbare/app/server`, with words written for the user: a message for the
1101
- request as a whole, a message per column, or both.
1102
-
1103
- ```ts
1104
- throw refusal("That series already has paychecks entered.");
1105
- throw refusal({ amount: "Must be more than zero" });
1106
- throw refusal("Two fields need attention.", { amount: "…", pay_date: "…" });
1107
- ```
1108
-
1109
- It is thrown rather than returned, so it may come from anything the handler
1110
- calls, and rolls back whatever the application has open as any throw does.
1111
- `hubRoutes` answers it with status 422 and `{ refusal: { message?, columns? } }`.
1112
- Anything else thrown is a failure, answered 500 with no body, so no stack
1113
- or database message reaches the browser.
1114
-
1115
- The hub shows a refusal where it belongs:
1116
-
1117
- - It stamps the element that committed `lb-request-refused`, whose value is
1118
- the whole-request message, empty when there is none. A stylesheet shows
1119
- it with `attr()`.
1120
- - For each column, it calls `setCustomValidity(message)` on the control the
1121
- request gathered that column from, and `reportValidity()` on the first,
1122
- so the browser shows the message there and moves focus to it. A column
1123
- the request gathered no such control for is reported to the console.
1124
- - `lb-request-done` carries the refusal as `refusal`, beside `error`.
1125
-
1126
- A refusal is not a failure: `lb-request-error` is not stamped and nothing is
1127
- reported. It stands until the user edits any control the request gathered,
1128
- a form holding them resets, or the element that committed sends again;
1129
- then every message it put up is withdrawn at once. While a control carries
1130
- one, the browser holds its form's submit, as for any invalid control.
1131
-
1132
- A form-associated custom element takes a column's message when it defines
1133
- `setCustomValidity` and `reportValidity`; one wrapping a native control
1134
- passes both to it. A request for several rows takes the whole-request
1135
- message only.
1136
-
1137
- #### The round trip
1138
-
1139
- A request the hub sends has ten seconds to come back. Past that the hub
1140
- aborts it and treats it as a failure, since `fetch` imposes no deadline of
1141
- its own. An application cannot change the deadline.
1142
-
1143
- A round trip fails on a server error, on a network failure, or on that
1144
- deadline, and all three stamp `lb-request-error` on the element that issued
1145
- the request — see [Request state](#request-state). A refusal is not a
1146
- failure — see [Refusals](#refusals).
1147
-
1148
- After a successful `lb-row-insert` gathered from a form, the hub resets the
1149
- form, after it lands the response. A failed insert resets nothing, so the
1150
- entry can be corrected. An `lb-row-update` resets nothing: the row it sent
1151
- lands back on its controls.
1152
-
1153
- A reset of any form, whether the hub's, a reset button's or a script's,
1154
- returns its controls to their defaults, and the hub removes
1155
- `lb-column-value` from each of them. A cancelled reset keeps both.
1156
-
1157
- A page load that fails lands nothing and is reported to the console. A load
1158
- started by a request for `lb-url` stamps the element that issued it, as any
1159
- request stamps its element.
1160
-
1161
- A response answers for the URL its request was sent from, path and query
1162
- string both. One that returns after the URL has moved lands nothing, since
1163
- it describes what the user is no longer looking at: a request's answer,
1164
- including any `url()` it carries, and a page load overtaken by a newer one.
1165
- A write whose answer is dropped this way has still happened. Its
1166
- `lb-request-done` carries no items and no error, and an insert still resets
1167
- its form.
1168
-
1169
- ### The URL
1170
-
1171
- ---- UNEDITED ----
1172
-
1173
- The hub serves the query `lb-url`, of kind `row`, keyed by `lb-path`. It
1174
- lands wherever an element names it, like any server query.
1175
-
1176
- | Column | The hub sets it to |
1177
- | ----------------- | ---------------------------------------------- |
1178
- | `lb-path` | The path. The row's key |
1179
- | `lb-page-label` | The page's title, or its stub when it has none |
1180
- | `lb-page-unknown` | Whether no page's stub matches the path |
1181
- | any other name | That query parm |
1182
-
1183
- | Attribute | The hub |
1184
- | ---------------- | -------------------------------------------------------- |
1185
- | `lb-url-link` | On a plain primary click, sets `lb-path` from the `href` |
1186
- | `lb-url-push` | Pushes a history entry for this element's update |
1187
- | `lb-url-unknown` | Calls `showModal()` when `lb-page-unknown` is `true` |
1188
-
1189
- - `lb-url-link` goes on an `<a>`.
1190
- - `lb-url-push` goes on an element with `lb-request`.
1191
- - `lb-url-unknown` goes on a `<dialog>` inside `<lb-hub>`.
1192
-
1193
- The hub shows in `<main>` the page whose stub is `lb-path`. `/` is
1194
- `index`. When `lb-path` takes a new value, the hub loads the page: the
1195
- server runs its `onPageEnter`, then its queries. When a query parm takes a
1196
- new value, the hub reloads the page's queries, and keeps the page's DOM, so
1197
- live rows that come back keep their place.
1198
-
1199
- Once an entered page's queries have landed, the hub focuses the first
1200
- element in `<main>` the user can operate: not disabled, not in a closed
1201
- `<dialog>`, not `inert`, not `hidden`, not in an absent `lb-show` branch,
1202
- and one that takes the focus. Entering is a cold load, a new `lb-path`,
1203
- and Back or Forward to another page. A change of query parm enters no
1204
- page, and focus stays where it is, as it does when it is already in
1205
- `<main>`.
1206
-
1207
- The page's title is the text of the page file's `<title>`, which the builder
1208
- stamps on the page as `lb-page-title`. The hub sets the document title to
1209
- `lb-page-label` when it is not null.
1210
-
1211
- ```html
1212
- <header lb-query="lb-url">
1213
- <h1>Membership Roster</h1>
1214
- <h2 lb-column="lb-page-label"></h2>
1215
- </header>
1216
- ```
1217
-
1218
- #### What a URL names
1219
-
1220
- The path names a page, a place in the application. It never names a
1221
- resource, and Loadbare/app does not reproduce REST: `/accounts/42` is not a
1222
- way to reach account 42.
1223
-
1224
- The query string describes what that page has on screen:
1225
- `/accounts?acct=23&from=2026-09-09`. It never commands, and the server
1226
- remembers nothing, so a URL is a reproducible view. Reload it, bookmark it,
1227
- or mail it to someone, and what they see is what the sender saw.
1228
-
1229
- A page declares what it shows, its queries, and what it allows, its
1230
- requests. Loading a page runs its queries. A request names a query and,
1231
- where it has one, a key, taken from where the element sits, which is how the
1232
- database already names a row. Nothing is fetched by URL, so an application
1233
- designs no endpoints.
1234
-
1235
- That last is where query parms move Loadbare's position. A query still takes
1236
- no argument from the browser, and a parm arrives the way the session cookie
1237
- does, as part of the request the context is built from. But a user who
1238
- types `?acct=99999` influences what a query returns. A query parm is user
1239
- input, validated like any other where the application reads it, and a
1240
- per-session database role means an id outside the caller's reach finds
1241
- nothing.
1242
-
1243
- Loadbare is for applications, not sites. Every route is answered with the
1244
- same document, and a path that names no page is found out in the browser —
1245
- see [An unknown page](#an-unknown-page).
1246
-
1247
- #### Links
1248
-
1249
- An `<a>` carrying `lb-url-link` moves within the application. On a plain
1250
- primary click — the first button, no modifier key, not already handled — the
1251
- hub pushes a history entry for the `href`'s path and query string, and loads
1252
- the page there. Any other click, and any anchor without `lb-url-link`,
1253
- behaves like any other link, so leaving the application is the default and
1254
- staying in it is the opt-in.
1255
-
1256
- ```html
1257
- <nav>
1258
- <a href="/" lb-url-link>Home</a>
1259
- <a href="/members" lb-url-link>Members</a>
1260
- <a href="https://example.com/docs">Docs</a>
1261
- </nav>
1262
- ```
1263
-
1264
- The attribute takes no value.
1265
-
1266
- Path space is flat. A path such as `/members` links to the `members.*` files
1267
- on the server.
1268
-
1269
- The query string is kept. `/transactions?date_begin=2026-09-01` opens the
1270
- transactions page narrowed to those dates.
1271
-
1272
- The browser's Back and Forward load the page at the URL they arrive at, the
1273
- same way.
1274
-
1275
- #### Query parms
1276
-
1277
- A control that narrows what a page shows updates `lb-url` rather than
1278
- sending a request. Put it inside an element naming `lb-url`, with
1279
- `lb-column` naming the parm and `lb-request="lb-row-update"`:
1280
-
1281
- ```html
1282
- <div lb-query="lb-url">
1283
- <select lb-column="team" lb-request="lb-row-update">
1284
- <option value="">Every team</option>
1285
- <option value="Engines">Engines</option>
1286
- </select>
1287
- </div>
1288
- ```
1289
-
1290
- The hub answers the request itself. It sets the parms the request names in
1291
- the URL and leaves every other parm as it is, since one may have arrived by
1292
- link with no control on screen to say it again. An empty value takes the
1293
- parm out, so a URL is as long as the user has narrowed the page. A request
1294
- that leaves the URL as it was does nothing. `lb-page-label` and
1295
- `lb-page-unknown` are the hub's, and a request never sets them.
1296
-
1297
- The update replaces the current history entry: changing what a page shows is
1298
- not going anywhere, so Back leaves the page rather than walking back through
1299
- every choice. `lb-url-push` on the element carrying `lb-request` makes the
1300
- update push an entry instead.
1301
-
1302
- When `lb-path` takes a new value, the hub pushes a history entry, and the
1303
- new URL carries only the query parms the request names.
1304
-
1305
- Every load lands `lb-url`, and a parm the URL does not carry lands empty on
1306
- every element naming it as a column, so a control shows what the address
1307
- bar says after Back as well as after a reload
1308
- ([miswart](./miswarts.md#lb-url-lands-every-parm-it-names)): the hub reads the
1309
- column names under `lb-query="lb-url"` and names each one in the row it
1310
- lands.
1311
-
1312
- The hub sends the query parms with every round trip, a page load and a
1313
- request alike. The server passes them to `contextFor` — see
1314
- [The Express server](#the-express-server). Loadbare assigns no parm a
1315
- meaning: a column of `lb-url` whose name begins with `lb-` is never a query
1316
- parm. An order or a filter the hub applies to rows already on the page is
1317
- a [choice](#choices), not a parm.
1318
-
1319
- #### An unknown page
1320
-
1321
- When no page's stub matches the path, `lb-page-unknown` is `true` and
1322
- `lb-page-label` is null. The hub calls `showModal()` on the
1323
- `<dialog lb-url-unknown>`, where the chrome has one, and reports the path to
1324
- the console. A chrome with none shows nothing.
1325
-
1326
- `lb-url` lands before the page is looked up, so the dialog shows it by
1327
- naming it like any other element:
1328
-
1329
- ```html
1330
- <dialog lb-url-unknown lb-query="lb-url">
1331
- The URL <span lb-column="lb-path"></span> is not in this app.
1332
- </dialog>
1333
- ```
1334
-
1335
- A path that names no page is detected in the browser, after a successful
1336
- 200: every route gets the same document, so there is no server-delivered
1337
- 404.
1338
-
1339
- #### When a server response moves the URL
1340
-
1341
- Some query parms can only be known once a write has run. After an insert,
1342
- the key of the new row exists only on the server. After a delete, only the
1343
- server knows that the row the page was showing is gone.
1344
-
1345
- The usual web answer is Post/Redirect/Get: the server answers the write with
1346
- a redirect to a URL naming the result, and the browser makes a second request
1347
- to load it. Loadbare/app does the same work in one round trip.
1348
-
1349
- A handler returns `url()`, a new row for `lb-url`. The server loads the page
1350
- at the resulting URL, as a cold load of it would: `onPageEnter`, then every
1351
- query. The response carries the `lb-url` item followed by that load. The
1352
- hub writes the URL and then lands the load.
1353
-
1354
- ```ts
1355
- rowInsert: {
1356
- run: async (ctx, { values }) => {
1357
- const id = await ctx.db.addAccount(values);
1358
- return url({ acct: String(id) });
1359
- },
1360
- refresh: [],
1361
- },
1362
- ```
1363
-
1364
- - A column other than `lb-path` is a query parm. Parms the row does not
1365
- name are kept, and an empty value removes its parm.
1366
- - A row carrying `lb-path` for another page enters that page with only the
1367
- parms the row names, and pushes a history entry. Otherwise the update
1368
- replaces the history entry, or pushes one when the element that issued the
1369
- request carries `lb-url-push`.
1370
- - The row names at least one column, and every value is a string. Otherwise
1371
- the server warns, ignores the row, and runs the refresh set as usual.
1372
- - The refresh set does not run, and anything else the handler returned is
1373
- dropped. Both were answers for the URL the page is leaving.
1374
- - If loading the page fails, the write has still happened. The response
1375
- carries the `lb-url` item alone, and the hub loads the page itself. A
1376
- failure there is a page load's, and is not stamped on the element that
1377
- issued the request.
1378
- - A user who has moved by the time the response arrives, to another page or
1379
- to other query parms, keeps the URL they are on.
1380
-
1381
- `onPageEnter` may return `url()` the same way, to move the page before
1382
- anything shows, as an application restoring the parms the user last had
1383
- does. The page loads at the new URL in the same round trip, and
1384
- that load does not follow `onPageEnter` again. A `url()` leading to the
1385
- page and parms already in hand is ignored, so the page loads once. When to
1386
- restore is the application's rule, such as only when the URL carries no
1387
- parms of its own.
1388
-
1389
- ```ts
1390
- // The application put the parms on ctx in contextFor.
1391
- onPageEnter: async (ctx) => {
1392
- if (ctx.parms.size > 0) return;
1393
- const saved = await ctx.db.savedView("accounts");
1394
- if (saved) return url(saved);
1395
- },
1396
- ```
1397
-
1398
- Saving what the user has on screen is a declared request with
1399
- `refresh: []`, whose handler each page passes to a routine the
1400
- application's pages share. A custom element naming `lb-url` sees every
1401
- change of the URL and issues it.
1402
-
1403
- The page is loaded with a second context, built by `contextFor` from the new
1404
- parms — see [The Express server](#the-express-server).
1405
-
1406
- ### Choices
1407
-
1408
- The hub serves the query `lb-choices`, of kind `row`, keyed by `lb-path`,
1409
- with a column for each choice the user made for how rows show: an
1410
- [order](#order) or a [filter](#filters). The application names the
1411
- columns. It lands wherever an element names it, like any server query.
1412
-
1413
- A control inside `lb-query="lb-choices"` that sends `lb-row-update` makes a
1414
- choice. A request with `lb-row-update` for `lb-choices` or `lb-url` from
1415
- anywhere on the page is keyed by the path, so a custom element may
1416
- dispatch one without sitting inside the query.
1417
-
1418
- A choice joins the ones already made, and needs no round trip to show:
1419
- every element whose `lb-order` or `lb-filter` reads a column that changed
1420
- lands its query's last answer again, so it shows what a round trip carrying
1421
- the choice would. Then the hub sends `lb-row-update` for `lb-choices`,
1422
- keyed by the path, with every choice made on the page as `values`, and no
1423
- element pending. The server runs the page's `onChoices` with them, when it
1424
- has one, and otherwise answers nothing. Whatever `onChoices` returns lands,
1425
- and by convention it returns nothing.
1426
-
1427
- Choices hold until the page changes, and a change of query parm keeps them.
1428
- On entering a page, every column named under `lb-query="lb-choices"` lands
1429
- empty. `onPageEnter` restores them by returning `choices()`, alone or
1430
- spread beside `url()`:
1431
-
1432
- ```ts
1433
- onPageEnter: async (ctx) => {
1434
- const saved = await ctx.db.savedChoices("accounts");
1435
- if (saved) return choices(saved);
1436
- },
1437
- onChoices: (ctx, chosen) => ctx.db.saveChoices("accounts", chosen),
1438
- ```
1439
-
1440
- The page load then answers the `lb-choices` item first, then the queries.
1441
- When `url()` moves the page, the choices arrive with the load at the new
1442
- URL. A
1443
- `choices()` naming no column, or holding a value that is not a string, is
1444
- reported on the server console and ignored, as for `url()`.
1445
-
1446
- ### The markup checks
1447
-
1448
- The builder checks the chrome and every page, after expansion, and refuses
1449
- to build on any of these:
1450
-
1451
- - An `lb-` attribute that is not a developer attribute: `lb-query`,
1452
- `lb-column`, `lb-show`, `lb-group`, `lb-count`, `lb-sum`, `lb-avg`,
1453
- `lb-min`, `lb-max`, `lb-order`, `lb-filter`, `lb-request`, `lb-select`,
1454
- `lb-url-link`, `lb-url-push`, `lb-url-unknown`. Every other `lb-` attribute is a stamp.
1455
- `lb-exp-slot` and `lb-exp-template` are consumed by expansion and never
1456
- reach the check.
1457
- - `lb-request` naming a value that begins with `lb-` and is not one of the
1458
- three request names.
1459
- - `lb-query` naming a query the page does not declare. `lb-url` and
1460
- `lb-choices` are the hub's, and every page and the chrome may name them.
1461
- The chrome belongs to no page, and names no other query.
1462
- - `lb-request` naming a request the page does not declare under `handlers`.
1463
- The chrome sends none of them.
1464
- - One of the three request names for a query whose `crud` entry does not
1465
- declare it, and any but `lb-row-update` for `lb-url` or `lb-choices`. The
1466
- query is a form's own when the form carries `lb-query`, and otherwise the
1467
- nearest `lb-query` around the element.
1468
- - `lb-url-link` on anything but an `<a>`.
1469
- - `lb-url-push` on an element with no `lb-request`.
1470
- - `lb-url-unknown` on anything but a `<dialog>`, and in the chrome, outside
1471
- `<lb-hub>`.
1472
- - `lb-show` on a `<template>`, on a row template's root, or with no
1473
- `lb-query` around it.
1474
- - `lb-show="!"` or `lb-show="!!column"`, which reverse no column.
1475
- - `lb-order` or `lb-filter` on an element not carrying `lb-query`, or naming
1476
- no column.
1477
- - `lb-select` on anything but an `<input type="checkbox">`, outside a row
1478
- template, or beside `lb-column` or `lb-request`.
1479
- - A `command` beginning `--lb-` that is not one of the three
1480
- [commands](#a-form-pointed-at-a-row), and one of them with no
1481
- `commandfor`.
1482
- - `lb-group` on anything but a `<template>`, or on one holding no
1483
- `<template>` for its contents.
1484
- - An aggregate on a `<template>`, and any but `lb-count` naming no column.
1485
- - A chrome without exactly one `<lb-hub>` and exactly one `<main>`, a
1486
- `<main>` outside `<lb-hub>`, or a `<main>` with anything in it but
1487
- whitespace.
1488
- - A page carrying an `<lb-hub>` or a `<main>`.
1489
-
1490
- The builder also reads the script of every custom element it ships, other
1491
- than `<lb-hub>`, and refuses one that declares a method, accessor or field
1492
- whose name begins with `lb` and a capital and is not in
1493
- [Reserved methods](#reserved-methods).
1494
-
1495
- `lb-column` with no ancestor row is allowed: the hub gathers from it.
1496
-
1497
- To know what each page declares, the builder loads the page's
1498
- `.queries.ts` and `.requests.ts` modules, so their import-time code runs
1499
- during the build, as it does when the server starts.
1500
-
1501
- Script never assigns an `lb-` attribute, so the markup states every one
1502
- that exists, and checking the markup is sound.
1503
-
1504
- ### Uncommon layouts
1505
-
1506
- The hub supports these layouts and adds no check to refuse them. They are
1507
- uncommon, and Loadbare/app does not shape itself around them.
1508
-
1509
- - **A radio button in a row template.** The browser groups radio buttons
1510
- by `name`, and a row template repeats one `name` in every live row. The
1511
- hub stamps each radio button's `name` with the `name` written, or its
1512
- column when none is, followed by `-` and the live row's key, so each live
1513
- row holds a group of its own
1514
- ([miswart](./miswarts.md#radio-button-names-in-a-row-template)).
1515
- - **Several rows not yet entered, each holding radio buttons.** A
1516
- [row not yet entered](#a-row-not-yet-entered) has an empty key, so the
1517
- radio buttons of every such row share one `name` and form one group.
1518
- - **Radio buttons populated from a query.** A radio button in a query's row
1519
- template answers to that query's row, as any element there does, so which
1520
- row and column it lands from and gathers to follows from where the layout
1521
- puts it. The result is the same as in any framework that populates radio
1522
- buttons from a query. A choice among a query's rows is better made with a
1523
- form-associated control, such as `lb-options` in `@loadbare/widgets`.
1524
-
1525
- ## Application files
1526
-
1527
- ### Chrome
1528
-
1529
- The Loadbare/app builder always requires exactly one file
1530
- named `chrome.html`, located anywhere in builder `--src`.
1531
-
1532
- The chrome is the application's single HTML document. This is
1533
- where the standard landmarks go: header, nav, footer, main.
1534
-
1535
- The file is an HTML document, which must contain:
1536
- - `<lb-hub>` inside `<body>`. The hub can only act on its children,
1537
- that is why it is usually nested right below `<body>`.
1538
- - One empty `<main>` inside `<lb-hub>`. The builder refuses a chrome
1539
- without exactly one of each, and a page carrying either.
1540
- - The script tag for `/client.js`, the javascript bundle.
1541
-
1542
- It may also contain:
1543
- - `/app.css`, the stylesheet bundle
1544
- - A `<dialog lb-url-unknown>`, which the hub opens when the path names no
1545
- page. It goes inside `<lb-hub>`, like everything the hub acts on; the
1546
- builder rejects one placed elsewhere.
1547
- - The `hidden` attribute on `<body>`, which the hub removes once the first page has landed.
1548
-
1549
- ```html
1550
- <!-- src/chrome.html -->
1551
- <!doctype html>
1552
- <html lang="en">
1553
- <head>
1554
- <meta charset="utf-8" />
1555
- <title>Membership Roster</title>
1556
- <script src="/client.js" defer></script>
1557
- <link rel="stylesheet" href="/app.css" />
1558
- </head>
1559
- <body hidden>
1560
- <lb-hub>
1561
- <header lb-query="lb-url">
1562
- <h1>Membership Roster</h1>
1563
- <h2 lb-column="lb-page-label"></h2>
1564
- </header>
1565
- <nav>
1566
- <a href="/" lb-url-link>Home</a>
1567
- <a href="/members" lb-url-link>Members</a>
1568
- <a href="https://example.org/">Our website</a>
1569
- </nav>
1570
- <main></main>
1571
- <dialog lb-url-unknown lb-query="lb-url">
1572
- The URL <span lb-column="lb-path"></span> is not in this app.
1573
- </dialog>
1574
- </lb-hub>
1575
- </body>
1576
- </html>
1577
- ```
1578
-
1579
- The chrome's `<title>` shows until the first page lands, and each page's
1580
- title replaces it — see [The URL](#the-url).
1581
-
1582
- ### Pages
1583
-
1584
- The page namespace is flat. Loadbare/app does not care where in the `--src`
1585
- the page files are located, but they must be unique across the application.
1586
- A page `/deep/path/to/mypage.page.html` is routed to `/mypage`.
1587
-
1588
- Pages are grouped as
1589
- - `<stub>.page.html` is recognized as navigable and capable of
1590
- having associated queries and requests
1591
- - `<stub>.queries.ts` are the queries for a page
1592
- - `<stub>.requests.ts` respond to the requests from the browser
1593
-
1594
- The stub of a page must match the path used in links,
1595
- so that `<a href="/members" lb-url-link>Members</a>` has matching
1596
- files `members.page.html` et al.
1597
-
1598
- #### Page HTML
1599
-
1600
- The markup in `<stub>.page.html` is the same HTML as everywhere else
1601
- in a Loadbare/app application, see [HTML](#html).
1602
-
1603
- A page file may carry a `<title>`. The builder removes it and stamps its
1604
- text content as `lb-page-title` on the page, and the hub shows it as
1605
- `lb-page-label`.
1606
-
1607
- ```html
1608
- <!-- src/pages/members.page.html -->
1609
- <title>Members</title>
1610
- <h1>Members</h1>
1611
- ```
1612
-
1613
- The builder wraps each expanded page in
1614
- `<template lb-page="<stub>" lb-page-title="...">` and puts it in the built
1615
- document. The hub reads `lb-page` to find the page a path names.
1616
-
1617
- | Stamp | The builder stamps it with |
1618
- | --------------- | --------------------------------------------- |
1619
- | `lb-page` | The page's stub, which the hub reads |
1620
- | `lb-page-title` | The text content of the page file's `<title>` |
1621
-
1622
- ## The server
1623
-
1624
- A page's server half is two files, `<stub>.queries.ts` and
1625
- `<stub>.requests.ts`. Each pairs with `<stub>.page.html` by sharing its
1626
- stub, as stated in [Pages](#pages). Either may be absent, and one with no
1627
- matching page is a build error.
1628
-
1629
- ### The Express server
1630
-
1631
- Loadbare/app ships no server. The application writes an ordinary Express
1632
- app. Express is a peer dependency, installed by the application.
1633
-
1634
- The builder's artifacts oblige that server to handle four things: the
1635
- client script, the stylesheet, the hub's data channel, and the one HTML
1636
- document.
1637
-
1638
- | Handle | With |
1639
- | ---------------------------- | ------------------- |
1640
- | `/client.js` | `dist/client.js` |
1641
- | `/app.css` | `dist/app.css` |
1642
- | `hubRoutes(hub, contextFor)` | The hub's own route |
1643
- | Every other GET | `dist/app.html` |
1644
-
1645
- The builder writes `pages.ts` into `--out` whenever any page has a
1646
- `.queries.ts` or a `.requests.ts` file. It imports each of them, calls
1647
- `createHub()` once over the lot, and exports the result as `hub`. The
1648
- server imports that rather than maintaining the registry by hand.
1649
- `createHub` is the only implementation of the `Hub` interface, which may
1650
- gain members in any release; an application never implements it.
1651
-
1652
- ```ts
1653
- // server.ts
1654
- import path from "node:path";
1655
- import { readFileSync } from "node:fs";
1656
- import express, { type Request } from "express";
1657
- import { hubRoutes } from "@loadbare/app/express";
1658
- import type { HubContext } from "@loadbare/app/server";
1659
- import { hub } from "./dist/pages";
1660
- import { openDb } from "./src/database";
1661
-
1662
- const DIST = path.resolve("dist");
1663
- const app = express();
1664
-
1665
- // All data requests are handled by the hub
1666
- // This example shows an application-specific openDB()
1667
- function contextFor(_req: Request): HubContext {
1668
- return { db: openDb() };
1669
- }
1670
- app.use(hubRoutes(hub, contextFor));
1671
-
1672
- // Static assets, then the catch-all for app.html
1673
- app.get("/client.js", (_req, res) => res.sendFile(path.join(DIST, "client.js")));
1674
- app.get("/app.css", (_req, res) => res.sendFile(path.join(DIST, "app.css")));
1675
- app.get(/.*/, (_req, res) =>
1676
- res.type("html").send(readFileSync(path.join(DIST, "app.html"), "utf-8")),
1677
- );
1678
-
1679
- app.listen(8787);
1680
- ```
1681
-
1682
- Explaining Express is beyond the scope of this technical reference. The
1683
- only real requirement is that the catch-all for app.html is at the end,
1684
- so it does not catch any other files.
1685
-
1686
- `hubRoutes` mounts `express.json()` on its own routes, and takes that
1687
- function's options under `json` in an optional third argument. The default
1688
- limit of 100 KB refuses an oversized body with a 413 before any handler
1689
- runs, and the server log names the option. A page that sends a file's text
1690
- as a value needs more:
1691
-
1692
- ```ts
1693
- app.use(hubRoutes(hub, contextFor, { json: { limit: "5mb" } }));
1694
- ```
1695
-
1696
- `hubRoutes` answers `POST /lb/<stub>`, with the query string the browser is
1697
- showing after it, verbatim. It hands `contextFor` that query string's parms
1698
- as a second argument, and `contextFor` puts on the context whatever a query
1699
- reads from them. They are user input:
1700
-
1701
- ```ts
1702
- function contextFor(req: Request, parms: URLSearchParams): HubContext {
1703
- return { db: openDb(), acct: parms.get("acct") ?? "" };
1704
- }
1705
- ```
1706
-
1707
- Read the parms from that argument, not from `req.query`. After a request
1708
- whose handler returned `url()`, `contextFor` is called a second time for
1709
- the same request, with the new parms, to load the page at them. So it must
1710
- be safe to call twice, and a write must be visible to the second context by
1711
- the time its `run` returns. A handle opened per request without a
1712
- transaction around it is both.
1713
-
1714
- Give the server the origin root. The hub reaches its own endpoints by
1715
- absolute path, so an application cannot be hosted under a subpath such as
1716
- `example.com/myapp/`, and anything proxying in front of the server passes
1717
- the whole path space through unchanged.
1718
-
1719
- ### Page Queries
1720
-
1721
- `<stub>.queries.ts` exports one object named `queries`, typed `Queries`.
1722
- Each key is a query name, which the markup names with `lb-query`.
1723
-
1724
- A query's kind and key belong to its name. Declare each query with
1725
- `row(key, run)` or `rows(key, run, options)`, naming the key column first.
1726
- A `rows` query's `options` declare its [order](#order). A query
1727
- cannot be built without one of these functions. Every query has a key; an
1728
- aggregate row answers with a constant one.
1729
-
1730
- ```ts
1731
- // members.queries.ts
1732
- import { row, rows, type Queries } from "@loadbare/app/server";
1733
-
1734
- export const queries: Queries = {
1735
- roster: rows("id", (ctx) => ctx.db.members()),
1736
- summary: row("id", async (ctx) => ({
1737
- id: "all",
1738
- count: String(await ctx.db.memberCount()),
1739
- })),
1740
- };
1741
- ```
1742
-
1743
- A `row` query returns one object, or `null` when it finds no row, and a
1744
- `rows` query returns an array of objects, which the page shows in that order when the query declares none. An answer that disagrees with
1745
- its declared kind is left out of the response, with a warning naming the
1746
- query.
1747
-
1748
- Query names cannot begin with `lb-`, that namespace is reserved for
1749
- Loadbare/app queries the hub serves in the browser, such as
1750
- [`lb-url`](#the-url). `createHub` refuses at startup a page that declares
1751
- such a name as a query, a handler or a `crud` entry, and a query whose order
1752
- has a term naming no column.
1753
-
1754
- Every query and every handler receives `ctx`, the application's own request
1755
- context. The application builds it once per request and hands it to the hub,
1756
- see [The Express server](#the-express-server). Loadbare/app declares it empty
1757
- and never reads it, so a query finds exactly what the application put there,
1758
- which is usually a database handle opened for the authenticated caller.
1759
-
1760
- Queries should not return deltas, deltas are handled in the `.requests.ts` file
1761
- as explained in the next section.
1762
-
1763
- ### Page Requests
1764
-
1765
- `<stub>.requests.ts` exports one object named `requests`, typed `Requests`.
1766
- It has four optional keys.
1767
-
1768
- | Key | Keyed by | Answers |
1769
- | ------------- | --------------------- | -------------------------------------------------- |
1770
- | `handlers` | The request name | The page's declared requests |
1771
- | `crud` | A query name | The three requests Loadbare provides |
1772
- | `onPageEnter` | Nothing | Runs once on entering the page, before its queries, and may move the URL or restore [choices](#choices) |
1773
- | `onChoices` | Nothing | Runs with every choice made on the page, each time the user makes one |
1774
-
1775
- The server resolves a declared request against `handlers`, and a request
1776
- Loadbare provides against `crud` under the request's query. The builder
1777
- refuses markup naming a request the page does not declare; see
1778
- [The markup checks](#the-markup-checks). A name with no entry there can
1779
- still arrive, from a widget's script or from outside the page, and it
1780
- logs a server console warning naming the page and the missing entry, and
1781
- answers 200 with no response items, so nothing lands and
1782
- `lb-request-pending` clears from the element that issued the request. A
1783
- request carrying no name answers 400, and a `run` that throws answers 500.
1784
-
1785
- Every handler under `handlers` and `crud` has the same two members. `run`
1786
- performs the work, and `refresh` names the queries to re-run once it has.
1787
- A query belongs there when its answer changed, never to fill an element the
1788
- request's answer creates: that element is filled from the last answer its
1789
- query landed.
1790
-
1791
- `run` receives the same `ctx` and the request less its name: `query`, `key`
1792
- and `values`, as present.
1793
-
1794
- `run` may also return answers of its own, by query name, which are laid over
1795
- the refreshed ones. That is how a delta reaches the browser: wrap it in
1796
- `patch()`, naming the rows that arrived or changed and the keys that went.
1797
-
1798
- A refreshed `rows` query sends every row to say what changed. An update or
1799
- a delete names its row by key, and an insert has the new row's key once it
1800
- has written it, so each handler knows what changed and answers with a
1801
- patch, which the hub places by the query's [order](#order): `createHub`
1802
- refuses at startup a `crud` `rowInsert`, `rowUpdate` or `rowDelete` on a
1803
- `rows` query whose `refresh` names that same query. A `row` query sends one
1804
- row and may refresh itself.
1805
-
1806
- `run` may instead return `url()`, a new row for `lb-url`, naming query parms
1807
- only the write can know, such as the key of a row it inserted. The page
1808
- then loads at them in the same round trip, in place of the refresh set — see
1809
- [When a server response moves the URL](#when-a-server-response-moves-the-url).
1810
- A `rowDelete` that removes the row on screen returns `url({ acct: "" })`
1811
- and leaves any other delete to its refresh set.
1812
-
1813
- ```ts
1814
- // members.requests.ts
1815
- import { patch, type Requests } from "@loadbare/app/server";
1816
-
1817
- export const requests: Requests = {
1818
- handlers: {
1819
- resetRoster: {
1820
- run: (ctx) => ctx.db.resetMembers(),
1821
- refresh: ["roster"],
1822
- },
1823
- },
1824
- crud: {
1825
- roster: {
1826
- rowDelete: {
1827
- run: async (ctx, { key }) => {
1828
- await ctx.db.deleteMember(key);
1829
- return { roster: patch({ drop: [key] }) };
1830
- },
1831
- refresh: [],
1832
- },
1833
- },
1834
- },
1835
- };
1836
- ```
1837
-
1838
- Each key under a `crud` entry is a request name with its prefix stripped and
1839
- the rest camel-cased, so the attribute, the wire and this key are one
1840
- vocabulary. A query with no `crud` entry permits none of them.
1841
-
1842
- | Request name | Key under `crud` | `run` receives |
1843
- | --------------- | ---------------- | --------------- |
1844
- | `lb-row-insert` | `rowInsert` | `values` |
1845
- | `lb-row-update` | `rowUpdate` | `key`, `values` |
1846
- | `lb-row-delete` | `rowDelete` | `key` |
1847
-
1848
- A `rowUpdate` sets the columns `values` names and leaves the rest as they
1849
- are, as an SQL `UPDATE` does. A form sends the controls it holds and a
1850
- control carrying `lb-request` sends itself, so one handler answers both.
1851
-
1852
- ### The wire
1853
-
1854
- ---- UNEDITED ----
1855
-
1856
- One path, one method. The hub sends every round trip as
1857
- `POST /lb/<stub>?<query string>`, with the query string the browser is
1858
- showing. An empty body asks for the page load. Any other body is a
1859
- request:
1860
-
1861
- ```ts
1862
- interface HubRequest {
1863
- name: string;
1864
- query?: string;
1865
- key?: string;
1866
- keys?: string[];
1867
- values?: Record<string, string>;
1868
- rows?: Record<string, { key: string; values: Record<string, string> }[]>;
1869
- }
1870
- ```
1871
-
1872
- A refused request is answered with status 422 and
1873
- `{ refusal: { message?, columns? } }`, and any other failure with 500 and
1874
- no body — see [Refusals](#refusals). Every other response is an array of
1875
- response items:
1876
-
1877
- ```ts
1878
- interface ResponseItem {
1879
- query: string;
1880
- kind: "row" | "rows";
1881
- key: string;
1882
- row?: Row;
1883
- rows?: Row[];
1884
- patch?: { rows?: Row[]; drop?: unknown[] };
1885
- }
1886
- ```
1887
-
1888
- A response item carries exactly one of `row`, `rows` or `patch`. A patch
1889
- answers a `rows` query only: rows named in `rows` are added or updated, keys
1890
- in `drop` are removed, and every other row is left alone.
1891
-
1892
- A `rows` item also carries `order` when the query declares one.
1893
-
1894
- A choice is sent as `lb-row-update` for `lb-choices`, keyed by the path,
1895
- with every choice made on the page as `values`; see [Choices](#choices).
1896
-
1897
- `createHub(pages)` builds the `Hub`, which has two calls, both answering
1898
- with response items: `dataForPage(page, ctx, options)` for a page load, and
1899
- `runRequest(page, request, ctx)` for a request. `options.parms` are the
1900
- parms the page is loaded at, and `options.follow` false ignores a `url()`
1901
- from `onPageEnter`; `hubRoutes` passes both.
1902
-
1903
- ## Custom elements
1904
-
1905
- ---- UNEDITED ----
1906
-
1907
- A custom element is an element defined by `customElements.define`, and
1908
- follows these fixed rules in a Loadbare/app application:
1909
- - Its name contains a hyphen, as HTML requires, like `<my-custom-element>`.
1910
- - Its element file, if present, is `my-custom-element.html`.
1911
- - Its script, if present, is `my-custom-element.browser.ts`. The
1912
- `browser` segment is a safety feature, requiring the file to be
1913
- explicitly named as a browser file, to help prevent unfortunate
1914
- naming collisions where a server file happens to have the name of
1915
- a custom element and gets built into the browser bundle.
1916
-
1917
- A custom element must have one or the other of an element file and a
1918
- script, and it may have both. If it has neither, the builder reports an
1919
- error.
1920
-
1921
- To use custom elements from a widget library, add the library to
1922
- `imports.ts` anywhere in the builder's `--src`:
1923
-
1924
- ```ts
1925
- export default ["@scope/library-name"];
1926
- ```
1927
-
1928
- Import every attribute name from `@loadbare/app/constants` — see
1929
- [Constants](#constants). Never write one as a string literal.
1930
-
1931
- ### What the hub and an element say to each other
1932
-
1933
- Each direction has one mechanism for each kind of message:
1934
-
1935
- | Direction | What | How | Today |
1936
- | -------------- | ------------------------------------------- | ---------------------------- | ------------------------------------------------ |
1937
- | 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` |
1938
- | Hub to element | State the platform already names | A property | A control's `value` |
1939
- | Hub to element | What the hub did | An optional `lb` method | `lbAttached`, `lbRowsLanded`, `lbRequestDone`, `lbDetached` |
1940
- | Hub to element | A moment an element started | A bubbling event | `lb-request-done` |
1941
- | Element to hub | A moment | A bubbling event | `lb-request`, and `change`, `click` and `submit` |
1942
-
1943
- State is an attribute, because a stylesheet can select on it. A method is
1944
- how the hub tells one element what it just did to that element. A moment is an event, because the element that cares is
1945
- often an ancestor of the one it concerns, and an event reaches it with no
1946
- knowledge of either.
1947
-
1948
- ### Controls
1949
-
1950
- A form-associated custom element with a `value` property that fires
1951
- `change` is a control. The hub sets its `value` when a column lands, and
1952
- gathers its `value` when a request is issued, exactly as for a native
1953
- `<input>`. It commits on `change`, and belongs to its form owner, which the
1954
- browser tracks and the `form` attribute names.
1955
-
1956
- ```ts
1957
- class NoteField extends HTMLElement {
1958
- static formAssociated = true;
1959
- // a `value` property, and a `change` event when the edit commits
1960
- }
1961
- ```
1962
-
1963
- A custom element that is not a control receives `lb-column-value` and
1964
- renders it; its content is never replaced.
1965
-
1966
- ### Lifecycle
1967
-
1968
- A custom element hears the hub's lifecycle, never the browser's. The hub
1969
- calls four optional methods, which the `HubLifecycle` interface in
1970
- `@loadbare/app/types` declares:
1971
-
1972
- | The hub | A custom element hears |
1973
- | ------------------------------------------------ | ------------------------------- |
1974
- | Places it: chrome at startup, a page, a new live row, a new group | `lbAttached()`, once, after every column has landed on it |
1975
- | Lands rows on it | `lbRowsLanded()` |
1976
- | Finishes a request committed by it or inside it | `lbRequestDone(detail, origin)` |
1977
- | Removes it: a row or group done leaving, the page replaced | `lbDetached()`, once |
1978
- | Moves a live row the order put elsewhere | Nothing |
1979
- | Turns `lb-show` off or on | Nothing |
1980
- | Hides a row or a group for a filter | Nothing |
1981
-
1982
- The chrome and a page are placed as the page's first answer lands, or once
1983
- that round trip fails. An element under an `lb-show` that is off is placed
1984
- inside its template, so `lbAttached` runs whether it shows or not. `lbAttached` reaches an outer
1985
- custom element before the ones inside it, and `lbDetached` the inner ones
1986
- first. `lbRequestDone` reaches the committing element and every custom
1987
- element around it, innermost first, before the `lb-request-done` event.
1988
- An element a script creates hears none of them.
1989
-
1990
- A listener on the element itself goes in the constructor, which reads no
1991
- attribute and no child. A child is looked up when it is needed. Work done
1992
- once per instance goes in `lbAttached`, and its undoing in `lbDetached`.
1993
-
1994
- The build refuses a script that declares `connectedCallback`,
1995
- `disconnectedCallback`, `adoptedCallback` or `connectedMoveCallback`.
1996
-
1997
- ### Row hooks
1998
-
1999
- The hub calls `lbRowsLanded()` on a custom element with `lb-query` and a row
2000
- template, after the rows land, with every row placed and stamped. The hub
2001
- places every row and builds every group itself, so a custom element reacts
2002
- to rows and never places them. Scrolling to the row the page's request
2003
- created is the case:
2004
-
2005
- ```ts
2006
- lbRowsLanded() {
2007
- this.querySelector(`[${ATTR_ROW_REQUESTED}="${REQUESTED_CREATED}"]`)
2008
- ?.scrollIntoView({ block: "nearest" });
2009
- }
2010
- ```
2011
-
2012
- A custom element under an `lb-show` that is off holds its rows all the
2013
- same, and the hook runs as they land.
2014
-
2015
- `applyRow(root, row)`, exported by `@loadbare/app`, fills `root` from one
2016
- row, the same operation that fills a live row.
2017
-
2018
- ## Internal linkage
2019
-
2020
- ---- UNEDITED ----
2021
-
2022
- The endpoint path, `index` for the bare path, the constant holding the
2023
- request deadline, the generated client entry, and the `./build` export.
2024
- Nobody outside this package reads any of it. The deadline itself is
2025
- behavior an application sees — see [The round trip](#the-round-trip) — and
2026
- giving it a knob later takes nothing away.
2027
-
2028
- ## Cross-reference
2029
-
2030
- Every name Loadbare/app owns, in one place. Each row names the section
2031
- that defines it. The definition lives there and only there.
2032
-
2033
- ### Builder flags
2034
-
2035
- Loadbare/app owns every flag in this table.
2036
-
2037
- | Flag | Default | Names | Defined in |
2038
- | ---------- | ------- | ----------------------------------------- | ------------------------------------------------- |
2039
- | `--src` | `src` | The application's own tree, scanned whole | [What the builder scans](#what-the-builder-scans) |
2040
- | `--out` | `dist` | Where the builder writes | [Running the builder](#running-the-builder) |
2041
- | `--watch` | off | Rebuild on change under `--src` | [Running the builder](#running-the-builder) |
2042
- | `--minify` | off | Minify `client.js` and `app.css` | [Running the builder](#running-the-builder) |
2043
-
2044
- ### Reserved file names
2045
-
2046
- Loadbare/app owns every file name and pattern in this table. A file so
2047
- named carries its meaning wherever it sits in the `--src` tree, so an
2048
- application must not use one of these names for anything else.
2049
-
2050
- | File | How many | Holds | Defined in |
2051
- | ----------------------- | ------------------------------ | -------------------------------------------------------- | ------------------------------------------------------- |
2052
- | `chrome.html` | Exactly one | The application's one HTML document | [Chrome](#chrome) |
2053
- | `imports.ts` | Zero or one | Default-exports an array of widget library package names | [Imported widget libraries](#imported-widget-libraries) |
2054
- | `<stub>.page.html` | One per page | One page's markup, as a fragment | [Pages](#pages) |
2055
- | `<stub>.queries.ts` | Zero or one per page | That page's queries | [Page Queries](#page-queries) |
2056
- | `<stub>.requests.ts` | Zero or one per page | That page's requests | [Page Requests](#page-requests) |
2057
- | `<tag-name>.html` | Zero or one per custom element | One element file, as a fragment | [Custom elements](#custom-elements) |
2058
- | `<tag-name>.browser.ts` | Zero or one per custom element | One custom element's script, `.js` also accepted | [Custom elements](#custom-elements) |
2059
- | `*.css` | Any number | A stylesheet, concatenated into `app.css` | [Running the builder](#running-the-builder) |
2060
-
2061
- ### Build outputs
2062
-
2063
- Loadbare/app owns every file name in this table. The builder writes them
2064
- into `--out`. Which of them exist depends on the application: `app.css`
2065
- only when some origin has a stylesheet, `pages.ts` only when some page has a
2066
- `.queries.ts` or a `.requests.ts`.
2067
-
2068
- | Path | Written | Holds | Read by | Defined in |
2069
- | ------------------ | ----------------- | ------------------------------------------------------------------ | ---------- | ------------------------------------------- |
2070
- | `/app.html` | Always | The chrome, built | The server | [Chrome](#chrome) |
2071
- | `/client.js` | Always | `<lb-hub>` and every custom element the application uses | The chrome | [Chrome](#chrome) |
2072
- | `/client-entry.ts` | Always | The generated entry `client.js` is bundled from | Nothing | [Running the builder](#running-the-builder) |
2073
- | `/app.css` | With a stylesheet | Every stylesheet in the cascade, concatenated | The chrome | [Chrome](#chrome) |
2074
- | `/pages.ts` | With page data | The one `createHub()` call, over every page's queries and requests | The server | [The Express server](#the-express-server) |
2075
-
2076
- ### Reserved package.json keys
2077
-
2078
- Loadbare/app owns every key in this table. A widget library declares them.
2079
- An application never does.
2080
-
2081
- | Key | Value | Means | Defined in |
2082
- | ------------------ | ------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------- |
2083
- | `loadbare.widgets` | A path inside the package | Where this package's custom elements are. Absent means the whole installed directory | [Imported widget libraries](#imported-widget-libraries) |
2084
-
2085
- ### Reserved namespaces
2086
-
2087
- Loadbare/app owns every namespace in this table. Ownership of a name and
2088
- ownership of its behavior are stated separately, because they differ.
2089
-
2090
- | Namespace | Applies to | Loadbare owns | Defined in |
2091
- | --------- | ----------------------------------------------------------- | --------------------------------------------------- | ----------------------------------------------- |
2092
- | `lb-*` | Attributes, their values, queries, columns of `lb-` queries, requests, custom elements, events | The names and their behavior | [The lb-* namespace](#the-lb--namespace) |
2093
- | `--lb-*` | Values of HTML's `command` attribute | The names and their behavior | [Reserved commands](#reserved-commands) |
2094
- | `exp-*` | Attributes on a custom element | The behavior; element file authors pick the names | [Build time parameters](#build-time-parameters) |
2095
- | `lb*` | Methods on custom elements | The names and their behavior | [Row hooks](#row-hooks) |
2096
-
2097
- #### The lb-* namespace
2098
-
2099
- Every name beginning with `lb-` belongs to Loadbare/app — see
2100
- [Names](#names). An application writes the attributes this section names,
2101
- and invents none of its own, because a name Loadbare has not defined today
2102
- it may define tomorrow.
2103
-
2104
- | Attribute | Written by | On | Defined in |
2105
- | ----------------- | ------------- | ----------------------------------- | --------------------------------------------- |
2106
- | `lb-query` | Developer | Any element | [Landing](#landing) |
2107
- | `lb-column` | Developer | Any element | [Landing](#landing) |
2108
- | `lb-show` | Developer | Any element but a `<template>` | [Displaying by condition](#displaying-by-condition) |
2109
- | `lb-request` | Developer | Any element | [Requests](#requests) |
2110
- | `lb-select` | Developer | A checkbox in a row template | [Selecting rows](#selecting-rows) |
2111
- | `lb-url-link` | Developer | An `<a>` | [Links](#links) |
2112
- | `lb-url-push` | Developer | An element with `lb-request` | [Query parms](#query-parms) |
2113
- | `lb-url-unknown` | Developer | A `<dialog>` inside `<lb-hub>` | [An unknown page](#an-unknown-page) |
2114
- | `lb-group` | Developer | A `<template>` holding a `<template>` | [Groups](#groups) |
2115
- | `lb-count`, `lb-sum`, `lb-avg`, `lb-min`, `lb-max` | Developer | Any element but a `<template>` | [Aggregates](#aggregates) |
2116
- | `lb-order` | Developer | An element with `lb-query` | [Order](#order) |
2117
- | `lb-filter` | Developer | An element with `lb-query` | [Filters](#filters) |
2118
- | `lb-exp-slot` | Developer | One element in an element file | [Slots and templates](#slots-and-templates) |
2119
- | `lb-exp-template` | Developer | An element file, and a `<template>` | [Slots and templates](#slots-and-templates) |
2120
- | `lb-column-value` | Hub | Every element set from a column | [How a column lands](#how-a-column-lands) |
2121
- | `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) |
2122
- | `lb-row-editing` | Hub | The live row a form is pointed at | [A form pointed at a row](#a-form-pointed-at-a-row) |
2123
- | `lb-row-selected` | Hub | A live row its `lb-select` checkbox ticks | [Selecting rows](#selecting-rows) |
2124
- | `lb-row-live` | Hub | A live row | [Rows](#rows) |
2125
- | `lb-query-row-count` | Hub | An element with a row template | [Rows](#rows) |
2126
- | `lb-group-live`, `lb-group-column`, `lb-group-value` | Hub | Each top-level element of a group | [Groups](#groups) |
2127
- | `lb-row-created`, `lb-row-changed`, `lb-row-moved`, `lb-row-requested` | Hub | A live row | [What happened to a row](#what-happened-to-a-row) |
2128
- | `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) |
2129
- | `lb-request-pending` | Hub | The element that issued a request | [Request state](#request-state) |
2130
- | `lb-request-error` | Hub | The element that issued a request | [Request state](#request-state) |
2131
- | `lb-request-refused` | Hub | The element whose request was refused | [Refusals](#refusals) |
2132
- | `lb-show` on a `<template>` | Hub and builder | Where an absent element stands | [Displaying by condition](#displaying-by-condition) |
2133
- | `lb-page` | Builder | A page's `<template>` | [Page HTML](#page-html) |
2134
- | `lb-page-title` | Builder | A page's `<template>` | [Page HTML](#page-html) |
2135
-
2136
- The builder refuses any other `lb-` attribute in markup — see
2137
- [The markup checks](#the-markup-checks).
2138
-
2139
- ### Reserved queries
2140
-
2141
- | Query | Kind | Key | Columns | Defined in |
2142
- | -------- | ----- | --------- | --------------------------------------------------------------- | ------------------- |
2143
- | `lb-url` | `row` | `lb-path` | `lb-path`, `lb-page-label`, `lb-page-unknown`, every query parm | [The URL](#the-url) |
2144
- | `lb-choices` | `row` | `lb-path` | `lb-path`, every choice | [Choices](#choices) |
2145
-
2146
- ### Reserved query parms
2147
-
2148
- None. A column of `lb-url` whose name begins with `lb-` is never a query
2149
- parm — see [Query parms](#query-parms).
2150
-
2151
- ### Reserved request names
2152
-
2153
- | Request name | Runs under `crud` | Defined in |
2154
- | --------------- | ----------------- | ----------------------------------- |
2155
- | `lb-row-insert` | `rowInsert` | [The request names](#the-request-names) |
2156
- | `lb-row-update` | `rowUpdate` | [The request names](#the-request-names) |
2157
- | `lb-row-delete` | `rowDelete` | [The request names](#the-request-names) |
2158
-
2159
- ### Reserved commands
2160
-
2161
- Loadbare/app owns every value of HTML's `command` attribute beginning with
2162
- `--lb-`.
2163
-
2164
- | Command | Defined in |
2165
- | -------------- | --------------------------------------------------- |
2166
- | `--lb-edit` | [A form pointed at a row](#a-form-pointed-at-a-row) |
2167
- | `--lb-new` | [A form pointed at a row](#a-form-pointed-at-a-row) |
2168
- | `--lb-release` | [A form pointed at a row](#a-form-pointed-at-a-row) |
2169
-
2170
- ### Reserved tags
2171
-
2172
- Loadbare/app owns every tag in this table. An application writes them and
2173
- never defines them.
2174
-
2175
- | Tag | Written in | Means | Defined in |
2176
- | ---------- | ---------- | ------------------------------ | ----------------- |
2177
- | `<lb-hub>` | The chrome | The application's live element | [Chrome](#chrome) |
2178
-
2179
- ### Reserved events
2180
-
2181
- Loadbare/app owns every DOM event in this table, both the name and what its
2182
- `detail` carries.
2183
-
2184
- | Event | Dispatched from | Bubbles | Cancelable | Defined in |
2185
- | ------------ | -------------------------------- | ------- | ---------- | --------------------------------------- |
2186
- | `lb-request` | The element that committed | Yes | No | [The request event](#the-request-event) |
2187
- | `lb-request-done` | The element that committed | Yes | No | [The request event](#the-request-event) |
2188
-
2189
- ### Reserved methods
2190
-
2191
- | Method | On | Defined in |
2192
- | --------------- | --------------------------------------------------- | ----------------------- |
2193
- | `lbAttached` | Any custom element the hub places | [Lifecycle](#lifecycle) |
2194
- | `lbRowsLanded` | A custom element with `lb-query` and a row template | [Row hooks](#row-hooks) |
2195
- | `lbRequestDone` | A custom element that committed, or holds one that did | [Lifecycle](#lifecycle) |
2196
- | `lbDetached` | Any custom element the hub removes | [Lifecycle](#lifecycle) |
2197
-
2198
- ### Constants
2199
-
2200
- `@loadbare/app/constants` exports every name above. Custom element code
2201
- imports them rather than writing a string.
2202
-
2203
- | Constant | Value |
2204
- | ------------------------- | --------------------------------------- |
2205
- | `ATTR_QUERY` | `lb-query` |
2206
- | `ATTR_COLUMN` | `lb-column` |
2207
- | `ATTR_SHOW` | `lb-show` |
2208
- | `ATTR_COLUMN_VALUE` | `lb-column-value` |
2209
- | `ATTR_KEY_VALUE` | `lb-key-value` |
2210
- | `ATTR_ROW_LIVE` | `lb-row-live` |
2211
- | `LIVE_ROW` | `[lb-row-live]`, the selector for a live row |
2212
- | `ATTR_QUERY_ROW_COUNT` | `lb-query-row-count` |
2213
- | `ATTR_GROUP` | `lb-group` |
2214
- | `ATTR_GROUP_LIVE` | `lb-group-live` |
2215
- | `ATTR_GROUP_COLUMN` | `lb-group-column` |
2216
- | `ATTR_GROUP_VALUE` | `lb-group-value` |
2217
- | `LIVE_GROUP` | `[lb-group-live]`, the selector for a group's element |
2218
- | `ATTR_COUNT`, `ATTR_SUM`, `ATTR_AVG`, `ATTR_MIN`, `ATTR_MAX` | `lb-count`, `lb-sum`, `lb-avg`, `lb-min`, `lb-max` |
2219
- | `AGGREGATES` | All five aggregate attributes |
2220
- | `ORDER_DESCENDING` | `-`, written before a column in an order |
2221
- | `CHOICES_QUERY` | `lb-choices` |
2222
- | `ATTR_ORDER` | `lb-order` |
2223
- | `ATTR_FILTER` | `lb-filter` |
2224
- | `FILTER_OPERATORS` | Every filter operator, the longer first |
2225
- | `ATTR_ROW_CREATED` | `lb-row-created` |
2226
- | `ATTR_ROW_CHANGED` | `lb-row-changed` |
2227
- | `ATTR_ROW_MOVED` | `lb-row-moved` |
2228
- | `ATTR_ROW_REQUESTED` | `lb-row-requested` |
2229
- | `REQUESTED_CREATED`, `REQUESTED_CHANGED`, `REQUESTED_MOVED` | `created`, `changed`, `moved` |
2230
- | `ATTR_ROW_LEAVING` | `lb-row-leaving` |
2231
- | `ATTR_GROUP_LEAVING` | `lb-group-leaving` |
2232
- | `COMMAND_EDIT`, `COMMAND_NEW`, `COMMAND_RELEASE` | `--lb-edit`, `--lb-new`, `--lb-release` |
2233
- | `LB_COMMANDS` | All three commands |
2234
- | `LB_COMMAND_PREFIX` | `--lb-` |
2235
- | `ATTR_ROW_EDITING` | `lb-row-editing` |
2236
- | `ATTR_SELECT` | `lb-select` |
2237
- | `ATTR_ROW_SELECTED` | `lb-row-selected` |
2238
- | `ATTR_REQUEST` | `lb-request` |
2239
- | `ATTR_REQUEST_PENDING` | `lb-request-pending` |
2240
- | `ATTR_REQUEST_ERROR` | `lb-request-error` |
2241
- | `ATTR_REQUEST_REFUSED` | `lb-request-refused` |
2242
- | `LB_REFUSAL_STATUS` | `422` |
2243
- | `ATTR_URL_LINK` | `lb-url-link` |
2244
- | `ATTR_URL_PUSH` | `lb-url-push` |
2245
- | `ATTR_URL_UNKNOWN` | `lb-url-unknown` |
2246
- | `ATTR_EXP_SLOT` | `lb-exp-slot` |
2247
- | `ATTR_EXP_TEMPLATE` | `lb-exp-template` |
2248
- | `ATTR_PAGE` | `lb-page` |
2249
- | `ATTR_PAGE_TITLE` | `lb-page-title` |
2250
- | `DEVELOPER_ATTRIBUTES` | The sixteen attributes a developer writes |
2251
- | `LB_EVENT_NAME` | `lb-request` |
2252
- | `LB_DONE_EVENT_NAME` | `lb-request-done` |
2253
- | `SHOW_NOT` | `!`, written before a column in `lb-show` |
2254
- | `LB_RESERVED_PREFIX` | `lb-` |
2255
- | `REQUEST_ROW_INSERT` | `lb-row-insert` |
2256
- | `REQUEST_ROW_UPDATE` | `lb-row-update` |
2257
- | `REQUEST_ROW_DELETE` | `lb-row-delete` |
2258
- | `LB_ROW_REQUESTS` | All three request names |
2259
- | `KIND_ROW` | `row` |
2260
- | `KIND_ROWS` | `rows` |
2261
- | `URL_QUERY` | `lb-url` |
2262
- | `URL_COLUMN_PATH` | `lb-path` |
2263
- | `URL_COLUMN_PAGE_LABEL` | `lb-page-label` |
2264
- | `URL_COLUMN_PAGE_UNKNOWN` | `lb-page-unknown` |
2265
- | `HUB_TAG_NAME` | `lb-hub` |
2266
- | `LB_ENDPOINT` | `/lb` |
2267
- | `LB_REQUEST_TIMEOUT_MS` | `10000` |