@loadbare/app 0.8.0 → 0.8.2

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 (47) hide show
  1. package/dist/build/skills-cli.d.ts +12 -0
  2. package/dist/build/skills-cli.d.ts.map +1 -0
  3. package/dist/build/skills-cli.js +81 -0
  4. package/dist/build/skills-cli.js.map +1 -0
  5. package/dist/build/skills.d.ts +47 -0
  6. package/dist/build/skills.d.ts.map +1 -0
  7. package/dist/build/skills.js +124 -0
  8. package/dist/build/skills.js.map +1 -0
  9. package/dist/core/lb-constants.d.ts +2 -0
  10. package/dist/core/lb-constants.d.ts.map +1 -1
  11. package/dist/core/lb-constants.js +18 -4
  12. package/dist/core/lb-constants.js.map +1 -1
  13. package/dist/hub/lb-apply.d.ts +10 -0
  14. package/dist/hub/lb-apply.d.ts.map +1 -1
  15. package/dist/hub/lb-apply.js +15 -1
  16. package/dist/hub/lb-apply.js.map +1 -1
  17. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  18. package/dist/hub/lb-hub.browser.js +116 -35
  19. package/dist/hub/lb-hub.browser.js.map +1 -1
  20. package/dist/server/lb-express.d.ts +13 -5
  21. package/dist/server/lb-express.d.ts.map +1 -1
  22. package/dist/server/lb-express.js +26 -7
  23. package/dist/server/lb-express.js.map +1 -1
  24. package/dist/server/lb-server.d.ts +3 -0
  25. package/dist/server/lb-server.d.ts.map +1 -1
  26. package/dist/server/lb-server.js.map +1 -1
  27. package/docs/TECHREF-1.0.md +93 -22
  28. package/docs/comparison.md +8 -7
  29. package/docs/reference/chrome.md +5 -3
  30. package/docs/reference/data-binding.md +28 -0
  31. package/docs/reference/server.md +11 -0
  32. package/docs/reference/widgets.md +8 -4
  33. package/docs/roadmap.md +16 -0
  34. package/docs/theory.md +8 -1
  35. package/docs/tutorials/010-pages-and-navigation.md +2 -1
  36. package/package.json +8 -4
  37. package/skills/loadbare-app/SKILL.md +275 -0
  38. package/skills/loadbare-app/references/TECHREF-1.0.md +1260 -0
  39. package/skills/loadbare-app/references/builder.md +134 -0
  40. package/skills/loadbare-app/references/chrome.md +160 -0
  41. package/skills/loadbare-app/references/css.md +44 -0
  42. package/skills/loadbare-app/references/custom-elements.md +397 -0
  43. package/skills/loadbare-app/references/data-binding.md +485 -0
  44. package/skills/loadbare-app/references/overview.md +38 -0
  45. package/skills/loadbare-app/references/page-files.md +194 -0
  46. package/skills/loadbare-app/references/server.md +153 -0
  47. package/skills/loadbare-app/references/widgets.md +178 -0
@@ -0,0 +1,1260 @@
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
+ ## Blockers
9
+
10
+ We cannot declare 1.0 until we determine if these blockers can be
11
+ added later w/o breaking changes. If we are reasonably confident
12
+ that they can be decided later w/o breaking changes, then we rename
13
+ them to "Out of Scope" or move them to the roadmap. Otherwise,
14
+ they must be resolved before we declare 1.0.
15
+
16
+ ### Data types
17
+
18
+ Loadbare/app enforces no data types on values between the database and the
19
+ browser. A uniform and useful approach is difficult to discern, and we do
20
+ not want to pollute 1.0 with a potentially sub-optimal solution. Until then
21
+ a value is rendered by browser coercion when it lands on an HTML text
22
+ element, or by whatever the widget does when it lands on a widget.
23
+
24
+ We will then see if a useful solution emerges that Loadbare/app should
25
+ handle.
26
+
27
+ ### Form controls
28
+
29
+ - **Checkboxes and radio buttons are not implemented.** A value does not
30
+ land on one and a form does not gather one. Landing needs a decision on
31
+ what counts as checked, which waits on [Data types](#data-types), and a
32
+ radio group is several elements answering to one cell.
33
+
34
+ ### Run-time state attributes
35
+
36
+ The hub stamps `lb-row-count`, `lb-pending` and `lb-error` for a
37
+ stylesheet to read. Their consumer is CSS rather than code.
38
+
39
+ - **Decide `data-` against `lb-`.** These are the only names the hub writes
40
+ outside its own namespace, which is either a deliberate signal that CSS
41
+ owns them or an inconsistency.
42
+ - **Say what `lb-row-count` holds.** It is stamped and never explained.
43
+ - **Decide whether an unarrived value needs a signal.** The roadmap proposes
44
+ deriving it from an absent `lb-value`. Recommend confirming that and
45
+ adding nothing.
46
+
47
+ ### The widget protocol
48
+
49
+ Loadbare/app owns every method name beginning with `lb` on a custom element.
50
+ That decision is firm; what the set contains is not.
51
+
52
+ - **Decide what the build does with an unknown `lb*` method.** An element
53
+ carrying a method beginning with `lb` that Loadbare/app does not define may
54
+ be an error, a warning, or ignored.
55
+ - **Decide whether a widget may receive a whole row.** A cell lands one
56
+ element at a time and offers no escape hatch. An optional `lbAcceptRow`
57
+ would sit beside `lbPlaceRow` and `lbRowsLanded`, and this is the most
58
+ likely first request from a widget author.
59
+
60
+ ### Lists
61
+
62
+ Imagine we have an HTML `<select>` that displays a fixed list for all
63
+ rows in a table, such as `customer_type` for a table of customers. When
64
+ all customers can be any customer type, our system as written today
65
+ is fine, because all HTML `<select>` elements get the same list.
66
+
67
+ But it may be that the list of allowed values is dependent on other values
68
+ in the row.
69
+
70
+
71
+ - **Decide how a new row fills a nested list.** A row added to the outer
72
+ list starts with an empty nested list, which fills only when the nested
73
+ list's name lands again. See [Master-detail](#master-detail) for what a
74
+ nested list is for.
75
+
76
+ ```html
77
+ <tbody lb-list="accounts">
78
+ <template lb-key="id"><tr>
79
+ <td lb-cell="name"></td>
80
+ <td><select lb-list="statuses"><template lb-key="id"><option lb-cell="label"></option></template></select></td>
81
+ </tr></template>
82
+ </tbody>
83
+ ```
84
+
85
+ ### The server API
86
+
87
+ - **Give the chrome a way to state its own queries.** A widget in the chrome
88
+ bound to a query forces every page to declare that query, since the page
89
+ data run covers one page's set. `lb-navigation` shows the pattern from the
90
+ framework side, and an application has no equivalent. Recommend a
91
+ chrome-level query set on `createHub`.
92
+ - **State that only `createHub` implements `Hub`.** Otherwise a sixth
93
+ operation breaks anyone who wrote the interface by hand.
94
+ - **Decide the options-argument shape once.** Global hooks, transaction
95
+ wrapping, error handling and a CSRF token all want the same trailing
96
+ parameter on `createHub` and `hubRoutes`. Build none of them, but pick
97
+ where they go.
98
+ - **Land `staticRoutes`.** See NEXT.md. It closes the last place where an
99
+ application hand-writes facts the builder owns.
100
+
101
+ ### The builder
102
+
103
+ - **Adopt a configuration file and fold `imports.ts` into it.** It holds
104
+ `out`, `minify` and `imports`, and lives where `imports.ts` lives now, so a
105
+ repository with several applications gets one per application. `--src`
106
+ stays a flag, since it is what locates the file. State the precedence
107
+ between a flag and a key once. Its key names join the permanent surface,
108
+ so this lands before 1.0 or not at all.
109
+ - **Decide CSS pairing.** Naming a stylesheet `<tag>.css` beside its
110
+ definition would let the builder ship only what survives expansion.
111
+ Recommend deferring the mechanism and reserving the configuration key, so
112
+ that it lands as an opt-in rather than as a silent drop.
113
+ - **Land the remaining validations.** One `lb-hub`, one empty `<main>`, and
114
+ every `lb-` attribute known. Recommend landing these now, because refusing
115
+ markup that used to build is the kind of change 1.0 gives up.
116
+ - **Confirm the three output names.** `app.html`, `client.js` and `app.css`
117
+ are about to be fixed in `staticRoutes` as well as in the builder.
118
+
119
+ ### The internal surface
120
+
121
+ None of this is surface. It is listed here because trimming it is free
122
+ today and breaking after 1.0.
123
+
124
+ - **Drop `./build` from the exports map.** Nothing in the repository imports
125
+ it, and nothing outside the CLI should call `resolveElements` or
126
+ `clientEntrySource`.
127
+ - **Audit what the hub does.** It appears to have routines that go beyond
128
+ what the `lb-*` attribute namespace specifies.
129
+ - **Keep the rest as constants.** The endpoint, `index` and the timeout are
130
+ linkage between the builder and the hub rather than facts an application
131
+ varies. Adding a knob for any of them buys a second shape of application
132
+ to reason about.
133
+ - **Give the hub a configuration channel without using it.** `<lb-hub>` reads
134
+ no attributes at all, which is the free landing spot for the roadmap's
135
+ static-origin split and for a timeout an application eventually outgrows.
136
+ Reserve the idea, build nothing.
137
+
138
+ ## The build
139
+
140
+ ### Running the builder
141
+
142
+ ```
143
+ loadbare-app-build [--src src] [--out dist] [--watch] [--minify]
144
+ ```
145
+
146
+ `--src` names the tree the builder scans, and is defined under
147
+ [What the builder scans](#what-the-builder-scans).
148
+
149
+ `--out` names the directory the builder writes into, and defaults to `dist`.
150
+ Every output path in this document is written relative to it, so an
151
+ application that passes `--out` reads `dist/app.html` here as
152
+ `<out>/app.html`. See [Build outputs](#build-outputs).
153
+
154
+ `--watch` builds once, then rebuilds on every change to an `.html`, `.ts` or
155
+ `.css` file under `--src`. A build that throws is reported and the watch
156
+ continues. There is no dev server and no browser reload.
157
+
158
+ `--minify` minifies `client.js` and `app.css`. It leaves `app.html` alone,
159
+ which stays markup an application can read.
160
+
161
+ The builder bundles `client.js` from a `client-entry.ts` it generates into
162
+ `--out` and leaves there. Nothing reads it after the bundle, and an
163
+ application neither writes nor imports it.
164
+
165
+ Every `.css` file in every origin joins `app.css`. There is no widget, page
166
+ or chrome stylesheet: the builder concatenates them all, with nothing added,
167
+ removed or scoped, so what ships is what was authored. Within one origin
168
+ they are ordered by filename, the full path breaking a tie, and a
169
+ subdirectory never affects the order. Origins follow the cascade — this
170
+ package's own widgets, then each package named in `imports.ts`, then `--src`
171
+ last — so an application's own stylesheet always lands after the ones it
172
+ imported. A file named `00-global.css` sorts first by saying so.
173
+
174
+ ### What the builder scans
175
+
176
+ #### The current project
177
+
178
+ The builder scans one directory named by its `--src` parameter,
179
+ which defaults to `src/`.
180
+
181
+ The sections below often state something like, "anywhere in
182
+ the `--src` tree", because Loadbare/app assigns semantic meaning
183
+ to file names, not subdir names.
184
+
185
+ #### Imported widget libraries
186
+
187
+ Any number of widget libraries can be used. They are named
188
+ in the file `imports.ts`, located anywhere in builder `--src`.
189
+ A project can leave out `imports.ts`, or have exactly one, but
190
+ two or more is an error.
191
+
192
+ ```ts
193
+ // imports.ts, anywhere in --src
194
+ export default ["@namespace/widget-library"];
195
+ ```
196
+
197
+ If the named package's `package.json` declares where the widgets
198
+ are, Loadbare scans that. Otherwise it scans the entire installed
199
+ directory. The declaration, if present, would look like this:
200
+
201
+ ```json
202
+ "loadbare": { "widgets": "./dist" }
203
+ ```
204
+
205
+ ### File names and the builder
206
+
207
+ The builder finds a file by its name. A subdirectory carries no meaning, so
208
+ the whole of `--src` is one flat namespace, as stated in
209
+ [What the builder scans](#what-the-builder-scans).
210
+
211
+ Three kinds of file hold HTML.
212
+
213
+ | File | Holds | Found in |
214
+ | ------------------ | ------------------------------------ | ------------ |
215
+ | `chrome.html` | The one HTML document | `--src` |
216
+ | `<stub>.page.html` | One page's markup, as a fragment | `--src` |
217
+ | `<tag-name>.html` | One widget definition, as a fragment | Every origin |
218
+
219
+ As stated in [Chrome](#chrome), `chrome.html` is a required singleton.
220
+
221
+ As stated in [Pages](#pages), the `<stub>` of a `.page.html` is the path the
222
+ page maps to. The builder puts all pages into `dist/app.html` as HTML `<template>` objects
223
+ that carry `lb-page="<stub>"`.
224
+
225
+ A widget file is exactly `<kebab-case-name>.html`. A [widget](#widgets) is
226
+ an HTML custom element, suitable for organizing large HTML trees, or adding
227
+ custom behavior, or both. A widget definition is named for the tag it expands.
228
+
229
+ Any other HTML file, one that is not `chrome.html`, or `<stub>.page.html` or
230
+ `<kebab-case-name>.html` will either:
231
+ - be an error if it looks like a widget with capitalization
232
+ - be skipped
233
+
234
+ ## HTML
235
+
236
+ In a Loadbare/app application, HTML is static after the build, and once
237
+ it is sent to the browser on initial page load, no HTML is ever sent
238
+ again.
239
+
240
+ ### Widget Expansion
241
+
242
+ The builder executes a process we call "expansion". When it finds
243
+ a custom element in the HTML it is processing, such as `<my-element>`,
244
+ it looks for the file `<my-element>.html`, and follows the expansion
245
+ rules to place the contents of `<my-element>.html`.
246
+
247
+ A custom element with no definition file is left alone. A custom element
248
+ with neither a definition nor a `.browser.ts` script is a build error, as
249
+ stated in [Widgets](#widgets).
250
+
251
+ #### Build time parameters
252
+
253
+ A build time parameter is supplied as an attribute on a custom element,
254
+ and is converted to a fixed value within the HTML by the builder.
255
+
256
+ The attribute value carries an `exp-` prefix, as in `exp-label`, and
257
+ the substitution locations use `{{label}}` (no exp-prefix) inside the
258
+ widget's HTML definition file:
259
+
260
+ ```html
261
+ <!-- lb-input.html, the definition -->
262
+ <label>{{label}} <input readonly="{{readonly}}" /></label>
263
+ ```
264
+
265
+ ```html
266
+ <!-- a page -->
267
+ <lb-input lb-cell="name" exp-label="Name"></lb-input>
268
+ ```
269
+
270
+ ```html
271
+ <!-- dist/app.html -->
272
+ <lb-input lb-cell="name" exp-label="Name"
273
+ ><label>Name <input /></label
274
+ ></lb-input>
275
+ ```
276
+
277
+ A definition states a default with `{{name|default}}`, as in
278
+ `{{button-label|OK}}`. Whitespace around the name and around the default is
279
+ discarded. The default is a literal.
280
+
281
+ | The attribute is assigned | Expansion defines | Result |
282
+ | ------------------------- | ------------------- | -------------------------------- |
283
+ | `exp-name="text"` | `{{name}}` | `text` |
284
+ | `exp-name=""` | `{{name}}` | An empty string |
285
+ | Nothing | `{{name\|default}}` | `default` |
286
+ | Nothing | `{{name}}` | Nothing, and the attribute drops |
287
+ | `exp-name` | No `{{name}}` | A build error |
288
+
289
+ Attributes without the `exp-` prefix are ignored during expansion.
290
+
291
+ A placeholder is the whole of an attribute value or the whole of a text node.
292
+ `title="Hello {{name}}"` ships literally.
293
+
294
+ A placeholder name carrying a capital is a build error.
295
+
296
+ A parameter value cannot become markup.
297
+
298
+ #### Slots and templates
299
+
300
+ A widget definition may contain any number of named templates and optionally
301
+ one slot.
302
+
303
+ This simplified definition of `lb-table` names two templates, `head` and
304
+ `foot`, and marks `<tbody>` as the slot. A page supplies whichever templates
305
+ it wants, and everything else it writes goes to the slot.
306
+
307
+ ```html
308
+ <!-- lb-table.html, the definition -->
309
+ <table>
310
+ <caption>
311
+ {{caption}}
312
+ </caption>
313
+ <thead lb-template="head"></thead>
314
+ <tbody lb-slot></tbody>
315
+ <tfoot lb-template="foot"></tfoot>
316
+ </table>
317
+ ```
318
+
319
+ When we use `lb-table` in HTML, the example below supplies the header
320
+ template and an unnamed template. The unnamed one names neither `head` nor
321
+ `foot`, so it goes to the slot. This usage writes no footer.
322
+
323
+ ```html
324
+ <!-- a page -->
325
+ <lb-table lb-list="staff" exp-caption="Everyone, by team">
326
+ <template lb-template="head">
327
+ <tr><th>Name</th><th>Role</th></tr>
328
+ </template>
329
+ <template lb-key="id">
330
+ <tr><td lb-cell="name"></td><td lb-cell="role"></td></tr>
331
+ </template>
332
+ </lb-table>
333
+ ```
334
+
335
+ When a named template is expanded, the `<template>` tag is discarded and its
336
+ contents are placed as children of the tag that names it. The built document
337
+ holds:
338
+
339
+ ```html
340
+ <!-- dist/app.html -->
341
+ <lb-table lb-list="staff" exp-caption="Everyone, by team"
342
+ ><table>
343
+ <caption>
344
+ Everyone, by team
345
+ </caption>
346
+ <thead>
347
+ <tr>
348
+ <th>Name</th>
349
+ <th>Role</th>
350
+ </tr>
351
+ </thead>
352
+ <tbody>
353
+ <template lb-key="id">
354
+ <tr>
355
+ <td lb-cell="name"></td>
356
+ <td lb-cell="role"></td>
357
+ </tr>
358
+ </template>
359
+ </tbody>
360
+ <tfoot></tfoot></table
361
+ ></lb-table>
362
+ ```
363
+
364
+ These are build errors:
365
+
366
+ - A definition with two templates of one name
367
+ - A definition with more than one `lb-slot`
368
+ - A page naming a template the definition does not have
369
+ - A page giving two templates for one name
370
+ - Content written in a widget whose definition has no slot
371
+
372
+ ### Binding
373
+
374
+ Binding attributes determine how the hub updates the DOM, either by
375
+ directly updating a plain HTML element or instructing
376
+ custom widgets to update themselves.
377
+
378
+
379
+ | Attribute | Assigned By | Behavior |
380
+ | ------------ | ----------- | ----------------------------------------------------------------------------------------------- |
381
+ | lb-list | Developer | Scopes DOM children to a named set of rows; a nested lb-list or lb-row begins a new scope |
382
+ | lb-row | Developer | Scopes DOM children to one named row; a nested lb-list or lb-row begins a new scope |
383
+ | lb-cell | Developer | This DOM node displays this column of the row in scope |
384
+ | lb-show | Developer | This DOM node is present when this column of the row in scope is neither null nor false |
385
+ | lb-key | Developer | Names the column that identifies a row, on the row template inside an lb-list |
386
+ | lb-key-value | Hub | Stamped on a live row: that row's value of `lb-key` |
387
+ | lb-value | Hub | The value that landed on a cell; a widget updates itself from it and a stylesheet selects on it |
388
+
389
+ #### How a value lands
390
+
391
+ A cell lands one of three ways, and the element decides which. Every cell
392
+ that receives a value also carries it as `lb-value`.
393
+
394
+ | Element | Receives the value as |
395
+ | -------------------------------------- | --------------------------------- |
396
+ | A custom element, tag hyphenated | Its `lb-value` attribute |
397
+ | `<select>`, `<textarea>`, or `<input>` | Its `value`, and `lb-value` |
398
+ | Any other native element | Its `textContent`, and `lb-value` |
399
+
400
+ A widget owns whatever control it wraps, so it is handed the value and
401
+ renders it itself. A form control shows its state as its `value`, so a
402
+ `<select>` keeps its options. Any other native element has no behavior of its
403
+ own, so its value is its text.
404
+
405
+ A form gathers from the same controls it lands on, so a value read back on
406
+ submit is the one that landed.
407
+
408
+ An `<input>` of type `checkbox`, `radio`, or `file` receives nothing, not even
409
+ `lb-value`, and the hub reports it to the console. A form gathering one skips it the same way.
410
+
411
+ A `<select>` with no option for the value shows no selection.
412
+
413
+ Only the hyphen and these three form controls are recognized. The hub holds
414
+ no other knowledge of any particular element, and the hyphen also decides
415
+ that the hub leaves a widget's own clicks alone — see [Requests](#requests).
416
+
417
+ Nothing an application writes ever sets `lb-value`. Loadbare writes it, and
418
+ a widget or a stylesheet reads it.
419
+
420
+ Loadbare also removes it, when a successful insert resets the cells it
421
+ gathered — see [The round trip](#the-round-trip). A widget reads a removed
422
+ `lb-value` as "nothing landed here" and returns its control to its default,
423
+ the way a form control with no `value` attribute shows its default. A blank
424
+ `lb-value` is a value like any other.
425
+
426
+ The value arrives as the query produced it, with no conversion, so the
427
+ browser decides what a non-string looks like.
428
+
429
+ A cell holds one value. A set of values is a list of its own, never an array
430
+ in a cell.
431
+
432
+ A widget sees `attributeChangedCallback` for an `lb-value` already present
433
+ when it upgrades, so a widget cannot tell a first landing from a refresh,
434
+ and does not need to.
435
+
436
+ #### Displaying by condition
437
+
438
+ `lb-show` names the column that decides whether an element is present.
439
+ `null` and `false` are off, and every other value is on. No string is read,
440
+ so `"false"` is on, and a query spells a condition as a boolean or a null.
441
+
442
+ ```html
443
+ <template lb-key="id">
444
+ <tr>
445
+ <td lb-cell="name"></td>
446
+ <td><button lb-action="lb-row-delete" lb-show="removable">Remove</button></td>
447
+ </tr>
448
+ </template>
449
+ ```
450
+
451
+ It binds as `lb-cell` does, to the row on the nearest scoped ancestor, and on
452
+ an element that is itself a scope the column belongs to the row around it. A row
453
+ that does not carry the column leaves the element as it is.
454
+
455
+ An element that is off is moved into a `<template lb-show="column">` standing
456
+ where it stood, and moved back out when the column turns on. It is not
457
+ rendered, focused, clicked, announced or gathered. It is moved and never
458
+ rebuilt, so a widget keeps its instance. Landing reaches into that template,
459
+ and nothing else does, so the element and every cell and scope inside it
460
+ return current. A custom element sees `disconnectedCallback` then
461
+ `adoptedCallback` going in, and `adoptedCallback` then `connectedCallback`
462
+ coming out, and still receives `lb-value` while it is away.
463
+
464
+ The builder ships every `lb-show` element already inside its template, so
465
+ nothing conditional shows until its row lands. An absent element's template
466
+ keeps its place among its siblings, and a position selector counts it.
467
+
468
+ These are build errors: `lb-show` on a row template's root, with no row
469
+ around it (outside every scope, on a scope with none around it, or in a list
470
+ scope outside its row template), or on a `<template>`.
471
+
472
+ Hiding is presentation, and the server still refuses what a request may not
473
+ do.
474
+
475
+ #### Master-detail
476
+
477
+ A master is a row and its detail is a list, as two names answered side by
478
+ side. A cell never holds rows.
479
+
480
+ ```html
481
+ <section lb-row="invoice">
482
+ <h2 lb-cell="number"></h2>
483
+ <span lb-cell="customer"></span>
484
+ </section>
485
+
486
+ <table>
487
+ <tbody lb-list="invoiceLines">
488
+ <template lb-key="id"><tr><td lb-cell="item"></td><td lb-cell="amount"></td></tr></template>
489
+ </tbody>
490
+ </table>
491
+ ```
492
+
493
+ Many masters, each with its own detail, is one list of joined rows: each
494
+ detail row carries its master's columns. A widget's `lbPlaceRow` groups them
495
+ for display, as `lb-table` builds sections and `lb-options` builds
496
+ `<optgroup>`s.
497
+
498
+ A list nested in another list's rows receives the same rows in every outer
499
+ row. That serves a picker offering the same choices on every row, and is
500
+ not a way to show a different detail per row.
501
+
502
+ Which master a page shows is a query parm, which a query reads off `ctx`
503
+ since it takes no argument from the browser — see [Query parms](#query-parms).
504
+
505
+ ### Requests
506
+
507
+ ---- UNEDITED ----
508
+
509
+ Any element can carry the `lb-action` attribute. The hub listens for
510
+ events `click` and `submit`, and fires a server request when it catches
511
+ one of those events.
512
+
513
+ The hub does not act on `click` or `submit` on a custom widget, they
514
+ must fire their own event. The assumption is a custom widget is present
515
+ because custom behavior is desired, and we don't want the hub to conflict
516
+ with that custom behavior. A widget is told apart by the hyphen in its tag
517
+ name, the same test that decides how a value lands — see
518
+ [How a value lands](#how-a-value-lands).
519
+
520
+ | lb-action | Written on |
521
+ | -------------- | ------------------------------------------------- |
522
+ | lb-row-insert | a `<form>`, or a button in a `<tr>`, in a list |
523
+ | lb-row-delete | anything inside a live row |
524
+ | lb-row-update | a `<form>`, a button in a live row, or a widget cell |
525
+ | anything else | must be a named routine in the page's server code |
526
+
527
+ The wire format is not visible to the user, but uses the same lb-*
528
+ attributes minus their prefix, so that it is intelligible when working on
529
+ Loadbare/app itself.
530
+
531
+ The hub scopes every request, whether a native element or a widget
532
+ dispatched it. It reads the scope from the dispatching element before any
533
+ ancestor sees the event, and never overwrites a field the request already
534
+ carries.
535
+
536
+ | `action` | Filled from scope | Required |
537
+ | ---------------- | ------------------------------ | ---------------------- |
538
+ | a declared name | `list` or `row`, `key`, `cell` | nothing |
539
+ | `lb-row-insert` | `list` | `list`, and `values` |
540
+ | `lb-row-delete` | `list`, `key` | both |
541
+ | `lb-row-update` | `list`, `key` | both, and `values` |
542
+
543
+ An element's own `lb-list` or `lb-row` names what it displays, never where
544
+ its request goes. A request belongs to the scope around the element, the
545
+ way a control belongs to the form around it. `list` or `row` comes from the
546
+ nearest ancestor scope, `key` from the nearest live row inside that scope,
547
+ and `cell` from the dispatching element's own `lb-cell`.
548
+ A request missing a required field is not sent. The hub never fills
549
+ `value`. An `lb-row-insert` or `lb-row-update` from an element carrying
550
+ `lb-cell` is a record of one cell, the way a control has a value and a form
551
+ has values: `values` holds that cell alone, taken from the `value` the widget
552
+ sent or else read from the control it is or wraps, and `value` is not sent
553
+ beside it. A widget that sends a `value` from an element carrying no
554
+ `lb-cell` is refused, since nothing names the column. Any other
555
+ `lb-row-insert` or `lb-row-update` gathers the row it belongs
556
+ to: `values` holds every `lb-cell` with a control to read in the nearest
557
+ `<form>`, `<tr>` or live row around the dispatching element, itself
558
+ included, inside its scope. The cells are found the way a row lands, so a
559
+ cell inside a scope nested in the row is that scope's and is not gathered,
560
+ while an element carrying a scope and `lb-cell` both is the row's cell.
561
+ This is a button's form owner: what else sits
562
+ beside the element never changes what is sent. A `<tr>` counts because a
563
+ form cannot go around a table row's controls, so a new row in a table is its
564
+ own form; a live row counts because it is the row the key names. The scope
565
+ itself is never the row, since its cells belong to other rows. A request
566
+ from an element in no form or row is not sent, and the hub reports that its
567
+ cells belong in a `<form>`. A request that already carries `values` keeps
568
+ them, and one whose row has no cell to read is not sent. A click on a
569
+ native element that carries either operation and is a cell or
570
+ holds cells is not sent, since clicking into one of its controls
571
+ would send the row: put the action on a form, on a button, or on a widget
572
+ that decides when its cell has changed.
573
+
574
+ A widget dispatches the action and, where it wraps a control, that control's
575
+ value.
576
+
577
+ ```
578
+ { action: "lb-row-insert", list: "rosterList", values: {...} }
579
+ { action: "lb-row-delete", list: "rosterList", key: "42" }
580
+ { action: "lb-row-update", list: "rosterList", key: "42", values: {...} }
581
+ { action: "lb-row-update", list: "rosterList", key: "42", values: { name: "Ann" } }
582
+ { action: "selectTab", row: "prefs", cell: "active_tab", value: "two" }
583
+ ```
584
+
585
+ #### The request event
586
+
587
+ The hub does not send a request the moment it catches a click or a submit.
588
+ It builds the request, dispatches it from the element that acted as a
589
+ bubbling `lb-request` event carrying the request as its `detail`, and a
590
+ listener on the hub sends it.
591
+
592
+ A widget uses that same event, and it is the only channel a widget has for
593
+ firing a request of its own. A native element and a hand-written widget
594
+ therefore produce identical events.
595
+
596
+ An ancestor sees the request on its way up and may stop it. The event is
597
+ not cancelable, so an interceptor calls `stopPropagation`, not
598
+ `preventDefault`. This is what makes a confirmation wrapper possible
599
+ without the wrapped element knowing about it.
600
+
601
+ #### The round trip
602
+
603
+ A request the hub sends has ten seconds to come back. Past that the hub
604
+ aborts it and treats it as a failure, since `fetch` imposes no deadline of
605
+ its own. An application cannot change the deadline.
606
+
607
+ A round trip fails on a server error, on a network failure, or on that
608
+ deadline, and all three set `lb-error` on the element that dispatched the
609
+ request — see [Request state](#request-state).
610
+
611
+ After a write succeeds, the cells it gathered show what the server holds.
612
+ An `lb-row-update` does this by landing the row on them. An
613
+ `lb-row-insert` does it by resetting them, the way `form.reset()` resets a
614
+ form, because the row they held now lives in the list. The hub resets
615
+ after it lands the response, and only what it gathered:
616
+
617
+ - Each control returns to its default: an `<input>` or `<textarea>` to its
618
+ `defaultValue`, a `<select>` to the options marked `selected`. A page
619
+ that wants a prefilled insert writes a `value` attribute, as in plain
620
+ HTML.
621
+ - A control showing something other than the value the hub read holds an
622
+ edit made during the round trip. That edit was not sent, and is left.
623
+ - `lb-value` comes off each reset cell, and off its control, before the
624
+ control resets — see [How a value lands](#how-a-value-lands).
625
+ - Values a widget supplied in the request were not gathered, and are not
626
+ reset.
627
+ - Nothing is dispatched, as `form.reset()` fires no `change`.
628
+
629
+ A failed insert resets nothing, so the entry can be corrected.
630
+
631
+ A navigation that fails to load its data sets nothing. No element
632
+ dispatched it, so there is nothing to stamp, and the hub reports it to the
633
+ console. A load started by a control writing a query parm stamps that
634
+ control, as a request stamps its origin.
635
+
636
+ ### Links
637
+
638
+ ---- UNEDITED ----
639
+
640
+ An anchor carrying `lb-nav-link` navigates inside the application: the hub
641
+ catches the click, pushes the anchor's path and query string onto history,
642
+ and swaps the page host in `<main>`. An anchor without it is left alone and behaves like any
643
+ other link, so leaving the application is the default and staying in it is
644
+ the opt-in.
645
+
646
+ | Attribute | Assigned By | Behavior |
647
+ | ----------- | ----------- | --------------------------------------------------------------------------------------- |
648
+ | lb-nav-link | Developer | On an `<a>`: the hub shows the page the anchor's path names, without loading a document |
649
+
650
+ The attribute takes no value. The hub looks for the nearest ancestor
651
+ link to determine the path.
652
+
653
+ Path space is flat. A path such as `/members` links to the `members.*` files
654
+ on the server.
655
+
656
+ The query string is kept. `/transactions?date_begin=2026-09-01` opens the
657
+ transactions page narrowed to those dates, and a link to the page it is
658
+ already on loads that page again at the new URL without replacing its DOM —
659
+ see [Query parms](#query-parms).
660
+
661
+ The bare path `/` resolves to `index`.
662
+
663
+ A path that names no page is detected in the browser, after a successful
664
+ 200: every route gets the same document, so there is no server-delivered
665
+ 404. A chrome that declares an `lb-unknown-page` dialog gets it opened.
666
+ One that declares none gets a console error and nothing on screen. See
667
+ [Chrome](#chrome).
668
+
669
+ ```html
670
+ <nav>
671
+ <a href="/" lb-nav-link>Home</a>
672
+ <a href="/members" lb-nav-link>Members</a>
673
+ <a href="https://example.com/docs">Docs</a>
674
+ </nav>
675
+ ```
676
+
677
+ See also [Navigation Row lb-navigation](#lb-navigation).
678
+
679
+ #### What a URL names
680
+
681
+ The path names a page, a place in the application. It never names a
682
+ resource, and Loadbare/app does not reproduce REST: `/accounts/42` is not a
683
+ way to reach account 42.
684
+
685
+ The query string describes what that page has on screen:
686
+ `/accounts?acct=23&from=2026-09-09`. It never commands, and the server
687
+ remembers nothing, so a URL is a reproducible view. Reload it, bookmark it,
688
+ or mail it to someone, and what they see is what the sender saw.
689
+
690
+ A page declares what it shows, its queries, and what it allows, its actions.
691
+ Loading a page runs its queries. A request performs an action at a position.
692
+ A row is addressed by `list` and `key`, taken from where the element sits,
693
+ which is how the database already names it. Nothing is fetched by URL, so an
694
+ application designs no endpoints.
695
+
696
+ That last is where query parms move Loadbare's position. A query still takes
697
+ no argument from the browser, and a parm arrives the way the session cookie
698
+ does, as part of the request the context is built from. But a user who
699
+ types `?acct=99999` now influences what a query returns. A query parm is
700
+ user input, validated like any other where the application reads it, and a
701
+ per-session database role means an id outside the caller's reach finds
702
+ nothing.
703
+
704
+ Loadbare is for applications, not sites. Every route is answered with the
705
+ same document, and a path that names no page is found out in the browser —
706
+ see [Links](#links).
707
+
708
+ #### Query parms
709
+
710
+ A control that narrows what a page shows writes its value into the query
711
+ string, rather than sending it:
712
+
713
+ ```html
714
+ <lb-options lb-list="teams" lb-query-parm="team" exp-label="Team:">
715
+ <option value="">Every team</option>
716
+ <template lb-key="id"><option lb-cell="name"></option></template>
717
+ </lb-options>
718
+ ```
719
+
720
+ | Attribute | Written by | Behavior |
721
+ | -------------------- | ---------- | --------------------------------------------------------------- |
722
+ | `lb-query-parm` | Developer | On a control: its `change` writes this parm and reloads the page |
723
+ | `lb-query-parm-push` | Developer | With `lb-query-parm`: the write pushes a history entry |
724
+
725
+ On `change`, the hub reads the control's value the way a form reads a cell,
726
+ and sets that one parm in the URL. Every other parm is left as it is, since
727
+ it may have arrived by link with no control on screen to say it again. An
728
+ empty value takes the parm out, so a URL is as long as the user has narrowed
729
+ the page. A value the URL already carries does nothing.
730
+
731
+ The write replaces the current history entry: changing what a page shows is
732
+ not going anywhere, so Back leaves the page rather than walking back through
733
+ every choice. `lb-query-parm-push` makes the write push an entry instead.
734
+
735
+ The page then loads at the new URL exactly as a cold load of that URL would:
736
+ `onPageEnter`, then every query. The page's DOM is kept, and the answer
737
+ lands by key, so a list that gets its rows back keeps them and its scroll
738
+ position.
739
+
740
+ After every load — cold, by link, by Back, by a write — the hub lands each
741
+ parm on the control that writes it, and an absent parm lands empty. A
742
+ control therefore shows what the address bar says.
743
+
744
+ A control that writes a query parm sends no request. One that also carries
745
+ `lb-action` has that request refused, since the choice would otherwise be
746
+ sent twice.
747
+
748
+ Every round trip carries the query string the browser is showing, a page
749
+ load and an action alike. The server reads the parms off `req.query` in
750
+ `contextFor` — see [The Express server](#the-express-server). Nothing in
751
+ Loadbare assigns a parm a meaning.
752
+
753
+ ### lb-navigation
754
+
755
+ ---- UNEDITED ----
756
+
757
+ Status: 1.0-RC.
758
+
759
+ Current page, published by the hub as a row. Can be bound anywhere just
760
+ like a server-produced result.
761
+
762
+ | Cell | Holds |
763
+ | ------------ | ------------------------------------------------------------------- |
764
+ | `page-label` | The text of the link to the current page, empty if no link names it |
765
+ | `page-uri` | The path and query string as the browser has them, such as `/members?team=Engines` |
766
+
767
+ The `page-uri` is taken from the current URL. The `page-label` is taken from the first
768
+ `lb-nav-link` link in the document (presumably in a nav bar) whose path matches the
769
+ current path, so the query string does not change it.
770
+
771
+ The row lands before the page is looked up, so a path that names no page has
772
+ it too, and an `lb-unknown-page` dialog, where the chrome has one, displays
773
+ it by naming the row like any other subtree.
774
+
775
+ It lands only where a subtree names it. A chrome that displays no
776
+ navigation is not warned about a row with nowhere to go.
777
+
778
+ ```html
779
+ <header lb-row="lb-navigation">
780
+ <h2 lb-cell="page-label"></h2>
781
+ </header>
782
+ ```
783
+
784
+ See also [Links](#links).
785
+
786
+ ## Application files
787
+
788
+ ### Chrome
789
+
790
+ The Loadbare/app builder always requires exactly one file
791
+ named `chrome.html`, located anywhere in builder `--src`.
792
+
793
+ The chrome is the application's single HTML document. This is
794
+ where the standard landmarks go: header, nav, footer, main.
795
+
796
+ The file is an HTML document, which must contain:
797
+ - `<lb-hub>` inside `<body>`. The hub can only act on its children,
798
+ that is why it is usually nested right below `<body>`.
799
+ - One empty `<main>` inside `<lb-hub>`.
800
+ - The script tag for `/client.js`, the javascript bundle.
801
+
802
+ It may also contain:
803
+ - `/app.css`, the stylesheet bundle
804
+ - A `<dialog>` marked with attribute `lb-unknown-page`, which the hub
805
+ will display to the user if an attempt is made to navigate to an
806
+ unknown page. It goes inside `<lb-hub>`, like everything the hub acts
807
+ on; the builder rejects one placed elsewhere.
808
+ - The `hidden` attribute on `<body>`, which the hub removes once the first page has landed.
809
+
810
+ ```html
811
+ <!-- src/chrome.html -->
812
+ <!doctype html>
813
+ <html lang="en">
814
+ <head>
815
+ <meta charset="utf-8" />
816
+ <title>Membership Roster</title>
817
+ <script src="/client.js" defer></script>
818
+ <link rel="stylesheet" href="/app.css" />
819
+ </head>
820
+ <body hidden>
821
+ <lb-hub>
822
+ <header lb-row="lb-navigation">
823
+ <h1>Membership Roster</h1>
824
+ <h2 lb-cell="page-label"></h2>
825
+ </header>
826
+ <nav>
827
+ <a href="/" lb-nav-link>Home</a>
828
+ <a href="/members" lb-nav-link>Members</a>
829
+ <a href="https://example.org/">Our website</a>
830
+ </nav>
831
+ <main></main>
832
+ <dialog lb-unknown-page lb-row="lb-navigation">
833
+ The URL <span lb-cell="page-uri"></span> is not in this app.
834
+ </dialog>
835
+ </lb-hub>
836
+ </body>
837
+ </html>
838
+ ```
839
+
840
+ The `lb-row`, `lb-cell` and `lb-nav-link` attributes in that example are
841
+ ordinary Loadbare/app binding. See [Binding](#binding),
842
+ [Links](#links) and [lb-navigation](#lb-navigation).
843
+
844
+ ### Pages
845
+
846
+ The page namespace is flat. Loadbare/app does not care where in the `--src`
847
+ the page files are located, but they must be unique across the application.
848
+ A page `/deep/path/to/mypage.html` is routed to `/mypage`.
849
+
850
+ Pages are grouped as
851
+ - <stub>.page.html is recognized as navigable and capable of
852
+ having associated queries and requests
853
+ - <stub>.queries.ts are the queries for a page
854
+ - <stub>.requests.ts respond to the requests from the browser
855
+
856
+ The <stub> value for a page must match the path used in links,
857
+ so that `<a href='/members' lb-nav-link>Members</a>` has matching
858
+ files `members.pages.html` et al.
859
+
860
+ #### Page HTML
861
+
862
+ The markup in `<stub>.page.html` is the same HTML as everywhere else
863
+ in a Loadbare/app application, see [HTML](#html).
864
+
865
+ The builder wraps each expanded page in `<template lb-page="<stub>">` and
866
+ puts it in the built document. The application never writes `lb-page`; the
867
+ hub reads it to find the page a path names.
868
+
869
+ ## The server
870
+
871
+ A page's server half is two files, `<stub>.queries.ts` and
872
+ `<stub>.requests.ts`. Each pairs with `<stub>.page.html` by sharing its
873
+ stub, as stated in [Pages](#pages). Either may be absent, and one with no
874
+ matching page is a build error.
875
+
876
+ ### The Express server
877
+
878
+ Loadbare/app ships no server. The application writes an ordinary Express
879
+ app. Express is a peer dependency, installed by the application.
880
+
881
+ The builder's artifacts oblige that server to handle four things: the
882
+ client script, the stylesheet, the hub's data channel, and the one HTML
883
+ document.
884
+
885
+ | Handle | With |
886
+ | ---------------------------- | ------------------- |
887
+ | `/client.js` | `dist/client.js` |
888
+ | `/app.css` | `dist/app.css` |
889
+ | `hubRoutes(hub, contextFor)` | The hub's own route |
890
+ | Every other GET | `dist/app.html` |
891
+
892
+ The builder writes `pages.ts` into `--out` whenever any page has a
893
+ `.queries.ts` or a `.requests.ts` file. It imports each of them, calls
894
+ `createHub()` once over the lot, and exports the result as `hub`. The
895
+ server imports that rather than maintaining the registry by hand.
896
+
897
+ ```ts
898
+ // server.ts
899
+ import path from "node:path";
900
+ import { readFileSync } from "node:fs";
901
+ import express, { type Request } from "express";
902
+ import { hubRoutes } from "@loadbare/app/express";
903
+ import type { HubContext } from "@loadbare/app/server";
904
+ import { hub } from "./dist/pages";
905
+ import { openDb } from "./src/database";
906
+
907
+ const DIST = path.resolve("dist");
908
+ const app = express();
909
+
910
+ // All data requests are handled by the hub
911
+ // This example shows an application-specific openDB()
912
+ function contextFor(_req: Request): HubContext {
913
+ return { db: openDb() };
914
+ }
915
+ app.use(hubRoutes(hub, contextFor));
916
+
917
+ // Static assets, then the catch-all for app.html
918
+ app.get("/client.js", (_req, res) => res.sendFile(path.join(DIST, "client.js")));
919
+ app.get("/app.css", (_req, res) => res.sendFile(path.join(DIST, "app.css")));
920
+ app.get(/.*/, (_req, res) =>
921
+ res.type("html").send(readFileSync(path.join(DIST, "app.html"), "utf-8")),
922
+ );
923
+
924
+ app.listen(8787);
925
+ ```
926
+
927
+ Explaining Express is beyond the scope of this technical reference. The
928
+ only real requirement is that the catch-all for app.html is at the end,
929
+ so it does not catch any other files.
930
+
931
+ `hubRoutes` answers `POST /lb/<page>`, with the query string the browser is
932
+ showing after it, verbatim. So `req.query` holds the page's query parms, and
933
+ `contextFor` puts on the context whatever a query reads from them. They are
934
+ user input:
935
+
936
+ ```ts
937
+ function contextFor(req: Request): HubContext {
938
+ const { acct } = req.query;
939
+ return { db: openDb(), acct: typeof acct === "string" ? acct : "" };
940
+ }
941
+ ```
942
+
943
+ Give the server the origin root. The hub reaches its own endpoints by
944
+ absolute path, so an application cannot be hosted under a subpath such as
945
+ `example.com/myapp/`, and anything proxying in front of the server passes
946
+ the whole path space through unchanged.
947
+
948
+ ### Page Queries
949
+
950
+ `<stub>.queries.ts` exports one object named `queries`, typed `Queries`.
951
+ Each key is a query name, and the markup binds to that name through
952
+ `lb-list` or `lb-row`.
953
+
954
+ Cardinality belongs to the named query. One named query always returns a single
955
+ row or an array of rows. Build a query using `row()` or `list()` to return
956
+ the two shapes. A query cannot be built without one of these functions.
957
+
958
+ Queries names cannot begin with `lb-`, that namespace is reserved for
959
+ Loadbare/app queries the hub makes available in the browser, such
960
+ as [lb-navigation](#lb-navigation).
961
+
962
+ ```ts
963
+ // members.queries.ts
964
+ import { list, row, type Queries } from "@loadbare/app/server";
965
+
966
+ export const queries: Queries = {
967
+ roster: list((ctx) => ctx.db.members()),
968
+ summary: row(async (ctx) => ({ count: String(await ctx.db.memberCount()) })),
969
+ };
970
+ ```
971
+
972
+ Every query and every request receives `ctx`, the application's own request
973
+ context. The application builds it once per request and hands it to the hub,
974
+ see [The Express server](#the-express-server). Loadbare/app declares it empty
975
+ and never reads it, so a query finds exactly what the application put there,
976
+ which is usually a database handle opened for the authenticated caller.
977
+
978
+ Queries should not return deltas, deltas are handled in the `.requests.ts` file
979
+ as explained in the next section.
980
+
981
+ ### Page Requests
982
+
983
+ `<stub>.requests.ts` exports one object named `requests`, typed `Requests`.
984
+ It has three optional keys.
985
+
986
+ | Key | Keyed by | Answers |
987
+ | ------------- | --------------------- | -------------------------------------------------- |
988
+ | `actions` | The `lb-action` value | Anything the page chooses to declare |
989
+ | `crud` | A query name | The three reserved `lb-action` values |
990
+ | `onPageEnter` | Nothing | Runs once on entering the page, before its queries |
991
+
992
+
993
+ The hub resolves an `lb-action` value against `actions`, and a reserved
994
+ operation against `crud` under the bound list name. A name with no entry
995
+ there logs a server console warning naming the page and the missing entry,
996
+ and answers 200 with `{}`. The browser applies `{}`, so no cell is set and
997
+ no row is added or removed, and `lb-pending` clears from the element that
998
+ sent the request. A request carrying no action answers 400, and a `run`
999
+ that throws answers 500.
1000
+
1001
+ Every entry under `actions` and `crud` has the same two members. `run`
1002
+ performs the work, and `refresh` names the queries to re-run once the action
1003
+ is complete.
1004
+
1005
+ `run` receives the same `ctx` and a `where`: the binding in scope when the
1006
+ interaction happened, as `list`, `row`, `key`, `cell` and `value`. Under
1007
+ `crud` the `where` carries only what that operation is typed to carry.
1008
+
1009
+ `run` may also return results of its own, which are laid over the refreshed
1010
+ ones. That is how a delta reaches the browser: wrap it in `patch()`, naming
1011
+ the rows that arrived or changed and the keys that went.
1012
+
1013
+ ```ts
1014
+ // members.requests.ts
1015
+ import { patch, type Requests } from "@loadbare/app/server";
1016
+
1017
+ export const requests: Requests = {
1018
+ actions: {
1019
+ resetRoster: {
1020
+ run: (ctx) => ctx.db.resetMembers(),
1021
+ refresh: ["roster"],
1022
+ },
1023
+ },
1024
+ crud: {
1025
+ roster: {
1026
+ rowDelete: {
1027
+ run: async (ctx, where) => {
1028
+ await ctx.db.deleteMember(where.key);
1029
+ return { roster: patch({ drop: [where.key] }) };
1030
+ },
1031
+ refresh: [],
1032
+ },
1033
+ },
1034
+ },
1035
+ };
1036
+ ```
1037
+
1038
+ Each key under a `crud` entry is a reserved `lb-action` value with its prefix
1039
+ stripped and the rest camel-cased, so the attribute, the wire and this key
1040
+ are one vocabulary. All three operate on a list, because each needs a key and
1041
+ a key exists only on a live row.
1042
+
1043
+ | `lb-action` | Key under `crud` | `where` carries |
1044
+ | ---------------- | ---------------- | ---------------------- |
1045
+ | `lb-row-delete` | `rowDelete` | `key` |
1046
+ | `lb-row-insert` | `rowInsert` | `values` |
1047
+ | `lb-row-update` | `rowUpdate` | `key`, `values` |
1048
+
1049
+ A `rowUpdate` sets the columns `values` names and leaves the rest as they
1050
+ are, as an SQL `UPDATE` does. A form sends the cells it holds and a widget
1051
+ cell sends itself, so one handler answers both.
1052
+
1053
+ ## Widgets
1054
+
1055
+ ---- UNEDITED ----
1056
+
1057
+ A widget is an HTML custom element following these fixed rules:
1058
+ - Element name must contain a hyphen, as per hTML rules, like <my-custom-element>
1059
+ - HTML code, if present, is `my-custom-element.html`
1060
+ - TS code, if present, is in `my-custom-element.browser.ts`. The
1061
+ 'browser' segment is a safety feature, requiring the file to be
1062
+ explicitly named as a browser file, to help prevent unfortunate
1063
+ naming collisions where a server file happens to have the name of
1064
+ a widget and gets built into the browser bundle.
1065
+
1066
+ A widget must have either one or the other of HTML and Typescript, and
1067
+ it may have both. If it has neither, the builder reports an error.
1068
+
1069
+ To use a widget from a library, add the library to `imports.ts` anywhere
1070
+ in the builders `--src`:
1071
+
1072
+ ```ts
1073
+ export default ["@scope/library-name"];
1074
+ ```
1075
+
1076
+ ### Widget authoring
1077
+
1078
+ ---- UNEDITED ----
1079
+
1080
+ The expansion grammar, `exp-`, `lb-slot`, `lb-template`, the reserved `LB-`
1081
+ namespace, then `lbPlaceRow` and `lbRowsLanded`. One bucket spanning build
1082
+ time and run time, because a widget is a definition and a script together.
1083
+
1084
+ #### Request state
1085
+
1086
+ The hub stamps these on the element that dispatched a request, which is the
1087
+ widget itself when a widget fired it. A widget observes them and reacts; it
1088
+ must name them in `observedAttributes` to see them change.
1089
+
1090
+ | Attribute | Written on | Holds |
1091
+ | -------------- | ----------------------- | --------------------------------------------------------------------------------------- |
1092
+ | `lb-pending` | The dispatching element | The round trip is in flight |
1093
+ | `lb-error` | The dispatching element | The last round trip failed, cleared on the next — see [The round trip](#the-round-trip) |
1094
+ | `lb-row-count` | A list scope | How many rows the scope is showing |
1095
+
1096
+ They are also stamped on plain HTML, where a stylesheet is the only
1097
+ consumer: dim a pending button, mark a failed one, and style an empty list
1098
+ against `lb-row-count` rather than carrying an empty-state element.
1099
+
1100
+ The hub reads one of them. A native `lb-action` element, a button or a
1101
+ form, that is performed again while it carries `lb-pending` is ignored: the
1102
+ click or submit is cancelled and nothing is sent. A pending native action is
1103
+ therefore disabled in fact, and a stylesheet only has to show it. A widget is
1104
+ not held back this way, because a widget that sends on change must have its
1105
+ latest value sent rather than dropped.
1106
+
1107
+ Alongside `lb-pending` the hub sets `aria-busy="true"` on the same element and
1108
+ removes it when the round trip settles, so assistive technology hears the
1109
+ state a stylesheet shows.
1110
+
1111
+ #### Row hooks
1112
+
1113
+ | Name | Implemented By | Behavior |
1114
+ | ------------------------------- | -------------- | ----------------------------------------------------------- |
1115
+ | `applyRow(root, row)` | Loadbare | Fills one scope from one row |
1116
+ | `lbPlaceRow(el, row, template)` | Developer | Optional on a list scope: where a row goes |
1117
+ | `lbRowsLanded()` | Developer | Optional on a list scope: scaffolding derived from the rows |
1118
+
1119
+ ## Internal linkage
1120
+
1121
+ ---- UNEDITED ----
1122
+
1123
+ The endpoint path, `index` for the bare path, the constant holding the
1124
+ request deadline, the generated client entry, and the `./build` export.
1125
+ Nobody outside this package reads any of it. The deadline itself is
1126
+ behavior an application sees — see [The round trip](#the-round-trip) — and
1127
+ giving it a knob later takes nothing away.
1128
+
1129
+ ## Cross-reference
1130
+
1131
+ Every name Loadbare/app owns, in one place. Each row names the section
1132
+ that defines it. The definition lives there and only there.
1133
+
1134
+ ### Builder flags
1135
+
1136
+ Loadbare/app owns every flag in this table.
1137
+
1138
+ | Flag | Default | Names | Defined in |
1139
+ | ---------- | ------- | ----------------------------------------- | ------------------------------------------------- |
1140
+ | `--src` | `src` | The application's own tree, scanned whole | [What the builder scans](#what-the-builder-scans) |
1141
+ | `--out` | `dist` | Where the builder writes | [Running the builder](#running-the-builder) |
1142
+ | `--watch` | off | Rebuild on change under `--src` | [Running the builder](#running-the-builder) |
1143
+ | `--minify` | off | Minify `client.js` and `app.css` | [Running the builder](#running-the-builder) |
1144
+
1145
+ ### Reserved file names
1146
+
1147
+ Loadbare/app owns every file name and pattern in this table. A file so
1148
+ named carries its meaning wherever it sits in the `--src` tree, so an
1149
+ application must not use one of these names for anything else.
1150
+
1151
+ | File | How many | Holds | Defined in |
1152
+ | ----------------------- | ---------------------- | -------------------------------------------------------- | ------------------------------------------------------- |
1153
+ | `chrome.html` | Exactly one | The application's one HTML document | [Chrome](#chrome) |
1154
+ | `imports.ts` | Zero or one | Default-exports an array of widget library package names | [Imported widget libraries](#imported-widget-libraries) |
1155
+ | `<stub>.page.html` | One per page | One page's markup, as a fragment | [Pages](#pages) |
1156
+ | `<stub>.queries.ts` | Zero or one per page | That page's queries | [Page Queries](#page-queries) |
1157
+ | `<stub>.requests.ts` | Zero or one per page | That page's requests | [Page Requests](#page-requests) |
1158
+ | `<tag-name>.html` | Zero or one per widget | One widget definition, as a fragment | [Widgets](#widgets) |
1159
+ | `<tag-name>.browser.ts` | Zero or one per widget | One widget's script, `.js` also accepted | [Widgets](#widgets) |
1160
+ | `*.css` | Any number | A stylesheet, concatenated into `app.css` | [Running the builder](#running-the-builder) |
1161
+
1162
+ ### Build outputs
1163
+
1164
+ Loadbare/app owns every file name in this table. The builder writes them
1165
+ into `--out`. Which of them exist depends on the application: `app.css`
1166
+ only when some origin has a stylesheet, `pages.ts` only when some page has a
1167
+ `.queries.ts` or a `.requests.ts`.
1168
+
1169
+ | Path | Written | Holds | Read by | Defined in |
1170
+ | ------------------ | ----------------- | ------------------------------------------------------------------ | ---------- | ------------------------------------------- |
1171
+ | `/app.html` | Always | The chrome, built | The server | [Chrome](#chrome) |
1172
+ | `/client.js` | Always | `<lb-hub>` and every widget the application uses | The chrome | [Chrome](#chrome) |
1173
+ | `/client-entry.ts` | Always | The generated entry `client.js` is bundled from | Nothing | [Running the builder](#running-the-builder) |
1174
+ | `/app.css` | With a stylesheet | Every stylesheet in the cascade, concatenated | The chrome | [Chrome](#chrome) |
1175
+ | `/pages.ts` | With page data | The one `createHub()` call, over every page's queries and requests | The server | [The Express server](#the-express-server) |
1176
+
1177
+ ### Reserved package.json keys
1178
+
1179
+ Loadbare/app owns every key in this table. A widget library declares them.
1180
+ An application never does.
1181
+
1182
+ | Key | Value | Means | Defined in |
1183
+ | ------------------ | ------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------- |
1184
+ | `loadbare.widgets` | A path inside the package | Where this package's widgets are. Absent means the whole installed directory | [Imported widget libraries](#imported-widget-libraries) |
1185
+
1186
+ ### Reserved namespaces
1187
+
1188
+ Loadbare/app owns every namespace in this table. Ownership of a name and
1189
+ ownership of its behavior are stated separately, because they differ.
1190
+
1191
+ | Namespace | Applies to | Loadbare owns | Defined in |
1192
+ | --------- | -------------------------- | -------------------------------------------- | ----------------------------------------------- |
1193
+ | `lb-*` | HTML attributes | The names and their behavior | [The lb-* namespace](#the-lb--namespace) |
1194
+ | `exp-*` | HTML attributes | The behavior; widget authors pick the values | [Build time parameters](#build-time-parameters) |
1195
+ | `lb*` | Methods on custom elements | The names and their behavior | [Row hooks](#row-hooks) |
1196
+
1197
+ #### The lb-* namespace
1198
+
1199
+ Every HTML attribute beginning with `lb-` belongs to Loadbare/app. An
1200
+ application writes the ones this table names and invents none of its own,
1201
+ because a name Loadbare has not defined today it may define tomorrow.
1202
+
1203
+ The prefix reaches past HTML. A query name cannot begin with `lb-` either,
1204
+ which is what keeps the three operations apart from an application's own
1205
+ actions on the wire — see [Page Queries](#page-queries).
1206
+
1207
+ Each attribute is defined in one section, and this table says which.
1208
+
1209
+ | Attribute | Written by | Defined in |
1210
+ | ----------------- | ------------- | --------------------------------------------------- |
1211
+ | `lb-list` | Developer | [Binding](#binding) |
1212
+ | `lb-row` | Developer | [Binding](#binding) |
1213
+ | `lb-cell` | Developer | [Binding](#binding) |
1214
+ | `lb-show` | Developer | [Displaying by condition](#displaying-by-condition) |
1215
+ | `lb-key` | Developer | [Binding](#binding) |
1216
+ | `lb-key-value` | Hub | [Binding](#binding) |
1217
+ | `lb-value` | Hub | [How a value lands](#how-a-value-lands) |
1218
+ | `lb-action` | Developer | [Requests](#requests) |
1219
+ | `lb-nav-link` | Developer | [Links](#links) |
1220
+ | `lb-query-parm` | Developer | [Query parms](#query-parms) |
1221
+ | `lb-query-parm-push` | Developer | [Query parms](#query-parms) |
1222
+ | `lb-pending` | Hub | [Request state](#request-state) |
1223
+ | `lb-error` | Hub | [Request state](#request-state) |
1224
+ | `lb-row-count` | Hub | [Request state](#request-state) |
1225
+ | `lb-unknown-page` | Developer | [Chrome](#chrome) |
1226
+ | `lb-slot` | Widget author | [Slots and templates](#slots-and-templates) |
1227
+ | `lb-template` | Widget author | [Slots and templates](#slots-and-templates) |
1228
+ | `lb-page` | Builder | [Pages](#pages) |
1229
+
1230
+ An attribute the builder does not recognize is left alone today. Refusing
1231
+ one is on the list of validations still to land — see
1232
+ [The builder](#the-builder).
1233
+
1234
+ ### Reserved tags
1235
+
1236
+ Loadbare/app owns every tag in this table. An application writes them and
1237
+ never defines them.
1238
+
1239
+ | Tag | Written in | Means | Defined in |
1240
+ | ---------- | ---------- | ------------------------------ | ----------------- |
1241
+ | `<lb-hub>` | The chrome | The application's live element | [Chrome](#chrome) |
1242
+
1243
+ ### Reserved events
1244
+
1245
+ Loadbare/app owns every DOM event in this table, both the name and what its
1246
+ `detail` carries.
1247
+
1248
+ | Event | Dispatched from | Bubbles | Cancelable | Defined in |
1249
+ | ------------ | ---------------------- | ------- | ---------- | --------------------------------------- |
1250
+ | `lb-request` | The element that acted | Yes | No | [The request event](#the-request-event) |
1251
+
1252
+ ### Reserved attributes
1253
+
1254
+ Loadbare/app owns every attribute in this table, both the name and its
1255
+ behavior.
1256
+
1257
+ | Attribute | Written on | Takes a value | Defined in |
1258
+ | ----------------- | ----------------------------------- | ------------- | ----------------- |
1259
+ | `lb-unknown-page` | A `<dialog>` in chrome | No | [Chrome](#chrome) |
1260
+ | `lb-page` | A `<template>`, by the builder only | Yes | [Pages](#pages) |