@loadbare/app 0.13.0 → 0.14.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.
- package/dist/build/assemble.d.ts.map +1 -1
- package/dist/build/assemble.js +31 -1
- package/dist/build/assemble.js.map +1 -1
- package/dist/core/lb-constants.d.ts +11 -1
- package/dist/core/lb-constants.d.ts.map +1 -1
- package/dist/core/lb-constants.js +34 -0
- package/dist/core/lb-constants.js.map +1 -1
- package/dist/core/lb-types.d.ts +27 -0
- package/dist/core/lb-types.d.ts.map +1 -1
- package/dist/core/lb-types.js.map +1 -1
- package/dist/hub/lb-apply.d.ts +20 -3
- package/dist/hub/lb-apply.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +64 -8
- package/dist/hub/lb-apply.js.map +1 -1
- package/dist/hub/lb-hub.browser.d.ts.map +1 -1
- package/dist/hub/lb-hub.browser.js +341 -21
- package/dist/hub/lb-hub.browser.js.map +1 -1
- package/dist/server/lb-express.d.ts +13 -3
- package/dist/server/lb-express.d.ts.map +1 -1
- package/dist/server/lb-express.js +30 -12
- package/dist/server/lb-express.js.map +1 -1
- package/dist/server/lb-server.d.ts +31 -1
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/server/lb-server.js +112 -2
- package/dist/server/lb-server.js.map +1 -1
- package/docs/TECHREF-1.0.md +294 -21
- package/docs/comparison.md +6 -4
- package/docs/possible-ideas.md +146 -26
- package/docs/reference/custom-elements.md +97 -1
- package/docs/reference/data-binding.md +303 -4
- package/docs/reference/page-files.md +76 -3
- package/docs/reference/server.md +20 -0
- package/docs/reference/widgets.md +33 -10
- package/docs/roadmap.md +9 -2
- package/package.json +1 -1
- package/skills/loadbare-app/SKILL.md +50 -7
- package/skills/loadbare-app/references/TECHREF-1.0.md +294 -21
- package/skills/loadbare-app/references/custom-elements.md +97 -1
- package/skills/loadbare-app/references/data-binding.md +303 -4
- package/skills/loadbare-app/references/page-files.md +76 -3
- package/skills/loadbare-app/references/server.md +20 -0
- package/skills/loadbare-app/references/widgets.md +33 -10
package/docs/possible-ideas.md
CHANGED
|
@@ -4,6 +4,51 @@
|
|
|
4
4
|
> 2026-10-02 against `@loadbare/app` 0.12.0, from a session in ef. Nothing
|
|
5
5
|
> here is decided or planned.
|
|
6
6
|
|
|
7
|
+
## Why these are being considered
|
|
8
|
+
|
|
9
|
+
A custom element is the platform's one way to attach code to markup, and
|
|
10
|
+
widget work keeps running into what that costs.
|
|
11
|
+
|
|
12
|
+
- No decomposition. Code that concerns one row has nowhere to live but a
|
|
13
|
+
wrapper around the whole table, so `lb-table` owns everything inside it.
|
|
14
|
+
That is how page-specific behavior ends up in a shared widget: the COA
|
|
15
|
+
page's ghost row per section grew because row-level code could attach
|
|
16
|
+
only to the table. It is the overfit the README's test guards against.
|
|
17
|
+
- The browser's lifecycle is not the hub's. A moved row is disconnected and
|
|
18
|
+
connected, the copy a move leaves behind is constructed, and an `lb-show`
|
|
19
|
+
toggle adopts twice, so `connectedCallback` must be written to run again.
|
|
20
|
+
- Clutter. The page's source is the widget's dialect of named templates
|
|
21
|
+
rather than HTML, and the shipped page keeps a wrapper tag around what it
|
|
22
|
+
expanded.
|
|
23
|
+
- Plumbing through the wrapper. The tag carrying the code is not the
|
|
24
|
+
control: `WrappedControl` forwards `value`, fires `change` again, and needs
|
|
25
|
+
`ElementInternals` to join a form. Focus is searched for inside the
|
|
26
|
+
wrapper, and `autofocus` on a wrapped `<input>` is lost.
|
|
27
|
+
- Content models reach forms too. A `<form>` cannot wrap a `<tr>`, so the
|
|
28
|
+
ghost row joins its controls to a hidden form outside the table by the
|
|
29
|
+
`form` attribute. A form cannot hold a form, so `lb-insert-dialog` is
|
|
30
|
+
written once outside every row instead of beside the control it serves.
|
|
31
|
+
- Ids repeat in a row template. `lb-confirm` finds its dialog by position,
|
|
32
|
+
because `commandfor`, HTML's own way to open one with no script, works
|
|
33
|
+
only by id.
|
|
34
|
+
- Upgrade timing is the hub's problem. An element shipped inside an
|
|
35
|
+
`lb-show` template, or whose definition loads late, has not upgraded, so
|
|
36
|
+
the hub keeps `waiting()`, lands kept answers again once it upgrades, and
|
|
37
|
+
stamps `lb-column-value` for a control to take up on first connect.
|
|
38
|
+
- A few lines cost a wrapper. `lb-options` has a script mostly to copy
|
|
39
|
+
`lb-group-value` into an `<optgroup>`'s `label` and each key into an
|
|
40
|
+
option's `value`, and for that the whole `<select>` sits inside a custom
|
|
41
|
+
element.
|
|
42
|
+
- Parameters cannot carry structure. Expansion parameters are words, so
|
|
43
|
+
when groups became a template, `lb-picker` lost `exp-group` and a grouped
|
|
44
|
+
picker went back to a hand-written `lb-options`.
|
|
45
|
+
- CSS and accessibility see an element HTML does not define. A wrapper tag
|
|
46
|
+
has no role, shifts child and `:nth-child` selectors, and has no place in
|
|
47
|
+
a table. The usual remedy, `display: contents`, has a history of
|
|
48
|
+
accessibility bugs.
|
|
49
|
+
- One element, one class. A ghost row that should also save as it is typed
|
|
50
|
+
in needs a widget combining the two.
|
|
51
|
+
|
|
7
52
|
## Code on elements the parser constrains
|
|
8
53
|
|
|
9
54
|
Loadbare's data vocabulary is attributes, and an attribute goes on any
|
|
@@ -20,7 +65,8 @@ table. Each new need is another named destination, and the page's source
|
|
|
20
65
|
reads as `lb-table`'s shape rather than a table's. The shipped HTML is
|
|
21
66
|
honest; the source is a dialect.
|
|
22
67
|
|
|
23
|
-
Two ways to put code on the element itself follow
|
|
68
|
+
Two ways to put code on the element itself follow, and only the second
|
|
69
|
+
survives.
|
|
24
70
|
|
|
25
71
|
### Customized built-ins
|
|
26
72
|
|
|
@@ -28,25 +74,11 @@ Two ways to put code on the element itself follow.
|
|
|
28
74
|
<tr is="lb-ghost-row">…</tr>
|
|
29
75
|
```
|
|
30
76
|
|
|
31
|
-
The platform's own answer: a class extending `HTMLTableRowElement`,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
- Chrome and Firefox ship it. WebKit has declined to, on principle
|
|
37
|
-
(subclassing a built-in breaks when its internals change), so Safari needs
|
|
38
|
-
a polyfill for as long as the feature exists.
|
|
39
|
-
- Every browser on iOS and iPadOS is WebKit, Chrome included. The polyfill
|
|
40
|
-
is therefore not a Safari fallback but the code path on every iPhone and
|
|
41
|
-
iPad, permanently. The usual one, `@ungap/custom-elements`, watches the
|
|
42
|
-
document with a `MutationObserver`, so on those devices the lifecycle runs
|
|
43
|
-
because something noticed.
|
|
44
|
-
- `is` must be present when the element is created; setting it later does
|
|
45
|
-
nothing. Cloning a template preserves it, so a row template carrying
|
|
46
|
-
`is` yields upgraded rows.
|
|
47
|
-
- A customized built-in cannot be form-associated (`ElementInternals` is for
|
|
48
|
-
autonomous custom elements), which matters little: the built-in elements
|
|
49
|
-
that controls are already take part in forms.
|
|
77
|
+
The platform's own answer: a class extending `HTMLTableRowElement`, which
|
|
78
|
+
the parser treats as a `<tr>`. WebKit has declined to ship it, and every
|
|
79
|
+
browser on iOS and iPadOS is WebKit, so a polyfill, usually a
|
|
80
|
+
`MutationObserver`, would be the code path on every iPhone and iPad for
|
|
81
|
+
good. That rules it out.
|
|
50
82
|
|
|
51
83
|
### Behavior by attribute
|
|
52
84
|
|
|
@@ -114,13 +146,12 @@ A rule for choosing: a custom element makes a new kind of element (its own
|
|
|
114
146
|
markup, expansion, or a control built from scratch); a behavior adds code to
|
|
115
147
|
an element HTML already has.
|
|
116
148
|
|
|
117
|
-
### Where
|
|
149
|
+
### Where it would land
|
|
118
150
|
|
|
119
|
-
|
|
120
|
-
that need it, and leaves `lb-table` to supply only what is truly
|
|
121
|
-
table's.
|
|
122
|
-
|
|
123
|
-
matrix out of it, at the cost of a lifecycle the hub would define.
|
|
151
|
+
Behavior by attribute lets a page write a real `<table lb-query>` with code
|
|
152
|
+
on the rows that need it, and leaves `lb-table` to supply only what is truly
|
|
153
|
+
the table's. It keeps the parser and the browser matrix out of it, at the
|
|
154
|
+
cost of a lifecycle the hub would define.
|
|
124
155
|
|
|
125
156
|
## Expansion that replaces the tag
|
|
126
157
|
|
|
@@ -186,3 +217,92 @@ source, the `lb-behavior` attribute and the comment already give.
|
|
|
186
217
|
the page passes them as parameters (`exp-column`, written into the file as
|
|
187
218
|
`lb-column="{{column}}"`), which works today but reads less naturally
|
|
188
219
|
than `lb-column`. This wants worked examples before a rule.
|
|
220
|
+
|
|
221
|
+
## The case for behaviors
|
|
222
|
+
|
|
223
|
+
Today's model is simple to state: a widget is a name, and a name is one or
|
|
224
|
+
both of a `.html` and a `.browser.ts`. One name is one element the
|
|
225
|
+
developer writes, so what a widget does is easy to predict. Most of what
|
|
226
|
+
that costs is paid inside Loadbare, in service of the simple model. The one
|
|
227
|
+
place it leaks to the developer is the lifecycle, where a widget author must
|
|
228
|
+
follow strict rules to get the behavior they expect.
|
|
229
|
+
|
|
230
|
+
Behaviors would simplify the hub, but they seem to spread code that a
|
|
231
|
+
custom element keeps in one file. The containment comes from the element
|
|
232
|
+
file, though, not from the tag.
|
|
233
|
+
|
|
234
|
+
### What one file bundles today
|
|
235
|
+
|
|
236
|
+
`lb-table`'s script does three jobs, joined by where they sit rather than by
|
|
237
|
+
any data they share:
|
|
238
|
+
|
|
239
|
+
| Job | Reads and writes |
|
|
240
|
+
| -------------------------- | -------------------------------------------------- |
|
|
241
|
+
| Table scaffolding | Caption, head and foot destinations; expansion only |
|
|
242
|
+
| The ghost row | The ghost row's controls, its form, and focus |
|
|
243
|
+
| Scrolling to the saved row | The list's rows and their stamps |
|
|
244
|
+
|
|
245
|
+
It is one file, and three concerns in one class, none reusable without the
|
|
246
|
+
others. A `<ul>` cannot have a ghost row without becoming an `lb-table`.
|
|
247
|
+
|
|
248
|
+
### Two units
|
|
249
|
+
|
|
250
|
+
- A widget is markup. Its element file decides what elements exist, at
|
|
251
|
+
build time, as now.
|
|
252
|
+
- A behavior is what one element does. Its code attaches to the element
|
|
253
|
+
that carries it, and runs on the hub's lifecycle.
|
|
254
|
+
|
|
255
|
+
An element file may write `lb-behavior` on its own inner elements:
|
|
256
|
+
|
|
257
|
+
```html
|
|
258
|
+
<!-- lb-table.html -->
|
|
259
|
+
<table lb-behavior="scroll-to-saved">
|
|
260
|
+
<thead lb-exp-template="head"></thead>
|
|
261
|
+
<tbody lb-exp-slot></tbody>
|
|
262
|
+
<tbody lb-exp-template="ghost" lb-behavior="ghost-row"></tbody>
|
|
263
|
+
</table>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
`lb-table` stays the one place to read, and its element file says which code
|
|
267
|
+
runs on which element. The code is split into files by job, and the widget
|
|
268
|
+
assembles them. A page that wants a ghost row in a `<ul>` writes
|
|
269
|
+
`lb-behavior="ghost-row"` on it.
|
|
270
|
+
|
|
271
|
+
### Where code goes
|
|
272
|
+
|
|
273
|
+
Code goes on the element it reads and writes.
|
|
274
|
+
|
|
275
|
+
- The ghost row's code touches the ghost row's controls and form, so it is a
|
|
276
|
+
behavior on the ghost row.
|
|
277
|
+
- Scrolling touches the list's rows, so it is a behavior on the list.
|
|
278
|
+
- Code that seems to need two elements is the sign to stop. Behaviors never
|
|
279
|
+
call each other. They speak only through what widgets use today:
|
|
280
|
+
attributes the hub stamps, and bubbling events such as `lb-request-done`.
|
|
281
|
+
The scrolling behavior knows nothing of the ghost row; it reads
|
|
282
|
+
`lb-row-requested`.
|
|
283
|
+
|
|
284
|
+
The rule also refuses the overfit that started this: a ghost row per
|
|
285
|
+
section would be a behavior on the table reaching into every section.
|
|
286
|
+
|
|
287
|
+
### Where the leak moves
|
|
288
|
+
|
|
289
|
+
Today the leak is the browser's lifecycle: `connectedCallback` written to
|
|
290
|
+
run again, and a value stamp adopted on first connect. With behaviors it
|
|
291
|
+
becomes three rules Loadbare states:
|
|
292
|
+
|
|
293
|
+
1. Only elements the hub places get behaviors.
|
|
294
|
+
2. A behavior is attached and detached exactly once.
|
|
295
|
+
3. Behaviors speak only through stamps and events.
|
|
296
|
+
|
|
297
|
+
They are fewer, and in the hub's terms rather than the browser's. The cost
|
|
298
|
+
moves to Loadbare's design: the ordering the platform gives custom elements
|
|
299
|
+
must be stated, such as whether a host's behavior attaches before or after
|
|
300
|
+
its rows land, and the order among nested behaviors.
|
|
301
|
+
|
|
302
|
+
### Both, not one
|
|
303
|
+
|
|
304
|
+
Custom elements stay. A control built from scratch still needs
|
|
305
|
+
`ElementInternals`, and a tag with no code is expansion alone. Behaviors
|
|
306
|
+
cover what a tag does badly: code on an element HTML already has, inside
|
|
307
|
+
content models that forbid a tag. The developer's question becomes whether
|
|
308
|
+
they are making a new kind of element, or giving an existing element a job.
|
|
@@ -412,9 +412,104 @@ including through the HTML `form` attribute, so a form gathers it on submit.
|
|
|
412
412
|
Write `formResetCallback` to restore the default value: the hub resets a form
|
|
413
413
|
after a successful insert.
|
|
414
414
|
|
|
415
|
+
A control takes the message of a refusal naming its column when it has
|
|
416
|
+
`setCustomValidity` and `reportValidity`, as a native control does; see
|
|
417
|
+
[Refusals](./data-binding.md#refusals). A control wrapping a native one
|
|
418
|
+
passes both to it, so the browser shows the message there and focus lands
|
|
419
|
+
there.
|
|
420
|
+
|
|
415
421
|
The `<lb-input>`, `<lb-select>`, `<lb-options>` and `<lb-picker>` widgets
|
|
416
422
|
are controls; see [The Basic Widget Library](./widgets.md).
|
|
417
423
|
|
|
424
|
+
### Example: a file's text as a value
|
|
425
|
+
|
|
426
|
+
The hub takes no file input as a control. A custom element wraps one and is
|
|
427
|
+
the control in its place. This one reads the chosen file as text, which
|
|
428
|
+
suits a CSV or a bank statement, and sends the file's name under a second
|
|
429
|
+
column:
|
|
430
|
+
|
|
431
|
+
```html
|
|
432
|
+
<!-- src/file-text.html -->
|
|
433
|
+
<label>{{label}} <input type="file" accept="{{accept}}" /></label>
|
|
434
|
+
<input type="hidden" lb-column="{{name-column}}" />
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
// src/file-text.browser.ts
|
|
439
|
+
class FileText extends HTMLElement {
|
|
440
|
+
static formAssociated = true;
|
|
441
|
+
#internals = this.attachInternals();
|
|
442
|
+
#value = "";
|
|
443
|
+
|
|
444
|
+
constructor() {
|
|
445
|
+
super();
|
|
446
|
+
this.addEventListener("change", (e) => {
|
|
447
|
+
const input = e.target as HTMLInputElement;
|
|
448
|
+
if (e.target === this || input.type !== "file") return;
|
|
449
|
+
e.stopPropagation();
|
|
450
|
+
void this.read(input);
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
get value(): string {
|
|
455
|
+
return this.#value;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
set value(value: string) {
|
|
459
|
+
this.#value = value ?? "";
|
|
460
|
+
this.#internals.setFormValue(this.#value);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
formResetCallback() {
|
|
464
|
+
this.value = "";
|
|
465
|
+
this.setName("");
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async read(input: HTMLInputElement) {
|
|
469
|
+
const file = input.files?.[0] ?? null;
|
|
470
|
+
this.value = file ? await file.text() : "";
|
|
471
|
+
this.setName(file?.name ?? "");
|
|
472
|
+
this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
setName(name: string) {
|
|
476
|
+
const hidden = this.querySelector<HTMLInputElement>('input[type="hidden"]');
|
|
477
|
+
if (hidden) hidden.value = name;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
customElements.define("file-text", FileText);
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
```html
|
|
485
|
+
<!-- src/uploads.page.html -->
|
|
486
|
+
<form lb-query="uploads" lb-request="lb-row-insert">
|
|
487
|
+
<file-text
|
|
488
|
+
lb-column="filetext"
|
|
489
|
+
exp-name-column="filename"
|
|
490
|
+
exp-label="Statement file"
|
|
491
|
+
exp-accept=".csv,.ofx"
|
|
492
|
+
></file-text>
|
|
493
|
+
<button type="submit">Upload</button>
|
|
494
|
+
</form>
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Reading a file is asynchronous, so the element stops the file input's own
|
|
498
|
+
`change` and fires its own once the text is in hand. A form submitted before
|
|
499
|
+
then gathers the empty value, and the handler refuses it as no file.
|
|
500
|
+
|
|
501
|
+
A control gathers one column, so the name rides in a hidden input that the
|
|
502
|
+
hub gathers as a control of its own. The form gathers both on submit. Its
|
|
503
|
+
reset after the insert clears the file input but not the hidden one, whose
|
|
504
|
+
`value` is its default, so `formResetCallback` clears the name itself.
|
|
505
|
+
|
|
506
|
+
The text travels in the request's JSON body, which `hubRoutes` limits to
|
|
507
|
+
100 KB unless the server raises it; see [Options](./server.md#options).
|
|
508
|
+
|
|
509
|
+
The choices here are this application's: text rather than binary, the whole
|
|
510
|
+
file in the request, the name in a second column. A different application
|
|
511
|
+
writes its own element, under its own name, to make different ones.
|
|
512
|
+
|
|
418
513
|
### Receiving a value
|
|
419
514
|
|
|
420
515
|
A custom element that is not a control keeps the content the builder placed
|
|
@@ -494,7 +589,8 @@ this.addEventListener(LB_DONE_EVENT_NAME, (e) => {
|
|
|
494
589
|
```
|
|
495
590
|
|
|
496
591
|
`items` is every response item that landed because of the request, in
|
|
497
|
-
order.
|
|
592
|
+
order. `refusal` is set beside `error` when the handler refused the
|
|
593
|
+
request, holding its `message` and `columns`. An answer that moved the URL contributes its `lb-url` item, which
|
|
498
594
|
carries a key only the server knew, and the page load. `error` is set when
|
|
499
595
|
the round trip failed, and then nothing landed. A new row's key is in the
|
|
500
596
|
answer only when the handler returned it in a patch: a refresh sends all
|
|
@@ -121,6 +121,7 @@ no row fills.
|
|
|
121
121
|
| Element | Receives the value as |
|
|
122
122
|
|------------------------------------------------------|-----------------------------------------|
|
|
123
123
|
| `<input>`, `<select>`, `<textarea>` | Its `value`, and `lb-column-value` |
|
|
124
|
+
| `<input type="checkbox">` | Its `checked`, and `lb-column-value` |
|
|
124
125
|
| A form-associated custom element with `value` | Its `value`, and `lb-column-value` |
|
|
125
126
|
| Any other custom element | `lb-column-value` only |
|
|
126
127
|
| Any other element | Its text content, and `lb-column-value` |
|
|
@@ -132,12 +133,26 @@ element that is not a control keeps the content the builder placed in it
|
|
|
132
133
|
from its element file, and renders `lb-column-value` itself; see
|
|
133
134
|
[Custom Elements](./custom-elements.md#receiving-a-value).
|
|
134
135
|
|
|
135
|
-
A
|
|
136
|
-
|
|
136
|
+
A radio button and a file input receive nothing, and the hub reports it to
|
|
137
|
+
the console.
|
|
137
138
|
|
|
138
139
|
The hub hands every value to the browser as the server sent it. What a
|
|
139
140
|
number, a date or a null looks like is decided in the query.
|
|
140
141
|
|
|
142
|
+
### Checkboxes
|
|
143
|
+
|
|
144
|
+
A checkbox is a control. It is checked when its column is anything but
|
|
145
|
+
`false` or null, and it sends `"true"` or `"false"`:
|
|
146
|
+
|
|
147
|
+
```html
|
|
148
|
+
<input type="checkbox" lb-column="active" lb-request="lb-row-update" />
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Checked follows the rule `lb-show` follows, so a query spells a flag as a
|
|
152
|
+
boolean, and the string `"false"` is checked. Unchecked sends `"false"`
|
|
153
|
+
rather than nothing, so a flag can be set back. The checkbox's `value`
|
|
154
|
+
attribute means nothing to the hub.
|
|
155
|
+
|
|
141
156
|
### Rows
|
|
142
157
|
|
|
143
158
|
Write a `<template>` inside the element carrying `lb-query`, holding one
|
|
@@ -414,7 +429,8 @@ Every request has one shape:
|
|
|
414
429
|
```
|
|
415
430
|
|
|
416
431
|
The hub takes `query`, `key` and `values` from where the element sits, and
|
|
417
|
-
sends each when it has one.
|
|
432
|
+
sends each when it has one. A request acting on several rows carries `keys`
|
|
433
|
+
in place of `key`; see [Selecting rows](#selecting-rows). The server runs the handler for the name and
|
|
418
434
|
answers with response items, which land like any others.
|
|
419
435
|
|
|
420
436
|
### Committing
|
|
@@ -461,7 +477,8 @@ carrying `lb-request`:
|
|
|
461
477
|
4. Otherwise: every control whose form owner is the element's form owner.
|
|
462
478
|
|
|
463
479
|
The hub gathers from controls only, and skips a control that belongs to a
|
|
464
|
-
query nested inside the group
|
|
480
|
+
query nested inside the group, except that a form gathers the rows of a
|
|
481
|
+
query nested in its own; see [A form over a set of rows](#a-form-over-a-set-of-rows). A form owner includes a control outside the
|
|
465
482
|
form that names it with the HTML `form` attribute, which is how a table row,
|
|
466
483
|
where a form cannot go, sends one:
|
|
467
484
|
|
|
@@ -492,6 +509,99 @@ ancestor `lb-query` and row of the element carrying `lb-request` instead.
|
|
|
492
509
|
Controls gathered from two different queries are an error, and the hub
|
|
493
510
|
sends nothing.
|
|
494
511
|
|
|
512
|
+
### A form over a set of rows
|
|
513
|
+
|
|
514
|
+
A form gathers the live rows of a query nested in its own query as well as
|
|
515
|
+
its own controls: a master and its details, entered together. Each nested
|
|
516
|
+
row goes as its key and the values of its own controls, under the nested
|
|
517
|
+
query's name, in `rows`:
|
|
518
|
+
|
|
519
|
+
```html
|
|
520
|
+
<form lb-query="paychecks" lb-request="lb-row-insert">
|
|
521
|
+
<input lb-column="pay_date" type="date" />
|
|
522
|
+
<table>
|
|
523
|
+
<tbody lb-query="deduction_types">
|
|
524
|
+
<template>
|
|
525
|
+
<tr>
|
|
526
|
+
<td lb-column="label"></td>
|
|
527
|
+
<td><input lb-column="amount" /></td>
|
|
528
|
+
</tr>
|
|
529
|
+
</template>
|
|
530
|
+
</tbody>
|
|
531
|
+
</table>
|
|
532
|
+
<button type="submit">Save</button>
|
|
533
|
+
</form>
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
```json
|
|
537
|
+
{
|
|
538
|
+
"name": "lb-row-insert",
|
|
539
|
+
"query": "paychecks",
|
|
540
|
+
"values": { "pay_date": "2026-10-02" },
|
|
541
|
+
"rows": {
|
|
542
|
+
"deduction_types": [
|
|
543
|
+
{ "key": "401k", "values": { "amount": "50" } },
|
|
544
|
+
{ "key": "dental", "values": { "amount": "" } }
|
|
545
|
+
]
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
The list of deduction types lives only in the database: the nested query
|
|
551
|
+
answers it, and the form shows one input per row.
|
|
552
|
+
|
|
553
|
+
- Every live row of the nested query goes, whatever its values. What an
|
|
554
|
+
empty value means, such as no deduction of that type, is the handler's.
|
|
555
|
+
- One level only. A query nested inside a nested row shows the same rows in
|
|
556
|
+
every row around it, so it is not gathered.
|
|
557
|
+
- An insert, an update and a declared request carry `rows`. A delete does
|
|
558
|
+
not.
|
|
559
|
+
- A form whose only controls are in nested rows still sends, for its own
|
|
560
|
+
query, with empty `values`.
|
|
561
|
+
- A live row gathers nothing nested, as before.
|
|
562
|
+
|
|
563
|
+
The handler for the form's query receives `rows` beside `values`, and writes
|
|
564
|
+
the master and its details together; see [crud](./page-files.md#crud).
|
|
565
|
+
|
|
566
|
+
A nested query answers once for the page, so its rows are the same wherever
|
|
567
|
+
they show. The form enters a new master with its details, or edits the
|
|
568
|
+
master the page shows, whose details the nested query reads from the same
|
|
569
|
+
query parm. A form [pointed at a row](#editing-a-row-in-one-form) fills its
|
|
570
|
+
own controls from that row; its nested rows stay the nested query's answer.
|
|
571
|
+
|
|
572
|
+
### A row not yet entered
|
|
573
|
+
|
|
574
|
+
A row whose key is empty is a new one, not yet entered. It arises two ways:
|
|
575
|
+
a `row` query answers with an empty key, as one showing the master a query
|
|
576
|
+
parm names does when the parm is empty, or New points a form (see
|
|
577
|
+
[Editing a row in one form](#editing-a-row-in-one-form)). Either way the
|
|
578
|
+
element carries `lb-key-value=""`.
|
|
579
|
+
|
|
580
|
+
| Request | From a row not yet entered |
|
|
581
|
+
|------------------|---------------------------------------|
|
|
582
|
+
| `lb-row-insert` | Sent, with no `key` |
|
|
583
|
+
| A declared one | Sent, with no `key` |
|
|
584
|
+
| `lb-row-update` | Not sent: there is nothing to update |
|
|
585
|
+
| `lb-row-delete` | Not sent: there is nothing to delete |
|
|
586
|
+
|
|
587
|
+
An update or delete not sent is the row's state, not a mistake, and nothing
|
|
588
|
+
is reported. So a control that saves its own column with
|
|
589
|
+
`lb-request="lb-row-update"` saves on a row on record, and on a new row only
|
|
590
|
+
holds what a submit will gather:
|
|
591
|
+
|
|
592
|
+
```html
|
|
593
|
+
<section lb-query="account">
|
|
594
|
+
<form id="account-form">
|
|
595
|
+
<input lb-column="name" lb-request="lb-row-update" />
|
|
596
|
+
</form>
|
|
597
|
+
<button type="submit" form="account-form" lb-request="lb-row-insert">Save new</button>
|
|
598
|
+
</section>
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
The same holds for a request a custom element sends carrying `key: ""`.
|
|
602
|
+
An update or delete with no row around it at all has no key either, and
|
|
603
|
+
that is reported to the console.
|
|
604
|
+
|
|
495
605
|
### Inserting, updating and deleting rows
|
|
496
606
|
|
|
497
607
|
Loadbare provides three requests. Each runs the handler the page declares
|
|
@@ -528,6 +638,152 @@ failed insert leaves the entry for the user to correct.
|
|
|
528
638
|
Leave `lb-request` off a control inside a form or a live row that sends its
|
|
529
639
|
own update, or the same edit is written twice.
|
|
530
640
|
|
|
641
|
+
### Editing a row in one form
|
|
642
|
+
|
|
643
|
+
A table, or any other set of rows, that the user edits or adds to has one
|
|
644
|
+
form on the page, naming the same query. It sits anywhere, outside every
|
|
645
|
+
row. Buttons point it with HTML's `commandfor` and `command`:
|
|
646
|
+
|
|
647
|
+
| Command | On a button | The hub |
|
|
648
|
+
|----------------|-------------------------------------|---------------------------------------------|
|
|
649
|
+
| `--lb-edit` | In a live row of the form's query | Fills the form from that row, keyed by it |
|
|
650
|
+
| `--lb-new` | Anywhere | Starts the form empty, for an insert |
|
|
651
|
+
| `--lb-release` | Anywhere | Lets the form go without saving |
|
|
652
|
+
|
|
653
|
+
HTML requires a custom command to begin with `--`.
|
|
654
|
+
|
|
655
|
+
```html
|
|
656
|
+
<tbody lb-query="paychecks">
|
|
657
|
+
<template>
|
|
658
|
+
<tr>
|
|
659
|
+
<td lb-column="pay_date"></td>
|
|
660
|
+
<td lb-column="gross"></td>
|
|
661
|
+
<td><button type="button" commandfor="paycheck" command="--lb-edit">Edit</button></td>
|
|
662
|
+
</tr>
|
|
663
|
+
</template>
|
|
664
|
+
</tbody>
|
|
665
|
+
<button type="button" commandfor="paycheck" command="--lb-new">New</button>
|
|
666
|
+
|
|
667
|
+
<dialog>
|
|
668
|
+
<form id="paycheck" lb-query="paychecks" lb-request="lb-row-update">
|
|
669
|
+
<input lb-column="pay_date" type="date" />
|
|
670
|
+
<input lb-column="gross" />
|
|
671
|
+
<button type="submit">Save</button>
|
|
672
|
+
<button type="submit" lb-request="lb-row-insert">Save new</button>
|
|
673
|
+
<button type="button" commandfor="paycheck" command="--lb-release">Cancel</button>
|
|
674
|
+
</form>
|
|
675
|
+
</dialog>
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
Edit fills the form from the row as the hub already holds it, with no
|
|
679
|
+
round trip, including columns no element in the table shows. The user
|
|
680
|
+
edits a row they can see, so the row is on the page. A row that has to be
|
|
681
|
+
fetched to be edited is another page, with that row as its master.
|
|
682
|
+
|
|
683
|
+
The hub stamps the form with the row's key, so the form's request carries
|
|
684
|
+
it, and stamps the row `lb-row-editing`. New stamps an empty key, making the
|
|
685
|
+
form [a row not yet entered](#a-row-not-yet-entered), and resets every
|
|
686
|
+
control to its default. The form's state is in its `lb-key-value`
|
|
687
|
+
stamp:
|
|
688
|
+
|
|
689
|
+
| `lb-key-value` | The form is |
|
|
690
|
+
|----------------|-----------------------------|
|
|
691
|
+
| A key | Editing that row |
|
|
692
|
+
| Empty | Entering a new row |
|
|
693
|
+
| Absent | Not in use |
|
|
694
|
+
|
|
695
|
+
A stylesheet shows Save or Save new by it:
|
|
696
|
+
|
|
697
|
+
```css
|
|
698
|
+
form[lb-key-value=""] [type="submit"]:not([lb-request]),
|
|
699
|
+
form:not([lb-key-value=""]) [lb-request="lb-row-insert"] {
|
|
700
|
+
display: none;
|
|
701
|
+
}
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
An `lb-show` in the form reads from the row it was pointed at. With no row,
|
|
705
|
+
every column is null, so `lb-show="!id"` shows only while entering a new
|
|
706
|
+
row.
|
|
707
|
+
|
|
708
|
+
A successful request from the form lets it go, as Cancel does: the form
|
|
709
|
+
empties, its controls return to their defaults, and the stamps go. A failed
|
|
710
|
+
one leaves the form as the user had it, to correct.
|
|
711
|
+
|
|
712
|
+
Inside a `<dialog>`, Edit and New open the dialog modally, and letting go
|
|
713
|
+
closes it. The dialog closing in any way HTML closes one lets the form go:
|
|
714
|
+
Escape, `command="close"`, or `method="dialog"`. A form in no dialog opens
|
|
715
|
+
nothing. It is always showing, as a detail panel beside the table is, or a
|
|
716
|
+
stylesheet shows it while it has a key:
|
|
717
|
+
|
|
718
|
+
```css
|
|
719
|
+
form:not([lb-key-value]) {
|
|
720
|
+
display: none;
|
|
721
|
+
}
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
The form names a `rows` query and holds no row template, so the query
|
|
725
|
+
landing again lands nothing on it, and what the user has typed stays.
|
|
726
|
+
|
|
727
|
+
A button inside a form that carries a command is `type="button"`, or it
|
|
728
|
+
submits the form instead.
|
|
729
|
+
|
|
730
|
+
### Selecting rows
|
|
731
|
+
|
|
732
|
+
A checkbox carrying `lb-select` in a row template selects the live row it
|
|
733
|
+
sits in. It is not a column: it carries no `lb-column`, lands nothing and
|
|
734
|
+
is not gathered. The hub stamps the row `lb-row-selected` while it is
|
|
735
|
+
ticked.
|
|
736
|
+
|
|
737
|
+
A request for the query that names no one row then acts on the rows
|
|
738
|
+
selected, and carries their keys as `keys`:
|
|
739
|
+
|
|
740
|
+
```html
|
|
741
|
+
<tbody lb-query="transactions">
|
|
742
|
+
<template>
|
|
743
|
+
<tr>
|
|
744
|
+
<td><input type="checkbox" lb-select lb-show="selecting" /></td>
|
|
745
|
+
<td lb-column="description"></td>
|
|
746
|
+
<td lb-column="amount"></td>
|
|
747
|
+
</tr>
|
|
748
|
+
</template>
|
|
749
|
+
</tbody>
|
|
750
|
+
|
|
751
|
+
<form lb-query="transactions" lb-request="lb-row-update">
|
|
752
|
+
<lb-picker lb-query="accounts" lb-column="acct" exp-label="Account" exp-column="name"></lb-picker>
|
|
753
|
+
<button type="submit">Apply</button>
|
|
754
|
+
<button type="button" lb-request="lb-row-delete">Delete</button>
|
|
755
|
+
</form>
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
```json
|
|
759
|
+
{ "name": "lb-row-update", "query": "transactions", "keys": ["4", "9"], "values": { "acct": "12" } }
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
| Request | With rows selected | With none ticked |
|
|
763
|
+
|------------------|----------------------------------------|----------------------|
|
|
764
|
+
| `lb-row-update` | Sent with `keys` | Not sent |
|
|
765
|
+
| `lb-row-delete` | Sent with `keys` | Not sent |
|
|
766
|
+
| A declared one | Sent with `keys` | Sent with `keys: []` |
|
|
767
|
+
| `lb-row-insert` | Sent as ever; the selection is not its | Sent as ever |
|
|
768
|
+
|
|
769
|
+
An update or delete with nothing ticked has nothing to act on, and nothing
|
|
770
|
+
is reported. A request from inside a row still names that row alone. The
|
|
771
|
+
server runs the page's `rowUpdate` or `rowDelete` once for each key, as for
|
|
772
|
+
each row alone, and refreshes once; see [crud](./page-files.md#crud).
|
|
773
|
+
|
|
774
|
+
After the request succeeds, the hub clears the selection. A failed one
|
|
775
|
+
leaves it, to try again.
|
|
776
|
+
|
|
777
|
+
Only a checkbox that is showing counts. Turning a selector off with
|
|
778
|
+
`lb-show` also clears its tick, so leaving a selecting mode abandons the
|
|
779
|
+
selection rather than keeping it out of sight. With every selector off, a
|
|
780
|
+
request for the query is an ordinary one again.
|
|
781
|
+
|
|
782
|
+
Which rows are selected, and whether the page is in a selecting mode, is
|
|
783
|
+
interaction state. A mode the URL keeps, such as `?selecting=1`, reaches
|
|
784
|
+
the rows as a column the query computes from the parm, which `lb-show`
|
|
785
|
+
reads.
|
|
786
|
+
|
|
531
787
|
### Declared requests
|
|
532
788
|
|
|
533
789
|
A request name the page declares under `handlers` runs the application's
|
|
@@ -557,6 +813,7 @@ The hub stamps the element that committed:
|
|
|
557
813
|
|----------------------|-----------------------------------------------|
|
|
558
814
|
| `lb-request-pending` | The round trip is in flight |
|
|
559
815
|
| `lb-request-error` | The round trip failed, until the next request |
|
|
816
|
+
| `lb-request-refused` | The handler refused it; see [Refusals](#refusals) |
|
|
560
817
|
|
|
561
818
|
`lb-request-error` covers a non-2xx response, a network failure and a
|
|
562
819
|
timeout alike. The hub aborts a round trip after ten seconds, and sets
|
|
@@ -574,6 +831,44 @@ pending button is already disabled and a stylesheet only shows it:
|
|
|
574
831
|
}
|
|
575
832
|
```
|
|
576
833
|
|
|
834
|
+
### Refusals
|
|
835
|
+
|
|
836
|
+
A handler that refuses a request says why, in words for the user; see
|
|
837
|
+
[Refusing a request](./page-files.md#refusing-a-request). The hub shows
|
|
838
|
+
them where they belong:
|
|
839
|
+
|
|
840
|
+
- A message for the request as a whole is the value of the
|
|
841
|
+
`lb-request-refused` stamp on the element that committed, empty when the
|
|
842
|
+
refusal has none.
|
|
843
|
+
- A message for a column goes on the control the request gathered it from,
|
|
844
|
+
as HTML's own validity: `setCustomValidity`, the `:invalid` pseudo-class,
|
|
845
|
+
and the browser's own bubble for the first, which takes focus.
|
|
846
|
+
|
|
847
|
+
A stylesheet shows the whole-request message with no script:
|
|
848
|
+
|
|
849
|
+
```css
|
|
850
|
+
form[lb-request-refused]:not([lb-request-refused=""])::after {
|
|
851
|
+
content: attr(lb-request-refused);
|
|
852
|
+
color: firebrick;
|
|
853
|
+
}
|
|
854
|
+
input:invalid {
|
|
855
|
+
outline: 2px solid firebrick;
|
|
856
|
+
}
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
A refusal is not a failure: `lb-request-error` is not stamped, and nothing
|
|
860
|
+
is reported to the console.
|
|
861
|
+
|
|
862
|
+
A refusal stands until the user edits any control the request gathered,
|
|
863
|
+
resets its form, or sends from the same element again. Then every message
|
|
864
|
+
it put up goes at once, since the next submit is a new question for the
|
|
865
|
+
handler. While a control carries a message the browser holds its form's
|
|
866
|
+
submit, as for any invalid control, so the user edits before trying again.
|
|
867
|
+
|
|
868
|
+
A form-associated custom element takes a column's message when it has
|
|
869
|
+
`setCustomValidity` and `reportValidity`; see
|
|
870
|
+
[Being a control](./custom-elements.md#being-a-control).
|
|
871
|
+
|
|
577
872
|
### The request event
|
|
578
873
|
|
|
579
874
|
The hub dispatches every request as a bubbling `lb-request` event from the
|
|
@@ -620,3 +915,7 @@ expansion, and refuses to build:
|
|
|
620
915
|
- `lb-show` on a `<template>`, or on a row template's root
|
|
621
916
|
- `lb-show` with no `lb-query` around it
|
|
622
917
|
- `lb-show="!"` or `lb-show="!!column"`, which reverse no column
|
|
918
|
+
- `lb-select` on anything but `<input type="checkbox">`, outside a row
|
|
919
|
+
template, or beside `lb-column` or `lb-request`
|
|
920
|
+
- a `command` beginning `--lb-` that is not one of the three commands
|
|
921
|
+
- one of the three commands with no `commandfor`
|