@loadbare/app 0.18.2 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +17 -17
  2. package/dist/build/assemble.d.ts +7 -1
  3. package/dist/build/assemble.d.ts.map +1 -1
  4. package/dist/build/assemble.js +81 -11
  5. package/dist/build/assemble.js.map +1 -1
  6. package/dist/build/cli.d.ts +3 -0
  7. package/dist/build/cli.d.ts.map +1 -1
  8. package/dist/build/cli.js +6 -1
  9. package/dist/build/cli.js.map +1 -1
  10. package/dist/build/declared.d.ts +25 -0
  11. package/dist/build/declared.d.ts.map +1 -0
  12. package/dist/build/declared.js +62 -0
  13. package/dist/build/declared.js.map +1 -0
  14. package/dist/build/pages.d.ts +6 -0
  15. package/dist/build/pages.d.ts.map +1 -1
  16. package/dist/build/pages.js +32 -12
  17. package/dist/build/pages.js.map +1 -1
  18. package/docs/TECHREF-1.0.md +27 -72
  19. package/docs/comparison.md +692 -480
  20. package/docs/departures.md +63 -0
  21. package/docs/miswarts.md +140 -0
  22. package/docs/opinions.md +155 -0
  23. package/docs/reference/builder.md +9 -2
  24. package/docs/reference/chrome.md +2 -1
  25. package/docs/reference/custom-elements.md +1 -1
  26. package/docs/reference/data-binding.md +36 -9
  27. package/docs/reference/page-files.md +36 -4
  28. package/docs/roadmap.md +55 -43
  29. package/docs/testing.md +62 -28
  30. package/docs/theory.md +348 -108
  31. package/package.json +1 -1
  32. package/skills/loadbare-app/SKILL.md +10 -6
  33. package/skills/loadbare-app/references/TECHREF-1.0.md +27 -72
  34. package/skills/loadbare-app/references/builder.md +9 -2
  35. package/skills/loadbare-app/references/chrome.md +2 -1
  36. package/skills/loadbare-app/references/custom-elements.md +1 -1
  37. package/skills/loadbare-app/references/data-binding.md +36 -9
  38. package/skills/loadbare-app/references/miswarts.md +140 -0
  39. package/skills/loadbare-app/references/page-files.md +36 -4
  40. package/docs/possible-ideas.md +0 -314
  41. package/docs/theory-old.md +0 -839
  42. package/docs/what-does-loadbare-extend.md +0 -124
@@ -0,0 +1,63 @@
1
+ # Departures
2
+
3
+ The theory says Loadbare/app follows HTML wherever HTML already has an
4
+ opinion, and does not reimplement browser behavior. In two places it
5
+ departs from HTML's model on purpose, both for the 300ms budget.
6
+
7
+ A departure is not a [miswart](./miswarts.md). A miswart is small,
8
+ orthogonal and terminating. A departure is large, other behavior rests
9
+ on it, and it cannot be removed without redesigning the framework.
10
+
11
+ Each entry states what HTML does, what Loadbare/app does instead, what
12
+ that buys, what it costs, and what rests on it.
13
+
14
+ ## Navigation
15
+
16
+ **What HTML does.** One document per URL. Following a link fetches a
17
+ new document from the server, and a URL that names nothing is answered
18
+ with a 404.
19
+
20
+ **What Loadbare/app does.** One document holds every page, each as a
21
+ `<template>`. The server answers every route with that one document,
22
+ and the hub shows the page the path names by cloning its template into
23
+ `<main>`. A path that names no page is found out in the browser, not
24
+ answered with a 404.
25
+
26
+ **What it buys.** Performance. Moving between pages fetches no HTML
27
+ and no code, only the page's data. The browser parses every page once,
28
+ when the document first loads.
29
+
30
+ **What it costs.**
31
+
32
+ - The first load carries every page, whether or not the user visits it.
33
+ - There is no 404 status for a path that names no page. Loadbare/app is
34
+ for applications, not sites, so no crawler is expected to care.
35
+ - The application must be served from the origin root, not a subpath.
36
+
37
+ **What rests on it.** The hub's link handling, the URL as the query
38
+ `lb-url`, the server's catch-all route, and the unknown-page dialog.
39
+
40
+ ## The wire
41
+
42
+ **What HTML does.** The server sends HTML, and the browser renders it.
43
+ Libraries that extend HTML, such as htmx, keep that model and send HTML
44
+ fragments.
45
+
46
+ **What Loadbare/app does.** After the first load, the server sends only
47
+ data: rows as JSON, a patch of the rows that changed, or a refusal. The
48
+ HTML that shows the rows is already in the browser, and the hub lands
49
+ the rows into it.
50
+
51
+ **What it buys.** Performance. Rows are far smaller than the markup
52
+ that displays them, the server renders nothing, and nearly all of its
53
+ time for a request is the database's time.
54
+
55
+ **What it costs.**
56
+
57
+ - The application does not work without JavaScript.
58
+ - The server cannot shape the markup per request. What varies by
59
+ request must be data, landed by the rules every row follows.
60
+
61
+ **What rests on it.** Landing, patches, the response items every
62
+ request answers with, and the server's page code, which returns rows
63
+ and never markup.
@@ -0,0 +1,140 @@
1
+ # Miswarts
2
+
3
+ A miswart is a place where Loadbare/app breaks one of its own rules.
4
+ Anybody paying attention will see it as a violation, and they will be
5
+ right. Yet in context it turns out to be the RightThing(tm), and doing it
6
+ any other way would be worse for the user or the developer.
7
+
8
+ The reference describes each miswart's behavior where it applies, and
9
+ links here. This document is the one place that admits they are
10
+ exceptions, and says why each one is allowed to stay.
11
+
12
+ ## The test
13
+
14
+ A miswart must be:
15
+
16
+ - **Small.** It can be stated in a sentence or two.
17
+ - **Orthogonal.** It has no interaction with any other miswart.
18
+ - **Terminating.** It does not, by logical extension, require another
19
+ miswart. If fixing the edges of one exception calls for a second, we
20
+ probably should be looking for a weakness in earlier implementation
21
+ decisions.
22
+
23
+ Each entry also names two things:
24
+
25
+ - **The rule it breaks.** If we cannot name the rule, it is not a
26
+ miswart, it is undocumented behavior.
27
+ - **The motivation that justifies it**: performance, correctness or
28
+ ergonomics, as set out in the theory document. That is what makes it
29
+ the RightThing(tm) rather than a convenience.
30
+
31
+ ## Radio button names in a row template
32
+
33
+ **What.** The hub stamps the `name` of each radio button in a row
34
+ template with the `name` written, or its column when none is, followed by
35
+ `-` and the live row's key.
36
+
37
+ **Rule broken.** The hub writes only its own stamps, which always
38
+ begin with `lb-`.
39
+
40
+ **Why it is the RightThing(tm).** Correctness. The browser groups radio
41
+ buttons by `name`, and a row template repeats one `name` in every live
42
+ row, so without the stamp every row's buttons would form a single group,
43
+ and checking one row would uncheck all the others.
44
+
45
+ **Why it terminates.** It applies only to radio buttons inside a row
46
+ template, and only the `name`.
47
+
48
+ ## Checkboxes and radio groups always gather a value
49
+
50
+ **What.** An unchecked checkbox gathers `"false"`, a checked one
51
+ `"true"`, and its `value` attribute means nothing to the hub. A radio
52
+ group with none checked gathers `""`.
53
+
54
+ **Rule broken.** Where HTML already has an opinion, Loadbare/app
55
+ follows it. HTML sends a checkbox's `value` when it
56
+ is checked, and sends nothing for an unchecked checkbox or an empty radio
57
+ group.
58
+
59
+ **Why it is the RightThing(tm).** Correctness. A write must be able to
60
+ set a flag back to false, or clear a choice, and nothing on the wire
61
+ cannot say either.
62
+
63
+ **Why it terminates.** It applies only to gathering from these two
64
+ controls. Landing still follows HTML's own idea of checked.
65
+
66
+ ## Rows hidden by a filter
67
+
68
+ **What.** A row that does not pass the user's filter gets the `hidden`
69
+ attribute, and so does a group whose rows are all hidden.
70
+
71
+ **Rule broken.** The hub writes only its own stamps, which always
72
+ begin with `lb-`.
73
+
74
+ **Why it is the RightThing(tm).** Performance and ergonomics. A filter
75
+ over rows already on the page needs no round trip, and `hidden` is HTML's
76
+ own word for an element that is present but not shown. A stamp of our
77
+ own would make every stylesheet restate what the browser already does.
78
+
79
+ **Why it terminates.** It applies only to live rows and groups under a
80
+ filter.
81
+
82
+ ## Leaving rows are inert
83
+
84
+ **What.** A row on its way out takes `inert` while its animation runs,
85
+ and is removed when the animation finishes.
86
+
87
+ **Rule broken.** The hub writes only its own stamps, which always
88
+ begin with `lb-`.
89
+
90
+ **Why it is the RightThing(tm).** Correctness. A row that is gone from
91
+ the database must not take a click or the focus during its exit
92
+ animation, and `inert` is exactly HTML's word for that.
93
+
94
+ **Why it terminates.** It applies only to a row already stamped
95
+ `lb-row-leaving`, which never comes back.
96
+
97
+ ## Busy elements carry aria-busy
98
+
99
+ **What.** While an element's request is in flight, it carries
100
+ `aria-busy="true"` alongside `lb-request-pending`.
101
+
102
+ **Rule broken.** The hub writes only its own stamps, which always
103
+ begin with `lb-`.
104
+
105
+ **Why it is the RightThing(tm).** Correctness, for a user of assistive
106
+ technology. `lb-request-pending` tells a stylesheet, and `aria-busy` tells
107
+ a screen reader, the same fact.
108
+
109
+ **Why it terminates.** It rides with `lb-request-pending` and nothing
110
+ else.
111
+
112
+ ## The hub unhides the body
113
+
114
+ **What.** When the chrome writes `<body hidden>`, the hub removes
115
+ `hidden` once the first page has landed.
116
+
117
+ **Rule broken.** The hub writes only its own stamps, which always
118
+ begin with `lb-`.
119
+
120
+ **Why it is the RightThing(tm).** Performance, as the user perceives it.
121
+ Without it, the first load blinks an empty page before the data lands.
122
+ Only the hub knows when the first page has landed.
123
+
124
+ **Why it terminates.** It happens once, to `<body>`, and only when the
125
+ application opted in by writing `hidden`.
126
+
127
+ ## `lb-url` lands every parm it names
128
+
129
+ **What.** When `lb-url` lands, a query parm the URL does not carry lands
130
+ empty on every element naming it as a column.
131
+
132
+ **Rule broken.** Landing a row sets the columns the row carries, and
133
+ leaves the rest as they were.
134
+
135
+ **Why it is the RightThing(tm).** Correctness. A control must show what
136
+ the address bar says, after Back as well as after a reload, and a parm
137
+ that is absent from the URL is a parm whose value is empty.
138
+
139
+ **Why it terminates.** It applies only to `lb-url`, the one query whose
140
+ columns are the URL itself.
@@ -0,0 +1,155 @@
1
+ # The Opinions of Loadbare/app
2
+
3
+ Every framework forces some decisions on the people who use it. This
4
+ document lists the ones Loadbare/app forces, so a developer can see
5
+ them in one place before writing an application. The reasons for each
6
+ are in the [theory](./theory.md).
7
+
8
+ ## What counts as an opinion
9
+
10
+ An opinion is a position that is:
11
+
12
+ 1. Forced. There is no supported way around it. A position the
13
+ developer can opt out of is a default.
14
+ 2. Contested. Competent frameworks in the wild decide otherwise. A
15
+ position nobody takes differently is just how the platform works.
16
+ 3. Pervasive. It shows up in most of what the developer writes, most
17
+ pages and most handlers, rather than in one feature.
18
+
19
+ An opinion limits what the developer can express. A convention limits
20
+ only where they put it. The file names `<stub>.page.html`,
21
+ `<stub>.queries.ts` and `<stub>.requests.ts` are forced and pervasive,
22
+ but they are a convention, and are not listed here.
23
+
24
+ The [departures](./departures.md) are measured against HTML, and the
25
+ [miswarts](./miswarts.md) against Loadbare/app's own rules. The
26
+ opinions are measured against the developer's freedom. An entry can be
27
+ more than one of these: the wire is both a departure and an opinion.
28
+
29
+ ## Two shapes, and every row has a key
30
+
31
+ Loadbare/app understands two data shapes, a `row` and a set of `rows`.
32
+ Every row arrives with its key. Column values are atomic. A document
33
+ that embeds child rows in a parent row must be split into two query
34
+ results before Loadbare/app can show it.
35
+
36
+ Other frameworks accept any JSON the server sends, nested to any depth,
37
+ and leave identity to the developer. In Loadbare/app, every query a
38
+ developer writes answers in one of the two shapes, and every element
39
+ that shows data is bound to one of them.
40
+
41
+ ## HTML is static
42
+
43
+ The developer writes each page's markup once. The server does not
44
+ generate markup per request, and there is no templating language in
45
+ the browser. Whatever varies from one request to the next must be
46
+ data, and it reaches the screen by the same rules every row follows.
47
+
48
+ Server-side templates, JSX, and libraries that send HTML fragments all
49
+ generate markup at run time. In Loadbare/app, every page is plain
50
+ HTML, and every visible difference between two users or two moments is
51
+ a column value.
52
+
53
+ ## The server answers only with data
54
+
55
+ After the first load, the server sends rows, a patch of the rows that
56
+ changed, or a refusal. It never sends markup, and it never sends
57
+ instructions for the browser to carry out.
58
+
59
+ Applications in the wild answer with HTML, with JSON, with redirects,
60
+ with scripts, and with every combination of them. In Loadbare/app,
61
+ every handler a developer writes returns rows or refuses. See
62
+ [the wire](./departures.md#the-wire).
63
+
64
+ ## The screen changes only when the server answers
65
+
66
+ Loadbare/app changes the screen from what the server sent, and never
67
+ ahead of it. There are no optimistic updates. A write shows on screen
68
+ when the rows it changed come back.
69
+
70
+ Many frameworks update the screen first and reconcile when the server
71
+ answers. In Loadbare/app, every write a developer designs is shown by
72
+ the rows its handler returns.
73
+
74
+ ## Requests come from the document
75
+
76
+ A request is named on an element with `lb-request`. The hub supplies
77
+ the query and the key from where the element sits, and gathers the
78
+ values from the controls bound to columns. The developer writes no
79
+ `fetch`, designs no endpoints, and never passes a key by hand.
80
+
81
+ Most frameworks have the developer write the call to the server, choose
82
+ its URL, and assemble its body. In Loadbare/app, every button, form,
83
+ and control that writes is a named request on an element.
84
+
85
+ ## A page declares what it shows and what it allows
86
+
87
+ A page's queries are what it shows, and its requests are what it
88
+ allows. The server refuses any request a page did not declare.
89
+ Nothing is reachable from the browser that a page has not published.
90
+
91
+ Elsewhere, any route the server registers is reachable, and access is
92
+ checked inside each handler. In Loadbare/app, every page carries its
93
+ own list, and the list is the whole of what the browser can ask for.
94
+
95
+ ## The URL names a place
96
+
97
+ The path names a page. It never names a resource, so `/accounts/42`
98
+ is not a way to reach account 42. The query string describes what the
99
+ page has on screen, and it never commands. The server remembers
100
+ nothing about the view, so a URL reloads, bookmarks, and mails as the
101
+ same view.
102
+
103
+ REST names resources in the path, and many applications keep view
104
+ state on the server or in the browser's memory. In Loadbare/app,
105
+ every link and every query parm follows this rule. See
106
+ [What a URL names](./TECHREF-1.0.md#what-a-url-names).
107
+
108
+ ## Framework state is a row
109
+
110
+ What the framework itself keeps is presented as rows. The URL is the
111
+ query `lb-url`, of kind `row`, and lands like any server query. User
112
+ preferences and choices on a page are sent as `lb-row-update` for `lb-choices`,
113
+ and can land anywhere on the page.
114
+
115
+ Most frameworks keep their own state in a store with its own interface.
116
+ In Loadbare/app, a developer shows the current page's label or a
117
+ query parm the same way they show a customer's name.
118
+
119
+ ## Mistakes stop the build or the server
120
+
121
+ What can be checked before the application runs is checked, and a
122
+ mistake stops the build or the server. The builder refuses a custom
123
+ element it cannot find, and any `lb-` attribute the developer is not
124
+ allowed to write. The server refuses known wrong patterns at startup.
125
+ What cannot be checked ahead of time is reported on the console when
126
+ it happens.
127
+
128
+ Many frameworks tolerate a mistake and render what they can. In
129
+ Loadbare/app, every page and every handler passes these checks before
130
+ it runs.
131
+
132
+ ## Constraints belong to the database
133
+
134
+ Business rules are enforced by the database or by the application's
135
+ server code. A handler refuses a request with words written for the
136
+ user, for the request as a whole or for each column, and the browser
137
+ shows them on the controls they belong to. Any other failure reaches
138
+ the browser as a plain failure, with no stack trace or database
139
+ message.
140
+
141
+ Many frameworks validate in the browser and again on the server, with
142
+ two sets of rules to keep in step. In Loadbare/app, every rule the
143
+ user can break is answered by a refusal from the server.
144
+
145
+ ## Widgets are custom elements
146
+
147
+ A widget, what other frameworks call a component, is a custom element.
148
+ There is no component model of Loadbare/app's own. A widget talks to
149
+ the hub through the attributes, events, and methods described in
150
+ [Custom elements](./TECHREF-1.0.md#custom-elements).
151
+
152
+ Most frameworks define their own component model, with its own
153
+ lifecycle and its own way of passing data. In Loadbare/app, every
154
+ widget, those in `@loadbare/widgets` and those an application writes,
155
+ is a standard custom element.
@@ -128,11 +128,18 @@ substitution rules — wraps each expanded page in
128
128
  `<template lb-page="<stub>">`, and splices them into the chrome's `<body>`.
129
129
  It removes a page file's `<title>` and stamps its text on the page's
130
130
  template as `lb-page-title`; see [The page title](./page-files.md#the-page-title).
131
- It formats the result with Prettier when the application has it installed.
131
+ It formats the result with Prettier, which ships with `@loadbare/app`, and
132
+ keeps the unformatted result if Prettier fails.
132
133
 
133
134
  The builder checks the `lb-` attributes of the chrome and every page after
134
135
  expansion, and stops on the first file with a problem; see
135
- [The markup checks](./data-binding.md#the-markup-checks).
136
+ [The markup checks](./data-binding.md#the-markup-checks). The names
137
+ `lb-query` and `lb-request` carry are checked against what each page
138
+ declares, which the builder reads by loading the page's `.queries.ts` and
139
+ `.requests.ts` modules. Their import-time code runs during the build, as it
140
+ does when the server starts, so a module that connects to a database at
141
+ import connects during the build too; open connections in `contextFor`
142
+ instead.
136
143
 
137
144
  The builder writes `app.css` only when it finds a stylesheet, and `pages.ts`
138
145
  only when some page has a `.requests.ts` or a `.queries.ts` file. Import `hub`
@@ -224,7 +224,8 @@ being visibly empty — so it's the whole document that needs to wait, not
224
224
  just the piece that was empty.
225
225
 
226
226
  The `<noscript>` block is the escape hatch for a visitor with JavaScript
227
- disabled. `<lb-hub>` is what removes `hidden` from `<body>`, so a browser
227
+ disabled. `<lb-hub>` is what removes `hidden` from `<body>`
228
+ ([miswart](../miswarts.md#the-hub-unhides-the-body)), so a browser
228
229
  that never runs `client.js` would otherwise be stuck looking at a
229
230
  permanently blank page. `<noscript>` content is only rendered when scripting
230
231
  is off, so the fallback rule only ever applies in exactly that case — it
@@ -290,7 +290,7 @@ Each direction has one mechanism for each kind of message:
290
290
 
291
291
  | Direction | What | How | Today |
292
292
  |----------------|--------------------------------------------|-----------------------------|-------|
293
- | Hub to element | State that lasts | An attribute the hub stamps | `lb-column-value`, `lb-key-value`, `lb-query-row-count`, `lb-group-*`, `lb-row-*`, `lb-request-pending`, `lb-request-error` |
293
+ | Hub to element | State that lasts | An attribute the hub stamps | `lb-column-value`, `lb-key-value`, `lb-query-row-count`, `lb-group-*`, `lb-row-*`, `lb-request-pending`, `lb-request-error`, `lb-request-refused` |
294
294
  | Hub to element | State the platform already names | A property | A control's `value` |
295
295
  | Hub to element | What the hub did | An optional `lb` method | `lbAttached`, `lbRowsLanded`, `lbRequestDone`, `lbDetached` |
296
296
  | Hub to element | A moment an element started | A bubbling event | `lb-request-done` |
@@ -2,7 +2,7 @@
2
2
 
3
3
  A page shows server data by naming queries and columns in its HTML, and asks
4
4
  the server to change that data by naming requests. The developer writes
5
- nine `lb-` attributes. The server declares every query a page may show and
5
+ sixteen `lb-` attributes. The server declares every query a page may show and
6
6
  every request it may send, and refuses anything it has not declared.
7
7
 
8
8
  Everything is a query. A query is a name for rows, of kind `row` or `rows`,
@@ -48,6 +48,10 @@ each with its key, and permits the three requests on `roster`; see
48
48
  | `lb-show` | Any element but `<template>` | The column that decides whether it is present |
49
49
  | `lb-order` | An element with `lb-query` | The choice holding the user's order; see [Order](#order) |
50
50
  | `lb-filter` | An element with `lb-query` | The columns a choice filters; see [Filters](#filters) |
51
+ | `lb-group` | A `<template>` | Nothing: it marks a group template; see [Groups](#groups) |
52
+ | `lb-count` | Any element | Nothing: it shows a count of rows; see [Aggregates](#aggregates) |
53
+ | `lb-sum`, `lb-avg`, `lb-min`, `lb-max` | Any element | The column aggregated; see [Aggregates](#aggregates) |
54
+ | `lb-select` | A checkbox in a row template | Nothing: it selects its live row; see [Selecting rows](#selecting-rows) |
51
55
  | `lb-request` | Any element | The request it sends when it commits |
52
56
  | `lb-url-link` | An `<a>` | A link between pages; see [chrome](./chrome.md#links) |
53
57
  | `lb-url-push` | An element with `lb-request` | A URL change that pushes a history entry |
@@ -62,8 +66,14 @@ builder writes a stamp, and a stylesheet or a custom element reads it.
62
66
  | `lb-key-value` | The key of a live row, or of a `row` landed on an element |
63
67
  | `lb-row-live` | The element is a live row |
64
68
  | `lb-query-row-count` | The number of live rows an element shows |
69
+ | `lb-group-live`, `lb-group-column`, `lb-group-value` | A group, the column it breaks on, and its value; see [Groups](#groups) |
70
+ | `lb-row-created`, `lb-row-changed`, `lb-row-moved`, `lb-row-requested` | What happened to a row; see [What happened to a row](#what-happened-to-a-row) |
71
+ | `lb-row-leaving`, `lb-group-leaving` | A row or group on its way out |
72
+ | `lb-row-editing` | The row a form is pointed at; see [Editing a row in one form](#editing-a-row-in-one-form) |
73
+ | `lb-row-selected` | A row whose `lb-select` is ticked |
65
74
  | `lb-request-pending` | The element's request is in flight |
66
75
  | `lb-request-error` | The element's last request failed |
76
+ | `lb-request-refused` | The handler refused it; see [Refusals](#refusals) |
67
77
  | `lb-page` | The stub of a page, on its `<template>` |
68
78
  | `lb-page-title` | The text of a page file's `<title>` |
69
79
 
@@ -151,7 +161,8 @@ column the user sorts or filters raw, and format it in the element.
151
161
  ### Checkboxes
152
162
 
153
163
  A checkbox is a control. It is checked when its column is anything but
154
- `false` or null, and it sends `"true"` or `"false"`:
164
+ `false` or null, and it sends `"true"` or `"false"`
165
+ ([miswart](../miswarts.md#checkboxes-and-radio-groups-always-gather-a-value)):
155
166
 
156
167
  ```html
157
168
  <input type="checkbox" lb-column="active" lb-request="lb-row-update" />
@@ -176,8 +187,9 @@ when none is:
176
187
 
177
188
  In a row template, the hub stamps each radio button's `name` with the
178
189
  `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.
190
+ row's key, so each live row holds a group of its own
191
+ ([miswart](../miswarts.md#radio-button-names-in-a-row-template)). Rows not yet
192
+ entered share the empty key, and so share one group.
181
193
 
182
194
  A radio button in a query's row template answers to that query's row, as
183
195
  any element there does. A choice among a query's rows is better made with
@@ -280,7 +292,8 @@ its rows the user may filter, separated by commas. The column of
280
292
  ```
281
293
 
282
294
  A row that does not pass every filter is `hidden`, and a ticked `lb-select`
283
- in it is cleared. A group whose rows are all hidden is hidden too. An empty
295
+ in it is cleared. A group whose rows are all hidden is hidden too
296
+ ([miswart](../miswarts.md#rows-hidden-by-a-filter)). An empty
284
297
  choice filters nothing.
285
298
 
286
299
  A filter is one or more terms, each an operator followed by a value:
@@ -371,8 +384,9 @@ Every change to a list is one a stylesheet can see:
371
384
  | `lb-row-leaving` | A row on its way out |
372
385
  | `lb-group-leaving` | A group on its way out |
373
386
 
374
- A row that leaves loses `lb-row-live`, takes `inert`, and is removed once
375
- its animations finish, at once when there are none. `lb-row-requested` is
387
+ A row that leaves loses `lb-row-live`, takes `inert`
388
+ ([miswart](../miswarts.md#leaving-rows-are-inert)), and is removed once its
389
+ animations finish, at once when there are none. `lb-row-requested` is
376
390
  cleared at every landing; the others when the hub next touches the row.
377
391
 
378
392
  A row is moved when its values changed and the rows whose values did not
@@ -927,7 +941,8 @@ The hub stamps the element that committed:
927
941
 
928
942
  `lb-request-error` covers a non-2xx response, a network failure and a
929
943
  timeout alike. The hub aborts a round trip after ten seconds, and sets
930
- `aria-busy="true"` for as long as `lb-request-pending` is present.
944
+ `aria-busy="true"` for as long as `lb-request-pending` is present
945
+ ([miswart](../miswarts.md#busy-elements-carry-aria-busy)).
931
946
 
932
947
  The hub ignores a commit on an element carrying `lb-request-pending`, so a
933
948
  pending button is already disabled and a stylesheet only shows it:
@@ -1041,8 +1056,16 @@ The developer writes `lb-` attributes in markup, and script never assigns
1041
1056
  them. The builder checks the markup of the chrome and of every page, after
1042
1057
  expansion, and refuses to build:
1043
1058
 
1044
- - an `lb-` attribute that is not one of the nine the developer writes
1059
+ - an `lb-` attribute that is not one of the sixteen the developer writes
1045
1060
  - `lb-request` naming an `lb-` request other than the three Loadbare provides
1061
+ - `lb-query` naming a query the page does not declare; every page and the
1062
+ chrome may name `lb-url` and `lb-choices`, and the chrome names no other
1063
+ - `lb-request` naming a request the page does not declare under `handlers`;
1064
+ the chrome sends none
1065
+ - one of the three requests for a query whose `crud` entry does not declare
1066
+ it, or any but `lb-row-update` for `lb-url` or `lb-choices`; the query is
1067
+ a form's own when the form carries `lb-query`, and otherwise the nearest
1068
+ `lb-query` around the element
1046
1069
  - `lb-url-link` on anything but an `<a>`
1047
1070
  - `lb-url-push` on an element without `lb-request`
1048
1071
  - `lb-url-unknown` on anything but a `<dialog>`, or outside `<lb-hub>`
@@ -1055,3 +1078,7 @@ expansion, and refuses to build:
1055
1078
  template, or beside `lb-column` or `lb-request`
1056
1079
  - a `command` beginning `--lb-` that is not one of the three commands
1057
1080
  - one of the three commands with no `commandfor`
1081
+
1082
+ To know what each page declares, the builder loads its `.queries.ts` and
1083
+ `.requests.ts` modules, so their import-time code runs during the build, as
1084
+ it does when the server starts.
@@ -229,8 +229,9 @@ export const requests: Requests = {
229
229
  };
230
230
  ```
231
231
 
232
- Declare every request the page allows. The server refuses a name the page
233
- does not declare.
232
+ Declare every request the page allows. The builder refuses markup naming a
233
+ request the page does not declare, and the server refuses one that arrives
234
+ anyway; see [The markup checks](./data-binding.md#the-markup-checks).
234
235
 
235
236
  ### crud
236
237
 
@@ -293,8 +294,9 @@ export const requests: Requests = {
293
294
  };
294
295
  ```
295
296
 
296
- Declare every request the query permits. The server refuses a request a
297
- query does not declare, and a query with no `crud` entry permits none.
297
+ Declare every request the query permits. The builder refuses markup sending
298
+ a request a query does not declare, and the server refuses one that arrives
299
+ anyway. A query with no `crud` entry permits none.
298
300
 
299
301
  Write `rowUpdate` to set the columns `values` names and leave every other
300
302
  column as it is. A form or a live row sends every control it gathers, and a
@@ -303,6 +305,36 @@ columns the page may edit.
303
305
 
304
306
  Every value in `values` is a string, as a control's value is.
305
307
 
308
+ Two users writing the same row overlap only on the columns their requests
309
+ carry, and the last write wins. A single control sends its own column, so
310
+ two users editing different columns of a row do not overwrite each other.
311
+ A form sends every control it gathers, so two forms saved over one row
312
+ overlap on all of their columns.
313
+
314
+ A write whose outcome matters states its precondition in its own SQL, and
315
+ refuses when no row was updated:
316
+
317
+ ```ts
318
+ handlers: {
319
+ approve: {
320
+ run: async (ctx, { key }) => {
321
+ const updated = await ctx.db.run(
322
+ "UPDATE invoice SET status = 'approved' WHERE id = $1 AND status = 'pending'",
323
+ [key],
324
+ );
325
+ if (updated === 0) {
326
+ throw refusal("Someone else has already decided this invoice.");
327
+ }
328
+ },
329
+ refresh: ["invoice"],
330
+ },
331
+ },
332
+ ```
333
+
334
+ The request's name says what the user meant, and the precondition follows
335
+ from it, so no earlier value is needed. See
336
+ [Refusing a request](#refusing-a-request).
337
+
306
338
  ### refresh and patch
307
339
 
308
340
  List in `refresh` every query whose whole answer the request changed.