@loadbare/app 0.7.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build/assemble.d.ts.map +1 -1
- package/dist/build/assemble.js +67 -6
- package/dist/build/assemble.js.map +1 -1
- package/dist/core/lb-constants.d.ts +2 -2
- package/dist/core/lb-constants.d.ts.map +1 -1
- package/dist/core/lb-constants.js +28 -14
- package/dist/core/lb-constants.js.map +1 -1
- package/dist/core/lb-types.d.ts +4 -10
- package/dist/core/lb-types.d.ts.map +1 -1
- package/dist/core/lb-types.js.map +1 -1
- package/dist/hub/lb-apply.d.ts +2 -2
- package/dist/hub/lb-apply.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +98 -8
- package/dist/hub/lb-apply.js.map +1 -1
- package/dist/hub/lb-hub.browser.d.ts.map +1 -1
- package/dist/hub/lb-hub.browser.js +61 -26
- package/dist/hub/lb-hub.browser.js.map +1 -1
- package/dist/server/lb-express.d.ts.map +1 -1
- package/dist/server/lb-express.js +2 -7
- package/dist/server/lb-express.js.map +1 -1
- package/dist/server/lb-server.d.ts +8 -15
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/server/lb-server.js +0 -3
- package/dist/server/lb-server.js.map +1 -1
- package/docs/TECHREF-1.0.md +141 -100
- package/docs/analysis-closed-set.md +210 -0
- package/docs/comparison.md +1124 -0
- package/docs/prior-art.md +216 -0
- package/docs/reference/custom-elements.md +21 -6
- package/docs/reference/data-binding.md +120 -40
- package/docs/reference/page-files.md +13 -9
- package/docs/reference/widgets.md +2 -2
- package/docs/roadmap.md +1 -1
- package/docs/testing.md +7 -1
- package/docs/theory.md +737 -408
- package/docs/tutorials/080-widget-requests.md +9 -28
- package/package.json +1 -1
|
@@ -0,0 +1,1124 @@
|
|
|
1
|
+
# Loadbare/app and current front-end frameworks
|
|
2
|
+
|
|
3
|
+
> LLM-authored, not yet revised by a person. Drafted by Claude on
|
|
4
|
+
> 2026-09-16 against the working tree of `@loadbare/app` 0.7.3, which
|
|
5
|
+
> includes `lb-show`. Statements about other frameworks are from general
|
|
6
|
+
> knowledge of those projects as of 2026 and carry no per-claim sources.
|
|
7
|
+
|
|
8
|
+
This document states how `@loadbare/app` differs from the frameworks and
|
|
9
|
+
libraries most used for front-end development today. It repeats material
|
|
10
|
+
from [Theory](./theory.md), [TECHREF-1.0](./TECHREF-1.0.md),
|
|
11
|
+
[Prior art](./prior-art.md) and
|
|
12
|
+
[Assessing the accidental complexity claim](./analysis-accidental-complexity.md)
|
|
13
|
+
so that it can be read on its own. Where this document and
|
|
14
|
+
[Theory](./theory.md) disagree, Theory is authoritative.
|
|
15
|
+
|
|
16
|
+
Loadbare/app has not reached 1.0. The [Blockers](./TECHREF-1.0.md#blockers)
|
|
17
|
+
section of TECHREF-1.0 lists open decisions, and five of its sections are
|
|
18
|
+
marked `UNEDITED`. Anything below that describes Loadbare/app describes the
|
|
19
|
+
code as it stands.
|
|
20
|
+
|
|
21
|
+
## Contents
|
|
22
|
+
|
|
23
|
+
1. [Summary](#summary)
|
|
24
|
+
2. [The comparison set](#the-comparison-set)
|
|
25
|
+
3. [Stated goals and scope](#stated-goals-and-scope)
|
|
26
|
+
4. [Where state lives](#where-state-lives)
|
|
27
|
+
5. [Rendering](#rendering)
|
|
28
|
+
6. [Templates and markup](#templates-and-markup)
|
|
29
|
+
7. [Components and widgets](#components-and-widgets)
|
|
30
|
+
8. [Data on the wire](#data-on-the-wire)
|
|
31
|
+
9. [Mutations, forms and request state](#mutations-forms-and-request-state)
|
|
32
|
+
10. [Routing and navigation](#routing-and-navigation)
|
|
33
|
+
11. [Build, tooling and checking](#build-tooling-and-checking)
|
|
34
|
+
12. [CSS](#css)
|
|
35
|
+
13. [The server](#the-server)
|
|
36
|
+
14. [Performance profile](#performance-profile)
|
|
37
|
+
15. [Surface area](#surface-area)
|
|
38
|
+
16. [Accessibility](#accessibility)
|
|
39
|
+
17. [Testing](#testing)
|
|
40
|
+
18. [Release history and churn](#release-history-and-churn)
|
|
41
|
+
19. [What Loadbare/app does not do](#what-loadbareapp-does-not-do)
|
|
42
|
+
20. [Requirements and fit](#requirements-and-fit)
|
|
43
|
+
21. [Appendix A: Matrix](#appendix-a-matrix)
|
|
44
|
+
22. [Appendix B: One page, three ways](#appendix-b-one-page-three-ways)
|
|
45
|
+
23. [Appendix C: Glossary of nearest equivalents](#appendix-c-glossary-of-nearest-equivalents)
|
|
46
|
+
|
|
47
|
+
## Summary
|
|
48
|
+
|
|
49
|
+
Loadbare/app is a framework for server-bound database applications. The
|
|
50
|
+
browser receives one HTML document containing every page as a `<template>`,
|
|
51
|
+
one script and one stylesheet. After that, only JSON rows cross the wire.
|
|
52
|
+
HTML attributes bind elements to named server queries, and one attribute,
|
|
53
|
+
`lb-action`, turns a click or a submit into a request. The server answers
|
|
54
|
+
every request with rows, sets of rows, or patches to sets of rows, and the
|
|
55
|
+
browser-side runtime, called the hub, writes those values into the elements
|
|
56
|
+
bound to them.
|
|
57
|
+
|
|
58
|
+
Compared with the client component frameworks (React, Vue, Angular, Svelte,
|
|
59
|
+
Solid), Loadbare/app has no component tree rendered at run time, no
|
|
60
|
+
client-side state, no virtual DOM or reactivity system, and no runtime
|
|
61
|
+
template expressions.
|
|
62
|
+
|
|
63
|
+
Compared with the server-integrated meta-frameworks (Next.js, Nuxt,
|
|
64
|
+
SvelteKit, React Router 7, Astro), Loadbare/app has no server rendering and
|
|
65
|
+
no hydration, ships no server of its own, and has no route parameters.
|
|
66
|
+
|
|
67
|
+
Compared with the hypermedia libraries (htmx, Hotwire Turbo, Unpoly),
|
|
68
|
+
Loadbare/app sends JSON rather than HTML after the first load, requires a
|
|
69
|
+
build step, and requires no endpoint design.
|
|
70
|
+
|
|
71
|
+
Compared with server-driven stateful UI (Phoenix LiveView, Laravel Livewire,
|
|
72
|
+
Blazor Server), Loadbare/app keeps no per-client process or component state
|
|
73
|
+
on the server, and uses ordinary HTTP requests rather than a persistent
|
|
74
|
+
connection.
|
|
75
|
+
|
|
76
|
+
| Group | Rendering happens | State lives | After first load, the wire carries |
|
|
77
|
+
|--------------------------|-----------------------|------------------------------|------------------------------------|
|
|
78
|
+
| Loadbare/app | Build time, once | Server database, and the DOM | JSON rows |
|
|
79
|
+
| Client component | Browser, on change | Browser memory | JSON (application-designed) |
|
|
80
|
+
| Meta-frameworks | Server, then browser | Server and browser memory | JSON, RSC payloads, or HTML |
|
|
81
|
+
| Hypermedia | Server, per request | Server, and the DOM | HTML |
|
|
82
|
+
| Server-driven stateful | Server, per event | Server memory per client | DOM diffs or HTML |
|
|
83
|
+
|
|
84
|
+
## The comparison set
|
|
85
|
+
|
|
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 |
|
|
93
|
+
|
|
94
|
+
React Router 7 is the continuation of Remix. TanStack Query appears below
|
|
95
|
+
where client-side server-state caching is discussed, because React
|
|
96
|
+
applications commonly pair it with a data API.
|
|
97
|
+
|
|
98
|
+
## Stated goals and scope
|
|
99
|
+
|
|
100
|
+
Theory states two goals, in order.
|
|
101
|
+
|
|
102
|
+
1. Performance. A full round trip, from user action through durable
|
|
103
|
+
persistence to final paint, should complete in a median of 300ms.
|
|
104
|
+
Assuming roughly 200ms of wire time leaves 100ms for the database, the
|
|
105
|
+
application server and the browser together, so the framework's own
|
|
106
|
+
processing time in the browser and on the server should approach zero.
|
|
107
|
+
2. Developer efficiency, defined as reducing accidental complexity (Brooks):
|
|
108
|
+
work demanded by the tools rather than by the problem.
|
|
109
|
+
|
|
110
|
+
The scope follows from a hypothesis stated in Theory: in a data-oriented
|
|
111
|
+
application, the set of DOM changes needed is closed and knowable, so a
|
|
112
|
+
framework optimized for that set does not need the machinery required to
|
|
113
|
+
support arbitrary DOM updates.
|
|
114
|
+
|
|
115
|
+
Loadbare/app restricts itself to applications whose data can be sent as rows
|
|
116
|
+
and sets of rows. Theory states that the claims about accidental complexity
|
|
117
|
+
are most likely to hold "when the existing stack can support an application
|
|
118
|
+
that wants data to be rows and tables, or easily mapped into them and out of
|
|
119
|
+
them, and the desired affordances can be easily mapped to the standard set of
|
|
120
|
+
operations against that data."
|
|
121
|
+
|
|
122
|
+
Each of the other projects in the comparison set presents itself as a
|
|
123
|
+
general-purpose tool for building web user interfaces, and none restricts the
|
|
124
|
+
shape of the data an application works with.
|
|
125
|
+
|
|
126
|
+
## Where state lives
|
|
127
|
+
|
|
128
|
+
### Loadbare/app
|
|
129
|
+
|
|
130
|
+
The hub holds no application data. There is no store, no signal, no
|
|
131
|
+
observable, and no client cache of query results.
|
|
132
|
+
|
|
133
|
+
- A value that has landed exists in the DOM: as `textContent` or `value`, and
|
|
134
|
+
as the `lb-value` attribute.
|
|
135
|
+
- A list exists as the DOM rows the hub cloned, each stamped with
|
|
136
|
+
`lb-key-value`.
|
|
137
|
+
- An element hidden by `lb-show` exists inside a `<template>` standing where
|
|
138
|
+
it stood.
|
|
139
|
+
- The hub keeps two pieces of state of its own: the name of the current page,
|
|
140
|
+
and, until an insert completes, a record of which controls it read.
|
|
141
|
+
|
|
142
|
+
The server is the source of truth for everything else, including view state.
|
|
143
|
+
A query takes no argument from the browser; its only input is `ctx`, which
|
|
144
|
+
the application builds from the Express request. Which record a page shows,
|
|
145
|
+
or which step of a wizard is current, is held on the server where `ctx`
|
|
146
|
+
reaches it. A reload runs the page's queries again and shows the same state.
|
|
147
|
+
|
|
148
|
+
A URL names a page and nothing finer. Whether a page URL may carry
|
|
149
|
+
parameters is an open blocker in TECHREF-1.0.
|
|
150
|
+
|
|
151
|
+
### Elsewhere
|
|
152
|
+
|
|
153
|
+
| Project | Where application state is kept |
|
|
154
|
+
|-------------------|-----------------------------------------------------------------------------------------|
|
|
155
|
+
| 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 |
|
|
156
|
+
| Vue | Reactive proxies (`ref`, `reactive`); Pinia stores |
|
|
157
|
+
| Angular | Signals, services provided through dependency injection, RxJS observables |
|
|
158
|
+
| Svelte | Runes (`$state`, `$derived`); stores |
|
|
159
|
+
| Solid | Signals and stores |
|
|
160
|
+
| Next.js | Server Components read data on the server; Client Components hold React state; the client router caches RSC payloads |
|
|
161
|
+
| React Router 7 | Loader data held by the router in the browser; component state |
|
|
162
|
+
| SvelteKit, Nuxt | Load function or `useFetch` data held in the browser; component state |
|
|
163
|
+
| htmx, Turbo | The server, and the HTML currently in the DOM. Turbo keeps a snapshot cache of visited pages for back navigation |
|
|
164
|
+
| Phoenix LiveView | Socket assigns in a server process per connected client |
|
|
165
|
+
| Livewire | Component properties serialized into a snapshot in the page and sent back with each request |
|
|
166
|
+
| Blazor Server | Component state in server memory, per circuit |
|
|
167
|
+
| Alpine.js | Reactive objects declared with `x-data` |
|
|
168
|
+
|
|
169
|
+
## Rendering
|
|
170
|
+
|
|
171
|
+
### Loadbare/app
|
|
172
|
+
|
|
173
|
+
HTML is produced once, by the builder. Widget expansion, build-time
|
|
174
|
+
parameters, slots and named templates are resolved at build time, and every
|
|
175
|
+
page is placed in the one document as `<template lb-page="name">`. The
|
|
176
|
+
browser parses that document once.
|
|
177
|
+
|
|
178
|
+
At run time the hub performs a closed set of operations:
|
|
179
|
+
|
|
180
|
+
| Trigger | Operation |
|
|
181
|
+
|----------------------------|----------------------------------------------------------------------------|
|
|
182
|
+
| Navigation | Clone the page's `<template>` into `<main>` |
|
|
183
|
+
| A value for a native element | Set `textContent`, and set `lb-value` |
|
|
184
|
+
| A value for a form control | Set `value`, and set `lb-value` |
|
|
185
|
+
| A value for a custom element | Set `lb-value` |
|
|
186
|
+
| A new key in a list | Clone the row template, stamp `lb-key-value`, fill it, insert it |
|
|
187
|
+
| A whole list | Place every row in the order given, remove rows whose keys did not arrive |
|
|
188
|
+
| A patch | Upsert the rows named, remove the keys named, leave other rows in place |
|
|
189
|
+
| After any list result | Stamp `lb-row-count` |
|
|
190
|
+
| An `lb-show` column | Move the element into, or out of, a `<template lb-show>` |
|
|
191
|
+
| A request starts or ends | Set or remove `lb-pending` and `aria-busy`; set `lb-error` on failure |
|
|
192
|
+
| An insert succeeds | Reset the controls it gathered to their defaults |
|
|
193
|
+
|
|
194
|
+
The hub finds targets with `querySelectorAll` on the attributes written in
|
|
195
|
+
the HTML. No tree is compared with a previous tree. A response names what
|
|
196
|
+
changed, so the hub does not determine what changed. Between interactions
|
|
197
|
+
the hub runs no timers, observers or polling loops.
|
|
198
|
+
|
|
199
|
+
There is no server-side rendering and no hydration. The server returns the
|
|
200
|
+
values its queries return, serialized as JSON.
|
|
201
|
+
|
|
202
|
+
### Elsewhere
|
|
203
|
+
|
|
204
|
+
| Project | How the DOM is produced and updated |
|
|
205
|
+
|------------------|---------------------------------------------------------------------------------------|
|
|
206
|
+
| React | Components re-run on state change and return an element tree, which is reconciled against the previous tree; keys identify list items |
|
|
207
|
+
| Vue | Templates compile to render functions producing a virtual DOM; reactivity tracks which components depend on which state |
|
|
208
|
+
| Angular | Templates compile to instructions; change detection checks bindings, and signals mark views that need updating |
|
|
209
|
+
| Svelte | The compiler emits code that updates the DOM nodes bound to changed state |
|
|
210
|
+
| Solid | JSX compiles to DOM creation code; components run once; signals update bound nodes |
|
|
211
|
+
| 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 |
|
|
212
|
+
| Astro | HTML rendered at build or request time; interactive islands are hydrated per `client:*` directive |
|
|
213
|
+
| htmx | The server renders HTML fragments per request; htmx swaps them into a target |
|
|
214
|
+
| Turbo | The server renders pages; Turbo replaces the body, a `<turbo-frame>`, or applies Turbo Stream actions; page refreshes can morph |
|
|
215
|
+
| Phoenix LiveView | The server re-renders changed parts of a template and sends diffs over a WebSocket; the client patches the DOM |
|
|
216
|
+
| Livewire | The server re-renders component HTML; the client morphs the DOM |
|
|
217
|
+
| Blazor Server | The server renders the component tree, computes a diff, and sends it over SignalR |
|
|
218
|
+
| Alpine.js | Directives on server-rendered HTML update bound nodes from reactive data |
|
|
219
|
+
| Lit | Tagged template literals update the dynamic parts of a template, by default inside a shadow root |
|
|
220
|
+
|
|
221
|
+
## Templates and markup
|
|
222
|
+
|
|
223
|
+
### Loadbare/app
|
|
224
|
+
|
|
225
|
+
The HTML written in `.html` files is the HTML that ships, after expansion.
|
|
226
|
+
Attribute values in the `lb-` namespace are names: a query name, a column
|
|
227
|
+
name, or an action name. No attribute value is an expression.
|
|
228
|
+
|
|
229
|
+
There are two kinds of placeholder, and they run at different times.
|
|
230
|
+
|
|
231
|
+
- Build time. A widget definition writes `{{name}}` or `{{name|default}}` as
|
|
232
|
+
a whole attribute value or a whole text node. The tag supplies it with an
|
|
233
|
+
`exp-name` attribute. An unsupplied placeholder with no default drops the
|
|
234
|
+
attribute. The value becomes permanent in the shipped HTML.
|
|
235
|
+
- Run time. Data lands through `lb-cell`, `lb-list` with a keyed
|
|
236
|
+
`<template>`, and `lb-show`. There is no run-time interpolation into
|
|
237
|
+
attribute values or text.
|
|
238
|
+
|
|
239
|
+
Interpolation inside a longer string, such as `title="Hello {{name}}"`, is
|
|
240
|
+
listed as decided against in the [Roadmap](./roadmap.md#decided-against).
|
|
241
|
+
|
|
242
|
+
| Task | Loadbare/app | React (JSX) | Vue | Angular | Svelte | Alpine.js |
|
|
243
|
+
|-------------------|-----------------------------------------------|--------------------------------------|--------------------------------------|---------------------------------------|-------------------------------|--------------------------------------------|
|
|
244
|
+
| Show a value | `<span lb-cell="name">` | `{row.name}` | `{{ row.name }}` | `{{ row.name }}` | `{row.name}` | `<span x-text="row.name">` |
|
|
245
|
+
| Keyed list | `<template lb-key="id">` inside `lb-list` | `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">` |
|
|
246
|
+
| Condition | `lb-show="column"` | `{cond && <X />}` | `v-if`, `v-show` | `@if (cond) { }` | `{#if cond}` | `x-if`, `x-show` |
|
|
247
|
+
| Expression in markup | Not supported | Any JavaScript expression | JavaScript expressions | Template expressions | JavaScript expressions | JavaScript expressions |
|
|
248
|
+
| Build-time constant | `exp-label="Name"` into `{{label}}` | A prop | A prop | An input | A prop | Not applicable |
|
|
249
|
+
|
|
250
|
+
In htmx and Turbo, markup for data is produced by whatever server template
|
|
251
|
+
language the application uses. LiveView uses HEEx, with `{@value}`
|
|
252
|
+
interpolation, `:for` and `:if`.
|
|
253
|
+
|
|
254
|
+
### Conditions
|
|
255
|
+
|
|
256
|
+
`lb-show` names a column. `null` and `false` are off; every other value is
|
|
257
|
+
on, including the string `"false"`. An element that is off is moved into a
|
|
258
|
+
`<template lb-show="column">` standing where it stood, and moved back when the
|
|
259
|
+
column turns on. It is not rendered, focused, clicked, announced or gathered.
|
|
260
|
+
Values keep landing inside the template, so the element returns current. The
|
|
261
|
+
builder ships every `lb-show` element already inside its template.
|
|
262
|
+
|
|
263
|
+
| Framework | A false branch | State kept | Updated while hidden |
|
|
264
|
+
|----------------------------------|-----------------------------------------------------------------------|------------|----------------------|
|
|
265
|
+
| Loadbare/app `lb-show` | Moved into a `<template>` in place | yes | yes |
|
|
266
|
+
| Alpine `<template x-if>` | Destroyed and removed; recloned on show | no | no |
|
|
267
|
+
| Knockout `if` | Contents re-rendered on show | no | no |
|
|
268
|
+
| Angular `@if` / `*ngIf` | View destroyed unless detached and reinserted by hand | if detached | no |
|
|
269
|
+
| Aurelia `if.bind` | Removed; views cached and reused by default | yes | not documented |
|
|
270
|
+
| Vue `v-if` in `<KeepAlive>` | Detached, not unmounted | yes | no |
|
|
271
|
+
| Lit `cache()` | DOM kept in a `DocumentFragment` | yes | on swap-in |
|
|
272
|
+
| Polymer `dom-if` | `display: none` | yes | yes |
|
|
273
|
+
| React `<Activity mode="hidden">` | `display: none`, Effects destroyed | yes | yes, at lower priority |
|
|
274
|
+
|
|
275
|
+
Sources for the rows other than Loadbare/app are in
|
|
276
|
+
[Prior art](./prior-art.md#conditional-rendering-elsewhere).
|
|
277
|
+
|
|
278
|
+
A condition that is not data, such as an open menu, has no column. The
|
|
279
|
+
reference directs it to `<details>`, a stylesheet, or a widget.
|
|
280
|
+
|
|
281
|
+
## Components and widgets
|
|
282
|
+
|
|
283
|
+
### Loadbare/app
|
|
284
|
+
|
|
285
|
+
A widget is a custom element defined by one or two files sharing its tag
|
|
286
|
+
name, found by the builder anywhere in the source tree:
|
|
287
|
+
|
|
288
|
+
| File | Holds |
|
|
289
|
+
|-------------------------|-----------------------------------------------------|
|
|
290
|
+
| `<tag-name>.html` | Markup the tag expands into, at build time |
|
|
291
|
+
| `<tag-name>.browser.ts` | A class registered with `customElements.define` |
|
|
292
|
+
|
|
293
|
+
A tag with neither file is a build error. A module named `<tag-name>.ts`
|
|
294
|
+
without `.browser` is not bundled, and the build error names it.
|
|
295
|
+
|
|
296
|
+
- Light DOM throughout. Loadbare/app uses no shadow DOM.
|
|
297
|
+
- No base class. A widget extends `HTMLElement`.
|
|
298
|
+
- No props at run time. Build-time parameters arrive as `exp-` attributes.
|
|
299
|
+
Data arrives as the `lb-value` attribute, which the widget observes through
|
|
300
|
+
`attributeChangedCallback`. The browser calls that callback for an
|
|
301
|
+
attribute already present at upgrade, so first render and refresh are one
|
|
302
|
+
code path.
|
|
303
|
+
- Composition at build time. `lb-slot` marks where authored content goes;
|
|
304
|
+
`lb-template` names additional destinations filled by
|
|
305
|
+
`<template lb-template="name">`. A definition may use other widgets; a
|
|
306
|
+
cycle is a build error.
|
|
307
|
+
- A widget sends a request by dispatching a bubbling `lb-request`
|
|
308
|
+
`CustomEvent`. The hub adds the scope (`list` or `row`, `key`, `cell`). An
|
|
309
|
+
ancestor may stop the event.
|
|
310
|
+
- A list widget may implement `lbPlaceRow(el, row, template)` to decide where
|
|
311
|
+
a row goes, and `lbRowsLanded()` to build scaffolding such as group
|
|
312
|
+
headings. The hub does the cloning, matching, removal and counting.
|
|
313
|
+
- Method names beginning with `lb` on a custom element are reserved.
|
|
314
|
+
|
|
315
|
+
Widget libraries are npm packages listed in `imports.ts`. The builder scans
|
|
316
|
+
each listed package for `<tag>.html` and `<tag>.browser.js` files, in the
|
|
317
|
+
directory named by `loadbare.widgets` in its `package.json`, or the whole
|
|
318
|
+
package when that key is absent. Only tags used in the chrome or a page are
|
|
319
|
+
bundled. `@loadbare/widgets` ships `lb-input`, `lb-select`, `lb-options`,
|
|
320
|
+
`lb-table`, `lb-picker` and `lb-unknown-page`.
|
|
321
|
+
|
|
322
|
+
### Using web components and other libraries
|
|
323
|
+
|
|
324
|
+
The following follow from the builder and hub code.
|
|
325
|
+
|
|
326
|
+
- A third-party custom element used in markup must have a script or a
|
|
327
|
+
definition in some origin, or the build fails. A package that does not name
|
|
328
|
+
its files `<tag>.browser.js` can be used by writing `<tag>.browser.ts` in the
|
|
329
|
+
application's own tree that imports the package's module.
|
|
330
|
+
- A `.browser.ts` file is bundled by esbuild and may import any package
|
|
331
|
+
esbuild resolves. A custom element may mount another framework's component
|
|
332
|
+
tree inside itself.
|
|
333
|
+
- The hub does not turn a click on a hyphenated element into a request. A
|
|
334
|
+
third-party element carrying `lb-action` sends nothing unless it dispatches
|
|
335
|
+
`lb-request` itself.
|
|
336
|
+
- A bound value lands on a third-party element as `lb-value`. The element
|
|
337
|
+
displays it only if it observes that attribute.
|
|
338
|
+
- Gathering for `lb-row-insert` and `lb-row-update` reads `<input>`, `<select>`
|
|
339
|
+
and `<textarea>` elements found with `querySelectorAll`, which does not
|
|
340
|
+
enter a shadow root.
|
|
341
|
+
|
|
342
|
+
### Elsewhere
|
|
343
|
+
|
|
344
|
+
| Project | Unit | Inputs | Children | Style scope |
|
|
345
|
+
|-------------|------------------------------------|-------------------------------|-----------------------------------|-----------------------------|
|
|
346
|
+
| React | Function returning JSX | Props | `children` prop, render props | None built in |
|
|
347
|
+
| Vue | Single-file component (`.vue`) | Props, emits | Default and named slots | `<style scoped>` available |
|
|
348
|
+
| Angular | Class with `@Component` decorator | Inputs, outputs, signals | Content projection (`ng-content`) | Emulated encapsulation by default |
|
|
349
|
+
| Svelte | `.svelte` file | `$props()` | Snippets | Scoped by default |
|
|
350
|
+
| Solid | Function returning JSX, run once | Props | `children` | None built in |
|
|
351
|
+
| Lit | `LitElement` subclass | Reactive properties, attributes | `<slot>` in shadow DOM | Shadow DOM by default |
|
|
352
|
+
| Stimulus | Controller class attached by `data-controller` | Values, targets | Server-rendered HTML | None |
|
|
353
|
+
| LiveView | Function components and LiveComponents | Assigns | Slots | None built in |
|
|
354
|
+
|
|
355
|
+
React 19 added full support for custom elements, including setting
|
|
356
|
+
properties, so web components can be used in React applications directly.
|
|
357
|
+
Component libraries written for one framework (for example MUI for React or
|
|
358
|
+
Vuetify for Vue) are not usable in another without a wrapper.
|
|
359
|
+
|
|
360
|
+
## Data on the wire
|
|
361
|
+
|
|
362
|
+
### Loadbare/app
|
|
363
|
+
|
|
364
|
+
Every request from the hub is `POST /lb?page=<name>` with a JSON body.
|
|
365
|
+
|
|
366
|
+
- An empty body asks for the page's whole query set. The server runs the
|
|
367
|
+
page's `onPageEnter`, then every query the page declares.
|
|
368
|
+
- A body with an `action` runs that action and its refresh set.
|
|
369
|
+
- A request without an action answers 400. A `run` that throws answers 500.
|
|
370
|
+
|
|
371
|
+
Every response has one shape: an object keyed by query name.
|
|
372
|
+
|
|
373
|
+
| Value | Means |
|
|
374
|
+
|------------------|-----------------------------------------------------|
|
|
375
|
+
| An object | One row, for a name declared with `row()` |
|
|
376
|
+
| An array | The whole set, and its order, for a `list()` |
|
|
377
|
+
| `{ rows, drop }` | A patch to a `list()`: upsert these, remove these keys |
|
|
378
|
+
|
|
379
|
+
A cell holds one value. A set of values is a list of its own. A row never
|
|
380
|
+
contains rows. Master-detail is a row and a list answered under two names.
|
|
381
|
+
Many masters with their details is one list of joined rows, grouped for
|
|
382
|
+
display by a widget's `lbPlaceRow`.
|
|
383
|
+
|
|
384
|
+
The application designs no endpoints, routes or serialization format. The
|
|
385
|
+
names in the markup are the keys in the page's `.queries.ts` and
|
|
386
|
+
`.requests.ts` files. The browser can reach only the queries, actions and
|
|
387
|
+
CRUD operations a page declares; any other name answers `{}` and logs a
|
|
388
|
+
server warning.
|
|
389
|
+
|
|
390
|
+
Values are not converted. The hub hands each value to the browser as the
|
|
391
|
+
query produced it. Formatting a date or an amount is done in the query.
|
|
392
|
+
Data types are an open blocker in TECHREF-1.0.
|
|
393
|
+
|
|
394
|
+
### Elsewhere
|
|
395
|
+
|
|
396
|
+
| Project | Data requests after first load |
|
|
397
|
+
|-------------------|---------------------------------------------------------------------------------|
|
|
398
|
+
| React, Vue, Angular, Svelte, Solid (client only) | Whatever the application designs: REST, GraphQL, tRPC, or other |
|
|
399
|
+
| Next.js | Server Functions as POST requests identified by a `Next-Action` header; navigations fetch RSC payloads |
|
|
400
|
+
| React Router 7 | Loader data fetched by `.data` requests; actions posted to the route URL |
|
|
401
|
+
| SvelteKit | Load data from `__data.json`; form actions posted to `?/actionName` |
|
|
402
|
+
| Nuxt | Server routes under `server/api` (Nitro), fetched with `useFetch` or `$fetch` |
|
|
403
|
+
| htmx | Any HTTP verb to any URL named in `hx-get`, `hx-post` and related attributes; HTML response |
|
|
404
|
+
| Turbo | Ordinary page and form requests; HTML responses; Turbo Stream responses |
|
|
405
|
+
| Phoenix LiveView | Events and diffs over a WebSocket |
|
|
406
|
+
| Livewire | POST requests carrying component snapshots and calls |
|
|
407
|
+
| Blazor Server | SignalR messages |
|
|
408
|
+
|
|
409
|
+
## Mutations, forms and request state
|
|
410
|
+
|
|
411
|
+
### Loadbare/app
|
|
412
|
+
|
|
413
|
+
`lb-action` names what an interaction asks for. A value beginning with `lb-`
|
|
414
|
+
is one of three reserved operations; any other value is an action the page
|
|
415
|
+
declares.
|
|
416
|
+
|
|
417
|
+
| `lb-action` | Written on | Carries | Server key under `crud` |
|
|
418
|
+
|------------------|---------------------------------------------|---------------------------------|-------------------------|
|
|
419
|
+
| `lb-row-insert` | A `<form>`, or a button in a `<tr>`, in a list | `list`, `values` | `rowInsert` |
|
|
420
|
+
| `lb-row-update` | A `<form>`, a button in a live row, or a widget cell | `list`, `key`, `values` | `rowUpdate` |
|
|
421
|
+
| `lb-row-delete` | Anything inside a live row | `list`, `key` | `rowDelete` |
|
|
422
|
+
| Any other name | Any element | Scope in effect, and a widget's `value` | Under `actions` |
|
|
423
|
+
|
|
424
|
+
- Position comes from the document. `list` or `row` comes from the nearest
|
|
425
|
+
ancestor scope, `key` from the nearest live row, `cell` from the element's
|
|
426
|
+
own `lb-cell`. The author writes the action and not the position.
|
|
427
|
+
- Values are gathered from the nearest `<form>`, `<tr>` or live row around
|
|
428
|
+
the element, the way a button submits its form owner. A widget carrying
|
|
429
|
+
`lb-cell` sends its own cell alone, so one `rowUpdate` answers a form and a
|
|
430
|
+
per-field edit.
|
|
431
|
+
- A native element sends on click, a form on submit. A widget sends its own.
|
|
432
|
+
- A declared action carries no argument list.
|
|
433
|
+
- A single-row scope is read-only for the three operations, because each needs
|
|
434
|
+
a key and a key exists only on a live row in a list.
|
|
435
|
+
|
|
436
|
+
On the server, each entry has `run` and `refresh`. `run` does the work and
|
|
437
|
+
may return results, such as a `patch()`. `refresh` names queries to run
|
|
438
|
+
again. What `run` returns is laid over the refreshed results.
|
|
439
|
+
|
|
440
|
+
The hub stamps the dispatching element:
|
|
441
|
+
|
|
442
|
+
| Attribute | Means |
|
|
443
|
+
|----------------|------------------------------------------------------------|
|
|
444
|
+
| `lb-pending` | The round trip is in flight; `aria-busy="true"` is set with it |
|
|
445
|
+
| `lb-error` | The last round trip from this element failed |
|
|
446
|
+
|
|
447
|
+
A native button or form performed again while it carries `lb-pending` is
|
|
448
|
+
ignored. A widget is not held back. A round trip has a ten-second deadline,
|
|
449
|
+
after which it is aborted and treated as a failure. `lb-error` records that
|
|
450
|
+
a round trip failed, and carries no message.
|
|
451
|
+
|
|
452
|
+
After a successful `lb-row-insert`, the hub resets each control it gathered
|
|
453
|
+
to its default, as `form.reset()` does, unless the control was edited during
|
|
454
|
+
the round trip. A failed insert resets nothing.
|
|
455
|
+
|
|
456
|
+
The browser displays only values the server returned. There is no
|
|
457
|
+
optimistic update. Per-keystroke validation is an open question in the
|
|
458
|
+
roadmap.
|
|
459
|
+
|
|
460
|
+
### Elsewhere
|
|
461
|
+
|
|
462
|
+
| Project | Sending a mutation | Refreshing afterward | Pending state | Optimistic update |
|
|
463
|
+
|------------------|------------------------------------------------------|-------------------------------------------------------------|----------------------------------------|---------------------------|
|
|
464
|
+
| React | `<form action={fn}>`, event handlers | Application code | `useActionState`, `useFormStatus`, `useTransition` | `useOptimistic` |
|
|
465
|
+
| TanStack Query | `useMutation` | `invalidateQueries` by query key | `isPending` | `onMutate` |
|
|
466
|
+
| Next.js | Server Functions | `revalidatePath`, `revalidateTag` in the function | React hooks | `useOptimistic` |
|
|
467
|
+
| 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 |
|
|
468
|
+
| 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 |
|
|
469
|
+
| Angular | Event bindings, `HttpClient` | Application code | Application code | Application code |
|
|
470
|
+
| 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 |
|
|
471
|
+
| 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 |
|
|
472
|
+
| Phoenix LiveView | `phx-click`, `phx-submit`, `phx-change` | Assign changes re-render | `phx-click-loading` and related classes, `phx-disable-with` | `JS` commands |
|
|
473
|
+
| Livewire | `wire:click`, `wire:submit`, `wire:model` | Component re-render | `wire:loading` | Not built in |
|
|
474
|
+
|
|
475
|
+
## Routing and navigation
|
|
476
|
+
|
|
477
|
+
### Loadbare/app
|
|
478
|
+
|
|
479
|
+
- A page is `<name>.page.html`, found anywhere under `--src`. The directory
|
|
480
|
+
carries no meaning; page names are unique across the application.
|
|
481
|
+
- The path `/members` names the page `members`. The path `/` names `index`.
|
|
482
|
+
- There are no route parameters, nested routes or per-page layouts. The
|
|
483
|
+
chrome, `chrome.html`, is the one document surrounding every page.
|
|
484
|
+
- An anchor carrying `lb-nav-link` navigates inside the application: the hub
|
|
485
|
+
calls `history.pushState`, clones the page template into `<main>`, then
|
|
486
|
+
fetches the page's data. An anchor without the attribute is an ordinary
|
|
487
|
+
link. `popstate` navigates back and forward.
|
|
488
|
+
- Navigation fetches no HTML and no code. Every page's markup is already in
|
|
489
|
+
the document.
|
|
490
|
+
- The server answers every GET with the same `app.html` and status 200. A
|
|
491
|
+
path that names no page is detected in the browser, which opens a
|
|
492
|
+
`<dialog lb-unknown-page>` if the chrome has one.
|
|
493
|
+
- The hub publishes its own row, `lb-navigation`, with `page-uri` and
|
|
494
|
+
`page-label` (the text of the matching `lb-nav-link` anchor), bound like
|
|
495
|
+
any server row.
|
|
496
|
+
- The hub reads `location.pathname` only. A query string does not reach the
|
|
497
|
+
server.
|
|
498
|
+
- The application must be served from the origin root. The hub reaches
|
|
499
|
+
`/lb`, `/client.js` and `/app.css` by absolute path.
|
|
500
|
+
- The hub does not change `document.title`, move focus, or restore scroll
|
|
501
|
+
position on navigation.
|
|
502
|
+
|
|
503
|
+
### Elsewhere
|
|
504
|
+
|
|
505
|
+
| Project | Routes declared by | Parameters | Layouts | Unknown path |
|
|
506
|
+
|------------------|-----------------------------------------------------|-------------------------------|-----------------------------|------------------------|
|
|
507
|
+
| Next.js (App Router) | Directories under `app/` with `page.tsx` | `[id]`, `[...slug]` segments | Nested `layout.tsx` | `not-found.tsx`, status 404 from the server |
|
|
508
|
+
| Nuxt | Files under `pages/` | `[id]` segments | `layouts/`, nested routes | Error page, status 404 |
|
|
509
|
+
| SvelteKit | Directories under `src/routes/` with `+page.svelte` | `[id]` segments | Nested `+layout.svelte` | `+error.svelte`, status 404 |
|
|
510
|
+
| React Router 7 | Route config or file conventions | `:id` segments | Nested routes with `<Outlet>` | Catch-all route |
|
|
511
|
+
| Angular | `Routes` array | `:id` segments | Nested router outlets | `**` wildcard route |
|
|
512
|
+
| Vue Router | Routes array | `:id` segments | Nested `<RouterView>` | Catch-all route |
|
|
513
|
+
| Phoenix LiveView | `live` routes in the router | `:id` segments, `handle_params` | Root and app layouts | Status 404 from the server |
|
|
514
|
+
| htmx, Turbo | Server routes | Server routes | Server templates | Server status |
|
|
515
|
+
|
|
516
|
+
A client-routed single-page application served by a catch-all route also
|
|
517
|
+
answers 200 for an unknown path, as Loadbare/app does.
|
|
518
|
+
|
|
519
|
+
## Build, tooling and checking
|
|
520
|
+
|
|
521
|
+
### Loadbare/app
|
|
522
|
+
|
|
523
|
+
```
|
|
524
|
+
loadbare-app-build [--src src] [--out dist] [--watch] [--minify]
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
- Four flags. No configuration file. An open blocker proposes one.
|
|
528
|
+
- Files are classified by name: `chrome.html`, `*.page.html`,
|
|
529
|
+
`*.queries.ts`, `*.requests.ts`, `imports.ts`, `*.css`, `<tag>.html`,
|
|
530
|
+
`<tag>.browser.ts`.
|
|
531
|
+
- Origins are scanned in order: `@loadbare/app` itself (which supplies
|
|
532
|
+
`lb-hub`), each package in `imports.ts`, then `--src`. A later origin
|
|
533
|
+
overrides an earlier one for the same tag.
|
|
534
|
+
- The client bundle is built by esbuild from a generated entry that imports
|
|
535
|
+
the script of every custom element left in the assembled document. A
|
|
536
|
+
widget script whose tag is never used is never imported.
|
|
537
|
+
- Outputs: `app.html`, `client.js`, `app.css` when there is a stylesheet,
|
|
538
|
+
`pages.ts` when there is page data, and `client-entry.ts`.
|
|
539
|
+
- `--watch` rebuilds on changes to `.html`, `.ts` and `.css` files. There is
|
|
540
|
+
no dev server, no hot module replacement and no browser reload.
|
|
541
|
+
- `app.html` is formatted with Prettier and never minified.
|
|
542
|
+
|
|
543
|
+
Checked at build time, as errors:
|
|
544
|
+
|
|
545
|
+
- a tag with neither a definition nor a script
|
|
546
|
+
- a module named for a tag without `.browser`
|
|
547
|
+
- two files claiming one tag in one origin
|
|
548
|
+
- a cycle among definitions
|
|
549
|
+
- an `exp-` attribute the definition does not declare, and a placeholder name
|
|
550
|
+
containing a capital
|
|
551
|
+
- slot and template errors: two slots, two templates of one name, a template
|
|
552
|
+
naming a missing destination, content in a definition with no slot
|
|
553
|
+
- `lb-cell` outside every scope, `lb-key` on anything but a list's row
|
|
554
|
+
template, `lb-list` and `lb-row` on one element
|
|
555
|
+
- `lb-show` on a `<template>`, on a row template's root, or with no row
|
|
556
|
+
around it
|
|
557
|
+
- `lb-unknown-page` on anything but a `<dialog>` inside `<lb-hub>`
|
|
558
|
+
- a `.queries.ts` or `.requests.ts` with no matching page
|
|
559
|
+
|
|
560
|
+
Checked when the server starts: a query or action name beginning with `lb-`.
|
|
561
|
+
|
|
562
|
+
Not checked at build time: that an `lb-list` or `lb-row` name matches a
|
|
563
|
+
declared query, that an `lb-action` matches a declared action or CRUD entry,
|
|
564
|
+
that an `lb-` attribute is one Loadbare defines, that the chrome contains one
|
|
565
|
+
`<lb-hub>` and one empty `<main>`. The first two are a roadmap item; the
|
|
566
|
+
last two are listed under the builder blockers. A language server is a
|
|
567
|
+
roadmap item.
|
|
568
|
+
|
|
569
|
+
### Elsewhere
|
|
570
|
+
|
|
571
|
+
| Project | Build and dev tooling | Template type checking | Editor tooling |
|
|
572
|
+
|------------------|---------------------------------------------------------|-----------------------------------------------|---------------------------------|
|
|
573
|
+
| React | Chosen by the application or its framework; Create React App was deprecated in February 2025 | TypeScript checks JSX | TypeScript language service |
|
|
574
|
+
| Next.js | Turbopack or webpack; dev server with Fast Refresh | TypeScript; typed routes | TypeScript plugin |
|
|
575
|
+
| Vue, Nuxt | Vite; dev server with HMR | `vue-tsc` | Vue language tools (Volar) |
|
|
576
|
+
| Angular | Angular CLI (esbuild, Vite dev server) | Strict template type checking in the compiler | Angular Language Service |
|
|
577
|
+
| Svelte, SvelteKit| Vite; dev server with HMR | `svelte-check` | Svelte language server |
|
|
578
|
+
| Solid | Vite | TypeScript checks JSX | TypeScript language service |
|
|
579
|
+
| React Router 7 | Vite | TypeScript; generated route types | TypeScript language service |
|
|
580
|
+
| Astro | Vite | `astro check` | Astro language tools |
|
|
581
|
+
| htmx, Alpine.js | None required; a script tag | None | Community extensions |
|
|
582
|
+
| Hotwire (Rails) | Import maps or a JavaScript bundler | None for Turbo attributes | Community extensions |
|
|
583
|
+
| Phoenix | esbuild and Tailwind through Mix | Compile-time warnings for declared component attributes | Elixir language server |
|
|
584
|
+
|
|
585
|
+
## CSS
|
|
586
|
+
|
|
587
|
+
### Loadbare/app
|
|
588
|
+
|
|
589
|
+
The builder concatenates every `.css` file from every origin into `app.css`.
|
|
590
|
+
Nothing is scoped, renamed, added or removed.
|
|
591
|
+
|
|
592
|
+
- Order: each package in `imports.ts`, in first-mention order, then the
|
|
593
|
+
application's own `--src`. Within an origin, files sort by filename, with
|
|
594
|
+
the full path breaking a tie. Directories do not affect the order.
|
|
595
|
+
- Stylesheets are paired with nothing. Every stylesheet in every listed
|
|
596
|
+
package ships whether or not its widgets are used. Pairing a stylesheet
|
|
597
|
+
with a tag is a roadmap item.
|
|
598
|
+
- Light DOM means every selector reaches every element, in the chrome and in
|
|
599
|
+
widgets.
|
|
600
|
+
- The hub stamps attributes a stylesheet can select on: `lb-value`,
|
|
601
|
+
`lb-pending`, `lb-error`, `lb-row-count`. An empty list is styled with
|
|
602
|
+
`[lb-row-count="0"]`.
|
|
603
|
+
|
|
604
|
+
### Elsewhere
|
|
605
|
+
|
|
606
|
+
| Project | Built-in scoping |
|
|
607
|
+
|-------------|---------------------------------------------------------------------------|
|
|
608
|
+
| React | None; applications choose CSS Modules, CSS-in-JS, Tailwind, or global CSS |
|
|
609
|
+
| Next.js | CSS Modules, global CSS, Sass and Tailwind supported by the build |
|
|
610
|
+
| Vue | `<style scoped>` adds a data attribute per component; CSS Modules |
|
|
611
|
+
| Angular | Emulated encapsulation adds attributes per component by default; shadow DOM and none available |
|
|
612
|
+
| Svelte | Component styles scoped by a generated class by default |
|
|
613
|
+
| Lit | Shadow DOM scoping by default |
|
|
614
|
+
| htmx, Turbo, Alpine.js | None |
|
|
615
|
+
| Phoenix | None built in; Tailwind configured by default in new projects |
|
|
616
|
+
|
|
617
|
+
## The server
|
|
618
|
+
|
|
619
|
+
### Loadbare/app
|
|
620
|
+
|
|
621
|
+
Loadbare/app ships no server. It ships:
|
|
622
|
+
|
|
623
|
+
- `createHub(pages)`, an engine with no HTTP code, in `@loadbare/app/server`
|
|
624
|
+
- `hubRoutes(hub, contextFor)`, an Express 5 router, in
|
|
625
|
+
`@loadbare/app/express`. Express is an optional peer dependency.
|
|
626
|
+
- `pages.ts`, generated by the builder, which imports every page's queries
|
|
627
|
+
and requests and exports `hub`
|
|
628
|
+
|
|
629
|
+
The application's server handles four things:
|
|
630
|
+
|
|
631
|
+
```ts
|
|
632
|
+
app.use(hubRoutes(hub, contextFor));
|
|
633
|
+
app.get("/client.js", (_req, res) => res.sendFile(path.join(DIST, "client.js")));
|
|
634
|
+
app.get("/app.css", (_req, res) => res.sendFile(path.join(DIST, "app.css")));
|
|
635
|
+
app.get(/.*/, (_req, res) => res.sendFile(path.join(DIST, "app.html")));
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
These lines do not change as pages are added.
|
|
639
|
+
|
|
640
|
+
`contextFor(req)` returns `ctx`, which Loadbare passes to every query and
|
|
641
|
+
request and never reads. Its type is declared by the application through
|
|
642
|
+
TypeScript declaration merging on `HubContext`. Authentication, sessions,
|
|
643
|
+
logging, database access, and any ORM or business-logic layer are the
|
|
644
|
+
application's, placed in Express middleware and in `ctx`.
|
|
645
|
+
|
|
646
|
+
A page's server half:
|
|
647
|
+
|
|
648
|
+
| File | Exports | Contents |
|
|
649
|
+
|----------------------|------------|--------------------------------------------------------------|
|
|
650
|
+
| `<name>.queries.ts` | `queries` | Named queries, each built with `row()` or `list()` |
|
|
651
|
+
| `<name>.requests.ts` | `requests` | `onPageEnter`, `actions` by name, `crud` by list name |
|
|
652
|
+
|
|
653
|
+
A query's signature is `(ctx) => Row` or `(ctx) => Row[]`. An answer whose
|
|
654
|
+
shape disagrees with its declaration is dropped with a warning.
|
|
655
|
+
|
|
656
|
+
The data channel carries no CSRF token. Where an options argument for
|
|
657
|
+
hooks, transaction wrapping, error handling and CSRF would go is an open
|
|
658
|
+
blocker. Serving the static files from a separate origin is a roadmap item.
|
|
659
|
+
|
|
660
|
+
### Elsewhere
|
|
661
|
+
|
|
662
|
+
| Project | Server |
|
|
663
|
+
|------------------|------------------------------------------------------------------------------------|
|
|
664
|
+
| React, Vue, Angular, Svelte, Solid (client only) | None; the application supplies an API server |
|
|
665
|
+
| Next.js | Next.js server, or adapters for hosting platforms; Middleware or Proxy; Route Handlers |
|
|
666
|
+
| Nuxt | Nitro server with presets for hosting platforms |
|
|
667
|
+
| SvelteKit | Adapters (Node, static, platform-specific); hooks in `hooks.server.ts` |
|
|
668
|
+
| React Router 7 | Request handler with adapters for Node, Express and platforms |
|
|
669
|
+
| Astro | Static output, or server adapters |
|
|
670
|
+
| htmx | Any server that returns HTML |
|
|
671
|
+
| Hotwire | Any server; integrated with Rails |
|
|
672
|
+
| Phoenix LiveView | Phoenix |
|
|
673
|
+
| Livewire | Laravel |
|
|
674
|
+
| Blazor Server | ASP.NET Core |
|
|
675
|
+
|
|
676
|
+
## Performance profile
|
|
677
|
+
|
|
678
|
+
### Loadbare/app
|
|
679
|
+
|
|
680
|
+
First load of any path:
|
|
681
|
+
|
|
682
|
+
| Request | Returns |
|
|
683
|
+
|--------------------------|--------------------------------------------|
|
|
684
|
+
| `GET /<path>` | `app.html`: the chrome and every page as a `<template>` |
|
|
685
|
+
| `GET /client.js` | The hub and every used widget, one bundle |
|
|
686
|
+
| `GET /app.css` | Every stylesheet, one file |
|
|
687
|
+
| `POST /lb?page=<name>` | The page's query results as JSON |
|
|
688
|
+
|
|
689
|
+
The three static files are fixed at release and can be compressed, cached,
|
|
690
|
+
and served from a CDN. There are no per-route bundles, lazy chunks or module
|
|
691
|
+
waterfalls. The page template is cloned into `<main>` before its data
|
|
692
|
+
request is sent. An optional `<body hidden>` in the chrome keeps the
|
|
693
|
+
document hidden until the first page is in place.
|
|
694
|
+
|
|
695
|
+
Measured on this working tree:
|
|
696
|
+
|
|
697
|
+
| Item | Size |
|
|
698
|
+
|------------------------------------------------|--------------|
|
|
699
|
+
| Hub source, `lb-hub.browser.ts` and `lb-apply.ts` | 929 lines, comments included |
|
|
700
|
+
| Hub alone, esbuild `--minify` | 8,302 bytes |
|
|
701
|
+
| Hub alone, minified then `gzip -9` | 3,377 bytes |
|
|
702
|
+
|
|
703
|
+
Each navigation is one POST. Each interaction is one POST whose response
|
|
704
|
+
carries the refreshed and patched results. On the server, a page's queries
|
|
705
|
+
run one after another in declaration order, then the response is serialized
|
|
706
|
+
as JSON. No HTML is rendered on the server after the build.
|
|
707
|
+
|
|
708
|
+
`app.html` grows with the number and size of pages, and all of it is parsed
|
|
709
|
+
on first load. Lazy loading of page templates is a roadmap item. Theory
|
|
710
|
+
states that performance measurements will be published separately; none are
|
|
711
|
+
published as of this writing.
|
|
712
|
+
|
|
713
|
+
### Elsewhere
|
|
714
|
+
|
|
715
|
+
| Project | First load | Navigation | Interaction |
|
|
716
|
+
|------------------|-------------------------------------------------------------------|------------------------------------------------|------------------------------------------|
|
|
717
|
+
| 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 |
|
|
718
|
+
| 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 |
|
|
719
|
+
| Astro | HTML; JavaScript only for hydrated islands | Full page load, or client router if enabled | Island-local |
|
|
720
|
+
| htmx | Server-rendered HTML and the htmx script | Full page, or `hx-boost` body swap | HTML fragment per request |
|
|
721
|
+
| Turbo | Server-rendered HTML and Turbo | Fetch full page HTML, replace body | Form request and page or stream response |
|
|
722
|
+
| 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 |
|
|
723
|
+
| Blazor Server | Server-rendered HTML, then a SignalR circuit | Over the circuit | Event over the circuit, diff back |
|
|
724
|
+
|
|
725
|
+
## Surface area
|
|
726
|
+
|
|
727
|
+
TECHREF-1.0 lists every name Loadbare/app owns in one cross-reference.
|
|
728
|
+
|
|
729
|
+
| Owned | Count | Names |
|
|
730
|
+
|-------------------------------------|-------|---------------------------------------------------------------|
|
|
731
|
+
| `lb-*` HTML attributes | 16 | `lb-list`, `lb-row`, `lb-cell`, `lb-show`, `lb-key`, `lb-key-value`, `lb-value`, `lb-action`, `lb-nav-link`, `lb-pending`, `lb-error`, `lb-row-count`, `lb-unknown-page`, `lb-slot`, `lb-template`, `lb-page` |
|
|
732
|
+
| Reserved `lb-action` values | 3 | `lb-row-insert`, `lb-row-update`, `lb-row-delete` |
|
|
733
|
+
| `lb*` methods on custom elements | 2 | `lbPlaceRow`, `lbRowsLanded` |
|
|
734
|
+
| Attribute namespace for expansion | 1 | `exp-*` |
|
|
735
|
+
| Reserved tags | 1 | `<lb-hub>` |
|
|
736
|
+
| Reserved events | 1 | `lb-request` |
|
|
737
|
+
| Synthetic rows | 1 | `lb-navigation` |
|
|
738
|
+
| Builder flags | 4 | `--src`, `--out`, `--watch`, `--minify` |
|
|
739
|
+
| Reserved file names and patterns | 8 | `chrome.html`, `imports.ts`, `*.page.html`, `*.queries.ts`, `*.requests.ts`, `<tag>.html`, `<tag>.browser.ts`, `*.css` |
|
|
740
|
+
| Reserved `package.json` keys | 1 | `loadbare.widgets` |
|
|
741
|
+
| Server functions | 5 | `row`, `list`, `patch`, `createHub`, `hubRoutes` |
|
|
742
|
+
| Browser exports | 2 | `applyRow`, `applyData` |
|
|
743
|
+
|
|
744
|
+
The technical reference, TECHREF-1.0.md, is 1,181 lines. Theory states that
|
|
745
|
+
the vocabulary fits in an LLM's context window, and discloses that Anthropic
|
|
746
|
+
models writing `.requests.ts` files consistently refresh a list query after
|
|
747
|
+
returning a patch for it.
|
|
748
|
+
|
|
749
|
+
The other projects in the comparison set do not publish a comparable
|
|
750
|
+
cross-reference, so no count is given for them here.
|
|
751
|
+
|
|
752
|
+
## Accessibility
|
|
753
|
+
|
|
754
|
+
### Loadbare/app
|
|
755
|
+
|
|
756
|
+
- Pages and widgets are native HTML in light DOM. Loadbare/app adds no ARIA
|
|
757
|
+
roles and no elements of its own except `<lb-hub>`, `<template>` for
|
|
758
|
+
absent branches, and the custom elements an application writes.
|
|
759
|
+
- `aria-busy="true"` is set and removed with `lb-pending`.
|
|
760
|
+
- An element hidden by `lb-show` is inside template content, so it is not in
|
|
761
|
+
the accessibility tree.
|
|
762
|
+
- A widget's markup is in the document and in view-source.
|
|
763
|
+
- On navigation the hub does not move focus, announce the new page, or change
|
|
764
|
+
`document.title`.
|
|
765
|
+
- Checkboxes and radio buttons are not implemented for landing or gathering.
|
|
766
|
+
|
|
767
|
+
### Elsewhere
|
|
768
|
+
|
|
769
|
+
Next.js includes a route announcer for client navigation. SvelteKit moves
|
|
770
|
+
focus and announces the page title after client navigation. Nuxt provides a
|
|
771
|
+
`<NuxtRouteAnnouncer>` component for an application to place. React Router
|
|
772
|
+
and Vue Router leave announcements and focus to the application. In shadow
|
|
773
|
+
DOM components (Lit by default), `id` references such as `aria-labelledby`
|
|
774
|
+
and `<label for>` do not cross the shadow root boundary.
|
|
775
|
+
|
|
776
|
+
## Testing
|
|
777
|
+
|
|
778
|
+
### Loadbare/app
|
|
779
|
+
|
|
780
|
+
[Testing](./testing.md) documents how the package tests itself, in four
|
|
781
|
+
tiers, with `node:test` run through `tsx`, and jsdom for the browser tiers.
|
|
782
|
+
No browser test runner is installed.
|
|
783
|
+
|
|
784
|
+
For an application:
|
|
785
|
+
|
|
786
|
+
- A query is `{ kind, run }`, and `run(ctx)` is a function returning rows.
|
|
787
|
+
- A request entry is `{ run, refresh }`, and `run(ctx, where)` is a function.
|
|
788
|
+
- `createHub(pages)` returns an object whose methods take a page name and a
|
|
789
|
+
`ctx`, with no HTTP server.
|
|
790
|
+
- `applyData(root, data)` lands a response on any DOM root.
|
|
791
|
+
- A widget is a custom element.
|
|
792
|
+
|
|
793
|
+
The documentation describes no testing approach for applications.
|
|
794
|
+
|
|
795
|
+
### Elsewhere
|
|
796
|
+
|
|
797
|
+
| Project | Common testing tools |
|
|
798
|
+
|------------------|---------------------------------------------------------------|
|
|
799
|
+
| React | Vitest or Jest with React Testing Library; Playwright |
|
|
800
|
+
| Vue | Vitest with Vue Test Utils; Playwright |
|
|
801
|
+
| Angular | TestBed with Jasmine and Karma, or Jest or Vitest; Playwright |
|
|
802
|
+
| Svelte | Vitest with Svelte Testing Library; Playwright |
|
|
803
|
+
| htmx, Turbo | Server-side tests of HTML responses; Playwright or Capybara |
|
|
804
|
+
| Phoenix LiveView | `Phoenix.LiveViewTest`, which drives a LiveView without a browser |
|
|
805
|
+
|
|
806
|
+
## Release history and churn
|
|
807
|
+
|
|
808
|
+
### Loadbare/app
|
|
809
|
+
|
|
810
|
+
Loadbare/app is at 0.7.3 and has not reached 1.0. TECHREF-1.0 lists items
|
|
811
|
+
to decide before 1.0 because they would be breaking changes afterward.
|
|
812
|
+
Theory states that 1.0 is scoped so that foreseeable features can be added
|
|
813
|
+
without breaking existing code.
|
|
814
|
+
|
|
815
|
+
Application code consists of:
|
|
816
|
+
|
|
817
|
+
| Part | Standard or dependency |
|
|
818
|
+
|------------------|---------------------------------------------------------|
|
|
819
|
+
| Pages and chrome | HTML |
|
|
820
|
+
| Styles | CSS |
|
|
821
|
+
| Widgets | Custom Elements v1 and `<template>` |
|
|
822
|
+
| Server | Express 5 |
|
|
823
|
+
| Page server code | TypeScript functions taking `ctx` |
|
|
824
|
+
| Binding | The `lb-*` vocabulary |
|
|
825
|
+
|
|
826
|
+
The W3C HTML Templates specification was published as a Working Draft in
|
|
827
|
+
2013 (see [Prior art](./prior-art.md)). Autonomous custom elements have been
|
|
828
|
+
supported in every major browser since Chromium-based Edge shipped in January
|
|
829
|
+
2020. Express 5.0 was released in September 2024.
|
|
830
|
+
|
|
831
|
+
Loadbare's history begins with Andromeda in 2003, whose data layer is now
|
|
832
|
+
`@loadbare/db`.
|
|
833
|
+
|
|
834
|
+
### Elsewhere
|
|
835
|
+
|
|
836
|
+
| Date | Change |
|
|
837
|
+
|----------------|---------------------------------------------------------------------------|
|
|
838
|
+
| September 2016 | Angular 2 released, incompatible with AngularJS |
|
|
839
|
+
| February 2019 | React 16.8 introduces Hooks |
|
|
840
|
+
| September 2020 | Vue 3 released, with the Composition API |
|
|
841
|
+
| December 2021 | AngularJS support ends |
|
|
842
|
+
| October 2022 | Next.js 13 introduces the App Router alongside the Pages Router |
|
|
843
|
+
| December 2023 | Vue 2 reaches end of life |
|
|
844
|
+
| June 2024 | htmx 2.0 released |
|
|
845
|
+
| October 2024 | Svelte 5 released, replacing reactive declarations with runes |
|
|
846
|
+
| November 2024 | React Router 7 released, absorbing Remix |
|
|
847
|
+
| December 2024 | React 19 released, with Actions and stable Server Components |
|
|
848
|
+
| February 2025 | Create React App deprecated |
|
|
849
|
+
|
|
850
|
+
## What Loadbare/app does not do
|
|
851
|
+
|
|
852
|
+
Each item is current behavior or an open item named in TECHREF-1.0 or the
|
|
853
|
+
Roadmap.
|
|
854
|
+
|
|
855
|
+
| Area | Status in 0.7.3 |
|
|
856
|
+
|-------------------------------|------------------------------------------------------------------------|
|
|
857
|
+
| Nested data | A row never holds rows. Trees are sent as joined rows. |
|
|
858
|
+
| Record URLs and view parameters | A URL names a page. Parameters are an open blocker. |
|
|
859
|
+
| Query arguments from the browser | None. Selection is server state reached through `ctx`. |
|
|
860
|
+
| Checkboxes, radio buttons, file inputs | Not landed and not gathered. Open blocker. |
|
|
861
|
+
| Data types and formatting | No conversion. Queries format values. Open blocker. |
|
|
862
|
+
| Error messages | `lb-error` carries no message. |
|
|
863
|
+
| Field validation | No mechanism. Per-keystroke validation is a roadmap question. |
|
|
864
|
+
| Optimistic updates | None. |
|
|
865
|
+
| Concurrent writers | No version or conflict check. Roadmap question. |
|
|
866
|
+
| Server push | None. No WebSocket or server-sent events. |
|
|
867
|
+
| Offline or local-first use | None. |
|
|
868
|
+
| Server-side rendering for crawlers | None. Every path answers the same document with status 200. |
|
|
869
|
+
| Subpath hosting | Not possible. The hub uses absolute paths. |
|
|
870
|
+
| Chrome-level queries | A widget in the chrome bound to a query requires every page to declare it. Open blocker. |
|
|
871
|
+
| A widget receiving a whole row | Not available. Open blocker. |
|
|
872
|
+
| A nested list per outer row | A nested list receives the same rows in every outer row. A new outer row's nested list stays empty until its name lands again. Open blocker. |
|
|
873
|
+
| Configurable request deadline | Fixed at ten seconds. |
|
|
874
|
+
| Internationalization | Roadmap item. |
|
|
875
|
+
| Lazy page loading | Roadmap item. |
|
|
876
|
+
| Per-widget CSS shipping | Roadmap item. |
|
|
877
|
+
| Dev server, HMR | None. |
|
|
878
|
+
| Language server | Roadmap item. |
|
|
879
|
+
| Browser tests of the framework | Not written. |
|
|
880
|
+
|
|
881
|
+
## Requirements and fit
|
|
882
|
+
|
|
883
|
+
Theory states the fit condition: data that is, or maps readily to, rows and
|
|
884
|
+
sets of rows, and affordances that map to the standard operations on that
|
|
885
|
+
data.
|
|
886
|
+
|
|
887
|
+
Theory also states the strongest objection it knows of and does not dispute
|
|
888
|
+
it: that the binding attributes and the required relational shape exchange
|
|
889
|
+
one set of accidental complexity for another, and that the system is
|
|
890
|
+
"differently accidental" from other frameworks.
|
|
891
|
+
|
|
892
|
+
The table lists requirements an application may have, and whether each
|
|
893
|
+
approach provides them without additional libraries.
|
|
894
|
+
|
|
895
|
+
| Requirement | Loadbare/app 0.7.3 | Client component + API | Meta-frameworks | Hypermedia | Server-driven stateful |
|
|
896
|
+
|-----------------------------------------------|--------------------|------------------------|-----------------|------------|------------------------|
|
|
897
|
+
| Pages bound to relational query results | yes | application code | application code | server templates | server templates |
|
|
898
|
+
| No endpoint or route design | yes | no | partly (actions, loaders) | no | partly (routes, events) |
|
|
899
|
+
| No client-side state management | yes | no | no | yes | yes |
|
|
900
|
+
| No build step | no | no | no | yes | depends on stack |
|
|
901
|
+
| Server-rendered HTML for crawlers | no | no | yes | yes | yes |
|
|
902
|
+
| Route parameters and record URLs | no | yes | yes | yes | yes |
|
|
903
|
+
| Optimistic updates | no | yes | yes | no | partly |
|
|
904
|
+
| Offline or local-first operation | no | with libraries | with libraries | no | no |
|
|
905
|
+
| Real-time server push | no | with libraries | with libraries | extensions, Turbo Streams over WebSocket | yes |
|
|
906
|
+
| Nested JSON documents | no | yes | yes | server templates | yes |
|
|
907
|
+
| Client-only interaction (canvas, editors, drag and drop) | through custom elements | yes | yes | through scripts | through hooks |
|
|
908
|
+
| Works while the server is unreachable | no | with caching | with caching | no | no |
|
|
909
|
+
| No persistent connection | yes | yes | yes | yes | no |
|
|
910
|
+
| Server code in any language | no (Node, TypeScript) | yes | no (JavaScript, TypeScript) | yes | no (Elixir, PHP, .NET) |
|
|
911
|
+
| Stable 1.0 or later | no | yes | yes | yes | yes |
|
|
912
|
+
|
|
913
|
+
## Appendix A: Matrix
|
|
914
|
+
|
|
915
|
+
| Aspect | Loadbare/app | Client component | Meta-frameworks | Hypermedia | Server-driven stateful |
|
|
916
|
+
|----------------------|--------------------------------------|---------------------------------|-----------------------------------------|---------------------------------|---------------------------------|
|
|
917
|
+
| State | Server; DOM | Browser memory | Server and browser memory | Server; DOM | Server memory per client |
|
|
918
|
+
| Rendering | Build time | Browser | Server, then browser | Server per request | Server per event |
|
|
919
|
+
| 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 |
|
|
920
|
+
| Components | Light-DOM custom elements | Framework components | Framework components | Server partials; Stimulus controllers | Server components |
|
|
921
|
+
| Wire | One POST endpoint, JSON rows | Application-designed | Framework-defined | HTML over HTTP | WebSocket or snapshot POSTs |
|
|
922
|
+
| Mutation refresh | Declared `refresh` names; patches | Application code or query invalidation | Revalidation | Response HTML | Re-render |
|
|
923
|
+
| Routing | Flat page names; no parameters | Router library | File-based, with parameters and layouts | Server routes | Server routes |
|
|
924
|
+
| Build | `loadbare-app-build`, esbuild | Vite and others | Vite, Turbopack, others | None required | Stack-specific |
|
|
925
|
+
| CSS | Concatenated, unscoped | Varies | Varies, scoped options | None | None built in |
|
|
926
|
+
| Server | Application's Express app | Application's API | Framework server or adapters | Any | Phoenix, Laravel, ASP.NET Core |
|
|
927
|
+
|
|
928
|
+
## Appendix B: One page, three ways
|
|
929
|
+
|
|
930
|
+
An invoice page shows the invoice's number and customer, a table of its
|
|
931
|
+
lines, and a Remove button per line. Removing a line changes the invoice
|
|
932
|
+
row, so the invoice is refreshed.
|
|
933
|
+
|
|
934
|
+
### Loadbare/app
|
|
935
|
+
|
|
936
|
+
```html
|
|
937
|
+
<!-- invoice.page.html -->
|
|
938
|
+
<section lb-row="invoice">
|
|
939
|
+
<h2 lb-cell="number"></h2>
|
|
940
|
+
<span lb-cell="customer"></span>
|
|
941
|
+
</section>
|
|
942
|
+
|
|
943
|
+
<table>
|
|
944
|
+
<tbody lb-list="invoiceLines">
|
|
945
|
+
<template lb-key="id">
|
|
946
|
+
<tr>
|
|
947
|
+
<td lb-cell="item"></td>
|
|
948
|
+
<td lb-cell="amount"></td>
|
|
949
|
+
<td><button lb-action="lb-row-delete">Remove</button></td>
|
|
950
|
+
</tr>
|
|
951
|
+
</template>
|
|
952
|
+
</tbody>
|
|
953
|
+
</table>
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
```ts
|
|
957
|
+
// invoice.queries.ts
|
|
958
|
+
import { list, row, type Queries } from "@loadbare/app/server";
|
|
959
|
+
|
|
960
|
+
export const queries: Queries = {
|
|
961
|
+
invoice: row((ctx) => ctx.db.currentInvoice()),
|
|
962
|
+
invoiceLines: list((ctx) => ctx.db.currentInvoiceLines()),
|
|
963
|
+
};
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
```ts
|
|
967
|
+
// invoice.requests.ts
|
|
968
|
+
import { patch, type Requests } from "@loadbare/app/server";
|
|
969
|
+
|
|
970
|
+
export const requests: Requests = {
|
|
971
|
+
crud: {
|
|
972
|
+
invoiceLines: {
|
|
973
|
+
rowDelete: {
|
|
974
|
+
run: async (ctx, where) => {
|
|
975
|
+
await ctx.db.deleteLine(where.key);
|
|
976
|
+
return { invoiceLines: patch({ drop: [where.key] }) };
|
|
977
|
+
},
|
|
978
|
+
refresh: ["invoice"],
|
|
979
|
+
},
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
};
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
Which invoice is current is held on the server and reached through `ctx`.
|
|
986
|
+
The button carries `lb-pending` and `aria-busy` while the request is in
|
|
987
|
+
flight. The request sent is
|
|
988
|
+
`{ "action": "lb-row-delete", "list": "invoiceLines", "key": "42" }`. The
|
|
989
|
+
response carries the refreshed `invoice` row and a patch dropping key `42`.
|
|
990
|
+
|
|
991
|
+
### Next.js (App Router)
|
|
992
|
+
|
|
993
|
+
```tsx
|
|
994
|
+
// app/invoices/[id]/page.tsx
|
|
995
|
+
import { db } from "@/lib/db";
|
|
996
|
+
import { deleteLine } from "./actions";
|
|
997
|
+
|
|
998
|
+
export default async function InvoicePage({
|
|
999
|
+
params,
|
|
1000
|
+
}: {
|
|
1001
|
+
params: Promise<{ id: string }>;
|
|
1002
|
+
}) {
|
|
1003
|
+
const { id } = await params;
|
|
1004
|
+
const invoice = await db.invoice(id);
|
|
1005
|
+
const lines = await db.invoiceLines(id);
|
|
1006
|
+
return (
|
|
1007
|
+
<>
|
|
1008
|
+
<section>
|
|
1009
|
+
<h2>{invoice.number}</h2>
|
|
1010
|
+
<span>{invoice.customer}</span>
|
|
1011
|
+
</section>
|
|
1012
|
+
<table>
|
|
1013
|
+
<tbody>
|
|
1014
|
+
{lines.map((line) => (
|
|
1015
|
+
<tr key={line.id}>
|
|
1016
|
+
<td>{line.item}</td>
|
|
1017
|
+
<td>{line.amount}</td>
|
|
1018
|
+
<td>
|
|
1019
|
+
<form action={deleteLine.bind(null, id, line.id)}>
|
|
1020
|
+
<button>Remove</button>
|
|
1021
|
+
</form>
|
|
1022
|
+
</td>
|
|
1023
|
+
</tr>
|
|
1024
|
+
))}
|
|
1025
|
+
</tbody>
|
|
1026
|
+
</table>
|
|
1027
|
+
</>
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
```ts
|
|
1033
|
+
// app/invoices/[id]/actions.ts
|
|
1034
|
+
"use server";
|
|
1035
|
+
import { revalidatePath } from "next/cache";
|
|
1036
|
+
import { db } from "@/lib/db";
|
|
1037
|
+
|
|
1038
|
+
export async function deleteLine(invoiceId: string, lineId: string) {
|
|
1039
|
+
await db.deleteLine(lineId);
|
|
1040
|
+
revalidatePath(`/invoices/${invoiceId}`);
|
|
1041
|
+
}
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
The invoice is addressed by URL. `revalidatePath` causes the page's Server
|
|
1045
|
+
Component to render again, and the response carries the new RSC payload for
|
|
1046
|
+
the page. A pending state on the button requires a Client Component using
|
|
1047
|
+
`useFormStatus`.
|
|
1048
|
+
|
|
1049
|
+
### htmx with Express
|
|
1050
|
+
|
|
1051
|
+
```html
|
|
1052
|
+
<!-- rendered by the server for GET /invoices/7 -->
|
|
1053
|
+
<section hx-get="/invoices/7/header" hx-trigger="line-deleted from:body">
|
|
1054
|
+
<h2>INV-0007</h2>
|
|
1055
|
+
<span>Acme</span>
|
|
1056
|
+
</section>
|
|
1057
|
+
|
|
1058
|
+
<table>
|
|
1059
|
+
<tbody hx-target="closest tr" hx-swap="outerHTML">
|
|
1060
|
+
<tr>
|
|
1061
|
+
<td>Widget</td>
|
|
1062
|
+
<td>12.00</td>
|
|
1063
|
+
<td><button hx-delete="/invoices/7/lines/42">Remove</button></td>
|
|
1064
|
+
</tr>
|
|
1065
|
+
</tbody>
|
|
1066
|
+
</table>
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
```ts
|
|
1070
|
+
// server.ts, alongside the route and template that render the full page
|
|
1071
|
+
app.delete("/invoices/:id/lines/:line", async (req, res) => {
|
|
1072
|
+
await db.deleteLine(req.params.line);
|
|
1073
|
+
res.set("HX-Trigger", "line-deleted").send("");
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
app.get("/invoices/:id/header", async (req, res) => {
|
|
1077
|
+
const invoice = await db.invoice(req.params.id);
|
|
1078
|
+
res.send(
|
|
1079
|
+
`<h2>${escapeHtml(invoice.number)}</h2><span>${escapeHtml(invoice.customer)}</span>`,
|
|
1080
|
+
);
|
|
1081
|
+
});
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
The empty response replaces the row, which removes it. The `HX-Trigger`
|
|
1085
|
+
header fires `line-deleted`, and the section requests its own fragment. The
|
|
1086
|
+
application designs three routes and renders three HTML responses, and
|
|
1087
|
+
escapes values in them. The button carries the `htmx-request` class while
|
|
1088
|
+
the request is in flight.
|
|
1089
|
+
|
|
1090
|
+
## Appendix C: Glossary of nearest equivalents
|
|
1091
|
+
|
|
1092
|
+
Each equivalent is approximate.
|
|
1093
|
+
|
|
1094
|
+
| Loadbare/app | Nearest equivalents elsewhere |
|
|
1095
|
+
|-------------------------------|----------------------------------------------------------------------------------|
|
|
1096
|
+
| Hub | Client runtime; router plus data layer |
|
|
1097
|
+
| Chrome (`chrome.html`) | Root layout (`app/layout.tsx`, `+layout.svelte`); application shell |
|
|
1098
|
+
| Page (`<name>.page.html`) | Route component; page template |
|
|
1099
|
+
| Query (`row()`, `list()`) | Loader (React Router 7); load function (SvelteKit); data read in a Server Component |
|
|
1100
|
+
| `onPageEnter` | LiveView `mount`; code at the top of a loader |
|
|
1101
|
+
| Declared action | Route action (React Router 7); form action (SvelteKit); Server Function; LiveView `handle_event` |
|
|
1102
|
+
| `crud` operation | Resource controller action (Rails); REST verb on a row |
|
|
1103
|
+
| `refresh` | Revalidation; `invalidateQueries`; `revalidatePath` |
|
|
1104
|
+
| `patch({ rows, drop })` | Keyed partial update; Turbo Stream `append`/`replace`/`remove`; LiveView `stream_insert`/`stream_delete` |
|
|
1105
|
+
| Landing | Render and commit; swap; patch |
|
|
1106
|
+
| `lb-list`, `lb-row`, `lb-cell` | IE4 `DATASRC` and `DATAFLD`; `v-for` and `{{ }}`; `x-for` and `x-text` |
|
|
1107
|
+
| `lb-key` | `key` (React, Vue); `track` (Angular); keyed `each` (Svelte) |
|
|
1108
|
+
| `lb-show` | `v-if`, `@if`, `{#if}`, `x-if` |
|
|
1109
|
+
| `lb-value` | A prop; an observed attribute |
|
|
1110
|
+
| `lb-action` | `hx-post`; `phx-click`; `wire:click`; `<form action>` |
|
|
1111
|
+
| `lb-request` event | `htmx:beforeRequest`; a component `emit` |
|
|
1112
|
+
| `lb-pending` | `htmx-request` class; `phx-click-loading`; `useFormStatus().pending`; `wire:loading` |
|
|
1113
|
+
| `lb-error` | `htmx:responseError`; an error boundary |
|
|
1114
|
+
| `lb-row-count` | An empty-state conditional in a template |
|
|
1115
|
+
| `lb-nav-link` | `<Link>`; `hx-boost`; Turbo Drive |
|
|
1116
|
+
| `lb-navigation` | `useLocation`; `page.url` (SvelteKit); `$route` (Vue Router) |
|
|
1117
|
+
| Widget | Component; custom element |
|
|
1118
|
+
| Expansion | Server-side include; partial; build-time component render (Astro) |
|
|
1119
|
+
| `exp-` parameter | A prop fixed at build time |
|
|
1120
|
+
| `lb-slot`, `lb-template` | Default slot and named slots; `ng-content` with `select` |
|
|
1121
|
+
| `lbPlaceRow`, `lbRowsLanded` | Custom list rendering; a render prop |
|
|
1122
|
+
| `ctx` / `HubContext` | `event.locals` (SvelteKit); loader `context` (React Router 7); request-scoped dependency injection |
|
|
1123
|
+
| `imports.ts` | Installing and registering a component library |
|
|
1124
|
+
| `pages.ts` | Generated route manifest |
|