@loadbare/app 0.18.0 → 0.18.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.
@@ -96,7 +96,7 @@ export const queries: Queries = {
96
96
 
97
97
  | Declared with | Answers with |
98
98
  |----------------------------|-------------------------------------------|
99
- | `row(key, run)` | One row, an object |
99
+ | `row(key, run)` | One row, an object, or `null` for none |
100
100
  | `rows(key, run, options)` | All its rows, an array of objects |
101
101
 
102
102
  `options` are a `rows` query's order and column types. `order` is the
@@ -317,6 +317,7 @@ queries:
317
317
  | Answer | States |
318
318
  |--------------------------|---------------------------------------------|
319
319
  | `{ ... }` | The row of a `row` query |
320
+ | `null` | A `row` query found no row |
320
321
  | `[...]` | All rows of a `rows` query, and their order |
321
322
  | `patch({ rows: [...] })` | These rows arrive or change; the rest stand |
322
323
  | `patch({ drop: [...] })` | These keys are gone; the rest stand |
@@ -1,8 +1,8 @@
1
1
  # The Basic Widget Library
2
2
 
3
- Eleven widgets: `lb-input`, `lb-select`, `lb-options`, `lb-picker`,
3
+ Thirteen widgets: `lb-input`, `lb-select`, `lb-options`, `lb-picker`,
4
4
  `lb-table`, `lb-unknown-page`, `lb-confirm`, `lb-form`, `lb-insert-dialog`,
5
- `lb-master` and `lb-checkbox`. Every one of them is an ordinary custom element, written
5
+ `lb-master`, `lb-checkbox`, `lb-number` and `lb-date`. Every one of them is an ordinary custom element, written
6
6
  against the contracts in [Custom Elements](./custom-elements.md#html) for its
7
7
  definition and [Custom Elements](./custom-elements.md#code) for its class.
8
8
 
@@ -258,6 +258,28 @@ checked. The hub sets a native checkbox's `checked` directly.
258
258
  | `exp-label` | The label's text |
259
259
  | `exp-request` | The checkbox's `lb-request`, none when absent |
260
260
 
261
+ ## `lb-number` and `lb-date`
262
+
263
+ A number shown by the browser's `Intl.NumberFormat`, and a date or time
264
+ shown by `Intl.DateTimeFormat`. Neither is a control: `lb-column` goes on
265
+ the tag, and the widget renders the `lb-column-value` the hub stamps; see
266
+ [Receiving a value](./custom-elements.md#receiving-a-value).
267
+
268
+ ```html
269
+ <lb-number lb-column="balance" exp-style="currency" exp-currency="USD"></lb-number>
270
+ <lb-date lb-column="due" exp-date-style="medium"></lb-date>
271
+ ```
272
+
273
+ Each parameter is one of Intl's options in kebab-case, as `exp-currency-sign`
274
+ for `currencySign`, plus `exp-locale`. A value that is no number or date
275
+ shows as it arrived, and a date alone, `YYYY-MM-DD`, shows as that day in
276
+ every time zone. The stamp keeps the raw value, so a stylesheet selects on
277
+ it:
278
+
279
+ ```css
280
+ lb-number[lb-column-value^="-"] { color: var(--negative); }
281
+ ```
282
+
261
283
  ## `lb-unknown-page`
262
284
 
263
285
  The chrome's dialog for a URL that names no page, as one tag:
package/docs/roadmap.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Roadmap
2
2
 
3
- Release 1.0 is optimized to prove that a Loadbare app can be robust and
4
- highly performant with a codebase that has very low accidental complexity.
3
+ Release 1.0 is optimized to prove that a Loadbare app holds a median 300ms
4
+ round trip from click to paint. See [Theory](./theory.md).
5
5
 
6
6
  What follows is everything not yet decided or not yet built: release
7
7
  candidates, risks not worth solving speculatively, and open design
@@ -30,13 +30,6 @@ read from a query parm. Whether a response may say the server already
30
30
  sorted it is open: the server may not sort as the browser does, and for a
31
31
  thousand rows the hub's sort is a small cost.
32
32
 
33
- ### Nesting
34
-
35
- Whether a live row may hold rows of its own detail (master-detail, an
36
- expanding row). [Theory](./theory.md) already flags this as possibly load-bearing if
37
- disallowed. Worth a decision-in-principle the first time a master-detail
38
- page is built, even before the mechanism is needed elsewhere.
39
-
40
33
  ### Events while a request is in flight
41
34
 
42
35
  The hub ignores a commit on an element carrying `lb-request-pending`.
@@ -90,6 +83,9 @@ If the hub were configured with a URL for the data channel, the entire app
90
83
  could be delivered from a static origin, such as an S3 bucket or static web
91
84
  server.
92
85
 
86
+ `<lb-hub>` reads no attributes, so the data channel's URL, and a timeout an
87
+ application outgrows, can arrive as attributes on it.
88
+
93
89
  ### Tree-shaking CSS by tag name
94
90
 
95
91
  Release 1.0 concatenates every `.css` file under `src`, and every one inside
@@ -110,6 +106,18 @@ Most valuable against a large imported widget library, where an app uses a
110
106
  small fraction of what ships. Revisit when a real app's `app.css` is big
111
107
  enough to measure.
112
108
 
109
+ ### HTML validation in the build
110
+
111
+ A page can be well formed and still break HTML's rules for what an element
112
+ may hold, such as a `<div>` inside a `<p>`. The parser rearranges it
113
+ silently, the same way at build time and in the browser. The builder could
114
+ run html-validate on the source or the expanded output, as an opt-in.
115
+
116
+ ### A whole row for a custom element
117
+
118
+ A column lands one element at a time. An optional `lbAcceptRow` beside
119
+ `lbRowsLanded` would hand a custom element its whole row.
120
+
113
121
  ### Options on `createHub`
114
122
 
115
123
  Global hooks, transaction wrapping, error handling and a CSRF token each
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@loadbare/app",
3
3
  "description": "High performance web app framework for server-bound applications",
4
- "version": "0.18.0",
4
+ "version": "0.18.2",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
@@ -155,7 +155,8 @@ answers with a constant one.
155
155
  **There is no template language.** No `{#if}`, no `v-for`, no `map()`. A
156
156
  set of rows is an element carrying `lb-query` with a `<template>` inside it,
157
157
  the row template, which the hub clones once per row. A `row` query with no
158
- row template lands on the element itself. A `rows` query with no row
158
+ row template lands on the element itself, and one answering `null` empties
159
+ every column there and removes its `lb-key-value`. A `rows` query with no row
159
160
  template lands nothing, which is what an insert form naming its query is. A
160
161
  condition is `lb-show="<column>"`: write every possibility into the page and
161
162
  let a column decide which is present. The column is a boolean or null; the
@@ -244,8 +245,11 @@ row. Anything else gathers its form owner's controls. A `<form>` cannot
244
245
  wrap a `<tr>`, so an insert row in a table puts its controls in a form
245
246
  elsewhere with the HTML `form` attribute. A checkbox is a control: checked
246
247
  unless its column is `false` or null, and gathered as `"true"` or `"false"`.
247
- Radio buttons and file inputs are not controls here: they receive no value
248
- and are not gathered.
248
+ A radio button is a control: checked when its `value` is the column's
249
+ value, and the group gathers the one checked, or `""`. In a row template
250
+ the hub stamps its `name` with the row's key, so each row is its own group.
251
+ File inputs are not controls here: they receive no value and are not
252
+ gathered.
249
253
 
250
254
  **A form that adds to a set chosen elsewhere is written once, outside every
251
255
  row.** A form cannot sit inside another form or in a table row, so a dialog
@@ -331,7 +335,8 @@ reason. A list shared by several handlers is a warning sign.
331
335
  one page's elements in the form they show: formatted dates and amounts, and
332
336
  the words a reader sees, such as "12 postings". The hub does no type
333
337
  conversion, so what a number, date or null looks like is decided on the
334
- server. A value the page reads back stays data: a column on a control, the
338
+ server, or by `lb-number` and `lb-date`, which format a raw value with
339
+ `Intl`. Send a column the user sorts or filters raw. A value the page reads back stays data: a column on a control, the
335
340
  key, an `lb-show` condition, a value a stylesheet selects on. What pages
336
341
  share goes beneath them, in a database view or a helper module, never in a
337
342
  layer between the page and its queries.
@@ -454,7 +459,8 @@ own, or to act on rows once they land through the hook the hub calls,
454
459
 
455
460
  Before writing one, check `@loadbare/widgets`: `lb-input`, `lb-select`,
456
461
  `lb-options`, `lb-picker`, `lb-table`, `lb-unknown-page`, `lb-confirm`,
457
- `lb-form`, `lb-insert-dialog`, `lb-master`, `lb-checkbox`. The first four are
462
+ `lb-form`, `lb-insert-dialog`, `lb-master`, `lb-checkbox`, `lb-number`,
463
+ `lb-date`. The first four are
458
464
  form-associated controls that carry `lb-column` and `lb-request`
459
465
  themselves. Install the package and list it in `src/imports.ts`:
460
466
 
@@ -13,42 +13,6 @@ that they can be decided later w/o breaking changes, then we rename
13
13
  them to "Out of Scope" or move them to the roadmap. Otherwise,
14
14
  they must be resolved before we declare 1.0.
15
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 a control, and by whatever the custom element does with
23
- `lb-column-value` when it lands on one that is not a control.
24
-
25
- Three places read a value's type today. The [order](#order) compares
26
- values by their JSON type, a [filter](#filters) reads its value as the
27
- column's type, and an [aggregate](#aggregates) reads a sum or an average
28
- from a JSON number or a string holding a plain decimal. A `rows` query may
29
- declare a column's type, which the order, a filter, `lb-min` and `lb-max`
30
- read the column as in place of its JSON type. No declared type changes how
31
- a value renders.
32
-
33
- We will then see if a useful solution emerges that Loadbare/app should
34
- handle.
35
-
36
- ### Form controls
37
-
38
- - **Radio buttons are not implemented.** A value does not land on one and
39
- the hub does not gather one. A radio group is several elements answering
40
- to one column.
41
-
42
- ### Custom element hooks
43
-
44
- Loadbare/app owns every method name beginning with `lb` on a custom element.
45
- That decision is firm; what the set contains is not.
46
-
47
- - **Decide whether a custom element may receive a whole row.** A column
48
- lands one element at a time and offers no escape hatch. An optional
49
- `lbAcceptRow` would sit beside `lbRowsLanded`, and this is the most likely
50
- first request from a custom element author.
51
-
52
16
  ### Nested queries
53
17
 
54
18
  Imagine an HTML `<select>` that shows a fixed set of choices on every row of
@@ -83,18 +47,8 @@ But the allowed values may depend on other values in the row.
83
47
  repository with several applications gets one per application. `--src`
84
48
  stays a flag, since it is what locates the file. State the precedence
85
49
  between a flag and a key once. Its key names join the permanent surface,
86
- so this lands before 1.0 or not at all.
87
- - **Decide CSS pairing.** Naming a stylesheet `<tag>.css` beside its
88
- definition would let the builder ship only what survives expansion.
89
- Recommend deferring the mechanism and reserving the configuration key, so
90
- that it lands as an opt-in rather than as a silent drop.
91
- - **Decide whether the build validates HTML.** A page can be well formed and
92
- still break HTML's rules for what an element may hold, such as a `<div>`
93
- inside a `<p>`. The parser rearranges it silently, the same way at build
94
- time and in the browser, so nothing reports it. Checking those rules is a
95
- validator's job, and html-validate is one the builder could run on the
96
- source or on the expanded output. Deciding it after 1.0 is free if it
97
- lands as an opt-in, and it may need a configuration key.
50
+ so this lands before 1.0 or not at all. It reserves a key for the
51
+ roadmap's tree-shaking CSS by tag name.
98
52
  - **Confirm the three output names.** `app.html`, `client.js` and `app.css`
99
53
  are about to be fixed in `staticRoutes` as well as in the builder.
100
54
 
@@ -103,16 +57,15 @@ But the allowed values may depend on other values in the row.
103
57
  None of this is surface. It is listed here because trimming it is free
104
58
  today and breaking after 1.0.
105
59
 
106
- - **Audit what the hub does.** It appears to have routines that go beyond
107
- what the `lb-*` attribute namespace specifies.
60
+ - **Audit what the hub does.** Every routine it runs answers to an `lb-*`
61
+ attribute, a [reserved query](#reserved-queries), or a
62
+ [reserved event](#reserved-events) or [method](#reserved-methods). Trim
63
+ whatever does not, and leave what stays clean: no dead code, no routine
64
+ written twice, no comment the code contradicts.
108
65
  - **Keep the rest as constants.** The endpoint, `index` and the timeout are
109
66
  linkage between the builder and the hub rather than facts an application
110
67
  varies. Adding a knob for any of them buys a second shape of application
111
68
  to reason about.
112
- - **Give the hub a configuration channel without using it.** `<lb-hub>` reads
113
- no attributes at all, which is the free landing spot for the roadmap's
114
- static-origin split and for a timeout an application eventually outgrows.
115
- Reserve the idea, build nothing.
116
69
 
117
70
  ## The build
118
71
 
@@ -220,7 +173,8 @@ it is sent to the browser on initial page load, no HTML is ever sent
220
173
  again.
221
174
 
222
175
  The developer writes `lb-*` attributes in markup. Script never assigns
223
- them. The hub and the builder write only their stamps. See
176
+ them. The hub and the builder write only their stamps, and the hub writes
177
+ the `name` of a radio button in a row template. See
224
178
  [The markup checks](#the-markup-checks).
225
179
 
226
180
  ### Expansion
@@ -457,8 +411,8 @@ unless its name begins with `lb-`.
457
411
  | Any other element | Its text content |
458
412
 
459
413
  A control is an `<input>`, `<select>` or `<textarea>`, or a form-associated
460
- custom element with a `value` property that fires `change`. A checkbox is a
461
- control too, by the rule below. A
414
+ custom element with a `value` property that fires `change`. A checkbox and
415
+ a radio button are controls too, by the rules below. A
462
416
  form-associated custom element declares `static formAssociated = true`.
463
417
 
464
418
  The hub stamps `lb-column-value` with the value on every element it sets.
@@ -481,9 +435,18 @@ attribute means nothing to the hub.
481
435
  <input type="checkbox" lb-column="active" lb-request="lb-row-update" />
482
436
  ```
483
437
 
484
- An `<input>` of type `radio` or `file` receives nothing, not even
485
- `lb-column-value`, and the hub reports it to the console. The hub does not
486
- gather one either.
438
+ A radio button's state is `checked` as well. The hub checks it when its
439
+ `value` attribute equals the value as a string, and unchecks it otherwise,
440
+ `null` included. Of the radio buttons naming one column, the hub gathers
441
+ the `value` of the one checked, or `""` when none is.
442
+
443
+ ```html
444
+ <input type="radio" lb-column="shift" value="day" lb-request="lb-row-update" />
445
+ <input type="radio" lb-column="shift" value="night" lb-request="lb-row-update" />
446
+ ```
447
+
448
+ An `<input>` of type `file` receives nothing, not even `lb-column-value`,
449
+ and the hub reports it to the console. The hub does not gather one either.
487
450
 
488
451
  A `<select>` with no option for the value shows no selection.
489
452
 
@@ -509,6 +472,10 @@ landed inside another query, such as a total in a table's foot, is not one
509
472
  of that query's rows. A stylesheet or a custom element selects live rows
510
473
  with `[lb-row-live]`, and never with `[lb-key-value]`.
511
474
 
475
+ A `row` query answering `null` lands every column its element names as
476
+ null, and the element loses `lb-key-value`. Under a row template, its live
477
+ row leaves.
478
+
512
479
  A key is unique within a query. Two rows with one key in the same answer,
513
480
  or two live rows showing one key, are reported on the console.
514
481
 
@@ -1583,6 +1550,26 @@ whose name begins with `lb` and a capital and is not in
1583
1550
  Script never assigns an `lb-` attribute, so the markup states every one
1584
1551
  that exists, and checking the markup is sound.
1585
1552
 
1553
+ ### Uncommon layouts
1554
+
1555
+ The hub supports these layouts and adds no check to refuse them. They are
1556
+ uncommon, and Loadbare/app does not shape itself around them.
1557
+
1558
+ - **A radio button in a row template.** The browser groups radio buttons
1559
+ by `name`, and a row template repeats one `name` in every live row. The
1560
+ hub stamps each radio button's `name` with the `name` written, or its
1561
+ column when none is, followed by `-` and the live row's key, so each live
1562
+ row holds a group of its own.
1563
+ - **Several rows not yet entered, each holding radio buttons.** A
1564
+ [row not yet entered](#a-row-not-yet-entered) has an empty key, so the
1565
+ radio buttons of every such row share one `name` and form one group.
1566
+ - **Radio buttons populated from a query.** A radio button in a query's row
1567
+ template answers to that query's row, as any element there does, so which
1568
+ row and column it lands from and gathers to follows from where the layout
1569
+ puts it. The result is the same as in any framework that populates radio
1570
+ buttons from a query. A choice among a query's rows is better made with a
1571
+ form-associated control, such as `lb-options` in `@loadbare/widgets`.
1572
+
1586
1573
  ## Application files
1587
1574
 
1588
1575
  ### Chrome
@@ -1801,8 +1788,8 @@ export const queries: Queries = {
1801
1788
  };
1802
1789
  ```
1803
1790
 
1804
- A `row` query returns one object, and a `rows` query returns an array of
1805
- objects, which the page shows in that order when the query declares none. An answer that disagrees with
1791
+ A `row` query returns one object, or `null` when it finds no row, and a
1792
+ `rows` query returns an array of objects, which the page shows in that order when the query declares none. An answer that disagrees with
1806
1793
  its declared kind is left out of the response, with a warning naming the
1807
1794
  query.
1808
1795
 
@@ -42,3 +42,166 @@ See [`chrome.html`](./chrome.md) for the link, [The Express
42
42
  server](./server.md) for the route, and
43
43
  [`loadbare-app-build`](./builder.md#running-the-builder) for `--minify`.
44
44
  </content>
45
+
46
+ ## Selecting what the hub stamps
47
+
48
+ The hub tells a stylesheet everything it knows through attributes, so an
49
+ application styles state with selectors and writes no script for it. Each
50
+ stamp is an `lb-` attribute the hub writes; never write one in markup.
51
+
52
+ ### A value
53
+
54
+ Every element a column lands on carries the value as `lb-column-value`,
55
+ exactly as the query sent it, and loses it when the value is null. Select
56
+ on the column and the value:
57
+
58
+ ```css
59
+ [lb-column="status"][lb-column-value="locked"] { opacity: 0.5; }
60
+ [lb-column="balance"][lb-column-value^="-"] { color: var(--negative); }
61
+ [lb-column="note"]:not([lb-column-value]) { display: none; }
62
+ ```
63
+
64
+ A widget that formats its value, such as `lb-number`, keeps the raw value
65
+ in the stamp, so the selector reads the number and not its text. See
66
+ [Where a column lands](./data-binding.md#where-a-column-lands).
67
+
68
+ ### A row
69
+
70
+ | Select | For |
71
+ |----------------------|-------------------------------------------------|
72
+ | `[lb-row-live]` | Every live row of a list |
73
+ | `[lb-row-selected]` | A row its `lb-select` checkbox ticks |
74
+ | `[lb-row-editing]` | The row a pointed form is editing |
75
+ | `[lb-key-value="…"]` | One row, by its key |
76
+ | `[hidden]` | A row a filter hides; the browser hides it |
77
+
78
+ An element a `row` query lands on carries `lb-key-value` while the query has
79
+ a row, and loses it when the query answers `null`:
80
+
81
+ ```css
82
+ [lb-query="member"]:not([lb-key-value]) .member-missing { display: revert; }
83
+ ```
84
+
85
+ Select live rows with `[lb-row-live]`, never with `[lb-key-value]`: a `row`
86
+ landed inside a list, such as a total in a table's foot, carries a key and
87
+ is not one of the list's rows.
88
+
89
+ ```css
90
+ [lb-row-selected] { background: var(--selected); }
91
+ [lb-row-editing] { outline: 2px solid var(--accent); }
92
+ ```
93
+
94
+ See [Rows](./data-binding.md#rows), [Selecting rows](./data-binding.md#selecting-rows)
95
+ and [Editing a row in one form](./data-binding.md#editing-a-row-in-one-form).
96
+
97
+ ### A group
98
+
99
+ Each top-level element of a group carries `lb-group-live`, the column it
100
+ breaks on as `lb-group-column`, and its value there as `lb-group-value`:
101
+
102
+ ```css
103
+ [lb-group-column="category"][lb-group-value="Income"] { color: var(--income); }
104
+ ```
105
+
106
+ See [Groups](./data-binding.md#groups).
107
+
108
+ ### What just happened to a row
109
+
110
+ The hub stamps what its last landing did, so a stylesheet animates with
111
+ nothing else:
112
+
113
+ | Select | For |
114
+ |--------------------------------|------------------------------------------------|
115
+ | `[lb-row-created]` | A row that arrived |
116
+ | `[lb-row-changed]` | A row whose values changed |
117
+ | `[lb-row-moved]` | A row whose values changed and now stands elsewhere; it carries `lb-row-changed` too |
118
+ | `[lb-row-requested]` | Any of the three, done by this page's request, as `created`, `changed` or `moved` |
119
+ | `[lb-row-leaving]`, `[lb-group-leaving]` | A row or group on its way out; the hub removes it when its animations end |
120
+
121
+ ```css
122
+ [lb-row-created] { animation: arrive 200ms; }
123
+ [lb-row-requested="changed"] { animation: flash 600ms; }
124
+ [lb-row-leaving] { animation: depart 200ms forwards; }
125
+ ```
126
+
127
+ A row stamped again plays its animation again. See
128
+ [What happened to a row](./data-binding.md#what-happened-to-a-row).
129
+
130
+ ### How many rows show
131
+
132
+ An element carrying `lb-query` and a row template carries
133
+ `lb-query-row-count`, the number of live rows showing. A row a filter hides
134
+ is not counted.
135
+
136
+ ```css
137
+ .roster-empty { display: none; }
138
+ [lb-query-row-count="0"] .roster-empty { display: revert; }
139
+ ```
140
+
141
+ See [Counting rows](./data-binding.md#counting-rows).
142
+
143
+ ### A request
144
+
145
+ | Select | For |
146
+ |------------------------|---------------------------------------------------------|
147
+ | `[lb-request-pending]` | The element that committed, while its round trip is out; `aria-busy="true"` rides with it |
148
+ | `[lb-request-error]` | The element whose round trip failed, until it commits again |
149
+ | `[lb-request-refused]` | The element whose request the handler refused, holding the message, empty when there is none |
150
+ | `:invalid` | A control a refusal named a column for |
151
+
152
+ The hub ignores a commit on a pending element, so the stylesheet only has to
153
+ show it:
154
+
155
+ ```css
156
+ [lb-request-pending] { cursor: progress; opacity: 0.6; }
157
+ [lb-request-error] { outline: 2px solid var(--error); }
158
+ [lb-request-refused]:not([lb-request-refused=""])::after {
159
+ content: attr(lb-request-refused);
160
+ }
161
+ ```
162
+
163
+ See [Request state](./data-binding.md#request-state) and
164
+ [Refusals](./data-binding.md#refusals).
165
+
166
+ ### A form pointed at a row
167
+
168
+ A pointed form's `lb-key-value` says what it is doing:
169
+
170
+ | Select | The form is |
171
+ |------------------------------|--------------------|
172
+ | `form[lb-key-value]:not([lb-key-value=""])` | Editing a row |
173
+ | `form[lb-key-value=""]` | Entering a new row |
174
+ | `form:not([lb-key-value])` | Not in use |
175
+
176
+ ```css
177
+ form[lb-key-value=""] .save-existing,
178
+ form:not([lb-key-value=""]) .save-new { display: none; }
179
+ ```
180
+
181
+ See [Editing a row in one form](./data-binding.md#editing-a-row-in-one-form).
182
+
183
+ ### The user's choices
184
+
185
+ An order or a filter the user chose is a column of `lb-choices`, so the
186
+ element showing it carries it as `lb-column-value`. Style anything else by
187
+ it with `:has()`:
188
+
189
+ ```css
190
+ table:has([lb-column="accounts_order"][lb-column-value="-balance"])
191
+ th.balance::after { content: "▼"; }
192
+ ```
193
+
194
+ See [Choices](./data-binding.md#choices).
195
+
196
+ ### Content shown by condition
197
+
198
+ An element `lb-show` turns off is moved into a `<template>`, so no selector
199
+ reaches it and nothing about it needs styling. The `<template>` keeps its
200
+ place, so `:nth-child()` counts it; prefer `:nth-child(… of …)` or a class
201
+ over position. See [Where an absent element is](./data-binding.md#where-an-absent-element-is).
202
+
203
+ ### The first load
204
+
205
+ Write `hidden` on the chrome's `<body>`, and the hub removes it once the
206
+ first page has landed. See
207
+ [Preventing the first-load blink](./chrome.md#preventing-the-first-load-blink).
@@ -95,6 +95,10 @@ not `lb-row-live`, so a `row` shown inside another query, such as a total in
95
95
  a table's foot, is not one of that query's rows. Select live rows with
96
96
  `[lb-row-live]`, never with `[lb-key-value]`.
97
97
 
98
+ A `row` query answering `null` found no row. The hub lands every column the
99
+ element names as null and removes its `lb-key-value`; under a row template,
100
+ the live row leaves.
101
+
98
102
  Write `lb-column` on each element that shows a column. The element reads
99
103
  from its nearest ancestor row: the live row it is in, or the element a
100
104
  `row` landed on. A nested `lb-query` begins a new query, and what is inside
@@ -124,6 +128,7 @@ no row fills.
124
128
  |------------------------------------------------------|-----------------------------------------|
125
129
  | `<input>`, `<select>`, `<textarea>` | Its `value`, and `lb-column-value` |
126
130
  | `<input type="checkbox">` | Its `checked`, and `lb-column-value` |
131
+ | `<input type="radio">` | Its `checked`, and `lb-column-value` |
127
132
  | A form-associated custom element with `value` | Its `value`, and `lb-column-value` |
128
133
  | Any other custom element | `lb-column-value` only |
129
134
  | Any other element | Its text content, and `lb-column-value` |
@@ -135,11 +140,13 @@ element that is not a control keeps the content the builder placed in it
135
140
  from its element file, and renders `lb-column-value` itself; see
136
141
  [Custom Elements](./custom-elements.md#receiving-a-value).
137
142
 
138
- A radio button and a file input receive nothing, and the hub reports it to
139
- the console.
143
+ A file input receives nothing, and the hub reports it to the console.
140
144
 
141
145
  The hub hands every value to the browser as the server sent it. What a
142
- number, a date or a null looks like is decided in the query.
146
+ number, a date or a null looks like is decided in the query, or by a custom
147
+ element that formats it, such as
148
+ [`lb-number` and `lb-date`](./widgets.md#lb-number-and-lb-date). Send a
149
+ column the user sorts or filters raw, and format it in the element.
143
150
 
144
151
  ### Checkboxes
145
152
 
@@ -155,6 +162,27 @@ boolean, and the string `"false"` is checked. Unchecked sends `"false"`
155
162
  rather than nothing, so a flag can be set back. The checkbox's `value`
156
163
  attribute means nothing to the hub.
157
164
 
165
+ ### Radio buttons
166
+
167
+ A radio button is a control. It is checked when its `value` is the
168
+ column's value as a string, and unchecked otherwise. Of the radio buttons
169
+ naming one column, the hub gathers the `value` of the one checked, or `""`
170
+ when none is:
171
+
172
+ ```html
173
+ <input type="radio" lb-column="shift" value="day" lb-request="lb-row-update" />
174
+ <input type="radio" lb-column="shift" value="night" lb-request="lb-row-update" />
175
+ ```
176
+
177
+ In a row template, the hub stamps each radio button's `name` with the
178
+ `name` written, or its column when none is, followed by `-` and the live
179
+ row's key, so each live row holds a group of its own. Rows not yet entered
180
+ share the empty key, and so share one group.
181
+
182
+ A radio button in a query's row template answers to that query's row, as
183
+ any element there does. A choice among a query's rows is better made with
184
+ a form-associated control, such as `lb-options` in `@loadbare/widgets`.
185
+
158
186
  ### Rows
159
187
 
160
188
  Write a `<template>` inside the element carrying `lb-query`, holding one
@@ -96,7 +96,7 @@ export const queries: Queries = {
96
96
 
97
97
  | Declared with | Answers with |
98
98
  |----------------------------|-------------------------------------------|
99
- | `row(key, run)` | One row, an object |
99
+ | `row(key, run)` | One row, an object, or `null` for none |
100
100
  | `rows(key, run, options)` | All its rows, an array of objects |
101
101
 
102
102
  `options` are a `rows` query's order and column types. `order` is the
@@ -317,6 +317,7 @@ queries:
317
317
  | Answer | States |
318
318
  |--------------------------|---------------------------------------------|
319
319
  | `{ ... }` | The row of a `row` query |
320
+ | `null` | A `row` query found no row |
320
321
  | `[...]` | All rows of a `rows` query, and their order |
321
322
  | `patch({ rows: [...] })` | These rows arrive or change; the rest stand |
322
323
  | `patch({ drop: [...] })` | These keys are gone; the rest stand |
@@ -1,8 +1,8 @@
1
1
  # The Basic Widget Library
2
2
 
3
- Eleven widgets: `lb-input`, `lb-select`, `lb-options`, `lb-picker`,
3
+ Thirteen widgets: `lb-input`, `lb-select`, `lb-options`, `lb-picker`,
4
4
  `lb-table`, `lb-unknown-page`, `lb-confirm`, `lb-form`, `lb-insert-dialog`,
5
- `lb-master` and `lb-checkbox`. Every one of them is an ordinary custom element, written
5
+ `lb-master`, `lb-checkbox`, `lb-number` and `lb-date`. Every one of them is an ordinary custom element, written
6
6
  against the contracts in [Custom Elements](./custom-elements.md#html) for its
7
7
  definition and [Custom Elements](./custom-elements.md#code) for its class.
8
8
 
@@ -258,6 +258,28 @@ checked. The hub sets a native checkbox's `checked` directly.
258
258
  | `exp-label` | The label's text |
259
259
  | `exp-request` | The checkbox's `lb-request`, none when absent |
260
260
 
261
+ ## `lb-number` and `lb-date`
262
+
263
+ A number shown by the browser's `Intl.NumberFormat`, and a date or time
264
+ shown by `Intl.DateTimeFormat`. Neither is a control: `lb-column` goes on
265
+ the tag, and the widget renders the `lb-column-value` the hub stamps; see
266
+ [Receiving a value](./custom-elements.md#receiving-a-value).
267
+
268
+ ```html
269
+ <lb-number lb-column="balance" exp-style="currency" exp-currency="USD"></lb-number>
270
+ <lb-date lb-column="due" exp-date-style="medium"></lb-date>
271
+ ```
272
+
273
+ Each parameter is one of Intl's options in kebab-case, as `exp-currency-sign`
274
+ for `currencySign`, plus `exp-locale`. A value that is no number or date
275
+ shows as it arrived, and a date alone, `YYYY-MM-DD`, shows as that day in
276
+ every time zone. The stamp keeps the raw value, so a stylesheet selects on
277
+ it:
278
+
279
+ ```css
280
+ lb-number[lb-column-value^="-"] { color: var(--negative); }
281
+ ```
282
+
261
283
  ## `lb-unknown-page`
262
284
 
263
285
  The chrome's dialog for a URL that names no page, as one tag: