@loadbare/app 0.10.0 → 0.12.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 +51 -38
- package/dist/build/assemble.js.map +1 -1
- package/dist/build/expand.d.ts +6 -1
- package/dist/build/expand.d.ts.map +1 -1
- package/dist/build/expand.js +92 -7
- package/dist/build/expand.js.map +1 -1
- package/dist/core/lb-constants.d.ts +4 -0
- package/dist/core/lb-constants.d.ts.map +1 -1
- package/dist/core/lb-constants.js +16 -0
- package/dist/core/lb-constants.js.map +1 -1
- package/dist/core/lb-types.d.ts +11 -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 +7 -1
- package/dist/hub/lb-apply.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +136 -18
- 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 +121 -37
- package/dist/hub/lb-hub.browser.js.map +1 -1
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/server/lb-server.js +17 -0
- package/dist/server/lb-server.js.map +1 -1
- package/docs/TECHREF-1.0.md +151 -10
- package/docs/comparison.md +30 -5
- package/docs/reference/chrome.md +15 -0
- package/docs/reference/custom-elements.md +163 -9
- package/docs/reference/data-binding.md +45 -2
- package/docs/reference/page-files.md +59 -2
- package/docs/what-does-loadbare-extend.md +124 -0
- package/package.json +1 -1
- package/skills/loadbare-app/SKILL.md +86 -10
- package/skills/loadbare-app/references/TECHREF-1.0.md +151 -10
- package/skills/loadbare-app/references/chrome.md +15 -0
- package/skills/loadbare-app/references/custom-elements.md +163 -9
- package/skills/loadbare-app/references/data-binding.md +45 -2
- package/skills/loadbare-app/references/page-files.md +59 -2
|
@@ -190,6 +190,36 @@ path that closes it.
|
|
|
190
190
|
Expansion runs over `<template>` contents wherever they occur, so a
|
|
191
191
|
definition's row template ships already expanded, `{{placeholder}}` included.
|
|
192
192
|
|
|
193
|
+
### Where a custom element can go
|
|
194
|
+
|
|
195
|
+
HTML limits what an element may hold. A `<p>` holds only phrasing content,
|
|
196
|
+
such as text, `<span>` and `<button>`, and a `<button>` may not hold another
|
|
197
|
+
`<button>`. The parser does not refuse markup that breaks these rules; it
|
|
198
|
+
rearranges it. `<p>Total <div>12</div></p>` becomes a paragraph, then a div,
|
|
199
|
+
then an empty paragraph, with nothing logged.
|
|
200
|
+
|
|
201
|
+
Each file is parsed on its own, so a page file and an element file each get
|
|
202
|
+
the tree the browser would give them. Expansion then places one inside the
|
|
203
|
+
other, and that join is where a rule can break. A definition containing a
|
|
204
|
+
`<div>` or a `<dialog>` cannot go inside a `<p>`, since the browser would
|
|
205
|
+
close the paragraph early and move the rest of the definition out of its
|
|
206
|
+
element. The build refuses such a page, naming the file, the custom element
|
|
207
|
+
and the path to what would move:
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
assemble: 'src/pages/ledger.page.html': expand: <lb-confirm> puts <dialog>
|
|
211
|
+
where the browser's parser would not keep it, at p > lb-confirm > dialog.
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Put a custom element whose definition holds block content, such as a
|
|
215
|
+
`<div>`, a `<dialog>` or a `<p>`, in a `<div>`, a `<li>`, a `<td>` or another
|
|
216
|
+
element that holds flow content.
|
|
217
|
+
|
|
218
|
+
Never write a custom element inside a `<select>`, an `<option>` or a
|
|
219
|
+
`<textarea>`. The parser drops the tag inside the first two, and inside a
|
|
220
|
+
`<textarea>` it is text. Both happen when the page file is read, before
|
|
221
|
+
expansion, so the build cannot see the element to report it.
|
|
222
|
+
|
|
193
223
|
### What fails at build time
|
|
194
224
|
|
|
195
225
|
Each of these is reported with the tag and file name, and nothing is
|
|
@@ -204,6 +234,8 @@ shipped:
|
|
|
204
234
|
- a `<template lb-exp-template="name">` naming a destination the definition
|
|
205
235
|
does not have
|
|
206
236
|
- two templates for the same destination
|
|
237
|
+
- a custom element placed where the browser's parser would move its
|
|
238
|
+
contents; see [Where a custom element can go](#where-a-custom-element-can-go)
|
|
207
239
|
|
|
208
240
|
An unfilled `{{placeholder}}` is not among these. That is presence
|
|
209
241
|
propagation working as intended, indistinguishable from an author who left
|
|
@@ -228,12 +260,16 @@ one as a string literal:
|
|
|
228
260
|
| `ATTR_REQUEST` | `lb-request` |
|
|
229
261
|
| `ATTR_COLUMN_VALUE` | `lb-column-value` |
|
|
230
262
|
| `ATTR_KEY_VALUE` | `lb-key-value` |
|
|
263
|
+
| `ATTR_ROW_LIVE` | `lb-row-live` |
|
|
264
|
+
| `LIVE_ROW` | `[lb-row-live]` |
|
|
231
265
|
| `ATTR_QUERY_ROW_COUNT` | `lb-query-row-count` |
|
|
232
266
|
| `ATTR_REQUEST_PENDING` | `lb-request-pending` |
|
|
233
267
|
| `ATTR_REQUEST_ERROR` | `lb-request-error` |
|
|
234
268
|
| `REQUEST_ROW_INSERT`, `REQUEST_ROW_UPDATE`, `REQUEST_ROW_DELETE` | The requests Loadbare provides |
|
|
235
269
|
| `LB_ROW_REQUESTS` | All three, in one array |
|
|
236
270
|
| `LB_EVENT_NAME` | `lb-request`, the event every request travels as |
|
|
271
|
+
| `LB_DONE_EVENT_NAME` | `lb-request-done`, the event that says how it turned out |
|
|
272
|
+
| `SHOW_NOT` | `!`, written before a column in `lb-show` |
|
|
237
273
|
| `URL_QUERY` | `lb-url` |
|
|
238
274
|
| `URL_COLUMN_PATH`, `URL_COLUMN_PAGE_LABEL`, `URL_COLUMN_PAGE_UNKNOWN` | `lb-path`, `lb-page-label`, `lb-page-unknown` |
|
|
239
275
|
| `LB_RESERVED_PREFIX` | `lb-` |
|
|
@@ -244,6 +280,64 @@ writes them in markup, and the hub and the builder write their stamps.
|
|
|
244
280
|
Loadbare reserves method names beginning with `lb` on a custom element, for
|
|
245
281
|
the methods it calls; see [Holding rows](#holding-rows).
|
|
246
282
|
|
|
283
|
+
### What the hub and an element say to each other
|
|
284
|
+
|
|
285
|
+
Each direction has one mechanism for each kind of message:
|
|
286
|
+
|
|
287
|
+
| Direction | What | How | Today |
|
|
288
|
+
|----------------|--------------------------------------------|-----------------------------|-------|
|
|
289
|
+
| Hub to element | State that lasts | An attribute the hub stamps | `lb-column-value`, `lb-key-value`, `lb-query-row-count`, `lb-request-pending`, `lb-request-error` |
|
|
290
|
+
| Hub to element | State the platform already names | A property | A control's `value` |
|
|
291
|
+
| Hub to element | Work the hub needs done now, while landing | An optional `lb` method | `lbPlaceRow`, `lbRowsLanded` |
|
|
292
|
+
| Hub to element | A moment an element started | A bubbling event | `lb-request-done` |
|
|
293
|
+
| Element to hub | A moment | A bubbling event | `lb-request`, and `change`, `click` and `submit` |
|
|
294
|
+
|
|
295
|
+
State is an attribute: a stylesheet can select on it, and an element that
|
|
296
|
+
upgrades late still finds it. A method is for work the hub cannot go on
|
|
297
|
+
without, done by one element at one point in landing. A moment is an event,
|
|
298
|
+
because the element that cares is often an ancestor of the one it concerns,
|
|
299
|
+
and an event reaches it knowing neither.
|
|
300
|
+
|
|
301
|
+
### When its code runs
|
|
302
|
+
|
|
303
|
+
The hub creates, places and parks elements, and each of those reaches a
|
|
304
|
+
custom element as the browser's lifecycle callbacks. It is the same instance
|
|
305
|
+
from its constructor on, however often it moves:
|
|
306
|
+
|
|
307
|
+
| What the hub does | Callbacks, in order |
|
|
308
|
+
|----------------------------------------------------|---------------------------------------------|
|
|
309
|
+
| Shows a page | `constructor`, `connectedCallback` |
|
|
310
|
+
| Creates a live row, filled before it is placed | `constructor` |
|
|
311
|
+
| Places a live row for the first time | `connectedCallback` |
|
|
312
|
+
| Places it again, as it does on every set of all rows | `disconnectedCallback`, `connectedCallback` |
|
|
313
|
+
| Removes a live row | `disconnectedCallback` |
|
|
314
|
+
| Turns `lb-show` off | `disconnectedCallback`, `adoptedCallback` |
|
|
315
|
+
| Turns `lb-show` on | `adoptedCallback`, `connectedCallback` |
|
|
316
|
+
| Turns on an element the builder shipped absent | `constructor`, `connectedCallback` |
|
|
317
|
+
|
|
318
|
+
The builder ships every `lb-show` element inside its template, and nothing
|
|
319
|
+
in a template is upgraded, so such an element has no instance until its
|
|
320
|
+
column first turns on. See
|
|
321
|
+
[Conditional rendering](./data-binding.md#conditional-rendering).
|
|
322
|
+
|
|
323
|
+
So each kind of work has one place:
|
|
324
|
+
|
|
325
|
+
1. **A listener on the element itself goes in the constructor.** It is
|
|
326
|
+
attached once and survives every move, and a live row gets its own
|
|
327
|
+
because each row is a new instance. An event from a child bubbles to the
|
|
328
|
+
element, so the listener needs no child to exist yet. The constructor
|
|
329
|
+
reads no attribute and no child: an element made with
|
|
330
|
+
`document.createElement` has neither when it runs.
|
|
331
|
+
2. **A child is looked up when it is needed**, in a handler, a getter,
|
|
332
|
+
`lbPlaceRow` or `lbRowsLanded`, and never held from setup.
|
|
333
|
+
3. **`connectedCallback` runs on every move**, so it is written to run
|
|
334
|
+
again. It rearranges the element's own children into a state it checks
|
|
335
|
+
for first, or adds a listener to `document` or `window`, which
|
|
336
|
+
`disconnectedCallback` removes. Work done once per instance that needs
|
|
337
|
+
the element's attributes, such as a control taking up a value that landed
|
|
338
|
+
before it upgraded, goes behind a flag there; see
|
|
339
|
+
[Being a control](#being-a-control).
|
|
340
|
+
|
|
247
341
|
### Being a control
|
|
248
342
|
|
|
249
343
|
A form-associated custom element with a `value` property that fires `change`
|
|
@@ -253,10 +347,20 @@ and commits its `lb-request` on `change`:
|
|
|
253
347
|
|
|
254
348
|
```ts
|
|
255
349
|
// src/star-rating.browser.ts
|
|
350
|
+
import { ATTR_COLUMN_VALUE } from "@loadbare/app/constants";
|
|
351
|
+
|
|
256
352
|
class StarRating extends HTMLElement {
|
|
257
353
|
static formAssociated = true;
|
|
258
354
|
#internals = this.attachInternals();
|
|
259
355
|
#value = "";
|
|
356
|
+
#connected = false;
|
|
357
|
+
|
|
358
|
+
connectedCallback() {
|
|
359
|
+
if (this.#connected) return;
|
|
360
|
+
this.#connected = true;
|
|
361
|
+
const stamped = this.getAttribute(ATTR_COLUMN_VALUE);
|
|
362
|
+
if (stamped !== null) this.value = stamped;
|
|
363
|
+
}
|
|
260
364
|
|
|
261
365
|
get value(): string {
|
|
262
366
|
return this.#value;
|
|
@@ -292,6 +396,14 @@ customElements.define("star-rating", StarRating);
|
|
|
292
396
|
Dispatch `change` with `bubbles: true`, as a native control's does, when the
|
|
293
397
|
user commits an edit. Setting `value` from the hub fires nothing.
|
|
294
398
|
|
|
399
|
+
A control can receive its value before it is a control. An element the
|
|
400
|
+
builder shipped absent, or one whose definition loads after the hub has
|
|
401
|
+
landed, is not upgraded when its column lands, so the hub cannot set its
|
|
402
|
+
`value` and the value arrives as `lb-column-value` alone. Take the stamp up
|
|
403
|
+
the first time the control connects, as `connectedCallback` does above, and
|
|
404
|
+
never again: after that the hub sets `value` itself, and a later move must
|
|
405
|
+
not undo an edit.
|
|
406
|
+
|
|
295
407
|
A form owns a form-associated custom element the way it owns an `<input>`,
|
|
296
408
|
including through the HTML `form` attribute, so a form gathers it on submit.
|
|
297
409
|
Write `formResetCallback` to restore the default value: the hub resets a form
|
|
@@ -330,14 +442,9 @@ List `ATTR_COLUMN_VALUE` in `observedAttributes`. The browser calls
|
|
|
330
442
|
upgrades, as well as for one that changes afterward, so the first render and
|
|
331
443
|
every later one go through the one callback.
|
|
332
444
|
|
|
333
|
-
A custom element carrying `lb-show`, or inside an element that does,
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
Going in, it sees `disconnectedCallback` and then `adoptedCallback`; coming
|
|
337
|
-
out, `adoptedCallback` and then `connectedCallback`. It is the same instance
|
|
338
|
-
throughout, and it still receives its column while it is away, so write
|
|
339
|
-
`connectedCallback` to run more than once, as a live row the hub reorders
|
|
340
|
-
already requires.
|
|
445
|
+
A custom element carrying `lb-show`, or inside an element that does, still
|
|
446
|
+
receives its column while it is away, so it returns current; see
|
|
447
|
+
[When its code runs](#when-its-code-runs).
|
|
341
448
|
|
|
342
449
|
### Sending a request
|
|
343
450
|
|
|
@@ -363,6 +470,39 @@ missing a field its name needs, and stamps the dispatching element with
|
|
|
363
470
|
|
|
364
471
|
Let the event bubble, so an ancestor can stop it before the hub sends it.
|
|
365
472
|
|
|
473
|
+
### Hearing how it turned out
|
|
474
|
+
|
|
475
|
+
Once what a request brought back has landed, or its round trip has failed,
|
|
476
|
+
the hub dispatches `lb-request-done` from the element that committed. It
|
|
477
|
+
bubbles, so the element that cares need not be the one that committed: a
|
|
478
|
+
dialog hears it from the form inside it.
|
|
479
|
+
|
|
480
|
+
```ts
|
|
481
|
+
import { LB_DONE_EVENT_NAME } from "@loadbare/app/constants";
|
|
482
|
+
import type { HubRequestDone } from "@loadbare/app/types";
|
|
483
|
+
|
|
484
|
+
this.addEventListener(LB_DONE_EVENT_NAME, (e) => {
|
|
485
|
+
const { request, items, error } = (e as CustomEvent<HubRequestDone>).detail;
|
|
486
|
+
if (error) return;
|
|
487
|
+
const item = items.find((i) => i.query === request.query);
|
|
488
|
+
const created = item?.patch?.rows?.[0];
|
|
489
|
+
if (created) this.choose(String(created[item!.key]));
|
|
490
|
+
});
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
`items` is every response item that landed because of the request, in
|
|
494
|
+
order. An answer that moved the URL contributes its `lb-url` item, which
|
|
495
|
+
carries a key only the server knew, and the page load. `error` is set when
|
|
496
|
+
the round trip failed, and then nothing landed. A new row's key is in the
|
|
497
|
+
answer only when the handler returned it in a patch: a refresh sends all
|
|
498
|
+
rows, and nothing marks which one is new.
|
|
499
|
+
|
|
500
|
+
By the time the event is dispatched, `lb-request-pending` is gone and the
|
|
501
|
+
page is as the answer left it. A request that was never sent, because the
|
|
502
|
+
hub or an ancestor stopped it, gets no `lb-request-done`. An element the
|
|
503
|
+
answer removed, such as the row a delete took away, dispatches the event
|
|
504
|
+
outside the document, and no ancestor it had there hears it.
|
|
505
|
+
|
|
366
506
|
### Holding rows
|
|
367
507
|
|
|
368
508
|
The hub lands every row template itself. A plain element carrying `lb-query`
|
|
@@ -401,7 +541,7 @@ Everything else is the hub's:
|
|
|
401
541
|
| Cloning | Clones the row template once per new key |
|
|
402
542
|
| Matching | Fills the live row already showing that key |
|
|
403
543
|
| Removing | Removes the live rows the response says are gone |
|
|
404
|
-
| Stamping | Stamps each live row with `lb-key-value`
|
|
544
|
+
| Stamping | Stamps each live row with `lb-row-live` and `lb-key-value` |
|
|
405
545
|
| Counting | Stamps `lb-query-row-count` with the number of live rows |
|
|
406
546
|
|
|
407
547
|
`lbPlaceRow` receives the live row already filled and not yet in the
|
|
@@ -411,6 +551,20 @@ template, in arrival order. `lb-options.browser.ts` and
|
|
|
411
551
|
`lb-table.browser.ts` in [`@loadbare/widgets`](./widgets.md) are two
|
|
412
552
|
different placements over the same landing.
|
|
413
553
|
|
|
554
|
+
A custom element the builder shipped absent under `lb-show` has not
|
|
555
|
+
upgraded while its column is off, so it has no `lbPlaceRow` when rows land
|
|
556
|
+
on it then, and the hub places them before the template. When the column
|
|
557
|
+
first turns on and the element upgrades, the hub lands the query's last
|
|
558
|
+
answer on it again, as all rows, so every row goes through `lbPlaceRow` and
|
|
559
|
+
`lbRowsLanded` runs. Once upgraded it stays so: when its branch goes away and
|
|
560
|
+
comes back, it has placed every row that landed meanwhile, and nothing is
|
|
561
|
+
landed again.
|
|
562
|
+
|
|
563
|
+
A custom element that walks its own rows finds them with `LIVE_ROW` from
|
|
564
|
+
`@loadbare/app/constants`, never by `lb-key-value`: an element a `row` lands
|
|
565
|
+
on carries a key too, and may sit among the rows, as a total in a table's
|
|
566
|
+
foot does.
|
|
567
|
+
|
|
414
568
|
Style an empty query against `lb-query-row-count` rather than carrying an
|
|
415
569
|
empty state in the custom element — see
|
|
416
570
|
[Counting rows](./data-binding.md#counting-rows).
|
|
@@ -58,6 +58,7 @@ builder writes a stamp, and a stylesheet or a custom element reads it.
|
|
|
58
58
|
|----------------------|----------------------------------------------------|
|
|
59
59
|
| `lb-column-value` | The value the hub set from a column |
|
|
60
60
|
| `lb-key-value` | The key of a live row, or of a `row` landed on an element |
|
|
61
|
+
| `lb-row-live` | The element is a live row |
|
|
61
62
|
| `lb-query-row-count` | The number of live rows an element holds |
|
|
62
63
|
| `lb-request-pending` | The element's request is in flight |
|
|
63
64
|
| `lb-request-error` | The element's last request failed |
|
|
@@ -86,8 +87,11 @@ any nested `lb-query`:
|
|
|
86
87
|
| `rows` | no | Lands nothing |
|
|
87
88
|
|
|
88
89
|
A live row is an element the hub cloned from a row template for one row.
|
|
89
|
-
The hub stamps it with `lb-key-value`, the row's
|
|
90
|
-
lands on itself carries `lb-key-value` as well
|
|
90
|
+
The hub stamps it with `lb-row-live`, and with `lb-key-value`, the row's
|
|
91
|
+
key. An element a `row` lands on itself carries `lb-key-value` as well, but
|
|
92
|
+
not `lb-row-live`, so a `row` shown inside another query, such as a total in
|
|
93
|
+
a table's foot, is not one of that query's rows. Select live rows with
|
|
94
|
+
`[lb-row-live]`, never with `[lb-key-value]`.
|
|
91
95
|
|
|
92
96
|
Write `lb-column` on each element that shows a column. The element reads
|
|
93
97
|
from its nearest ancestor row: the live row it is in, or the element a
|
|
@@ -153,6 +157,9 @@ membership and order: a live row whose key did not arrive is removed. A
|
|
|
153
157
|
patch changes only the rows it names, and every other live row keeps its
|
|
154
158
|
content and its place.
|
|
155
159
|
|
|
160
|
+
A key is unique within a query. Two rows with one key in the same answer,
|
|
161
|
+
or two live rows showing one key, are reported on the console.
|
|
162
|
+
|
|
156
163
|
A live row's root counts as a column when it carries `lb-column`, which is
|
|
157
164
|
how an `<option>`, whose content is text, shows the column it is.
|
|
158
165
|
|
|
@@ -161,6 +168,28 @@ a row goes and add scaffolding around the rows; see
|
|
|
161
168
|
[Holding rows](./custom-elements.md#holding-rows) and
|
|
162
169
|
[The Basic Widget Library](./widgets.md).
|
|
163
170
|
|
|
171
|
+
### What arrives later
|
|
172
|
+
|
|
173
|
+
The hub keeps each query's last answer until the page changes, with any
|
|
174
|
+
patch since applied to it. An element that names a query and arrives after
|
|
175
|
+
that query's answer is filled from what was kept, at the end of the landing
|
|
176
|
+
that follows its arrival. Such an element is a picker in a new live row, or
|
|
177
|
+
scaffolding a custom element builds around its rows, such as a ghost row. So
|
|
178
|
+
a query nested in another query's rows need not land again, or land after
|
|
179
|
+
the outer query, for a new outer row to show its choices.
|
|
180
|
+
|
|
181
|
+
A custom element the builder shipped absent has not upgraded while its
|
|
182
|
+
`lb-show` column is off, so rows that land on it then are not placed by it.
|
|
183
|
+
When the column first turns on and the element upgrades, the hub lands the
|
|
184
|
+
kept answer on it again, and every row goes through its `lbPlaceRow`. So a
|
|
185
|
+
table that groups its rows can sit under `lb-show`. See
|
|
186
|
+
[Holding rows](./custom-elements.md#holding-rows).
|
|
187
|
+
|
|
188
|
+
Nothing is answered from what the hub kept: a request always goes to the
|
|
189
|
+
server, and a query re-runs only when a response names it. A patch that
|
|
190
|
+
arrives before all of a query's rows is not kept, since it is not the whole
|
|
191
|
+
of anything.
|
|
192
|
+
|
|
164
193
|
A `rows` query on an element with no row template lands nothing. That is the
|
|
165
194
|
insert form above: it sits inside `lb-query="roster"` so its request is for
|
|
166
195
|
`roster`, and it shows none of its rows.
|
|
@@ -218,6 +247,14 @@ have the query answer with a boolean or a null. A row that does not carry
|
|
|
218
247
|
the column leaves the element as it is, so a query answers with the column
|
|
219
248
|
in every row.
|
|
220
249
|
|
|
250
|
+
Write `!` before the column to reverse it. One column then decides between
|
|
251
|
+
two elements, rather than a column and its opposite, which could disagree:
|
|
252
|
+
|
|
253
|
+
```html
|
|
254
|
+
<h2 lb-show="chosen" lb-column="title"></h2>
|
|
255
|
+
<p lb-show="!chosen">Choose a batch.</p>
|
|
256
|
+
```
|
|
257
|
+
|
|
221
258
|
`lb-show` reads from the nearest ancestor row, as `lb-column` does. On an
|
|
222
259
|
element that also carries `lb-query`, the column belongs to the row around
|
|
223
260
|
it, so this picker takes its choices from `groups` and whether it is present
|
|
@@ -456,6 +493,11 @@ An ancestor may stop the event, and the request is not sent. A custom
|
|
|
456
493
|
element may dispatch the event itself; see
|
|
457
494
|
[Sending a request](./custom-elements.md#sending-a-request).
|
|
458
495
|
|
|
496
|
+
Once the answer has landed, or the round trip has failed, the hub dispatches
|
|
497
|
+
a bubbling `lb-request-done` event from the same element, carrying the
|
|
498
|
+
request and what landed because of it, or the error; see
|
|
499
|
+
[Hearing how it turned out](./custom-elements.md#hearing-how-it-turned-out).
|
|
500
|
+
|
|
459
501
|
## The URL
|
|
460
502
|
|
|
461
503
|
The hub serves one query of its own, `lb-url`: one row holding the path, the
|
|
@@ -488,3 +530,4 @@ expansion, and refuses to build:
|
|
|
488
530
|
- `lb-url-unknown` on anything but a `<dialog>`, or outside `<lb-hub>`
|
|
489
531
|
- `lb-show` on a `<template>`, or on a row template's root
|
|
490
532
|
- `lb-show` with no `lb-query` around it
|
|
533
|
+
- `lb-show="!"` or `lb-show="!!column"`, which reverse no column
|
|
@@ -16,6 +16,35 @@ every other path names the page whose stub it is.
|
|
|
16
16
|
Put the files anywhere under `src/`. The builder pairs them by stub, not by
|
|
17
17
|
directory; `src/pages/` is the convention.
|
|
18
18
|
|
|
19
|
+
## What the page files are
|
|
20
|
+
|
|
21
|
+
A page's queries are that page's view model, answered on the server. Each
|
|
22
|
+
is shaped for the elements that name it, on one page, so it answers in the
|
|
23
|
+
form the page shows: a date formatted, an amount with its separators, and
|
|
24
|
+
the phrase a reader sees, such as "no postings yet" or "12 postings",
|
|
25
|
+
rather than a count for the browser to word. The hub does no conversion,
|
|
26
|
+
so there is no later place for that work to go. The markup says where each
|
|
27
|
+
value goes, and the query says what it is.
|
|
28
|
+
|
|
29
|
+
A value the page reads back, rather than only shows, stays data:
|
|
30
|
+
|
|
31
|
+
| Value | Read back by | So it is |
|
|
32
|
+
|------------------------------------|---------------------------------|-----------------------------------|
|
|
33
|
+
| A column that lands on a control | The request that gathers it | What the handler expects to receive |
|
|
34
|
+
| The key column | Every request from the row | What the handler looks the row up by |
|
|
35
|
+
| A column `lb-show` names | The hub | `null` or `false` for off |
|
|
36
|
+
| A column a stylesheet selects on | `[lb-column-value="…"]` | A fixed token, not wording |
|
|
37
|
+
|
|
38
|
+
The page files are not a data layer; they sit on one. The application
|
|
39
|
+
opens its own database and keeps its rules there, or in code its handlers
|
|
40
|
+
share, and the page files stay thin because integrity is not their job.
|
|
41
|
+
|
|
42
|
+
What two pages share goes beneath them, not between. Two pages showing the
|
|
43
|
+
same data each write the query, or both read a database view. Shared
|
|
44
|
+
wording and shared parm reading go in a helper module the queries import.
|
|
45
|
+
Everything that belongs to one page stays in one place, so a change to a
|
|
46
|
+
page touches that page's files.
|
|
47
|
+
|
|
19
48
|
## HTML
|
|
20
49
|
|
|
21
50
|
Write the page as a fragment. The fragment lands in `<main>`, which
|
|
@@ -194,6 +223,9 @@ Every value in `values` is a string, as a control's value is.
|
|
|
194
223
|
### refresh and patch
|
|
195
224
|
|
|
196
225
|
List in `refresh` every query whose whole answer the request changed.
|
|
226
|
+
Never list one only to fill what the request's answer creates, such as the
|
|
227
|
+
picker in a new row: the hub fills that from the query's last answer. See
|
|
228
|
+
[What arrives later](./data-binding.md#what-arrives-later).
|
|
197
229
|
|
|
198
230
|
Return answers from `run` to state a narrower change than re-running a query
|
|
199
231
|
would. What `run` returns is keyed by query name and laid over the refreshed
|
|
@@ -207,8 +239,33 @@ queries:
|
|
|
207
239
|
| `patch({ drop: [...] })` | These keys are gone; the rest stand |
|
|
208
240
|
|
|
209
241
|
Return a patch for a change the request knows the extent of — one row added,
|
|
210
|
-
one row dropped, one row edited — and leave `refresh` empty.
|
|
211
|
-
|
|
242
|
+
one row dropped, one row edited — and leave `refresh` empty. A refreshed
|
|
243
|
+
`rows` query sends every row, and the hub places every row again, which moves
|
|
244
|
+
each element and takes focus from the control the user is in.
|
|
245
|
+
|
|
246
|
+
A change that seems to need a refresh usually has a patch:
|
|
247
|
+
|
|
248
|
+
- A placeholder row standing in for an empty set or section: drop its key in
|
|
249
|
+
the patch that adds the first real row, and add it back in the patch that
|
|
250
|
+
drops the last.
|
|
251
|
+
- A row whose position changes: the rows host places it, as
|
|
252
|
+
[`lb-table`](./widgets.md#lb-table) does by `data-sort`.
|
|
253
|
+
- A row whose other columns change with the edit: re-read the row and patch
|
|
254
|
+
it.
|
|
255
|
+
- Rows the database removes with a deleted one: select their keys before the
|
|
256
|
+
delete and drop them too.
|
|
257
|
+
|
|
258
|
+
An update or a delete always has a patch, since it names its row by key and
|
|
259
|
+
removing a row never reorders the rest. `createHub` refuses at startup a
|
|
260
|
+
`crud` `rowUpdate` or `rowDelete` on a `rows` query whose `refresh` names
|
|
261
|
+
that same query. An insert may refresh its own query: where a new row goes
|
|
262
|
+
depends on whether its host places it, which the server cannot see.
|
|
263
|
+
|
|
264
|
+
Each request's `refresh` is its own, and every query in it needs its own
|
|
265
|
+
reason. A list shared by several requests is a warning sign.
|
|
266
|
+
|
|
267
|
+
Re-run the query when membership or order changed in a way the request
|
|
268
|
+
cannot name:
|
|
212
269
|
|
|
213
270
|
```ts
|
|
214
271
|
resetRoster: {
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# What Does Loadbare Extend?
|
|
2
|
+
|
|
3
|
+
> **LLM-authored, not yet revised by a person.** Drafted by Claude on
|
|
4
|
+
> 2026-09-27 against `@loadbare/app` 0.10.0.
|
|
5
|
+
|
|
6
|
+
The question: what is `@loadbare/app`'s foundation, the thing it adds to
|
|
7
|
+
rather than replaces, and does the package stand on that foundation the way
|
|
8
|
+
`@loadbare/db` stands on Postgres?
|
|
9
|
+
|
|
10
|
+
The short answer: the browser half of `@loadbare/app` is honestly "HTML+", in
|
|
11
|
+
the same way `@loadbare/db` is "Postgres+". The package as a whole is not.
|
|
12
|
+
The framing that covers all of it, and ties it to db, is relational rather
|
|
13
|
+
than HTML.
|
|
14
|
+
|
|
15
|
+
## The precise reading
|
|
16
|
+
|
|
17
|
+
### Where "extension to HTML" is literally true
|
|
18
|
+
|
|
19
|
+
- **It uses the platform's own extension point.** Custom elements are how
|
|
20
|
+
HTML is meant to be extended. The whole browser runtime is one custom
|
|
21
|
+
element, `<lb-hub>`. Custom elements are also the only way Loadbare lets an
|
|
22
|
+
application split up markup or ship behaviour. There are no components, no
|
|
23
|
+
JSX, and no expressions evaluated at run time.
|
|
24
|
+
- **The attributes copy HTML's own ideas.** [Theory](./theory.md#data-binding)
|
|
25
|
+
already sets this out in a table:
|
|
26
|
+
- `lb-column` works like `name`.
|
|
27
|
+
- `lb-query` works like `<form>` or `<select>`, a container of records.
|
|
28
|
+
- `lb-show` works like `hidden`.
|
|
29
|
+
- `lb-key-value` works like an `<option>`'s `value` sitting apart from its
|
|
30
|
+
text.
|
|
31
|
+
- **HTML decides behaviour wherever it already has an opinion.** Commits
|
|
32
|
+
follow HTML's events: a form on submit, a control on change, anything else
|
|
33
|
+
on click. Gathering follows the form owner, including `form="…"`. Resets
|
|
34
|
+
go through `formResetCallback`. Busy state uses `aria-busy`. The unknown
|
|
35
|
+
page is a real `<dialog>`, and links are real `<a href>`.
|
|
36
|
+
- **What ships is plain HTML.** It can be read in view-source, it uses light
|
|
37
|
+
DOM, and CSS is left untouched. A page that shows no data is just an HTML
|
|
38
|
+
file.
|
|
39
|
+
- **There is historical precedent.** IE4's `datasrc`/`datafld` really was
|
|
40
|
+
data binding added to HTML, and [Prior art](./prior-art.md) finds its
|
|
41
|
+
vocabulary maps almost one to one onto Loadbare's.
|
|
42
|
+
|
|
43
|
+
### Where it is inaccurate or strained
|
|
44
|
+
|
|
45
|
+
- **The attributes don't conform to the spec.** HTML sets aside `data-*` for
|
|
46
|
+
author attributes, and a validator will flag `lb-*`. htmx and Alpine have
|
|
47
|
+
the same problem, so it's defensible. But a literal claim of "extension to
|
|
48
|
+
HTML" is only half true: Loadbare uses one of the sanctioned extension
|
|
49
|
+
points (custom elements) fully and skips the other (`data-*`).
|
|
50
|
+
- **Build-time expansion is a preprocessor.** `exp-` parameters and `{{…}}`
|
|
51
|
+
are a small template language that runs before HTML exists. The output is
|
|
52
|
+
HTML; the source a developer writes is not quite HTML.
|
|
53
|
+
- **Navigation overrides HTML's model on purpose.** HTML's model is one
|
|
54
|
+
document per URL. Loadbare ships every page as a `<template>` in one
|
|
55
|
+
document and answers every route with `app.html`, with no 404. Keeping the
|
|
56
|
+
address bar truthful softens this, but it is still a replacement.
|
|
57
|
+
- **The wire is JSON, not hypermedia.** In the ecosystem, "extends HTML"
|
|
58
|
+
usually means htmx's argument that HTML is unfinished hypertext. Loadbare
|
|
59
|
+
rejects that model after the first load. Calling it an HTML extension
|
|
60
|
+
invites exactly the comparison [Theory](./theory.md) spends paragraphs
|
|
61
|
+
getting out of.
|
|
62
|
+
- **Half of what a developer writes isn't HTML.** `queries.ts` and
|
|
63
|
+
`requests.ts`, with `row`/`rows`/`patch`, crud entries and refresh lists,
|
|
64
|
+
have no HTML counterpart at all.
|
|
65
|
+
|
|
66
|
+
## The db comparison
|
|
67
|
+
|
|
68
|
+
db earns "Postgres+" because it passes four tests:
|
|
69
|
+
|
|
70
|
+
1. What it builds is plain Postgres, inspectable with psql.
|
|
71
|
+
2. Its vocabulary extends Postgres's own concepts: the foreign key becomes the
|
|
72
|
+
channel for values flowing down and aggregates flowing up.
|
|
73
|
+
3. A developer can drop to plain SQL anywhere.
|
|
74
|
+
4. Postgres does the heavy lifting.
|
|
75
|
+
|
|
76
|
+
The browser half of app passes all four:
|
|
77
|
+
|
|
78
|
+
1. The output is plain HTML.
|
|
79
|
+
2. The vocabulary extends `name`, `form` and `hidden`.
|
|
80
|
+
3. Any HTML works, and a custom element is still just a custom element.
|
|
81
|
+
4. The platform does the work: `cloneNode` of templates, the selector engine,
|
|
82
|
+
form owners, `showModal`, the History API.
|
|
83
|
+
|
|
84
|
+
The rule behind it is the same as db's: **defer where the foundation has an
|
|
85
|
+
answer, and add only where it is silent.** HTML is silent about data, and
|
|
86
|
+
that's where the seven attributes live.
|
|
87
|
+
|
|
88
|
+
The difference is that db never overrides Postgres. It adds to Postgres and
|
|
89
|
+
replaces the application's logic layer. app overrides HTML twice, in
|
|
90
|
+
navigation and on the wire, both for the 300ms budget. Those are deliberate
|
|
91
|
+
departures, not add-ons.
|
|
92
|
+
|
|
93
|
+
## How it feels
|
|
94
|
+
|
|
95
|
+
- **Writing a page** feels like writing HTML: forms and tables with a few
|
|
96
|
+
attributes, much like data-bound HTML in 1999 without the ActiveX.
|
|
97
|
+
- **Writing a widget** feels like writing web components.
|
|
98
|
+
- **Writing `requests.ts`** feels like neither. A nested declaration such as
|
|
99
|
+
`crud.invoiceLines.rowDelete.{run, refresh}` feels like a framework's
|
|
100
|
+
configuration map. db has no equivalent seam, because its DSL reads as DDL
|
|
101
|
+
all the way down. That file is where the "HTML+" feel breaks.
|
|
102
|
+
|
|
103
|
+
## A better framing
|
|
104
|
+
|
|
105
|
+
The idea that runs through every layer of app is not HTML. It is the
|
|
106
|
+
**row**: query, row, rows, column and key. That's where the scope decision in
|
|
107
|
+
[Theory](./theory.md#data-binding) lands, and it's what the protocol carries.
|
|
108
|
+
HTML is the surface, and relations are the substance.
|
|
109
|
+
|
|
110
|
+
Framed that way, the two packages form one story rather than two:
|
|
111
|
+
|
|
112
|
+
- **db** is the relational model with derived values, built on Postgres.
|
|
113
|
+
- **app** is the relational model shown and edited on screen, with HTML as its
|
|
114
|
+
surface.
|
|
115
|
+
- **The row** is the only thing that crosses between them.
|
|
116
|
+
|
|
117
|
+
So "Relational+ from end to end, with Postgres at one end and HTML at the
|
|
118
|
+
other" is accurate everywhere. "HTML+" is accurate only for page and widget
|
|
119
|
+
authoring. It also explains the server half: `queries.ts` and `requests.ts`
|
|
120
|
+
aren't HTML because they're the relational half of the contract.
|
|
121
|
+
|
|
122
|
+
"HTML+" still works as a description of how page authoring feels. It fails
|
|
123
|
+
as a statement of what the package is, and it pulls readers toward the
|
|
124
|
+
hypermedia comparison Loadbare would rather avoid.
|