@loadbare/app 0.7.2 → 0.7.4

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.
@@ -0,0 +1,216 @@
1
+ # Prior art
2
+
3
+ > **LLM-researched, not yet revised by a person.** Compiled by Claude on
4
+ > 2026-09-16 from web sources, against `@loadbare/app` 0.7.3, while designing
5
+ > conditional rendering. The sources are listed under each section.
6
+
7
+ Loadbare binds HTML to data by attributes and lets the platform do the DOM
8
+ work. That idea has been tried before. This document records the two
9
+ closest predecessors, where Loadbare agrees with them and where it parts
10
+ ways, and what other frameworks do about conditional rendering.
11
+
12
+ ## The short version
13
+
14
+ Loadbare is not Model-Driven Views, although MDV is the better-known name.
15
+ MDV's model is a mutable object graph in the browser that the view observes.
16
+ Loadbare has no model in the browser: the view is driven by server responses
17
+ that say what changed.
18
+
19
+ The closer ancestor is Internet Explorer 4's data binding, from 1997, whose
20
+ vocabulary of record sets, single records and columns maps almost one to one
21
+ onto Loadbare's.
22
+
23
+ Loadbare can be described as IE4's relational binding vocabulary, on the
24
+ `<template>` element that MDV left behind, with the server as the model.
25
+ Andromeda (2003), where Loadbare began, sits between the two.
26
+
27
+ ## Internet Explorer 4 data binding (1997)
28
+
29
+ Internet Explorer 4.0 bound HTML to data through attributes, built from four
30
+ parts: a data source object (DSO), data consumers, a binding agent and a
31
+ table repetition agent. A DSO might take "an Open Database Connectivity
32
+ (ODBC) connection string and an Structured Query Language (SQL) statement,"
33
+ and had to expose its data through OLE DB.
34
+
35
+ | IE4 | Loadbare |
36
+ |-----------------------------------------------------------------------------|-----------------------|
37
+ | `DATASRC` on a table repeats "an entire set of records" ("set binding") | `lb-list` |
38
+ | A single-valued consumer takes one value "from the current record" | `lb-row` |
39
+ | `DATAFLD` names "a column in the data set" | `lb-cell` |
40
+ | The repetition agent "uses the table row (tr) in the table body as a template" | `<template lb-key>` |
41
+ | The DSO's SQL statement | a page's queries |
42
+ | The binding agent "work[s] completely behind the scenes" | the hub |
43
+
44
+ It differed from Loadbare in three ways:
45
+
46
+ - **The data lived in the browser.** "Since the DSO maintains the data on the
47
+ client, it also manages how the data is sorted and filtered."
48
+ - **Binding was two-way.** "When a user updates a databound element on the
49
+ page, the binding agent notifies the DSO." Loadbare sends a request and
50
+ lands the answer.
51
+ - **It was proprietary.** A DSO was an ActiveX control or applet, and the
52
+ feature ended with Internet Explorer.
53
+
54
+ Sources:
55
+ [About Data Binding Architecture](https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/ms531384(v=vs.85)),
56
+ [Using the Tabular Data Control](https://www.sitepoint.com/control-internet-explorer/).
57
+
58
+ ## Model-Driven Views (2011–2015)
59
+
60
+ ### What it was
61
+
62
+ Rafael Weinstein proposed MDV to the W3C WebApps group in April 2011:
63
+ "Myself and a few other chromium folks have been working on a design for a
64
+ formalized separation between View and Model in the browser." Its README
65
+ calls it "a way to write _dynamic_ HTML _using_ HTML," with the goal of native
66
+ implementation in browsers.
67
+
68
+ - `{{path.to.value}}` placeholders in text and attribute values, with a
69
+ custom syntax API for expressions.
70
+ - `<template bind>` for one instance, `<template repeat>` for one instance
71
+ per array item, `<template if>` to make either conditional, and
72
+ `<template ref>` to reuse another template's content.
73
+ - Two-way binding on inputs: "If DOM elements which collect user input are
74
+ bound, they _push_ the collected value into the model."
75
+ - A conditional attribute, `hidden?`: "the attribute will be set to the empty
76
+ string if the value is reachable and truthy, otherwise the attribute will be
77
+ removed."
78
+ - Models are plain JavaScript objects, observed through `Object.observe`.
79
+
80
+ In the TemplateBinding implementation, a template stays in the document and
81
+ its instances are inserted after it, tracked by a terminator node. When `if`
82
+ turns false, instances are removed and their bindings closed.
83
+
84
+ ### What became of it
85
+
86
+ The W3C group asked for MDV to be broken into primitives. In Weinstein's
87
+ account those became DOM Mutation Observers, **the HTML `<template>`
88
+ element**, and `Object.observe`. `<template>` was specified by Weinstein and
89
+ reported stable in May 2013. No source found states outright that
90
+ `<template>` was created for MDV, but they share an author, a period and a
91
+ design.
92
+
93
+ The rest did not survive:
94
+
95
+ - `Object.observe` was withdrawn from TC39 in November 2015. It disabled
96
+ optimization paths in V8, and Polymer found that complex applications made
97
+ "tens of thousands of O.o calls."
98
+ - Without native observation, MDV had to keep a list of bindings, and a node
99
+ discarded without being unbound was never garbage collected.
100
+ - The Polymer community debated whether the model or the markup should be the
101
+ source of truth, and settled on the model.
102
+ - The binding half continues in Apple's Template Instantiation proposal (2017)
103
+ and Chrome's DOM Parts, which as of 2025 was pending, with no signals from
104
+ Gecko or WebKit.
105
+
106
+ Sources:
107
+ [Weinstein, "Model-driven Views"](https://lists.w3.org/Archives/Public/public-webapps/2011AprJun/0309.html),
108
+ [MDV README](https://github.com/toolkitchen/mdv),
109
+ [MDV template.md](https://raw.githubusercontent.com/toolkitchen/mdv/master/docs/template.md),
110
+ [MDV node_bind.md](https://raw.githubusercontent.com/toolkitchen/mdv/master/docs/node_bind.md),
111
+ [TemplateBinding source](https://raw.githubusercontent.com/googlearchive/TemplateBinding/master/src/TemplateBinding.js),
112
+ [Will MDV become a standard?](https://groups.google.com/g/polymer-dev/c/4RSYaKmbtEk/m/uYnY3900wpIJ),
113
+ [MDV or markup as data](https://groups.google.com/g/polymer-dev/c/FJQLrcSKGT0),
114
+ [Polymer issue #154](https://github.com/Polymer/polymer/issues/154),
115
+ [W3C HTML Templates, 2013](https://www.w3.org/TR/2013/WD-html-templates-20130214/),
116
+ [InfoQ: Object.observe withdrawn](https://www.infoq.com/news/2015/11/object-observe-withdrawn/),
117
+ [esdiscuss: An update on Object.observe](https://esdiscuss.org/topic/an-update-on-object-observe),
118
+ [Template Instantiation](https://github.com/WICG/webcomponents/blob/gh-pages/proposals/Template-Instantiation.md),
119
+ [DOM Parts](https://github.com/WICG/webcomponents/blob/gh-pages/proposals/DOM-Parts.md),
120
+ [Intent to Prototype: DOM Parts](https://groups.google.com/a/chromium.org/g/blink-dev/c/wIADRnljZDA).
121
+
122
+ ## Where the three overlap
123
+
124
+ **All three:**
125
+
126
+ - Dynamic HTML declared in HTML.
127
+ - Attributes naming data on ordinary elements.
128
+ - The platform, not the application, performs the DOM work, with no virtual
129
+ DOM.
130
+
131
+ **Loadbare and MDV:**
132
+
133
+ - `<template>` as the unit of repetition and of conditions.
134
+ - Built toward web standards, not a proprietary runtime.
135
+
136
+ **Loadbare and IE4:**
137
+
138
+ - A relational vocabulary: a record set, a single record, a column per
139
+ element.
140
+ - The row as the repetition template.
141
+ - SQL behind the data source.
142
+
143
+ **MDV and IE4, and not Loadbare:**
144
+
145
+ - The data lives in the browser.
146
+ - Two-way binding.
147
+ - The view keeps itself synchronized with changes to that data.
148
+
149
+ **MDV only:**
150
+
151
+ - Runtime `{{}}` placeholders and expressions.
152
+ - Paths into nested object trees.
153
+ - Change observation, and bindings that have to be closed.
154
+ - A false `if` destroys its instances.
155
+
156
+ **Loadbare only:**
157
+
158
+ - The server is the source of truth, and the hub holds no data.
159
+ - A response says what changed, as a whole list, a patch or a row, so nothing
160
+ is observed.
161
+ - Writes are requests (`lb-action` and the CRUD operations) whose answers land
162
+ back.
163
+ - A request's position is read from the document.
164
+ - A build step: expansion, tree shaking, pages shipped as templates.
165
+ - Navigation and request state carried by the hub.
166
+
167
+ Two of Loadbare's central decisions reject what ended MDV. There is no
168
+ client-side model to observe, which is what made `Object.observe` too slow in
169
+ practice. And data comes only as rows and sets of rows, never as the nested
170
+ object paths MDV bound to.
171
+
172
+ ## Conditional rendering elsewhere
173
+
174
+ Frameworks handle a false branch in one of two ways. Either it is hidden
175
+ with CSS and keeps its state, or it is removed and rebuilt from scratch when
176
+ it returns. A few keep a removed branch alive in framework memory.
177
+
178
+ | Framework | The false branch | State kept | Updated while hidden |
179
+ |----------------------------------|-----------------------------------------------------------------------------------|------------|----------------------|
180
+ | MDV `<template if>` | Instances removed after the template, bindings closed | no | no |
181
+ | Alpine `<template x-if>` | Cloned from the template and inserted after it on show, destroyed and removed on hide | no | no |
182
+ | Knockout `if` | Comment nodes mark the place, contents re-rendered completely | no | no |
183
+ | Angular `*ngIf` | `<ng-template>` rendered as a comment anchor; a view can be detached and reinserted | if detached | no |
184
+ | Aurelia `if.bind` | Removed from the DOM, views cached by default and reused | yes | not documented |
185
+ | Vue `<KeepAlive>` | Detached from the document but not unmounted | yes | no |
186
+ | Lit `cache()` | DOM kept in a `DocumentFragment`, swapped back in | yes | on swap-in |
187
+ | Polymer `dom-if` | `style.display = 'none'`; `restamp` destroys and re-creates instead | yes | yes |
188
+ | React `<Activity mode="hidden">` | `display: none`, Effects destroyed | yes | yes, at lower priority |
189
+
190
+ Loadbare's `lb-show` moves the live element into a `<template>` that takes
191
+ its place, and landing still reaches it there. Each
192
+ part has precedent: templates as conditions (MDV, Alpine, Angular), branches
193
+ kept rather than rebuilt (Aurelia, Angular, Vue), and hidden branches kept
194
+ current (Polymer, React). No source found keeps the branch in a template in
195
+ the document, standing where the element stood, with no framework cache. The
196
+ hub has nowhere else to keep one.
197
+
198
+ Loadbare's recommendation before `lb-show`, a stylesheet rule on `lb-value`,
199
+ is the same family as Polymer's `dom-if` and MDV's `hidden?`: the element stays and
200
+ CSS hides it.
201
+
202
+ Sources:
203
+ [Alpine x-if source](https://raw.githubusercontent.com/alpinejs/alpine/main/packages/alpinejs/src/directives/x-if.js),
204
+ [Alpine x-if](https://alpinejs.dev/directives/if),
205
+ [Knockout virtual elements](https://knockoutjs.com/documentation/custom-bindings-for-virtual-elements.html),
206
+ [Knockout if/with re-rendering](https://www.knockmeout.net/2012/03/knockoutjs-performance-gotcha-1ifwith.html),
207
+ [Angular NgIf](https://angular.dev/api/common/NgIf),
208
+ [Reusing views in Angular](https://angular.love/optimization-techniques-reusing-views/),
209
+ [Aurelia conditional rendering](https://docs.aurelia.io/templates/conditional-rendering),
210
+ [Vue KeepAlive](https://vuejs.org/guide/built-ins/keep-alive.html),
211
+ [Lit directives](https://lit.dev/docs/templates/directives/),
212
+ [Lit cache.ts](https://github.com/lit/lit/blob/lit-html-1.x/src/directives/cache.ts),
213
+ [Polymer dom-if](https://polymer-library.polymer-project.org/2.0/docs/devguide/templates),
214
+ [Polymer display caching](https://github.com/Polymer/polymer/commit/2611285),
215
+ [Polymer issue #2712](https://github.com/Polymer/polymer/issues/2712),
216
+ [React Activity](https://react.dev/reference/react/Activity).
@@ -223,6 +223,7 @@ as a string literal.
223
223
  |------------------|------------|
224
224
  | `ATTR_VALUE` | `lb-value` |
225
225
  | `ATTR_CELL` | `lb-cell` |
226
+ | `ATTR_SHOW` | `lb-show` |
226
227
  | `ATTR_LIST` | `lb-list` |
227
228
  | `ATTR_ROW` | `lb-row` |
228
229
  | `ATTR_KEY` | `lb-key` |
@@ -260,6 +261,15 @@ cells a successful insert resets — see
260
261
  [TECHREF-1.0](../TECHREF-1.0.md#the-round-trip). Read it as nothing landed,
261
262
  and show the widget's default. A blank string is a value that landed.
262
263
 
264
+ A widget carrying `lb-show`, or inside an element that does, is moved into a
265
+ template while its column is off and back out when it turns on — see
266
+ [Conditional rendering](./data-binding.md#conditional-rendering). Going in, it
267
+ sees `disconnectedCallback` and then `adoptedCallback`; coming out,
268
+ `adoptedCallback` and then `connectedCallback`. It is the same instance
269
+ throughout, and it still receives `lb-value` while it is away, so write
270
+ `connectedCallback` to run more than once, as a row a list reorders already
271
+ requires.
272
+
263
273
  List `ATTR_VALUE` in `observedAttributes`, or `attributeChangedCallback`
264
274
  never fires. The browser calls it for an attribute already present when an
265
275
  element upgrades, not only for one that changes afterward, so a widget's
@@ -287,9 +297,14 @@ See [TECHREF-1.0](../TECHREF-1.0.md#requests) for what each operation is
287
297
  filled with.
288
298
 
289
299
  A widget sending `lb-row-insert` or `lb-row-update` doesn't read its own
290
- controls. It dispatches the bare action from the element holding the cells,
291
- a `<tr>` as readily as a `<form>`, or from anything inside it, and the hub
292
- gathers `values` from the nearest element holding a readable `lb-cell`:
300
+ controls. It dispatches the bare action from the row, or from anything
301
+ inside it, and the hub gathers `values` from the row the event came from:
302
+ the nearest `<form>`, `<tr>` or live row around the dispatching element,
303
+ itself included, inside its scope. Every readable `lb-cell` in that row is
304
+ gathered, wherever in the row it sits, except the cells of a scope nested in
305
+ it: a picker carrying `lb-list` and `lb-cell` gives its own value, not its
306
+ options'. A request from an element in no such
307
+ row is not sent, and the console says to use a `<form>`.
293
308
 
294
309
  ```ts
295
310
  row.dispatchEvent(
@@ -41,14 +41,15 @@ asks for are declared on the server; see [page files](./page-files.md).
41
41
 
42
42
  ## Binding
43
43
 
44
- | Attribute | Written by | Names |
45
- |----------------|-------------|-------------------------------------------|
46
- | `lb-list` | a developer | The set of rows a subtree displays |
47
- | `lb-row` | a developer | The one row a subtree displays |
48
- | `lb-key` | a developer | The column that identifies a row |
49
- | `lb-cell` | a developer | The column an element displays |
50
- | `lb-key-value` | the hub | A live row's own key |
51
- | `lb-value` | the hub | The value that landed on a cell |
44
+ | Attribute | Written by | Names |
45
+ |----------------|-------------|-------------------------------------------------------|
46
+ | `lb-list` | a developer | The set of rows a subtree displays |
47
+ | `lb-row` | a developer | The one row a subtree displays |
48
+ | `lb-key` | a developer | The column that identifies a row |
49
+ | `lb-cell` | a developer | The column an element displays |
50
+ | `lb-show` | a developer | The column that decides whether an element is present |
51
+ | `lb-key-value` | the hub | A live row's own key |
52
+ | `lb-value` | the hub | The value that landed on a cell |
52
53
 
53
54
  A binding is scoped by ancestry. Either scope attribute scopes its DOM
54
55
  children, and a nested one of either kind begins a new scope, so an element
@@ -70,6 +71,13 @@ arrives from scope. The scope's own root counts as a cell if it carries one,
70
71
  which is how an `<option>` — whose content model is text — displays the value
71
72
  it is.
72
73
 
74
+ An element that carries a scope and `lb-cell` both is a cell of the scope
75
+ around it. Its own `lb-list` or `lb-row` names what it displays, and its
76
+ ancestors name where it belongs, so a `<select lb-list="accounts"
77
+ lb-cell="account">` in a row displays the accounts and holds that row's
78
+ `account`. The value lands on it and is gathered from it; the cells inside
79
+ it are the accounts', and neither.
80
+
73
81
  Name the same query on more than one subtree to display it in more than one
74
82
  place. Every subtree gets the result.
75
83
 
@@ -160,7 +168,7 @@ anything else is a name the page declared.
160
168
 
161
169
  The hub sends the first four from a native element on the element's own
162
170
  event: a form on submit, anything else on click. An insert or update clicked
163
- from a button reads the cells around the button (see [Forms](#forms)). A
171
+ from a button reads the form or row the button is in (see [Forms](#forms)). A
164
172
  cell change needs a
165
173
  widget to say what a change is, so only a widget sends it: the shipped
166
174
  `<lb-input>` does when it carries `lb-action="lb-cell-change"`, and stays
@@ -207,8 +215,10 @@ The hub sends it from a native element. A widget sends its own request.
207
215
  ### Forms
208
216
 
209
217
  A `<form>` performs its `lb-action` on submit. `lb-row-insert` and `lb-row-update`
210
- both gather every `lb-cell` inside the form into one values map, read from
211
- the control each cell is or wraps. They differ in one thing: `lb-row-update`
218
+ both gather every `lb-cell` of the form into one values map, read from
219
+ the control each cell is or wraps. A cell inside a scope nested in the form
220
+ is that scope's and is not gathered, the same way a value landing on the
221
+ form's row does not reach it. They differ in one thing: `lb-row-update`
212
222
  also carries the key of the row it is inside, and `lb-row-insert` carries none,
213
223
  because there is no row yet. A declared name on a form sends that action on
214
224
  submit, carrying the binding and no values.
@@ -237,27 +247,32 @@ Give every `lb-cell` in a form a control to read. A cell that is neither an
237
247
  `<input>`, `<select>`, or `<textarea>` nor wraps one is left out of the
238
248
  values map, and a form with no cell to read sends nothing.
239
249
 
240
- Where the cells cannot sit in a form, as in a table row, put the action on a
241
- button beside them. The hub gathers from the nearest element around the
242
- button that holds a cell to read, so a button in a row for a new entry
243
- inserts that row's cells:
250
+ An insert or update gathers the row it belongs to: the nearest `<form>`,
251
+ `<tr>` or live row around the element that sends it, inside its scope. A
252
+ button submits its form the same way, whatever else sits beside it.
253
+
254
+ A form cannot go around a table row's controls, so there the row is the
255
+ form. Put the action on a button in the row, and it inserts every cell in
256
+ the row, wherever in the row the button and the cells are:
244
257
 
245
258
  ```html
246
259
  <table lb-list="roster">
247
260
  <tbody>
248
261
  <tr>
249
262
  <td><input lb-cell="name" /></td>
250
- <td><input lb-cell="role" /></td>
251
- <td><button lb-action="lb-row-insert">Add</button></td>
263
+ <td>
264
+ <input lb-cell="role" />
265
+ <button lb-action="lb-row-insert">Add</button>
266
+ </td>
252
267
  </tr>
253
268
  </tbody>
254
269
  </table>
255
270
  ```
256
271
 
257
- The search stops at the live row the button is in, so an `lb-row-update`
258
- button in a row saves only that row, and it never takes in the list scope
259
- itself. Keep the button's own cell apart from the others: a button sharing
260
- a `<td>` with one input gathers only that input.
272
+ An `lb-row-update` button in a live row saves that row and no other, and a
273
+ form nested in a row gathers only the form. Anywhere else, write the form:
274
+ cells and a button in a `<div>` belong to no row, so the hub refuses the
275
+ request and says to put them in a `<form>`.
261
276
 
262
277
  Put these two actions on a form or a button, not on an element that holds
263
278
  the cells. A click into one of its inputs would send the row, so the hub
@@ -269,42 +284,91 @@ gathers the same way; see
269
284
 
270
285
  Loadbare ships static HTML and hydrates elements that are already in the
271
286
  document. There is no `if`, and none is needed: write every possibility into
272
- the page, and control which of them is showing.
287
+ the page, and let a column decide which of them is present.
273
288
 
274
- Every cell carries the value that landed on it as `lb-value`, so a
275
- stylesheet can show or hide part of a page from a value the server sent. Bind
276
- a column the page does not display to a hidden element:
289
+ Write `lb-show` on an element, naming the column that decides it:
277
290
 
278
291
  ```html
279
292
  <template lb-key="id">
280
293
  <tr>
281
294
  <td lb-cell="name"></td>
282
- <td lb-cell="locked" hidden></td>
283
- <td><button lb-action="lb-row-delete">Remove</button></td>
295
+ <td><button lb-action="lb-row-delete" lb-show="removable">Remove</button></td>
284
296
  </tr>
285
297
  </template>
286
298
  ```
287
299
 
288
- ```css
289
- tr:has([lb-cell="locked"][lb-value="true"]) button {
290
- display: none;
291
- }
300
+ ```sql
301
+ (ledger_count = 0 AND system_behavior IS NULL) AS removable
292
302
  ```
293
303
 
294
- `lb-value` holds the string the query sent, so the query decides the spelling
295
- the selector matches. A stylesheet can hide a control but cannot disable one,
296
- and hiding is presentation: the server still refuses what a request may not
297
- do.
304
+ A value of `null` or `false` takes the element out of the page, and any other
305
+ value puts it back. The hub never reads a string, so `"false"` is a value like
306
+ any other: have the query answer with a boolean or a null. A row that does not
307
+ carry the column leaves the element as it is, so a query that answers with
308
+ whole rows returns the column in every row.
309
+
310
+ `lb-show` binds the way `lb-cell` does, to the row on its nearest scoped
311
+ ancestor. On an element that is itself a scope, the column belongs to the
312
+ row around it, so this picker takes its choices from `groups` and whether it
313
+ is present from the account row:
298
314
 
299
- Where showing a case takes more than a selector, a widget receives the value
300
- in `lb-value` and decides what to show:
315
+ ```html
316
+ <select lb-list="groups" lb-cell="group_id" lb-show="group_choice">
317
+ ```
318
+
319
+ An element may show a column and be decided by it, which shows a note only
320
+ when there is one:
301
321
 
302
- ```ts
303
- for (const step of steps) step.hidden = step.dataset.step !== value;
322
+ ```html
323
+ <span lb-cell="note" lb-show="note"></span>
304
324
  ```
305
325
 
306
- Ship the case that is showing on arrival unhidden, and hide the rest in the
307
- HTML.
326
+ An element that is not present cannot be clicked, but that is presentation:
327
+ the server still refuses what a request may not do.
328
+
329
+ ### Where an absent element is
330
+
331
+ An element whose column is off is moved into a `<template lb-show>` that
332
+ stands where it stood, and moved back out when the column turns on. The
333
+ developer never writes that template.
334
+
335
+ - Nothing renders it, whatever a stylesheet says, because a template's content
336
+ is not its children.
337
+ - It cannot be focused or clicked, assistive technology does not announce it,
338
+ and a form does not gather it.
339
+ - It is moved, never rebuilt, so a widget keeps its instance and a control
340
+ keeps what was typed into it.
341
+ - Values keep landing on it, and on every cell and scope inside it, while it
342
+ is away, so it returns current.
343
+
344
+ The builder ships every `lb-show` element already inside its template, so
345
+ nothing conditional shows until its row has landed.
346
+
347
+ A condition never changes the structure of a page. An absent element keeps
348
+ its place among its siblings, so a position selector (`:first-child`,
349
+ `:nth-child`, `:empty`, `+`, `~`) counts its template as a sibling. A selector
350
+ by tag, class or attribute is unaffected. Only a list changes a page's
351
+ structure.
352
+
353
+ A condition that is only a style is a class on an element that is present or
354
+ not:
355
+
356
+ ```html
357
+ <span lb-show="out_of_balance" class="danger">Out of balance</span>
358
+ ```
359
+
360
+ These are build errors:
361
+
362
+ - `lb-show` on a row template's root. A row that should not show is left out
363
+ by the query.
364
+ - `lb-show` with no row around it: outside every scope, on a scope with none
365
+ around it, or in a list scope outside its row template, where nothing lands.
366
+ - `lb-show` on a `<template>`.
367
+
368
+ A condition that is not data, such as a collapsed section or an open menu,
369
+ has no column. Use `<details>`, a stylesheet, or a widget. Every cell still
370
+ carries the value that landed on it as `lb-value`, for a widget to read or a
371
+ stylesheet to select on.
308
372
 
309
373
  ### An empty list
310
374
 
@@ -355,6 +419,12 @@ button in a stylesheet, or have a widget watch its own attributes and
355
419
  disable itself. An application that styles neither behaves correctly and
356
420
  shows nothing.
357
421
 
422
+ A native button or form pressed again while it carries `lb-pending` is
423
+ ignored, so a pending one is already disabled and the stylesheet only shows
424
+ it. A widget is not held back, since one that sends on change must send its
425
+ latest value. The hub also sets `aria-busy="true"` for as long as
426
+ `lb-pending` is present.
427
+
358
428
  ## Sending a request from a widget
359
429
 
360
430
  A widget can build and dispatch a request itself instead of carrying one of
package/docs/testing.md CHANGED
@@ -136,6 +136,8 @@ here.
136
136
  - a native element receives its value as text, a hyphenated one as `lb-value`
137
137
  - the root of a scope counts as a cell if it carries one, which is what makes
138
138
  an `<option>` row possible
139
+ - an element carrying both a scope and `lb-cell` is a cell of the row around
140
+ it, and the cells inside it are its own scope's
139
141
  - a query with no scope is reported and skipped; several scopes for one query
140
142
  are all filled; a set of rows landing on a scope bound with `lb-row`
141
143
  is reported rather than thrown
@@ -182,10 +184,18 @@ What the hub is tested for here:
182
184
  - a click finds the nearest `lb-action`, and skips a hyphenated tag and a
183
185
  form, both of which own the interaction themselves
184
186
  - a submit gathers the form's cells, and reports a cell with no control
187
+ - an insert or update gathers the nearest form, `<tr>` or live row inside
188
+ its scope, whatever shares the button's cell: one ghost row among its
189
+ neighbours, a whole live row, a form nested in a row, a button's form
190
+ owner; and refuses a `<div>` holding cells, pointing to `<form>`
191
+ - gathering skips the cells of a nested scope, so a picker in a row sends
192
+ its own cell and nothing about its options
185
193
  - a request arriving with no action, or with a reserved name that is not one
186
194
  of the four, is refused before it reaches the wire
187
195
  - `lb-pending` lands on the element that dispatched, `lb-error` replaces it
188
196
  on failure, and the next request clears it
197
+ - `aria-busy` comes and goes with `lb-pending`; a native button or form
198
+ performed again while pending sends nothing, and a widget still sends
189
199
  - a path with no page host reports and opens the unknown-page dialog; two
190
200
  hosts for one name report and take the first
191
201
  - `lb-navigation` lands with the path and the label of the link that names it