@loadbare/app 0.18.2 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +17 -17
  2. package/dist/build/assemble.d.ts +7 -1
  3. package/dist/build/assemble.d.ts.map +1 -1
  4. package/dist/build/assemble.js +81 -11
  5. package/dist/build/assemble.js.map +1 -1
  6. package/dist/build/cli.d.ts +3 -0
  7. package/dist/build/cli.d.ts.map +1 -1
  8. package/dist/build/cli.js +6 -1
  9. package/dist/build/cli.js.map +1 -1
  10. package/dist/build/declared.d.ts +25 -0
  11. package/dist/build/declared.d.ts.map +1 -0
  12. package/dist/build/declared.js +62 -0
  13. package/dist/build/declared.js.map +1 -0
  14. package/dist/build/pages.d.ts +6 -0
  15. package/dist/build/pages.d.ts.map +1 -1
  16. package/dist/build/pages.js +32 -12
  17. package/dist/build/pages.js.map +1 -1
  18. package/docs/TECHREF-1.0.md +27 -72
  19. package/docs/comparison.md +692 -480
  20. package/docs/departures.md +63 -0
  21. package/docs/miswarts.md +140 -0
  22. package/docs/opinions.md +155 -0
  23. package/docs/reference/builder.md +9 -2
  24. package/docs/reference/chrome.md +2 -1
  25. package/docs/reference/custom-elements.md +1 -1
  26. package/docs/reference/data-binding.md +36 -9
  27. package/docs/reference/page-files.md +36 -4
  28. package/docs/roadmap.md +55 -43
  29. package/docs/testing.md +62 -28
  30. package/docs/theory.md +348 -108
  31. package/package.json +1 -1
  32. package/skills/loadbare-app/SKILL.md +10 -6
  33. package/skills/loadbare-app/references/TECHREF-1.0.md +27 -72
  34. package/skills/loadbare-app/references/builder.md +9 -2
  35. package/skills/loadbare-app/references/chrome.md +2 -1
  36. package/skills/loadbare-app/references/custom-elements.md +1 -1
  37. package/skills/loadbare-app/references/data-binding.md +36 -9
  38. package/skills/loadbare-app/references/miswarts.md +140 -0
  39. package/skills/loadbare-app/references/page-files.md +36 -4
  40. package/docs/possible-ideas.md +0 -314
  41. package/docs/theory-old.md +0 -839
  42. package/docs/what-does-loadbare-extend.md +0 -124
@@ -1,9 +1,14 @@
1
1
  # Loadbare/app and current front-end frameworks
2
2
 
3
- > LLM-authored, not yet revised by a person. Drafted by Claude on
4
- > 2026-09-16 and brought up to date with the working tree that follows
5
- > `@loadbare/app` 0.9.0. Statements about other frameworks are from general
6
- > knowledge of those projects as of 2026 and carry no per-claim sources.
3
+ > LLM-authored. Final revision by Claude 2026-10-09 against
4
+ > release 0.18.2, factual claims checked against code.
5
+ > Statements about other frameworks are from LLM training set
6
+ > memory as of 2026 and carry no per-claim sources.
7
+ >
8
+ > Reviewed by Ken Downs Oct 9, 2026, found to be consistent with
9
+ > other documents and conclusions reached during development. Will
10
+ > be left as LLM-authored, as the style is straightforward and
11
+ > mostly free of annoying LLM idioms.
7
12
 
8
13
  This document states how `@loadbare/app` differs from the frameworks and
9
14
  libraries most used for front-end development today. It repeats material
@@ -11,45 +16,92 @@ from [Theory](./theory.md), [TECHREF-1.0](./TECHREF-1.0.md) and
11
16
  [Prior art](./prior-art.md) so that it can be read on its own. Where this document and
12
17
  [Theory](./theory.md) disagree, Theory is authoritative.
13
18
 
14
- Loadbare/app has not reached 1.0. The [Blockers](./TECHREF-1.0.md#blockers)
15
- section of TECHREF-1.0 lists open decisions, and five of its sections are
16
- marked `UNEDITED`. Anything below that describes Loadbare/app describes the
19
+ Two kinds of difference run through it. Most follow from Theory's three
20
+ goals and the one decision that serves them. Two are deliberate departures
21
+ from HTML's own model, in navigation and on the wire, and are set out in
22
+ [Departures](./departures.md).
23
+
24
+ Loadbare/app has not reached 1.0. Five sections of
25
+ [TECHREF-1.0](./TECHREF-1.0.md) are marked `UNEDITED`. Anything below that describes Loadbare/app describes the
17
26
  code as it stands.
18
27
 
19
28
  ## Contents
20
29
 
21
- 1. [Summary](#summary)
22
- 2. [The comparison set](#the-comparison-set)
23
- 3. [Stated goals and scope](#stated-goals-and-scope)
24
- 4. [Where state lives](#where-state-lives)
25
- 5. [Rendering](#rendering)
26
- 6. [Templates and markup](#templates-and-markup)
27
- 7. [Components and widgets](#components-and-widgets)
28
- 8. [Data on the wire](#data-on-the-wire)
29
- 9. [Mutations, forms and request state](#mutations-forms-and-request-state)
30
- 10. [Routing and navigation](#routing-and-navigation)
31
- 11. [Build, tooling and checking](#build-tooling-and-checking)
32
- 12. [CSS](#css)
33
- 13. [The server](#the-server)
34
- 14. [Performance profile](#performance-profile)
35
- 15. [Surface area](#surface-area)
36
- 16. [Accessibility](#accessibility)
37
- 17. [Testing](#testing)
38
- 18. [Release history and churn](#release-history-and-churn)
39
- 19. [What Loadbare/app does not do](#what-loadbareapp-does-not-do)
40
- 20. [Requirements and fit](#requirements-and-fit)
41
- 21. [Appendix A: Matrix](#appendix-a-matrix)
42
- 22. [Appendix B: One page, three ways](#appendix-b-one-page-three-ways)
43
- 23. [Appendix C: Glossary of nearest equivalents](#appendix-c-glossary-of-nearest-equivalents)
30
+ - [Loadbare/app and current front-end frameworks](#loadbareapp-and-current-front-end-frameworks)
31
+ - [Contents](#contents)
32
+ - [Summary](#summary)
33
+ - [The comparison set](#the-comparison-set)
34
+ - [Stated goals and scope](#stated-goals-and-scope)
35
+ - [Where state lives](#where-state-lives)
36
+ - [Loadbare/app](#loadbareapp)
37
+ - [Elsewhere](#elsewhere)
38
+ - [Rendering](#rendering)
39
+ - [Loadbare/app](#loadbareapp-1)
40
+ - [Elsewhere](#elsewhere-1)
41
+ - [Templates and markup](#templates-and-markup)
42
+ - [Loadbare/app](#loadbareapp-2)
43
+ - [Conditions](#conditions)
44
+ - [Components and widgets](#components-and-widgets)
45
+ - [Loadbare/app](#loadbareapp-3)
46
+ - [Using web components and other libraries](#using-web-components-and-other-libraries)
47
+ - [Elsewhere](#elsewhere-2)
48
+ - [Data on the wire](#data-on-the-wire)
49
+ - [Loadbare/app](#loadbareapp-4)
50
+ - [Elsewhere](#elsewhere-3)
51
+ - [Mutations, forms and request state](#mutations-forms-and-request-state)
52
+ - [Loadbare/app](#loadbareapp-5)
53
+ - [Elsewhere](#elsewhere-4)
54
+ - [Correctness](#correctness)
55
+ - [Loadbare/app](#loadbareapp-6)
56
+ - [Elsewhere](#elsewhere-5)
57
+ - [Routing and navigation](#routing-and-navigation)
58
+ - [Loadbare/app](#loadbareapp-7)
59
+ - [Elsewhere](#elsewhere-6)
60
+ - [Build, tooling and checking](#build-tooling-and-checking)
61
+ - [Loadbare/app](#loadbareapp-8)
62
+ - [Elsewhere](#elsewhere-7)
63
+ - [CSS](#css)
64
+ - [Loadbare/app](#loadbareapp-9)
65
+ - [Elsewhere](#elsewhere-8)
66
+ - [The server](#the-server)
67
+ - [Loadbare/app](#loadbareapp-10)
68
+ - [Elsewhere](#elsewhere-9)
69
+ - [Performance profile](#performance-profile)
70
+ - [Loadbare/app](#loadbareapp-11)
71
+ - [Elsewhere](#elsewhere-10)
72
+ - [Surface area](#surface-area)
73
+ - [Accessibility](#accessibility)
74
+ - [Loadbare/app](#loadbareapp-12)
75
+ - [Elsewhere](#elsewhere-11)
76
+ - [Testing](#testing)
77
+ - [Loadbare/app](#loadbareapp-13)
78
+ - [Elsewhere](#elsewhere-12)
79
+ - [Release history and churn](#release-history-and-churn)
80
+ - [Loadbare/app](#loadbareapp-14)
81
+ - [Elsewhere](#elsewhere-13)
82
+ - [What Loadbare/app does not do](#what-loadbareapp-does-not-do)
83
+ - [By decision](#by-decision)
84
+ - [Gaps and open items](#gaps-and-open-items)
85
+ - [Requirements and fit](#requirements-and-fit)
86
+ - [Appendix A: Matrix](#appendix-a-matrix)
87
+ - [Appendix B: One page, three ways](#appendix-b-one-page-three-ways)
88
+ - [Loadbare/app](#loadbareapp-15)
89
+ - [Next.js (App Router)](#nextjs-app-router)
90
+ - [htmx with Express](#htmx-with-express)
91
+ - [Appendix C: Glossary of nearest equivalents](#appendix-c-glossary-of-nearest-equivalents)
44
92
 
45
93
  ## Summary
46
94
 
47
- Loadbare/app is a framework for server-bound database applications. The
48
- browser receives one HTML document containing every page as a `<template>`,
95
+ Loadbare/app is a framework for server-bound database applications. Its
96
+ goals are correctness, performance and developer ergonomics, held at once:
97
+ the screen shows what the database held at the last round trip, it gets
98
+ there fast, and the correct, fast application is the easiest one to write.
99
+
100
+ The browser receives one HTML document containing every page as a `<template>`,
49
101
  one script and one stylesheet. After that, only JSON rows cross the wire.
50
102
  HTML attributes bind elements to named server queries, and one attribute,
51
103
  `lb-request`, turns a click, a change or a submit into a request. The
52
- server answers every request with response items: one row, a set of rows,
104
+ server answers every request it accepts with response items: one row, a set of rows,
53
105
  or a patch to a set of rows, each named by its query. The browser-side
54
106
  runtime, called the hub, lands those values on the elements that name the
55
107
  query.
@@ -73,23 +125,28 @@ Blazor Server), Loadbare/app keeps no per-client process or component state
73
125
  on the server, and uses ordinary HTTP requests rather than a persistent
74
126
  connection.
75
127
 
76
- | Group | Rendering happens | State lives | After first load, the wire carries |
77
- |--------------------------|-----------------------|------------------------------|------------------------------------|
78
- | Loadbare/app | Build time, once | Server database, and the DOM | JSON rows |
79
- | Client component | Browser, on change | Browser memory | JSON (application-designed) |
80
- | Meta-frameworks | Server, then browser | Server and browser memory | JSON, RSC payloads, or HTML |
81
- | Hypermedia | Server, per request | Server, and the DOM | HTML |
82
- | Server-driven stateful | Server, per event | Server memory per client | DOM diffs or HTML |
128
+ Compared with all of them, Loadbare/app paints only what the server
129
+ answered. There is no optimistic update to roll back and no client cache to
130
+ revalidate, and the framework, not the application, keeps a double click
131
+ from becoming a double write.
132
+
133
+ | Group | Rendering happens | State lives | After first load, the wire carries |
134
+ | ---------------------- | -------------------- | ---------------------------- | ---------------------------------- |
135
+ | Loadbare/app | Build time, once | Server database, and the DOM | JSON rows |
136
+ | Client component | Browser, on change | Browser memory | JSON (application-designed) |
137
+ | Meta-frameworks | Server, then browser | Server and browser memory | JSON, RSC payloads, or HTML |
138
+ | Hypermedia | Server, per request | Server, and the DOM | HTML |
139
+ | Server-driven stateful | Server, per event | Server memory per client | DOM diffs or HTML |
83
140
 
84
141
  ## The comparison set
85
142
 
86
- | Group | Members |
87
- |--------------------------|--------------------------------------------------|
88
- | Client component | React, Vue, Angular, Svelte, Solid |
89
- | Meta-frameworks | Next.js, Nuxt, SvelteKit, React Router 7, Astro |
90
- | Hypermedia | htmx, Hotwire (Turbo and Stimulus), Unpoly |
91
- | Server-driven stateful | Phoenix LiveView, Laravel Livewire, Blazor Server|
92
- | Sprinkles and components | Alpine.js, Lit |
143
+ | Group | Members |
144
+ | ------------------------ | ------------------------------------------------- |
145
+ | Client component | React, Vue, Angular, Svelte, Solid |
146
+ | Meta-frameworks | Next.js, Nuxt, SvelteKit, React Router 7, Astro |
147
+ | Hypermedia | htmx, Hotwire (Turbo and Stimulus), Unpoly |
148
+ | Server-driven stateful | Phoenix LiveView, Laravel Livewire, Blazor Server |
149
+ | Sprinkles and components | Alpine.js, Lit |
93
150
 
94
151
  React Router 7 is the continuation of Remix. TanStack Query appears below
95
152
  where client-side server-state caching is discussed, because React
@@ -97,27 +154,35 @@ applications commonly pair it with a data API.
97
154
 
98
155
  ## Stated goals and scope
99
156
 
100
- Theory states two goals, in order.
101
-
102
- 1. Performance. A full round trip, from user action through durable
103
- persistence to final paint, should complete in a median of 300ms.
104
- Assuming roughly 200ms of wire time leaves 100ms for the database, the
105
- application server and the browser together, so the framework's own
106
- processing time in the browser and on the server should approach zero.
107
- 2. Developer efficiency, defined as reducing accidental complexity (Brooks):
108
- work demanded by the tools rather than by the problem.
109
-
110
- The scope follows from a hypothesis stated in Theory: in a data-oriented
111
- application, the set of DOM changes needed is closed and knowable, so a
112
- framework optimized for that set does not need the machinery required to
113
- support arbitrary DOM updates.
114
-
115
- Loadbare/app restricts itself to applications whose data can be sent as rows
116
- and sets of rows. Theory states that the claims about accidental complexity
117
- are most likely to hold "when the existing stack can support an application
118
- that wants data to be rows and tables, or easily mapped into them and out of
119
- them, and the desired affordances can be easily mapped to the standard set of
120
- operations against that data."
157
+ [Theory](./theory.md) states three goals, which must all hold at once.
158
+
159
+ 1. Correctness. The screen has integrity with the database as of the last
160
+ round trip, a user's write reaches the database once, and a constraint
161
+ violation reaches the user intelligibly and in context. Correctness by
162
+ construction is part of it: a solution that loses that integrity must be
163
+ hard to write.
164
+ 2. Performance. A median round trip of 300ms from click to paint.
165
+ Allowing roughly 200ms for the wire leaves 100ms for the browser, the
166
+ application server and the database together, so the framework's own
167
+ share must be low double-digit milliseconds. Theory treats performance
168
+ as an aspect of correctness from the user's side: a slow answer leaves
169
+ the user unsure whether what they see is true.
170
+ 3. Developer ergonomics. The simplest way to write the application is the
171
+ one that is correct and fast, with little boilerplate. Theory rules
172
+ out what it calls elegant abstractions: those whose appeal is their own
173
+ design rather than what they do for the user, which hide the platform
174
+ instead of mapping onto it and must be serviced forever.
175
+
176
+ One decision serves all three: the framework speaks SQL and understands
177
+ only two data shapes, a `row` and a set of `rows`, with insert, update and
178
+ delete. The vocabulary is SQL, HTML, and a few small named families every
179
+ browser application needs, such as navigation and request status.
180
+
181
+ Theory scopes out currency, the screen changing because of another user or
182
+ another process, until it can be reconciled with correctness.
183
+
184
+ Loadbare/app therefore fits applications whose data is rows, or is easily
185
+ shaped into rows, and whose interactions map onto reading and writing them.
121
186
 
122
187
  Each of the other projects in the comparison set presents itself as a
123
188
  general-purpose tool for building web user interfaces, and none restricts the
@@ -138,13 +203,17 @@ observable, and no client cache that answers a request.
138
203
  the path, the page label and the query parms.
139
204
  - An element hidden by `lb-show` exists inside a `<template>` standing where
140
205
  it stood.
206
+ - A form pointed at a row carries that row's key as `lb-key-value`, and a
207
+ row ticked by its `lb-select` checkbox carries `lb-row-selected`.
141
208
  - The hub keeps each query's last answer until the page changes, only to
142
209
  fill an element that names the query and arrives after it, such as a
143
- picker in a new live row, and to place rows again when the user makes a
144
- choice. Nothing else is read from it in place of asking the server.
145
- - The hub keeps three pieces of state of its own: the name of the current
146
- page, the user's choices for it, and, until an insert completes, the form
147
- it gathered from.
210
+ picker in a new live row, to fill a form pointed at one of its rows, and
211
+ to place rows again when the user makes a choice. Nothing else is read
212
+ from it in place of asking the server.
213
+ - The hub keeps a little state of its own: the name of the current page,
214
+ the user's choices for it as the row of its query `lb-choices`, the live
215
+ row each form is pointed at, the refusals on show, and, until a request
216
+ completes, the form it gathered from.
148
217
 
149
218
  The server is the source of truth for everything else. A query takes no
150
219
  argument from the browser; its only input is `ctx`, which the application
@@ -158,21 +227,21 @@ parm.
158
227
 
159
228
  ### Elsewhere
160
229
 
161
- | Project | Where application state is kept |
162
- |-------------------|-----------------------------------------------------------------------------------------|
163
- | React | Component state (`useState`, `useReducer`) in the component tree; context; external stores such as Redux or Zustand; server-state caches such as TanStack Query or SWR |
164
- | Vue | Reactive proxies (`ref`, `reactive`); Pinia stores |
165
- | Angular | Signals, services provided through dependency injection, RxJS observables |
166
- | Svelte | Runes (`$state`, `$derived`); stores |
167
- | Solid | Signals and stores |
168
- | Next.js | Server Components read data on the server; Client Components hold React state; the client router caches RSC payloads |
169
- | React Router 7 | Loader data held by the router in the browser; component state |
170
- | SvelteKit, Nuxt | Load function or `useFetch` data held in the browser; component state |
171
- | htmx, Turbo | The server, and the HTML currently in the DOM. Turbo keeps a snapshot cache of visited pages for back navigation |
172
- | Phoenix LiveView | Socket assigns in a server process per connected client |
173
- | Livewire | Component properties serialized into a snapshot in the page and sent back with each request |
174
- | Blazor Server | Component state in server memory, per circuit |
175
- | Alpine.js | Reactive objects declared with `x-data` |
230
+ | Project | Where application state is kept |
231
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
232
+ | React | Component state (`useState`, `useReducer`) in the component tree; context; external stores such as Redux or Zustand; server-state caches such as TanStack Query or SWR |
233
+ | Vue | Reactive proxies (`ref`, `reactive`); Pinia stores |
234
+ | Angular | Signals, services provided through dependency injection, RxJS observables |
235
+ | Svelte | Runes (`$state`, `$derived`); stores |
236
+ | Solid | Signals and stores |
237
+ | Next.js | Server Components read data on the server; Client Components hold React state; the client router caches RSC payloads |
238
+ | React Router 7 | Loader data held by the router in the browser; component state |
239
+ | SvelteKit, Nuxt | Load function or `useFetch` data held in the browser; component state |
240
+ | htmx, Turbo | The server, and the HTML currently in the DOM. Turbo keeps a snapshot cache of visited pages for back navigation |
241
+ | Phoenix LiveView | Socket assigns in a server process per connected client |
242
+ | Livewire | Component properties serialized into a snapshot in the page and sent back with each request |
243
+ | Blazor Server | Component state in server memory, per circuit |
244
+ | Alpine.js | Reactive objects declared with `x-data` |
176
245
 
177
246
  ## Rendering
178
247
 
@@ -185,25 +254,37 @@ browser parses that document once.
185
254
 
186
255
  At run time the hub performs a closed set of operations:
187
256
 
188
- | Trigger | Operation |
189
- |----------------------------|----------------------------------------------------------------------------|
190
- | Navigation | Clone the page's `<template>` into `<main>`, land `lb-url` |
191
- | A value for a control | Set `value`, and stamp `lb-column-value` |
192
- | A value for another native element | Set `textContent`, and stamp `lb-column-value` |
193
- | A value for a custom element that is not a control | Stamp `lb-column-value` |
194
- | A `row` with no row template | Land it on the element itself, stamp `lb-key-value` |
195
- | A new key through a row template | Clone the row template, stamp `lb-row-live` and `lb-key-value`, fill it, insert it |
196
- | All rows | Place every row in the order given, remove rows whose keys did not arrive |
197
- | A patch | Upsert the rows named, remove the keys named, leave other rows in place |
198
- | After rows land | Stamp `lb-query-row-count` |
199
- | An `lb-show` column | Move the element into, or out of, a `<template lb-show>` |
200
- | A request starts or ends | Set or remove `lb-request-pending` and `aria-busy`; set `lb-request-error` on failure |
201
- | An insert from a form succeeds | Reset the form |
257
+ | Trigger | Operation |
258
+ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
259
+ | Navigation | Clone the page's `<template>` into `<main>`, land `lb-url`, land `lb-choices` empty |
260
+ | A value for a control | Set `value`, and stamp `lb-column-value` |
261
+ | A value for another native element | Set `textContent`, and stamp `lb-column-value` |
262
+ | A value for a custom element that is not a control | Stamp `lb-column-value` |
263
+ | A `row` with no row template | Land it on the element itself, stamp `lb-key-value` |
264
+ | A `row` that is `null` | Land its columns as null and remove `lb-key-value`; under a row template, the row leaves |
265
+ | A new key through a row template | Clone the row template, stamp `lb-row-live` and `lb-key-value`, fill it, insert it |
266
+ | All rows | Place every row by the query's order, or the user's; remove rows whose keys did not arrive |
267
+ | A patch | Upsert the rows named, remove the keys named, leave other rows in place |
268
+ | A group template | Build a group where the order breaks, keep it while its run goes on, remove it with its last row |
269
+ | An aggregate (`lb-count`, `lb-sum` and the rest) | Set the element from the rows showing in its group, or in the whole list |
270
+ | A row changes, moves or leaves | Stamp `lb-row-created`, `lb-row-changed`, `lb-row-moved`, `lb-row-requested` or `lb-row-leaving`; remove a leaving row once its animations finish |
271
+ | After rows land | Stamp `lb-query-row-count` |
272
+ | The user makes a choice | Land `lb-choices`, place and filter the rows already held again, tell the server |
273
+ | A row the user's filter does not pass | Set `hidden` on it |
274
+ | An `lb-show` column | Move the element into, or out of, a `<template lb-show>` |
275
+ | A `commandfor` button with `--lb-edit` or `--lb-new` | Fill the form from the held row, or empty it; stamp `lb-key-value` and `lb-row-editing`; open its `<dialog>` |
276
+ | An `lb-select` checkbox is ticked | Stamp `lb-row-selected` on its row |
277
+ | A request starts or ends | Set or remove `lb-request-pending` and `aria-busy`; set `lb-request-error` on failure, `lb-request-refused` and control validity on a refusal |
278
+ | An insert from a form succeeds | Reset the form |
279
+ | A request from a pointed form succeeds | Let the form go: empty it, drop its key, close its `<dialog>` |
280
+ | A page is entered and its queries have landed | Focus the first element in `<main>` the user can operate |
281
+ | The hub places or removes a custom element | Call its `lbAttached` or `lbDetached` |
202
282
 
203
283
  The hub finds targets with `querySelectorAll` on the attributes written in
204
284
  the HTML. Each response item names its query, its kind and its key, so the
205
- markup names none of the three beyond the query. No tree is compared with a previous tree. A response names what
206
- changed, so the hub does not determine what changed. Between interactions
285
+ markup names none of the three beyond the query. No tree is compared with a previous tree. The hub matches
286
+ rows by key, and compares a row's values with those it shows, to leave an
287
+ unchanged row alone and stamp a changed one. Between interactions
207
288
  the hub runs no timers, observers or polling loops.
208
289
 
209
290
  There is no server-side rendering and no hydration. The server returns the
@@ -211,22 +292,22 @@ values its queries return, serialized as JSON.
211
292
 
212
293
  ### Elsewhere
213
294
 
214
- | Project | How the DOM is produced and updated |
215
- |------------------|---------------------------------------------------------------------------------------|
216
- | React | Components re-run on state change and return an element tree, which is reconciled against the previous tree; keys identify list items |
217
- | Vue | Templates compile to render functions producing a virtual DOM; reactivity tracks which components depend on which state |
218
- | Angular | Templates compile to instructions; change detection checks bindings, and signals mark views that need updating |
219
- | Svelte | The compiler emits code that updates the DOM nodes bound to changed state |
220
- | Solid | JSX compiles to DOM creation code; components run once; signals update bound nodes |
295
+ | Project | How the DOM is produced and updated |
296
+ | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
297
+ | React | Components re-run on state change and return an element tree, which is reconciled against the previous tree; keys identify list items |
298
+ | Vue | Templates compile to render functions producing a virtual DOM; reactivity tracks which components depend on which state |
299
+ | Angular | Templates compile to instructions; change detection checks bindings, and signals mark views that need updating |
300
+ | Svelte | The compiler emits code that updates the DOM nodes bound to changed state |
301
+ | Solid | JSX compiles to DOM creation code; components run once; signals update bound nodes |
221
302
  | Next.js, Nuxt, SvelteKit, React Router 7 | HTML is rendered on the server per request or at build time, then hydrated in the browser (in Next.js, the Client Components), after which the client framework renders |
222
- | Astro | HTML rendered at build or request time; interactive islands are hydrated per `client:*` directive |
223
- | htmx | The server renders HTML fragments per request; htmx swaps them into a target |
224
- | Turbo | The server renders pages; Turbo replaces the body, a `<turbo-frame>`, or applies Turbo Stream actions; page refreshes can morph |
225
- | Phoenix LiveView | The server re-renders changed parts of a template and sends diffs over a WebSocket; the client patches the DOM |
226
- | Livewire | The server re-renders component HTML; the client morphs the DOM |
227
- | Blazor Server | The server renders the component tree, computes a diff, and sends it over SignalR |
228
- | Alpine.js | Directives on server-rendered HTML update bound nodes from reactive data |
229
- | Lit | Tagged template literals update the dynamic parts of a template, by default inside a shadow root |
303
+ | Astro | HTML rendered at build or request time; interactive islands are hydrated per `client:*` directive |
304
+ | htmx | The server renders HTML fragments per request; htmx swaps them into a target |
305
+ | Turbo | The server renders pages; Turbo replaces the body, a `<turbo-frame>`, or applies Turbo Stream actions; page refreshes can morph |
306
+ | Phoenix LiveView | The server re-renders changed parts of a template and sends diffs over a WebSocket; the client patches the DOM |
307
+ | Livewire | The server re-renders component HTML; the client morphs the DOM |
308
+ | Blazor Server | The server renders the component tree, computes a diff, and sends it over SignalR |
309
+ | Alpine.js | Directives on server-rendered HTML update bound nodes from reactive data |
310
+ | Lit | Tagged template literals update the dynamic parts of a template, by default inside a shadow root |
230
311
 
231
312
  ## Templates and markup
232
313
 
@@ -234,7 +315,8 @@ values its queries return, serialized as JSON.
234
315
 
235
316
  The HTML written in `.html` files is the HTML that ships, after expansion.
236
317
  Attribute values in the `lb-` namespace are names: a query name, a column
237
- name, or a request name. No attribute value is an expression.
318
+ name or a list of them, or a request name. `lb-show` may write `!` before
319
+ its column to reverse it. No attribute value is an expression.
238
320
 
239
321
  There are two kinds of placeholder, and they run at different times.
240
322
 
@@ -242,20 +324,20 @@ There are two kinds of placeholder, and they run at different times.
242
324
  a whole attribute value or a whole text node. The tag supplies it with an
243
325
  `exp-name` attribute. An unsupplied placeholder with no default drops the
244
326
  attribute. The value becomes permanent in the shipped HTML.
245
- - Run time. Data lands through `lb-column`, `lb-query` with a row
246
- `<template>`, and `lb-show`. There is no run-time interpolation into
247
- attribute values or text.
327
+ - Run time. Data lands through `lb-column`, the aggregate attributes,
328
+ `lb-query` with a row or group `<template>`, and `lb-show`. There is no
329
+ run-time interpolation into attribute values or text.
248
330
 
249
331
  Interpolation inside a longer string, such as `title="Hello {{name}}"`, is
250
332
  listed as decided against in the [Roadmap](./roadmap.md#decided-against).
251
333
 
252
- | Task | Loadbare/app | React (JSX) | Vue | Angular | Svelte | Alpine.js |
253
- |-------------------|-----------------------------------------------|--------------------------------------|--------------------------------------|---------------------------------------|-------------------------------|--------------------------------------------|
254
- | Show a value | `<span lb-column="name">` | `{row.name}` | `{{ row.name }}` | `{{ row.name }}` | `{row.name}` | `<span x-text="row.name">` |
255
- | Keyed list | `<template>` inside `lb-query`, key from the server | `rows.map(r => <li key={r.id}>)` | `<li v-for="r in rows" :key="r.id">` | `@for (r of rows; track r.id)` | `{#each rows as r (r.id)}` | `<template x-for="r in rows" :key="r.id">` |
256
- | Condition | `lb-show="column"` | `{cond && <X />}` | `v-if`, `v-show` | `@if (cond) { }` | `{#if cond}` | `x-if`, `x-show` |
257
- | Expression in markup | Not supported | Any JavaScript expression | JavaScript expressions | Template expressions | JavaScript expressions | JavaScript expressions |
258
- | Build-time constant | `exp-label="Name"` into `{{label}}` | A prop | A prop | An input | A prop | Not applicable |
334
+ | Task | Loadbare/app | React (JSX) | Vue | Angular | Svelte | Alpine.js |
335
+ | -------------------- | --------------------------------------------------- | -------------------------------- | ------------------------------------ | ------------------------------ | -------------------------- | ------------------------------------------ |
336
+ | Show a value | `<span lb-column="name">` | `{row.name}` | `{{ row.name }}` | `{{ row.name }}` | `{row.name}` | `<span x-text="row.name">` |
337
+ | Keyed list | `<template>` inside `lb-query`, key from the server | `rows.map(r => <li key={r.id}>)` | `<li v-for="r in rows" :key="r.id">` | `@for (r of rows; track r.id)` | `{#each rows as r (r.id)}` | `<template x-for="r in rows" :key="r.id">` |
338
+ | Condition | `lb-show="column"` | `{cond && <X />}` | `v-if`, `v-show` | `@if (cond) { }` | `{#if cond}` | `x-if`, `x-show` |
339
+ | Expression in markup | Not supported | Any JavaScript expression | JavaScript expressions | Template expressions | JavaScript expressions | JavaScript expressions |
340
+ | Build-time constant | `exp-label="Name"` into `{{label}}` | A prop | A prop | An input | A prop | Not applicable |
259
341
 
260
342
  In htmx and Turbo, markup for data is produced by whatever server template
261
343
  language the application uses. LiveView uses HEEx, with `{@value}`
@@ -264,23 +346,24 @@ interpolation, `:for` and `:if`.
264
346
  ### Conditions
265
347
 
266
348
  `lb-show` names a column. `null` and `false` are off; every other value is
267
- on, including the string `"false"`. An element that is off is moved into a
349
+ on, including the string `"false"`. `lb-show="!column"` reverses it, so one
350
+ column decides between two elements. An element that is off is moved into a
268
351
  `<template lb-show="column">` standing where it stood, and moved back when the
269
352
  column turns on. It is not rendered, focused, clicked, announced or gathered.
270
353
  Values keep landing inside the template, so the element returns current. The
271
354
  builder ships every `lb-show` element already inside its template.
272
355
 
273
- | Framework | A false branch | State kept | Updated while hidden |
274
- |----------------------------------|-----------------------------------------------------------------------|------------|----------------------|
275
- | Loadbare/app `lb-show` | Moved into a `<template>` in place | yes | yes |
276
- | Alpine `<template x-if>` | Destroyed and removed; recloned on show | no | no |
277
- | Knockout `if` | Contents re-rendered on show | no | no |
278
- | Angular `@if` / `*ngIf` | View destroyed unless detached and reinserted by hand | if detached | no |
279
- | Aurelia `if.bind` | Removed; views cached and reused by default | yes | not documented |
280
- | Vue `v-if` in `<KeepAlive>` | Detached, not unmounted | yes | no |
281
- | Lit `cache()` | DOM kept in a `DocumentFragment` | yes | on swap-in |
282
- | Polymer `dom-if` | `display: none` | yes | yes |
283
- | React `<Activity mode="hidden">` | `display: none`, Effects destroyed | yes | yes, at lower priority |
356
+ | Framework | A false branch | State kept | Updated while hidden |
357
+ | -------------------------------- | ----------------------------------------------------- | ----------- | ---------------------- |
358
+ | Loadbare/app `lb-show` | Moved into a `<template>` in place | yes | yes |
359
+ | Alpine `<template x-if>` | Destroyed and removed; recloned on show | no | no |
360
+ | Knockout `if` | Contents re-rendered on show | no | no |
361
+ | Angular `@if` / `*ngIf` | View destroyed unless detached and reinserted by hand | if detached | no |
362
+ | Aurelia `if.bind` | Removed; views cached and reused by default | yes | not documented |
363
+ | Vue `v-if` in `<KeepAlive>` | Detached, not unmounted | yes | no |
364
+ | Lit `cache()` | DOM kept in a `DocumentFragment` | yes | on swap-in |
365
+ | Polymer `dom-if` | `display: none` | yes | yes |
366
+ | React `<Activity mode="hidden">` | `display: none`, Effects destroyed | yes | yes, at lower priority |
284
367
 
285
368
  Sources for the rows other than Loadbare/app are in
286
369
  [Prior art](./prior-art.md#conditional-rendering-elsewhere).
@@ -295,10 +378,10 @@ reference directs it to `<details>`, a stylesheet, or a widget.
295
378
  A widget is a custom element defined by one or two files sharing its tag
296
379
  name, found by the builder anywhere in the source tree:
297
380
 
298
- | File | Holds |
299
- |-------------------------|-----------------------------------------------------|
300
- | `<tag-name>.html` | Markup the tag expands into, at build time |
301
- | `<tag-name>.browser.ts` | A class registered with `customElements.define` |
381
+ | File | Holds |
382
+ | ----------------------- | ----------------------------------------------- |
383
+ | `<tag-name>.html` | Markup the tag expands into, at build time |
384
+ | `<tag-name>.browser.ts` | A class registered with `customElements.define` |
302
385
 
303
386
  A tag with neither file is a build error. A module named `<tag-name>.ts`
304
387
  without `.browser` is not bundled, and the build error names it.
@@ -322,17 +405,23 @@ without `.browser` is not bundled, and the build error names it.
322
405
  where the element sits. An ancestor may stop the event.
323
406
  - A custom element hears the hub's lifecycle rather than the browser's:
324
407
  `lbAttached()` and `lbDetached()` once each, when the hub places and
325
- removes it, `lbRowsLanded()` once rows land on it, and `lbRequestDone()`
326
- when a request from inside it finishes. The hub does the cloning,
327
- matching, ordering, grouping, removal and counting.
328
- - Method names beginning with `lb` on a custom element are reserved.
408
+ removes it, `lbRowsLanded()` once rows land on it, and
409
+ `lbRequestDone(detail, origin)` when a request from it or from inside it
410
+ finishes. The hub also dispatches a bubbling `lb-request-done` event from
411
+ the element that committed. The hub does the cloning, matching, ordering,
412
+ filtering, grouping, removal and counting.
413
+ - Method names beginning with `lb` on a custom element are reserved. The
414
+ build refuses a script that declares one Loadbare does not call, or
415
+ declares `connectedCallback`, `disconnectedCallback`, `adoptedCallback` or
416
+ `connectedMoveCallback`.
329
417
 
330
418
  Widget libraries are npm packages listed in `imports.ts`. The builder scans
331
419
  each listed package for `<tag>.html` and `<tag>.browser.js` files, in the
332
420
  directory named by `loadbare.widgets` in its `package.json`, or the whole
333
421
  package when that key is absent. Only tags used in the chrome or a page are
334
422
  bundled. `@loadbare/widgets` ships `lb-input`, `lb-select`, `lb-options`,
335
- `lb-table`, `lb-picker` and `lb-unknown-page`.
423
+ `lb-picker`, `lb-table`, `lb-confirm`, `lb-form`, `lb-insert-dialog`,
424
+ `lb-master`, `lb-checkbox`, `lb-number`, `lb-date` and `lb-unknown-page`.
336
425
 
337
426
  ### Using web components and other libraries
338
427
 
@@ -357,16 +446,16 @@ The following follow from the builder and hub code.
357
446
 
358
447
  ### Elsewhere
359
448
 
360
- | Project | Unit | Inputs | Children | Style scope |
361
- |-------------|------------------------------------|-------------------------------|-----------------------------------|-----------------------------|
362
- | React | Function returning JSX | Props | `children` prop, render props | None built in |
363
- | Vue | Single-file component (`.vue`) | Props, emits | Default and named slots | `<style scoped>` available |
364
- | Angular | Class with `@Component` decorator | Inputs, outputs, signals | Content projection (`ng-content`) | Emulated encapsulation by default |
365
- | Svelte | `.svelte` file | `$props()` | Snippets | Scoped by default |
366
- | Solid | Function returning JSX, run once | Props | `children` | None built in |
367
- | Lit | `LitElement` subclass | Reactive properties, attributes | `<slot>` in shadow DOM | Shadow DOM by default |
368
- | Stimulus | Controller class attached by `data-controller` | Values, targets | Server-rendered HTML | None |
369
- | LiveView | Function components and LiveComponents | Assigns | Slots | None built in |
449
+ | Project | Unit | Inputs | Children | Style scope |
450
+ | -------- | ---------------------------------------------- | ------------------------------- | --------------------------------- | --------------------------------- |
451
+ | React | Function returning JSX | Props | `children` prop, render props | None built in |
452
+ | Vue | Single-file component (`.vue`) | Props, emits | Default and named slots | `<style scoped>` available |
453
+ | Angular | Class with `@Component` decorator | Inputs, outputs, signals | Content projection (`ng-content`) | Emulated encapsulation by default |
454
+ | Svelte | `.svelte` file | `$props()` | Snippets | Scoped by default |
455
+ | Solid | Function returning JSX, run once | Props | `children` | None built in |
456
+ | Lit | `LitElement` subclass | Reactive properties, attributes | `<slot>` in shadow DOM | Shadow DOM by default |
457
+ | Stimulus | Controller class attached by `data-controller` | Values, targets | Server-rendered HTML | None |
458
+ | LiveView | Function components and LiveComponents | Assigns | Slots | None built in |
370
459
 
371
460
  React 19 added full support for custom elements, including setting
372
461
  properties, so web components can be used in React applications directly.
@@ -383,17 +472,26 @@ the query string the browser is showing.
383
472
  - An empty body asks for the page's whole query set. The server runs the
384
473
  page's `onPageEnter`, then every query the page declares.
385
474
  - A body carrying a request, `{ name, query, key, values }`, runs the handler
386
- its name picks and then its refresh set.
387
- - A request without a name answers 400. A `run` that throws answers 500.
388
-
389
- Every response has one shape: an array of response items. Each names its
390
- query, the kind and key the server declared, and one of these:
391
-
392
- | Member | Means |
393
- |------------------|-------------------------------------------------------|
394
- | `row` | One row, for a query declared with `row()` |
395
- | `rows` | All rows, and their order, for a query declared with `rows()` |
396
- | `patch` | `{ rows, drop }` for a `rows` query: upsert these, remove these keys |
475
+ its name picks and then its refresh set. A request acting on selected
476
+ rows carries `keys` in place of `key`, and a form over a nested query
477
+ carries that query's rows as `rows`.
478
+ - A handler or `onPageEnter` that returns `url()` answers with the new
479
+ `lb-url` row and the page loaded at the new URL, in the same response.
480
+ - A request without a name, or a body that is not JSON, answers 400. A body
481
+ over the `express.json()` limit answers 413. A handler that throws
482
+ `refusal()` answers 422 with its words. Any other `run` that throws
483
+ answers 500 with no message.
484
+
485
+ Every successful response has one shape: an array of response items. Each
486
+ names its query, the kind and key the server declared, and one of these. On a page
487
+ load, an item for a `rows` query also carries the order and column types the
488
+ query declared.
489
+
490
+ | Member | Means |
491
+ | ------- | -------------------------------------------------------------------- |
492
+ | `row` | One row, or `null` for none, for a query declared with `row()` |
493
+ | `rows` | All rows, and their order, for a query declared with `rows()` |
494
+ | `patch` | `{ rows, drop }` for a `rows` query: upsert these, remove these keys |
397
495
 
398
496
  A column holds one value. A set of values is a query of its own. A row
399
497
  never contains rows. Master-detail is a row and rows answered under two
@@ -403,8 +501,9 @@ shown in groups by a group template.
403
501
  The application designs no endpoints, routes or serialization format. The
404
502
  names in the markup are the keys in the page's `.queries.ts` and
405
503
  `.requests.ts` files. The browser can reach only the queries, declared
406
- requests and `crud` entries a page declares; any other name answers `[]` and
407
- logs a server warning.
504
+ requests and `crud` entries a page declares. The builder refuses markup
505
+ naming any other, and one that arrives anyway answers `[]` and logs a server
506
+ warning.
408
507
 
409
508
  Values are not converted. The hub hands each value to the browser as the
410
509
  query produced it. Formatting a date or an amount is done in the query, or
@@ -413,18 +512,18 @@ by a custom element over `Intl`, such as `lb-number` and `lb-date` in
413
512
 
414
513
  ### Elsewhere
415
514
 
416
- | Project | Data requests after first load |
417
- |-------------------|---------------------------------------------------------------------------------|
418
- | React, Vue, Angular, Svelte, Solid (client only) | Whatever the application designs: REST, GraphQL, tRPC, or other |
419
- | Next.js | Server Functions as POST requests identified by a `Next-Action` header; navigations fetch RSC payloads |
420
- | React Router 7 | Loader data fetched by `.data` requests; actions posted to the route URL |
421
- | SvelteKit | Load data from `__data.json`; form actions posted to `?/actionName` |
422
- | Nuxt | Server routes under `server/api` (Nitro), fetched with `useFetch` or `$fetch` |
423
- | htmx | Any HTTP verb to any URL named in `hx-get`, `hx-post` and related attributes; HTML response |
424
- | Turbo | Ordinary page and form requests; HTML responses; Turbo Stream responses |
425
- | Phoenix LiveView | Events and diffs over a WebSocket |
426
- | Livewire | POST requests carrying component snapshots and calls |
427
- | Blazor Server | SignalR messages |
515
+ | Project | Data requests after first load |
516
+ | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
517
+ | React, Vue, Angular, Svelte, Solid (client only) | Whatever the application designs: REST, GraphQL, tRPC, or other |
518
+ | Next.js | Server Functions as POST requests identified by a `Next-Action` header; navigations fetch RSC payloads |
519
+ | React Router 7 | Loader data fetched by `.data` requests; actions posted to the route URL |
520
+ | SvelteKit | Load data from `__data.json`; form actions posted to `?/actionName` |
521
+ | Nuxt | Server routes under `server/api` (Nitro), fetched with `useFetch` or `$fetch` |
522
+ | htmx | Any HTTP verb to any URL named in `hx-get`, `hx-post` and related attributes; HTML response |
523
+ | Turbo | Ordinary page and form requests; HTML responses; Turbo Stream responses |
524
+ | Phoenix LiveView | Events and diffs over a WebSocket |
525
+ | Livewire | POST requests carrying component snapshots and calls |
526
+ | Blazor Server | SignalR messages |
428
527
 
429
528
  ## Mutations, forms and request state
430
529
 
@@ -434,12 +533,12 @@ by a custom element over `Intl`, such as `lb-number` and `lb-date` in
434
533
  `lb-` is one of three requests Loadbare provides; any other name is a
435
534
  request the page declares.
436
535
 
437
- | `lb-request` | Needs | Server key |
438
- |------------------|--------------------------|--------------------------------|
439
- | `lb-row-insert` | `query`, `values` | `crud.<query>.rowInsert` |
440
- | `lb-row-update` | `query`, `key`, `values` | `crud.<query>.rowUpdate` |
441
- | `lb-row-delete` | `query`, `key` | `crud.<query>.rowDelete` |
442
- | Any other name | Whatever is present | `handlers.<name>` |
536
+ | `lb-request` | Needs | Server key |
537
+ | --------------- | ------------------------ | ------------------------ |
538
+ | `lb-row-insert` | `query`, `values` | `crud.<query>.rowInsert` |
539
+ | `lb-row-update` | `query`, `key`, `values` | `crud.<query>.rowUpdate` |
540
+ | `lb-row-delete` | `query`, `key` | `crud.<query>.rowDelete` |
541
+ | Any other name | Whatever is present | `handlers.<name>` |
443
542
 
444
543
  - An element commits the way HTML says it does: a form on `submit`, a
445
544
  control on `change`, anything else on `click`. A submit button commits
@@ -453,11 +552,24 @@ request the page declares.
453
552
  `lb-column`, a form's controls, a live row's, or the controls whose form
454
553
  owner is the element's form owner, the HTML `form` attribute included. A
455
554
  control carrying `lb-column` and `lb-request` sends its own column alone,
456
- so one `rowUpdate` answers a form and a per-field edit.
457
- - Every request has one shape, `{ name, query, key, values }`, and a
458
- declared request receives the same fields.
555
+ so one `rowUpdate` answers a form and a per-field edit. A form also
556
+ gathers the live rows of a query nested in its own, so a master and its
557
+ details are entered together.
558
+ - Every request has one shape, `{ name, query, key, values }`, with `keys`
559
+ in place of `key` for selected rows and `rows` for a form's nested rows,
560
+ and a declared request receives the same fields.
459
561
  - The hub does not send a request Loadbare provides that lacks what the
460
- table lists, and says why in the console.
562
+ table lists, and says why in the console. An update or delete from a row
563
+ not yet entered, whose key is empty, is not sent.
564
+ - One form edits any row of a set. A button points it with HTML's
565
+ `commandfor` and one of three commands: `--lb-edit` fills the form from
566
+ the row the button sits in, as the hub holds it, with no round trip;
567
+ `--lb-new` starts it empty for an insert; `--lb-release` lets it go.
568
+ Inside a `<dialog>`, pointing opens the dialog and letting go closes it.
569
+ - `<input type="checkbox" lb-select>` in a row template selects its row. An
570
+ update or delete for the query that names no one row carries the selected
571
+ keys as `keys`, and the server runs `rowUpdate` or `rowDelete` once per
572
+ key.
461
573
 
462
574
  On the server, each entry has `run` and `refresh`. `run` does the work and
463
575
  may return results, such as a `patch()`, or a new row for the URL with
@@ -466,11 +578,11 @@ over the refreshed results.
466
578
 
467
579
  The hub stamps the element that committed:
468
580
 
469
- | Stamp | Means |
470
- |----------------------|------------------------------------------------------------|
581
+ | Stamp | Means |
582
+ | -------------------- | -------------------------------------------------------------- |
471
583
  | `lb-request-pending` | The round trip is in flight; `aria-busy="true"` is set with it |
472
- | `lb-request-error` | The last round trip from this element failed |
473
- | `lb-request-refused` | The handler refused it, carrying its message |
584
+ | `lb-request-error` | The last round trip from this element failed |
585
+ | `lb-request-refused` | The handler refused it, carrying its message |
474
586
 
475
587
  A commit on an element carrying `lb-request-pending` is ignored. A round
476
588
  trip has a ten-second deadline, after which it is aborted and treated as a
@@ -479,7 +591,9 @@ no message. A handler's `refusal()` carries words for the user, shown on the
479
591
  element and on each control it names.
480
592
 
481
593
  After a successful `lb-row-insert` gathered from a form, the hub resets the
482
- form. A failed insert resets nothing.
594
+ form. A successful request from a pointed form lets the form go, and one
595
+ acting on selected rows clears the selection. A failed request resets
596
+ nothing.
483
597
 
484
598
  The browser displays only values the server returned. There is no
485
599
  optimistic update. Per-keystroke validation is an open question in the
@@ -487,18 +601,60 @@ roadmap.
487
601
 
488
602
  ### Elsewhere
489
603
 
490
- | Project | Sending a mutation | Refreshing afterward | Pending state | Optimistic update |
491
- |------------------|------------------------------------------------------|-------------------------------------------------------------|----------------------------------------|---------------------------|
492
- | React | `<form action={fn}>`, event handlers | Application code | `useActionState`, `useFormStatus`, `useTransition` | `useOptimistic` |
493
- | TanStack Query | `useMutation` | `invalidateQueries` by query key | `isPending` | `onMutate` |
494
- | Next.js | Server Functions | `revalidatePath`, `revalidateTag` in the function | React hooks | `useOptimistic` |
495
- | React Router 7 | `<Form>`, `useFetcher`, route `action` | Loaders on the page revalidate by default; `shouldRevalidate` opts out | `useNavigation`, `fetcher.state` | Application code from pending form data |
496
- | SvelteKit | Form actions in `+page.server.ts`, `use:enhance` | `use:enhance` reruns load functions by default; `invalidate` for targeted reruns | Application code in `use:enhance` | Application code |
497
- | Angular | Event bindings, `HttpClient` | Application code | Application code | Application code |
498
- | htmx | `hx-post`, `hx-put`, `hx-delete` on any element | The HTML response; `hx-swap-oob`; `HX-Trigger` response header | `htmx-request` class, `hx-indicator`, `hx-disabled-elt` | Not built in |
499
- | Turbo | Forms; a successful non-GET submission redirects | Re-render after redirect; Turbo Streams | `aria-busy` on the form, `data-turbo-submits-with` | Not built in |
500
- | Phoenix LiveView | `phx-click`, `phx-submit`, `phx-change` | Assign changes re-render | `phx-click-loading` and related classes, `phx-disable-with` | `JS` commands |
501
- | Livewire | `wire:click`, `wire:submit`, `wire:model` | Component re-render | `wire:loading` | Not built in |
604
+ | Project | Sending a mutation | Refreshing afterward | Pending state | Optimistic update |
605
+ | ---------------- | ------------------------------------------------ | -------------------------------------------------------------------------------- | ----------------------------------------------------------- | --------------------------------------- |
606
+ | React | `<form action={fn}>`, event handlers | Application code | `useActionState`, `useFormStatus`, `useTransition` | `useOptimistic` |
607
+ | TanStack Query | `useMutation` | `invalidateQueries` by query key | `isPending` | `onMutate` |
608
+ | Next.js | Server Functions | `revalidatePath`, `revalidateTag` in the function | React hooks | `useOptimistic` |
609
+ | React Router 7 | `<Form>`, `useFetcher`, route `action` | Loaders on the page revalidate by default; `shouldRevalidate` opts out | `useNavigation`, `fetcher.state` | Application code from pending form data |
610
+ | SvelteKit | Form actions in `+page.server.ts`, `use:enhance` | `use:enhance` reruns load functions by default; `invalidate` for targeted reruns | Application code in `use:enhance` | Application code |
611
+ | Angular | Event bindings, `HttpClient` | Application code | Application code | Application code |
612
+ | htmx | `hx-post`, `hx-put`, `hx-delete` on any element | The HTML response; `hx-swap-oob`; `HX-Trigger` response header | `htmx-request` class, `hx-indicator`, `hx-disabled-elt` | Not built in |
613
+ | Turbo | Forms; a successful non-GET submission redirects | Re-render after redirect; Turbo Streams | `aria-busy` on the form, `data-turbo-submits-with` | Not built in |
614
+ | Phoenix LiveView | `phx-click`, `phx-submit`, `phx-change` | Assign changes re-render | `phx-click-loading` and related classes, `phx-disable-with` | `JS` commands |
615
+ | Livewire | `wire:click`, `wire:submit`, `wire:model` | Component re-render | `wire:loading` | Not built in |
616
+
617
+ ## Correctness
618
+
619
+ ### Loadbare/app
620
+
621
+ Theory leads with correctness, and most of what it asks for is built into
622
+ the framework rather than left to the application.
623
+
624
+ - The screen changes only from what the server sent. There are no
625
+ optimistic updates, and no client cache answers a request in place of the
626
+ server.
627
+ - Rows are matched to elements by key. A row that is gone leaves, a row
628
+ that changed is filled, and a row that did not change is left alone,
629
+ along with anything typed into it and not yet sent.
630
+ - An answer for a page or URL the user has already left does not land.
631
+ - A query that answers in a shape it did not declare is left out, rather
632
+ than shown wrongly.
633
+ - A write goes once: the hub takes the query and key from the document, and
634
+ ignores a commit on an element whose request is in flight.
635
+ - A handler's refusal reaches the user on the controls it names. Any other
636
+ server failure reaches the browser with no message in it.
637
+ - The builder refuses unknown custom elements and `lb-` attributes a
638
+ developer may not write, and the server refuses at startup patterns known
639
+ to be wrong, and at run time any request a page did not declare.
640
+
641
+ Theory's exceptions to its own rules are listed in
642
+ [Miswarts](./miswarts.md).
643
+
644
+ ### Elsewhere
645
+
646
+ Most of the comparison set treats correctness after a write as the
647
+ application's to arrange, from tools the framework provides.
648
+
649
+ | Group | What the screen shows after a write | A second click while the first is in flight |
650
+ | ---------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------- |
651
+ | Client component | Whatever the application sets: an optimistic value, or data refetched after invalidating a cache | Application code |
652
+ | Meta-frameworks | Revalidated loader or server data; optimistic values where the application adds them | Pending-state hooks the application uses |
653
+ | Hypermedia | The HTML the server returned | Opt-in attributes, or form handling in Turbo |
654
+ | Server-driven stateful | The server's re-render | Loading classes and disable attributes |
655
+
656
+ See [Mutations, forms and request state](#mutations-forms-and-request-state)
657
+ for the mechanisms by project.
502
658
 
503
659
  ## Routing and navigation
504
660
 
@@ -525,27 +681,32 @@ roadmap.
525
681
  - A control inside `lb-query="lb-url"` carrying `lb-request="lb-row-update"`
526
682
  writes a query parm. The hub answers that request itself: it replaces the
527
683
  history entry, or pushes one when the element carries `lb-url-push`, and
528
- reloads the page's queries. A handler moves the URL by returning
529
- `url({ ... })`, and the page loads there in the same round trip.
684
+ reloads the page's queries. A handler, or the page's `onPageEnter`, moves
685
+ the URL by returning `url({ ... })`, and the page loads there in the same
686
+ round trip.
530
687
  - The query string rides on every round trip, and the server hands its parms
531
688
  to `contextFor`.
689
+ - The user's order and filters for rows already on the page are not query
690
+ parms. They are choices, the row of the hub's query `lb-choices`, and
691
+ hold until the page changes.
532
692
  - The application must be served from the origin root. The hub reaches
533
- `/lb`, `/client.js` and `/app.css` by absolute path.
534
- - The hub sets `document.title` from `lb-page-label`. It does not move focus
535
- or restore scroll position on navigation.
693
+ `/lb` by absolute path, and the chrome links `/client.js` and `/app.css`.
694
+ - The hub sets `document.title` from `lb-page-label`. On entering a page,
695
+ once its queries have landed, it focuses the first element in `<main>` the
696
+ user can operate. It does not restore scroll position on navigation.
536
697
 
537
698
  ### Elsewhere
538
699
 
539
- | Project | Routes declared by | Parameters | Layouts | Unknown path |
540
- |------------------|-----------------------------------------------------|-------------------------------|-----------------------------|------------------------|
541
- | Next.js (App Router) | Directories under `app/` with `page.tsx` | `[id]`, `[...slug]` segments | Nested `layout.tsx` | `not-found.tsx`, status 404 from the server |
542
- | Nuxt | Files under `pages/` | `[id]` segments | `layouts/`, nested routes | Error page, status 404 |
543
- | SvelteKit | Directories under `src/routes/` with `+page.svelte` | `[id]` segments | Nested `+layout.svelte` | `+error.svelte`, status 404 |
544
- | React Router 7 | Route config or file conventions | `:id` segments | Nested routes with `<Outlet>` | Catch-all route |
545
- | Angular | `Routes` array | `:id` segments | Nested router outlets | `**` wildcard route |
546
- | Vue Router | Routes array | `:id` segments | Nested `<RouterView>` | Catch-all route |
547
- | Phoenix LiveView | `live` routes in the router | `:id` segments, `handle_params` | Root and app layouts | Status 404 from the server |
548
- | htmx, Turbo | Server routes | Server routes | Server templates | Server status |
700
+ | Project | Routes declared by | Parameters | Layouts | Unknown path |
701
+ | -------------------- | --------------------------------------------------- | ------------------------------- | ----------------------------- | ------------------------------------------- |
702
+ | Next.js (App Router) | Directories under `app/` with `page.tsx` | `[id]`, `[...slug]` segments | Nested `layout.tsx` | `not-found.tsx`, status 404 from the server |
703
+ | Nuxt | Files under `pages/` | `[id]` segments | `layouts/`, nested routes | Error page, status 404 |
704
+ | SvelteKit | Directories under `src/routes/` with `+page.svelte` | `[id]` segments | Nested `+layout.svelte` | `+error.svelte`, status 404 |
705
+ | React Router 7 | Route config or file conventions | `:id` segments | Nested routes with `<Outlet>` | Catch-all route |
706
+ | Angular | `Routes` array | `:id` segments | Nested router outlets | `**` wildcard route |
707
+ | Vue Router | Routes array | `:id` segments | Nested `<RouterView>` | Catch-all route |
708
+ | Phoenix LiveView | `live` routes in the router | `:id` segments, `handle_params` | Root and app layouts | Status 404 from the server |
709
+ | htmx, Turbo | Server routes | Server routes | Server templates | Server status |
549
710
 
550
711
  A client-routed single-page application served by a catch-all route also
551
712
  answers 200 for an unknown path, as Loadbare/app does.
@@ -558,7 +719,7 @@ answers 200 for an unknown path, as Loadbare/app does.
558
719
  loadbare-app-build [--src src] [--out dist] [--watch] [--minify]
559
720
  ```
560
721
 
561
- - Four flags. No configuration file. An open blocker proposes one.
722
+ - Four flags. No configuration file.
562
723
  - Files are classified by name: `chrome.html`, `*.page.html`,
563
724
  `*.queries.ts`, `*.requests.ts`, `imports.ts`, `*.css`, `<tag>.html`,
564
725
  `<tag>.browser.ts`.
@@ -584,42 +745,57 @@ Checked at build time, as errors:
584
745
  containing a capital
585
746
  - slot and template errors: two slots, two templates of one name, a template
586
747
  naming a missing destination, content in a definition with no slot
587
- - an `lb-` attribute in markup that is not one a developer writes (`lb-query`,
588
- `lb-column`, `lb-show`, `lb-request`, `lb-url-link`, `lb-url-push`,
589
- `lb-url-unknown`); the rest are stamps, and script never assigns them
748
+ - a custom element placed where the browser's parser would move its
749
+ definition's contents, such as a `<dialog>` inside a `<p>`
750
+ - a widget script declaring an `lb` method Loadbare does not call, or one of
751
+ the browser's lifecycle callbacks
752
+ - a chrome without exactly one `<lb-hub>` and one empty `<main>` inside it,
753
+ and a page carrying either
754
+ - an `lb-` attribute in markup that is not one of the sixteen a developer
755
+ writes; the rest are stamps, and script never assigns them
590
756
  - an `lb-request` value beginning with `lb-` that is not one of the three
591
757
  requests Loadbare provides
592
758
  - `lb-url-link` on anything but an `<a>`, `lb-url-push` without
593
759
  `lb-request`, `lb-url-unknown` on anything but a `<dialog>` inside
594
760
  `<lb-hub>`
595
- - `lb-show` on a `<template>`, on a row template's root, or with no
596
- `lb-query` around it
761
+ - `lb-show` on a `<template>`, on a row template's root, with no `lb-query`
762
+ around it, or written `!` or `!!column`
763
+ - `lb-group` on anything but a `<template>` holding a `<template>`, and an
764
+ aggregate on a `<template>` or naming no column
765
+ - `lb-order` or `lb-filter` on an element not carrying `lb-query`, or naming
766
+ no column
767
+ - `lb-select` on anything but a checkbox in a row template, or beside
768
+ `lb-column` or `lb-request`
769
+ - a `command` beginning `--lb-` that is not one of the three, and one of the
770
+ three with no `commandfor`
771
+ - an `lb-query` naming a query the page does not declare, an `lb-request`
772
+ naming a request it does not declare under `handlers`, and one of the
773
+ three requests for a query whose `crud` entry lacks it. The chrome names
774
+ only `lb-url` and `lb-choices`. The builder reads the declarations by
775
+ loading the page modules, so their import-time code runs during the build.
597
776
  - a `.queries.ts` or `.requests.ts` with no matching page
598
777
 
599
778
  Checked when the server starts: a query or declared request name beginning
600
- with `lb-`.
779
+ with `lb-`, a malformed `order`, an unknown column type, and a `crud` insert,
780
+ update or delete on a `rows` query whose `refresh` names that same query.
601
781
 
602
- Not checked at build time: that an `lb-query` name matches a declared query,
603
- that an `lb-request` matches a declared request or `crud` entry, that the
604
- chrome contains one `<lb-hub>` and one empty `<main>`. The first two are a
605
- roadmap item; the last is listed under the builder blockers. A language server is a
606
- roadmap item.
782
+ A language server is a roadmap item.
607
783
 
608
784
  ### Elsewhere
609
785
 
610
- | Project | Build and dev tooling | Template type checking | Editor tooling |
611
- |------------------|---------------------------------------------------------|-----------------------------------------------|---------------------------------|
612
- | React | Chosen by the application or its framework; Create React App was deprecated in February 2025 | TypeScript checks JSX | TypeScript language service |
613
- | Next.js | Turbopack or webpack; dev server with Fast Refresh | TypeScript; typed routes | TypeScript plugin |
614
- | Vue, Nuxt | Vite; dev server with HMR | `vue-tsc` | Vue language tools (Volar) |
615
- | Angular | Angular CLI (esbuild, Vite dev server) | Strict template type checking in the compiler | Angular Language Service |
616
- | Svelte, SvelteKit| Vite; dev server with HMR | `svelte-check` | Svelte language server |
617
- | Solid | Vite | TypeScript checks JSX | TypeScript language service |
618
- | React Router 7 | Vite | TypeScript; generated route types | TypeScript language service |
619
- | Astro | Vite | `astro check` | Astro language tools |
620
- | htmx, Alpine.js | None required; a script tag | None | Community extensions |
621
- | Hotwire (Rails) | Import maps or a JavaScript bundler | None for Turbo attributes | Community extensions |
622
- | Phoenix | esbuild and Tailwind through Mix | Compile-time warnings for declared component attributes | Elixir language server |
786
+ | Project | Build and dev tooling | Template type checking | Editor tooling |
787
+ | ----------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------- | --------------------------- |
788
+ | React | Chosen by the application or its framework; Create React App was deprecated in February 2025 | TypeScript checks JSX | TypeScript language service |
789
+ | Next.js | Turbopack or webpack; dev server with Fast Refresh | TypeScript; typed routes | TypeScript plugin |
790
+ | Vue, Nuxt | Vite; dev server with HMR | `vue-tsc` | Vue language tools (Volar) |
791
+ | Angular | Angular CLI (esbuild, Vite dev server) | Strict template type checking in the compiler | Angular Language Service |
792
+ | Svelte, SvelteKit | Vite; dev server with HMR | `svelte-check` | Svelte language server |
793
+ | Solid | Vite | TypeScript checks JSX | TypeScript language service |
794
+ | React Router 7 | Vite | TypeScript; generated route types | TypeScript language service |
795
+ | Astro | Vite | `astro check` | Astro language tools |
796
+ | htmx, Alpine.js | None required; a script tag | None | Community extensions |
797
+ | Hotwire (Rails) | Import maps or a JavaScript bundler | None for Turbo attributes | Community extensions |
798
+ | Phoenix | esbuild and Tailwind through Mix | Compile-time warnings for declared component attributes | Elixir language server |
623
799
 
624
800
  ## CSS
625
801
 
@@ -637,22 +813,27 @@ Nothing is scoped, renamed, added or removed.
637
813
  - Light DOM means every selector reaches every element, in the chrome and in
638
814
  widgets.
639
815
  - The hub stamps attributes a stylesheet can select on: `lb-column-value`,
640
- `lb-key-value`, `lb-row-live`, `lb-request-pending`, `lb-request-error`,
641
- `lb-query-row-count`. An empty `rows` query is styled with
642
- `[lb-query-row-count="0"]`.
816
+ `lb-key-value`, `lb-row-live`, `lb-query-row-count`, the group stamps,
817
+ `lb-row-editing`, `lb-row-selected`, `lb-request-pending`,
818
+ `lb-request-error` and `lb-request-refused`. An empty `rows` query is
819
+ styled with `[lb-query-row-count="0"]`.
820
+ - Every change the hub makes to a list is stamped: `lb-row-created`,
821
+ `lb-row-changed`, `lb-row-moved`, `lb-row-requested`, `lb-row-leaving` and
822
+ `lb-group-leaving`. A leaving row is removed once its animations finish,
823
+ so an application animates rows with CSS alone.
643
824
 
644
825
  ### Elsewhere
645
826
 
646
- | Project | Built-in scoping |
647
- |-------------|---------------------------------------------------------------------------|
648
- | React | None; applications choose CSS Modules, CSS-in-JS, Tailwind, or global CSS |
649
- | Next.js | CSS Modules, global CSS, Sass and Tailwind supported by the build |
650
- | Vue | `<style scoped>` adds a data attribute per component; CSS Modules |
651
- | Angular | Emulated encapsulation adds attributes per component by default; shadow DOM and none available |
652
- | Svelte | Component styles scoped by a generated class by default |
653
- | Lit | Shadow DOM scoping by default |
654
- | htmx, Turbo, Alpine.js | None |
655
- | Phoenix | None built in; Tailwind configured by default in new projects |
827
+ | Project | Built-in scoping |
828
+ | ---------------------- | ---------------------------------------------------------------------------------------------- |
829
+ | React | None; applications choose CSS Modules, CSS-in-JS, Tailwind, or global CSS |
830
+ | Next.js | CSS Modules, global CSS, Sass and Tailwind supported by the build |
831
+ | Vue | `<style scoped>` adds a data attribute per component; CSS Modules |
832
+ | Angular | Emulated encapsulation adds attributes per component by default; shadow DOM and none available |
833
+ | Svelte | Component styles scoped by a generated class by default |
834
+ | Lit | Shadow DOM scoping by default |
835
+ | htmx, Turbo, Alpine.js | None |
836
+ | Phoenix | None built in; Tailwind configured by default in new projects |
656
837
 
657
838
  ## The server
658
839
 
@@ -660,8 +841,9 @@ Nothing is scoped, renamed, added or removed.
660
841
 
661
842
  Loadbare/app ships no server. It ships:
662
843
 
663
- - `createHub(pages)`, an engine with no HTTP code, in `@loadbare/app/server`
664
- - `hubRoutes(hub, contextFor)`, an Express 5 router, in
844
+ - `createHub(pages)`, an engine with no HTTP code, in `@loadbare/app/server`,
845
+ beside `row`, `rows`, `patch`, `url`, `choices` and `refusal`
846
+ - `hubRoutes(hub, contextFor, options)`, an Express 5 router, in
665
847
  `@loadbare/app/express`. Express is an optional peer dependency.
666
848
  - `pages.ts`, generated by the builder, which imports every page's queries
667
849
  and requests and exports `hub`
@@ -686,26 +868,28 @@ application's, placed in Express middleware and in `ctx`.
686
868
 
687
869
  A page's server half:
688
870
 
689
- | File | Exports | Contents |
690
- |----------------------|------------|--------------------------------------------------------------|
691
- | `<name>.queries.ts` | `queries` | Named queries, each built with `row(key, run)` or `rows(key, run)` |
692
- | `<name>.requests.ts` | `requests` | `onPageEnter`, `handlers` by request name, `crud` by query name |
871
+ | File | Exports | Contents |
872
+ | -------------------- | ---------- | ---------------------------------------------------------------------------- |
873
+ | `<name>.queries.ts` | `queries` | Named queries, each built with `row(key, run)` or `rows(key, run, options)` |
874
+ | `<name>.requests.ts` | `requests` | `onPageEnter`, `onChoices`, `handlers` by request name, `crud` by query name |
693
875
 
694
- A query's `run` is `(ctx) => Row` or `(ctx) => Row[]`. An answer whose
695
- shape disagrees with its declaration is dropped with a warning.
876
+ A query's `run` is `(ctx) => Row | null` or `(ctx) => Row[]`, synchronous or
877
+ async. A `rows` query's options declare its order and its column types.
878
+ An answer whose shape disagrees with its declaration is dropped with a
879
+ warning.
696
880
 
697
881
  A page's queries are that page's view model, computed on the server: a
698
882
  backend-for-frontend scoped to one page, whose answers are the values the
699
883
  page shows.
700
884
 
701
- | | Data layer (repository, ORM, API) | Backend-for-frontend | Loadbare/app page files |
702
- |------------------|-----------------------------------|----------------------|-------------------------|
703
- | Serves | Any consumer | One client application | One page |
704
- | Shaped by | Entities | A client's screens | One page's elements |
705
- | Answers with | Domain objects, independent of any UI | Data the client still turns into a view | Values as the page shows them, formatted and worded |
706
- | Client-side work | Mapping, state, view models, components | View models, components | None: the hub lands each column on the element that names it |
707
- | Keeps the rules | Yes, in code | Usually not | No: the application keeps them beneath, in its database or in code its handlers share |
708
- | Reused by | Many screens | One application's screens | No other page |
885
+ | | Data layer (repository, ORM, API) | Backend-for-frontend | Loadbare/app page files |
886
+ | ---------------- | --------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------- |
887
+ | Serves | Any consumer | One client application | One page |
888
+ | Shaped by | Entities | A client's screens | One page's elements |
889
+ | Answers with | Domain objects, independent of any UI | Data the client still turns into a view | Values as the page shows them, formatted and worded |
890
+ | Client-side work | Mapping, state, view models, components | View models, components | None: the hub lands each column on the element that names it |
891
+ | Keeps the rules | Yes, in code | Usually not | No: the application keeps them beneath, in its database or in code its handlers share |
892
+ | Reused by | Many screens | One application's screens | No other page |
709
893
 
710
894
  A typical stack runs database, ORM or repository, service, API contract,
711
895
  client store, view model, component and template. A Loadbare/app page keeps
@@ -715,25 +899,27 @@ answer goes. Reuse does not happen through a layer. Two pages showing the
715
899
  same data repeat a query's shape or share a database view, and shared
716
900
  wording goes in a helper module.
717
901
 
718
- The data channel carries no CSRF token. Where an options argument for
719
- hooks, transaction wrapping, error handling and CSRF would go is an open
720
- blocker. Serving the static files from a separate origin is a roadmap item.
902
+ The data channel carries no CSRF token. `hubRoutes` takes an options
903
+ object, one key per concern, whose one key today, `json`, is passed to
904
+ `express.json()`. Options for global hooks, transaction wrapping, error
905
+ handling and a CSRF token are a roadmap item. Serving the static files
906
+ from a separate origin is a roadmap item.
721
907
 
722
908
  ### Elsewhere
723
909
 
724
- | Project | Server |
725
- |------------------|------------------------------------------------------------------------------------|
726
- | React, Vue, Angular, Svelte, Solid (client only) | None; the application supplies an API server |
727
- | Next.js | Next.js server, or adapters for hosting platforms; Middleware or Proxy; Route Handlers |
728
- | Nuxt | Nitro server with presets for hosting platforms |
729
- | SvelteKit | Adapters (Node, static, platform-specific); hooks in `hooks.server.ts` |
730
- | React Router 7 | Request handler with adapters for Node, Express and platforms |
731
- | Astro | Static output, or server adapters |
732
- | htmx | Any server that returns HTML |
733
- | Hotwire | Any server; integrated with Rails |
734
- | Phoenix LiveView | Phoenix |
735
- | Livewire | Laravel |
736
- | Blazor Server | ASP.NET Core |
910
+ | Project | Server |
911
+ | ------------------------------------------------ | -------------------------------------------------------------------------------------- |
912
+ | React, Vue, Angular, Svelte, Solid (client only) | None; the application supplies an API server |
913
+ | Next.js | Next.js server, or adapters for hosting platforms; Middleware or Proxy; Route Handlers |
914
+ | Nuxt | Nitro server with presets for hosting platforms |
915
+ | SvelteKit | Adapters (Node, static, platform-specific); hooks in `hooks.server.ts` |
916
+ | React Router 7 | Request handler with adapters for Node, Express and platforms |
917
+ | Astro | Static output, or server adapters |
918
+ | htmx | Any server that returns HTML |
919
+ | Hotwire | Any server; integrated with Rails |
920
+ | Phoenix LiveView | Phoenix |
921
+ | Livewire | Laravel |
922
+ | Blazor Server | ASP.NET Core |
737
923
 
738
924
  ## Performance profile
739
925
 
@@ -741,12 +927,12 @@ blocker. Serving the static files from a separate origin is a roadmap item.
741
927
 
742
928
  First load of any path:
743
929
 
744
- | Request | Returns |
745
- |---------------------------|--------------------------------------------|
930
+ | Request | Returns |
931
+ | ------------------------- | ------------------------------------------------------- |
746
932
  | `GET /<path>` | `app.html`: the chrome and every page as a `<template>` |
747
- | `GET /client.js` | The hub and every used widget, one bundle |
748
- | `GET /app.css` | Every stylesheet, one file |
749
- | `POST /lb/<page>?<query>` | The page's query results as JSON |
933
+ | `GET /client.js` | The hub and every used widget, one bundle |
934
+ | `GET /app.css` | Every stylesheet, one file |
935
+ | `POST /lb/<page>?<query>` | The page's query results as JSON |
750
936
 
751
937
  The three static files are fixed at release and can be compressed, cached,
752
938
  and served from a CDN. There are no per-route bundles, lazy chunks or module
@@ -754,61 +940,64 @@ waterfalls. The page template is cloned into `<main>` before its data
754
940
  request is sent. An optional `<body hidden>` in the chrome keeps the
755
941
  document hidden until the first page is in place.
756
942
 
757
- Measured on this working tree:
943
+ Measured on 0.18.2, 2026-10-09:
758
944
 
759
- | Item | Size |
760
- |------------------------------------------------|--------------|
761
- | Hub source, `lb-hub.browser.ts` and `lb-apply.ts` | 929 lines, comments included |
762
- | Hub alone, esbuild `--minify` | 8,302 bytes |
763
- | Hub alone, minified then `gzip -9` | 3,377 bytes |
945
+ | Item | Size |
946
+ | -------------------------------------------------------------------- | ------------------------------ |
947
+ | Hub source, `lb-hub.browser.ts`, `lb-apply.ts` and `lb-lifecycle.ts` | 3,151 lines, comments included |
948
+ | Hub alone, with the `core/` modules it imports, esbuild `--minify` | 31,405 bytes |
949
+ | Hub alone, minified then `gzip -9` | 11,596 bytes |
764
950
 
765
- Each navigation is one POST. Each interaction is one POST whose response
766
- carries the refreshed and patched results. On the server, a page's queries
951
+ Each navigation is one POST. Each request is one POST whose response
952
+ carries the refreshed and patched results, and the page's load when the
953
+ request moved the URL. A choice re-places rows the page already holds with
954
+ no round trip, then tells the server in a POST nothing waits on. Pointing a
955
+ form at a row takes no round trip. On the server, a page's queries
767
956
  run one after another in declaration order, then the response is serialized
768
957
  as JSON. No HTML is rendered on the server after the build.
769
958
 
770
959
  `app.html` grows with the number and size of pages, and all of it is parsed
771
- on first load. Lazy loading of page templates is a roadmap item. Theory
772
- states that performance measurements will be published separately; none are
773
- published as of this writing.
960
+ on first load. Lazy loading of page templates is a roadmap item. No
961
+ performance measurements are published as of this writing.
774
962
 
775
963
  ### Elsewhere
776
964
 
777
- | Project | First load | Navigation | Interaction |
778
- |------------------|-------------------------------------------------------------------|------------------------------------------------|------------------------------------------|
779
- | Client-only SPA | HTML shell, framework and application bundles, often split by route; data requests | Route chunk if not loaded; data requests | State update and re-render; data requests |
780
- | Next.js, Nuxt, SvelteKit, React Router 7 | Server-rendered HTML, framework and route bundles, hydration | RSC payload or loader data, route chunks | Server Function or action request; revalidation |
781
- | Astro | HTML; JavaScript only for hydrated islands | Full page load, or client router if enabled | Island-local |
782
- | htmx | Server-rendered HTML and the htmx script | Full page, or `hx-boost` body swap | HTML fragment per request |
783
- | Turbo | Server-rendered HTML and Turbo | Fetch full page HTML, replace body | Form request and page or stream response |
784
- | Phoenix LiveView | Server-rendered HTML, then a WebSocket connection and mount | `<.link patch>` and `<.link navigate>` over the socket | Event over the socket, diff back |
785
- | Blazor Server | Server-rendered HTML, then a SignalR circuit | Over the circuit | Event over the circuit, diff back |
965
+ | Project | First load | Navigation | Interaction |
966
+ | ---------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------- |
967
+ | Client-only SPA | HTML shell, framework and application bundles, often split by route; data requests | Route chunk if not loaded; data requests | State update and re-render; data requests |
968
+ | Next.js, Nuxt, SvelteKit, React Router 7 | Server-rendered HTML, framework and route bundles, hydration | RSC payload or loader data, route chunks | Server Function or action request; revalidation |
969
+ | Astro | HTML; JavaScript only for hydrated islands | Full page load, or client router if enabled | Island-local |
970
+ | htmx | Server-rendered HTML and the htmx script | Full page, or `hx-boost` body swap | HTML fragment per request |
971
+ | Turbo | Server-rendered HTML and Turbo | Fetch full page HTML, replace body | Form request and page or stream response |
972
+ | Phoenix LiveView | Server-rendered HTML, then a WebSocket connection and mount | `<.link patch>` and `<.link navigate>` over the socket | Event over the socket, diff back |
973
+ | Blazor Server | Server-rendered HTML, then a SignalR circuit | Over the circuit | Event over the circuit, diff back |
786
974
 
787
975
  ## Surface area
788
976
 
789
977
  TECHREF-1.0 lists every name Loadbare/app owns in one cross-reference.
790
978
 
791
- | Owned | Count | Names |
792
- |-------------------------------------|-------|---------------------------------------------------------------|
793
- | `lb-*` attributes a developer writes | 15 | `lb-query`, `lb-column`, `lb-show`, `lb-group`, `lb-count`, `lb-sum`, `lb-avg`, `lb-min`, `lb-max`, `lb-order`, `lb-filter`, `lb-request`, `lb-url-link`, `lb-url-push`, `lb-url-unknown` |
794
- | `lb-*` stamps | 17 | `lb-column-value`, `lb-key-value`, `lb-row-live`, `lb-query-row-count`, `lb-group-live`, `lb-group-column`, `lb-group-value`, `lb-row-created`, `lb-row-changed`, `lb-row-moved`, `lb-row-requested`, `lb-row-leaving`, `lb-group-leaving`, `lb-request-pending`, `lb-request-error`, `lb-page`, `lb-page-title` |
795
- | `lb-*` build-time attributes | 2 | `lb-exp-slot`, `lb-exp-template` |
796
- | Requests Loadbare provides | 3 | `lb-row-insert`, `lb-row-update`, `lb-row-delete` |
797
- | `lb*` methods on custom elements | 4 | `lbAttached`, `lbRowsLanded`, `lbRequestDone`, `lbDetached` |
798
- | Attribute namespace for expansion | 1 | `exp-*` |
799
- | Reserved tags | 1 | `<lb-hub>` |
800
- | Reserved events | 1 | `lb-request` |
801
- | Queries the hub serves | 2 | `lb-url`, with columns `lb-path`, `lb-page-label`, `lb-page-unknown`; `lb-choices`, keyed by `lb-path` |
802
- | Builder flags | 4 | `--src`, `--out`, `--watch`, `--minify` |
803
- | Reserved file names and patterns | 8 | `chrome.html`, `imports.ts`, `*.page.html`, `*.queries.ts`, `*.requests.ts`, `<tag>.html`, `<tag>.browser.ts`, `*.css` |
804
- | Reserved `package.json` keys | 1 | `loadbare.widgets` |
805
- | Server functions | 7 | `row`, `rows`, `patch`, `url`, `choices`, `createHub`, `hubRoutes` |
806
- | Browser exports | 2 | `applyRow`, `applyResponse` |
807
-
808
- The technical reference, TECHREF-1.0.md, is 1,181 lines. Theory states that
809
- the vocabulary fits in an LLM's context window, and discloses that Anthropic
810
- models writing `.requests.ts` files consistently refresh a `rows` query after
811
- returning a patch for it.
979
+ | Owned | Count | Names |
980
+ | ------------------------------------ | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
981
+ | `lb-*` attributes a developer writes | 16 | `lb-query`, `lb-column`, `lb-show`, `lb-group`, `lb-count`, `lb-sum`, `lb-avg`, `lb-min`, `lb-max`, `lb-order`, `lb-filter`, `lb-request`, `lb-select`, `lb-url-link`, `lb-url-push`, `lb-url-unknown` |
982
+ | `lb-*` stamps | 20 | `lb-column-value`, `lb-key-value`, `lb-row-live`, `lb-query-row-count`, `lb-group-live`, `lb-group-column`, `lb-group-value`, `lb-row-created`, `lb-row-changed`, `lb-row-moved`, `lb-row-requested`, `lb-row-leaving`, `lb-group-leaving`, `lb-row-editing`, `lb-row-selected`, `lb-request-pending`, `lb-request-error`, `lb-request-refused`, `lb-page`, `lb-page-title` |
983
+ | `lb-*` build-time attributes | 2 | `lb-exp-slot`, `lb-exp-template` |
984
+ | Requests Loadbare provides | 3 | `lb-row-insert`, `lb-row-update`, `lb-row-delete` |
985
+ | Commands for `commandfor` | 3 | `--lb-edit`, `--lb-new`, `--lb-release` |
986
+ | `lb*` methods on custom elements | 4 | `lbAttached`, `lbRowsLanded`, `lbRequestDone`, `lbDetached` |
987
+ | Attribute namespace for expansion | 1 | `exp-*` |
988
+ | Reserved tags | 1 | `<lb-hub>` |
989
+ | Reserved events | 2 | `lb-request`, `lb-request-done` |
990
+ | Queries the hub serves | 2 | `lb-url`, with columns `lb-path`, `lb-page-label`, `lb-page-unknown`; `lb-choices`, keyed by `lb-path` |
991
+ | Builder flags | 4 | `--src`, `--out`, `--watch`, `--minify` |
992
+ | Reserved file names and patterns | 8 | `chrome.html`, `imports.ts`, `*.page.html`, `*.queries.ts`, `*.requests.ts`, `<tag>.html`, `<tag>.browser.ts`, `*.css` |
993
+ | Reserved `package.json` keys | 1 | `loadbare.widgets` |
994
+ | Server functions | 8 | `row`, `rows`, `patch`, `url`, `choices`, `refusal`, `createHub`, `hubRoutes` |
995
+ | Browser exports | 2 | `applyRow`, `applyResponse` |
996
+
997
+ Theory states that the whole vocabulary fits in one technical reference,
998
+ and so in a developer's head and in an LLM's context window. Because SQL
999
+ and HTML are thoroughly represented in an LLM's training, the reference
1000
+ only has to spell out the mappings between them.
812
1001
 
813
1002
  The other projects in the comparison set do not publish a comparable
814
1003
  cross-reference, so no count is given for them here.
@@ -823,9 +1012,16 @@ cross-reference, so no count is given for them here.
823
1012
  - `aria-busy="true"` is set and removed with `lb-request-pending`.
824
1013
  - An element hidden by `lb-show` is inside template content, so it is not in
825
1014
  the accessibility tree.
1015
+ - A row leaving the list takes `inert` until it is removed, and a row the
1016
+ user's filter does not pass is `hidden`.
1017
+ - A refusal's message for a column goes on its control through HTML's own
1018
+ validity, `setCustomValidity`, and the browser treats it as it treats any
1019
+ invalid control.
826
1020
  - A widget's markup is in the document and in view-source.
827
- - On navigation the hub sets `document.title` from the page's `<title>`. It
828
- does not move focus or announce the new page.
1021
+ - On navigation the hub sets `document.title` from the page's `<title>`.
1022
+ On entering a page, once its queries have landed, it focuses the first
1023
+ element in `<main>` the user can operate. It does not announce the new
1024
+ page.
829
1025
 
830
1026
  ### Elsewhere
831
1027
 
@@ -846,45 +1042,46 @@ No browser test runner is installed.
846
1042
 
847
1043
  For an application:
848
1044
 
849
- - A query is `{ kind, run }`, and `run(ctx)` is a function returning rows.
850
- - A request entry is `{ run, refresh }`, and `run(ctx, where)` is a function.
1045
+ - A query is `{ kind, key, run }`, and `run(ctx)` is a function returning a
1046
+ row, `null` or rows.
1047
+ - A request entry is `{ run, refresh }`, and `run(ctx, request)` is a
1048
+ function.
851
1049
  - `createHub(pages)` returns an object whose methods take a page name and a
852
1050
  `ctx`, with no HTTP server.
853
- - `applyData(root, data)` lands a response on any DOM root.
1051
+ - `applyResponse(root, items)` lands response items on any DOM root.
854
1052
  - A widget is a custom element.
855
1053
 
856
1054
  The documentation describes no testing approach for applications.
857
1055
 
858
1056
  ### Elsewhere
859
1057
 
860
- | Project | Common testing tools |
861
- |------------------|---------------------------------------------------------------|
862
- | React | Vitest or Jest with React Testing Library; Playwright |
863
- | Vue | Vitest with Vue Test Utils; Playwright |
864
- | Angular | TestBed with Jasmine and Karma, or Jest or Vitest; Playwright |
865
- | Svelte | Vitest with Svelte Testing Library; Playwright |
866
- | htmx, Turbo | Server-side tests of HTML responses; Playwright or Capybara |
1058
+ | Project | Common testing tools |
1059
+ | ---------------- | ----------------------------------------------------------------- |
1060
+ | React | Vitest or Jest with React Testing Library; Playwright |
1061
+ | Vue | Vitest with Vue Test Utils; Playwright |
1062
+ | Angular | TestBed with Jasmine and Karma, or Jest or Vitest; Playwright |
1063
+ | Svelte | Vitest with Svelte Testing Library; Playwright |
1064
+ | htmx, Turbo | Server-side tests of HTML responses; Playwright or Capybara |
867
1065
  | Phoenix LiveView | `Phoenix.LiveViewTest`, which drives a LiveView without a browser |
868
1066
 
869
1067
  ## Release history and churn
870
1068
 
871
1069
  ### Loadbare/app
872
1070
 
873
- Loadbare/app is at 0.9.0 and has not reached 1.0. TECHREF-1.0 lists items
874
- to decide before 1.0 because they would be breaking changes afterward.
875
- Theory states that 1.0 is scoped so that foreseeable features can be added
876
- without breaking existing code.
1071
+ Loadbare/app has not reached 1.0. Theory argues that application code should have a long shelf
1072
+ life, because each part of it is a standard or a mature tool.
877
1073
 
878
1074
  Application code consists of:
879
1075
 
880
- | Part | Standard or dependency |
881
- |------------------|---------------------------------------------------------|
882
- | Pages and chrome | HTML |
883
- | Styles | CSS |
884
- | Widgets | Custom Elements v1 and `<template>` |
885
- | Server | Express 5 |
886
- | Page server code | TypeScript functions taking `ctx` |
887
- | Binding | The `lb-*` vocabulary |
1076
+ | Part | Standard or dependency |
1077
+ | ---------------- | ----------------------------------- |
1078
+ | Pages and chrome | HTML |
1079
+ | Styles | CSS |
1080
+ | Widgets | Custom Elements v1 and `<template>` |
1081
+ | Pointing a form | HTML's `command` and `commandfor` |
1082
+ | Server | Express 5 |
1083
+ | Page server code | TypeScript functions taking `ctx` |
1084
+ | Binding | The `lb-*` vocabulary |
888
1085
 
889
1086
  The W3C HTML Templates specification was published as a Working Draft in
890
1087
  2013 (see [Prior art](./prior-art.md)). Autonomous custom elements have been
@@ -896,97 +1093,112 @@ Loadbare's history begins with Andromeda in 2003, whose data layer is now
896
1093
 
897
1094
  ### Elsewhere
898
1095
 
899
- | Date | Change |
900
- |----------------|---------------------------------------------------------------------------|
901
- | September 2016 | Angular 2 released, incompatible with AngularJS |
902
- | February 2019 | React 16.8 introduces Hooks |
903
- | September 2020 | Vue 3 released, with the Composition API |
904
- | December 2021 | AngularJS support ends |
905
- | October 2022 | Next.js 13 introduces the App Router alongside the Pages Router |
906
- | December 2023 | Vue 2 reaches end of life |
907
- | June 2024 | htmx 2.0 released |
908
- | October 2024 | Svelte 5 released, replacing reactive declarations with runes |
909
- | November 2024 | React Router 7 released, absorbing Remix |
910
- | December 2024 | React 19 released, with Actions and stable Server Components |
911
- | February 2025 | Create React App deprecated |
1096
+ | Date | Change |
1097
+ | -------------- | --------------------------------------------------------------- |
1098
+ | September 2016 | Angular 2 released, incompatible with AngularJS |
1099
+ | February 2019 | React 16.8 introduces Hooks |
1100
+ | September 2020 | Vue 3 released, with the Composition API |
1101
+ | December 2021 | AngularJS support ends |
1102
+ | October 2022 | Next.js 13 introduces the App Router alongside the Pages Router |
1103
+ | December 2023 | Vue 2 reaches end of life |
1104
+ | June 2024 | htmx 2.0 released |
1105
+ | October 2024 | Svelte 5 released, replacing reactive declarations with runes |
1106
+ | November 2024 | React Router 7 released, absorbing Remix |
1107
+ | December 2024 | React 19 released, with Actions and stable Server Components |
1108
+ | February 2025 | Create React App deprecated |
912
1109
 
913
1110
  ## What Loadbare/app does not do
914
1111
 
915
- Each item is current behavior or an open item named in TECHREF-1.0 or the
916
- Roadmap.
917
-
918
- | Area | Status in the working tree after 0.9.0 |
919
- |-------------------------------|------------------------------------------------------------------------|
920
- | Nested data | A row never holds rows. Trees are sent as joined rows. |
921
- | Record URLs and view parameters | A path names a page. A record or a filter is a query parm, `/accounts?acct=23`. |
922
- | Query arguments from the browser | None. Query parms reach queries through `contextFor` and `ctx`. |
923
- | File inputs | Not landed and not gathered. |
924
- | Data types and formatting | No conversion. Queries, or custom elements over `Intl`, format values. |
925
- | Error messages | A handler's `refusal()` reaches the user; any other failure carries no message. |
926
- | Field validation | No mechanism. Per-keystroke validation is a roadmap question. |
927
- | Optimistic updates | None. |
928
- | Concurrent writers | No version or conflict check. Roadmap question. |
929
- | Server push | None. No WebSocket or server-sent events. |
930
- | Offline or local-first use | None. |
931
- | Server-side rendering for crawlers | None. Every path answers the same document with status 200. |
932
- | Subpath hosting | Not possible. The hub uses absolute paths. |
933
- | Chrome-level queries | A widget in the chrome bound to a query requires every page to declare it. On the roadmap. |
934
- | A widget receiving a whole row | Not available. Open blocker. |
935
- | A nested `rows` query per outer row | A nested query receives the same rows in every outer row. A new outer row's nested query stays empty until its name lands again. Open blocker. |
936
- | Configurable request deadline | Fixed at ten seconds. |
937
- | Internationalization | Roadmap item. |
938
- | Lazy page loading | Roadmap item. |
939
- | Per-widget CSS shipping | Roadmap item. |
940
- | Dev server, HMR | None. |
941
- | Language server | Roadmap item. |
942
- | Browser tests of the framework | Not written. |
1112
+ Some of what Loadbare/app does not do is a decision, made for one of
1113
+ Theory's goals. The rest is a gap: open, on the roadmap, or simply not
1114
+ built. The two are listed apart, because a reader weighing the framework
1115
+ should not mistake one for the other.
1116
+
1117
+ ### By decision
1118
+
1119
+ | Area | What it does instead, and why |
1120
+ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
1121
+ | Nested data | A row never holds rows; trees are sent as joined rows. The two-shape decision that keeps the browser code finite. |
1122
+ | Record URLs and view parameters | A path names a page; a record or a filter is a query parm, `/accounts?acct=23`. See [Departures](./departures.md). |
1123
+ | Query arguments from the browser | None. Query parms reach queries through `contextFor` and `ctx`, so the wire reaches nothing a page did not publish. Correctness. |
1124
+ | Data types and formatting | No conversion. Queries, or custom elements over `Intl`, format values. The browser already formats; Loadbare does not do it again. |
1125
+ | Error messages | A handler's `refusal()` reaches the user; any other failure carries no message. Correctness. |
1126
+ | Optimistic updates | None. The screen shows only what the server answered. Correctness. |
1127
+ | Server push | None. Currency is out of scope until it is reconciled with correctness. |
1128
+ | Offline or local-first use | None. Loadbare/app is for server-bound applications. |
1129
+ | Server-side rendering for crawlers | None. Every path answers the same document with status 200. A departure; see [Departures](./departures.md). |
1130
+ | Subpath hosting | Not possible. A cost of the navigation departure. |
1131
+
1132
+ ### Gaps and open items
1133
+
1134
+ Each is current behavior or an open item named in TECHREF-1.0 or the
1135
+ [Roadmap](./roadmap.md).
1136
+
1137
+ | Area | Status |
1138
+ | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
1139
+ | File inputs | Not landed and not gathered. |
1140
+ | Field validation | No per-keystroke mechanism. A handler's refusal lands on each control through the browser's own validity; validation placement is an open question. |
1141
+ | Concurrent writers | No version or conflict check. Open question. |
1142
+ | Chrome-level queries | The chrome names only `lb-url` and `lb-choices`; an application query there is a build error. On the roadmap. |
1143
+ | A widget receiving a whole row | Not available. On the roadmap. |
1144
+ | Compound foreign keys | A picker offers the same choices on every row; a choice restricted by another column of its row cannot be expressed. Roadmap. |
1145
+ | Configurable request deadline | Fixed at ten seconds. |
1146
+ | Internationalization | Roadmap item. |
1147
+ | Lazy page loading | Roadmap item. |
1148
+ | Per-widget CSS shipping | Roadmap item. |
1149
+ | Dev server, HMR | None. |
1150
+ | Language server | Roadmap item. |
1151
+ | Browser tests of the framework | Not written. |
943
1152
 
944
1153
  ## Requirements and fit
945
1154
 
946
- Theory states the fit condition: data that is, or maps readily to, rows and
947
- sets of rows, and affordances that map to the standard operations on that
948
- data.
1155
+ Theory states the fit condition: data that is rows, or can easily be shaped
1156
+ into rows, and interactions that map onto reading and writing them. It
1157
+ does not yet fit an application that needs currency.
949
1158
 
950
- Theory also states the strongest objection it knows of and does not dispute
951
- it: that the binding attributes and the required relational shape exchange
952
- one set of accidental complexity for another, and that the system is
953
- "differently accidental" from other frameworks.
1159
+ Theory also takes up the strongest objection it knows of: that the binding
1160
+ attributes and the required relational shape exchange one set of accidental
1161
+ complexity for another. Its answer is a rule rather than a count: every
1162
+ term must be a SQL concept, an HTML one, or belong to a small named family,
1163
+ and the accidents most likely to pile up are in the mapping rules SQL does
1164
+ not dictate.
954
1165
 
955
1166
  The table lists requirements an application may have, and whether each
956
1167
  approach provides them without additional libraries.
957
1168
 
958
- | Requirement | Loadbare/app 0.9 | Client component + API | Meta-frameworks | Hypermedia | Server-driven stateful |
959
- |-----------------------------------------------|--------------------|------------------------|-----------------|------------|------------------------|
960
- | Pages bound to relational query results | yes | application code | application code | server templates | server templates |
961
- | No endpoint or route design | yes | no | partly (actions, loaders) | no | partly (routes, events) |
962
- | No client-side state management | yes | no | no | yes | yes |
963
- | No build step | no | no | no | yes | depends on stack |
964
- | Server-rendered HTML for crawlers | no | no | yes | yes | yes |
965
- | Route parameters and record URLs | query parms only | yes | yes | yes | yes |
966
- | Optimistic updates | no | yes | yes | no | partly |
967
- | Offline or local-first operation | no | with libraries | with libraries | no | no |
968
- | Real-time server push | no | with libraries | with libraries | extensions, Turbo Streams over WebSocket | yes |
969
- | Nested JSON documents | no | yes | yes | server templates | yes |
970
- | Client-only interaction (canvas, editors, drag and drop) | through custom elements | yes | yes | through scripts | through hooks |
971
- | Works while the server is unreachable | no | with caching | with caching | no | no |
972
- | No persistent connection | yes | yes | yes | yes | no |
973
- | Server code in any language | no (Node, TypeScript) | yes | no (JavaScript, TypeScript) | yes | no (Elixir, PHP, .NET) |
974
- | Stable 1.0 or later | no | yes | yes | yes | yes |
1169
+ | Requirement | Loadbare/app | Client component + API | Meta-frameworks | Hypermedia | Server-driven stateful |
1170
+ | -------------------------------------------------------- | ----------------------- | ---------------------- | --------------------------- | ---------------------------------------- | ----------------------- |
1171
+ | Pages bound to relational query results | yes | application code | application code | server templates | server templates |
1172
+ | No endpoint or route design | yes | no | partly (actions, loaders) | no | partly (routes, events) |
1173
+ | No client-side state management | yes | no | no | yes | yes |
1174
+ | No build step | no | no | no | yes | depends on stack |
1175
+ | Server-rendered HTML for crawlers | no, by design | no | yes | yes | yes |
1176
+ | Route parameters and record URLs | query parms only | yes | yes | yes | yes |
1177
+ | Optimistic updates | no, by design | yes | yes | no | partly |
1178
+ | Offline or local-first operation | no | with libraries | with libraries | no | no |
1179
+ | Real-time server push | no, out of scope | with libraries | with libraries | extensions, Turbo Streams over WebSocket | yes |
1180
+ | Nested JSON documents | no | yes | yes | server templates | yes |
1181
+ | Client-only interaction (canvas, editors, drag and drop) | through custom elements | yes | yes | through scripts | through hooks |
1182
+ | Works while the server is unreachable | no | with caching | with caching | no | no |
1183
+ | No persistent connection | yes | yes | yes | yes | no |
1184
+ | Server code in any language | no (Node, TypeScript) | yes | no (JavaScript, TypeScript) | yes | no (Elixir, PHP, .NET) |
1185
+ | Stable 1.0 or later | no | yes | yes | yes | yes |
975
1186
 
976
1187
  ## Appendix A: Matrix
977
1188
 
978
- | Aspect | Loadbare/app | Client component | Meta-frameworks | Hypermedia | Server-driven stateful |
979
- |----------------------|--------------------------------------|---------------------------------|-----------------------------------------|---------------------------------|---------------------------------|
980
- | State | Server; DOM | Browser memory | Server and browser memory | Server; DOM | Server memory per client |
981
- | Rendering | Build time | Browser | Server, then browser | Server per request | Server per event |
982
- | Markup language | HTML with `lb-*` names | JSX or template syntax with expressions | JSX or template syntax | Server templates with `hx-*` or `data-turbo-*` | HEEx, Blade, Razor |
983
- | Components | Light-DOM custom elements | Framework components | Framework components | Server partials; Stimulus controllers | Server components |
984
- | Wire | One POST endpoint, JSON rows | Application-designed | Framework-defined | HTML over HTTP | WebSocket or snapshot POSTs |
985
- | Mutation refresh | Declared `refresh` names; patches | Application code or query invalidation | Revalidation | Response HTML | Re-render |
986
- | Routing | Flat page names; query parms | Router library | File-based, with parameters and layouts | Server routes | Server routes |
987
- | Build | `loadbare-app-build`, esbuild | Vite and others | Vite, Turbopack, others | None required | Stack-specific |
988
- | CSS | Concatenated, unscoped | Varies | Varies, scoped options | None | None built in |
989
- | Server | Application's Express app | Application's API | Framework server or adapters | Any | Phoenix, Laravel, ASP.NET Core |
1189
+ | Aspect | Loadbare/app | Client component | Meta-frameworks | Hypermedia | Server-driven stateful |
1190
+ | ---------------- | --------------------------------- | --------------------------------------- | ---------------------------------------- | ---------------------------------------------- | ------------------------------ |
1191
+ | State | Server; DOM | Browser memory | Server and browser memory | Server; DOM | Server memory per client |
1192
+ | Rendering | Build time | Browser | Server, then browser | Server per request | Server per event |
1193
+ | Markup language | HTML with `lb-*` names | JSX or template syntax with expressions | JSX or template syntax | Server templates with `hx-*` or `data-turbo-*` | HEEx, Blade, Razor |
1194
+ | Components | Light-DOM custom elements | Framework components | Framework components | Server partials; Stimulus controllers | Server components |
1195
+ | Wire | One POST endpoint, JSON rows | Application-designed | Framework-defined | HTML over HTTP | WebSocket or snapshot POSTs |
1196
+ | Mutation refresh | Declared `refresh` names; patches | Application code or query invalidation | Revalidation | Response HTML | Re-render |
1197
+ | After a write | Only the server's answer | The application's choice | Revalidated data; optimistic where added | The server's HTML | The server's re-render |
1198
+ | Routing | Flat page names; query parms | Router library | File-based, with parameters and layouts | Server routes | Server routes |
1199
+ | Build | `loadbare-app-build`, esbuild | Vite and others | Vite, Turbopack, others | None required | Stack-specific |
1200
+ | CSS | Concatenated, unscoped | Varies | Varies, scoped options | None | None built in |
1201
+ | Server | Application's Express app | Application's API | Framework server or adapters | Any | Phoenix, Laravel, ASP.NET Core |
990
1202
 
991
1203
  ## Appendix B: One page, three ways
992
1204
 
@@ -1157,38 +1369,38 @@ the request is in flight.
1157
1369
 
1158
1370
  Each equivalent is approximate.
1159
1371
 
1160
- | Loadbare/app | Nearest equivalents elsewhere |
1161
- |-------------------------------|----------------------------------------------------------------------------------|
1162
- | Hub | Client runtime; router plus data layer |
1163
- | Chrome (`chrome.html`) | Root layout (`app/layout.tsx`, `+layout.svelte`); application shell |
1164
- | Page (`<name>.page.html`) | Route component; page template |
1165
- | Query (`row()`, `rows()`) | Loader (React Router 7); load function (SvelteKit); data read in a Server Component |
1166
- | `onPageEnter` | LiveView `mount`; code at the top of a loader |
1167
- | Declared request (`handlers`) | Route action (React Router 7); form action (SvelteKit); Server Function; LiveView `handle_event` |
1168
- | `crud` operation | Resource controller action (Rails); REST verb on a row |
1169
- | `refresh` | Revalidation; `invalidateQueries`; `revalidatePath` |
1170
- | `patch({ rows, drop })` | Keyed partial update; Turbo Stream `append`/`replace`/`remove`; LiveView `stream_insert`/`stream_delete` |
1171
- | Landing | Render and commit; swap; patch |
1172
- | `lb-query`, `lb-column` | IE4 `DATASRC` and `DATAFLD`; `v-for` and `{{ }}`; `x-for` and `x-text` |
1173
- | Key declared by `row()`/`rows()` | `key` (React, Vue); `track` (Angular); keyed `each` (Svelte) |
1174
- | `lb-show` | `v-if`, `@if`, `{#if}`, `x-if` |
1175
- | `lb-column-value` | A prop; an observed attribute |
1176
- | `lb-request` | `hx-post`; `phx-click`; `wire:click`; `<form action>` |
1177
- | `lb-request` event | `htmx:beforeRequest`; a component `emit` |
1178
- | `lb-request-pending` | `htmx-request` class; `phx-click-loading`; `useFormStatus().pending`; `wire:loading` |
1179
- | `lb-request-error` | `htmx:responseError`; an error boundary |
1180
- | `lb-query-row-count` | An empty-state conditional in a template |
1181
- | `lb-url-link` | `<Link>`; `hx-boost`; Turbo Drive |
1182
- | `lb-url` | `useLocation` and `useSearchParams`; `page.url` (SvelteKit); `$route` (Vue Router) |
1183
- | `url()` from a handler | A redirect after POST; `redirect()` in a Server Function |
1184
- | Widget | Component; custom element |
1185
- | Expansion | Server-side include; partial; build-time component render (Astro) |
1186
- | `exp-` parameter | A prop fixed at build time |
1187
- | `lb-exp-slot`, `lb-exp-template` | Default slot and named slots; `ng-content` with `select` |
1188
- | `lbRowsLanded` | An effect after a list renders |
1189
- | `lbAttached`, `lbDetached` | Mount and unmount; `onMounted` and `onUnmounted` (Vue); `onMount` and `onDestroy` (Svelte) |
1190
- | `order`, `lb-group` | `ORDER BY` with report-writer control breaks (`BREAK ON`, group bands) |
1191
- | `lb-choices`, `lb-order`, `lb-filter` | A data grid's sort and filter model |
1192
- | `ctx` / `HubContext` | `event.locals` (SvelteKit); loader `context` (React Router 7); request-scoped dependency injection |
1193
- | `imports.ts` | Installing and registering a component library |
1194
- | `pages.ts` | Generated route manifest |
1372
+ | Loadbare/app | Nearest equivalents elsewhere |
1373
+ | ------------------------------------- | -------------------------------------------------------------------------------------------------------- |
1374
+ | Hub | Client runtime; router plus data layer |
1375
+ | Chrome (`chrome.html`) | Root layout (`app/layout.tsx`, `+layout.svelte`); application shell |
1376
+ | Page (`<name>.page.html`) | Route component; page template |
1377
+ | Query (`row()`, `rows()`) | Loader (React Router 7); load function (SvelteKit); data read in a Server Component |
1378
+ | `onPageEnter` | LiveView `mount`; code at the top of a loader |
1379
+ | Declared request (`handlers`) | Route action (React Router 7); form action (SvelteKit); Server Function; LiveView `handle_event` |
1380
+ | `crud` operation | Resource controller action (Rails); REST verb on a row |
1381
+ | `refresh` | Revalidation; `invalidateQueries`; `revalidatePath` |
1382
+ | `patch({ rows, drop })` | Keyed partial update; Turbo Stream `append`/`replace`/`remove`; LiveView `stream_insert`/`stream_delete` |
1383
+ | Landing | Render and commit; swap; patch |
1384
+ | `lb-query`, `lb-column` | IE4 `DATASRC` and `DATAFLD`; `v-for` and `{{ }}`; `x-for` and `x-text` |
1385
+ | Key declared by `row()`/`rows()` | `key` (React, Vue); `track` (Angular); keyed `each` (Svelte) |
1386
+ | `lb-show` | `v-if`, `@if`, `{#if}`, `x-if` |
1387
+ | `lb-column-value` | A prop; an observed attribute |
1388
+ | `lb-request` | `hx-post`; `phx-click`; `wire:click`; `<form action>` |
1389
+ | `lb-request` event | `htmx:beforeRequest`; a component `emit` |
1390
+ | `lb-request-pending` | `htmx-request` class; `phx-click-loading`; `useFormStatus().pending`; `wire:loading` |
1391
+ | `lb-request-error` | `htmx:responseError`; an error boundary |
1392
+ | `lb-query-row-count` | An empty-state conditional in a template |
1393
+ | `lb-url-link` | `<Link>`; `hx-boost`; Turbo Drive |
1394
+ | `lb-url` | `useLocation` and `useSearchParams`; `page.url` (SvelteKit); `$route` (Vue Router) |
1395
+ | `url()` from a handler | A redirect after POST; `redirect()` in a Server Function |
1396
+ | Widget | Component; custom element |
1397
+ | Expansion | Server-side include; partial; build-time component render (Astro) |
1398
+ | `exp-` parameter | A prop fixed at build time |
1399
+ | `lb-exp-slot`, `lb-exp-template` | Default slot and named slots; `ng-content` with `select` |
1400
+ | `lbRowsLanded` | An effect after a list renders |
1401
+ | `lbAttached`, `lbDetached` | Mount and unmount; `onMounted` and `onUnmounted` (Vue); `onMount` and `onDestroy` (Svelte) |
1402
+ | `order`, `lb-group` | `ORDER BY` with report-writer control breaks (`BREAK ON`, group bands) |
1403
+ | `lb-choices`, `lb-order`, `lb-filter` | A data grid's sort and filter model |
1404
+ | `ctx` / `HubContext` | `event.locals` (SvelteKit); loader `context` (React Router 7); request-scoped dependency injection |
1405
+ | `imports.ts` | Installing and registering a component library |
1406
+ | `pages.ts` | Generated route manifest |