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