@loadbare/app 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/core/lb-constants.d.ts +3 -0
  2. package/dist/core/lb-constants.d.ts.map +1 -1
  3. package/dist/core/lb-constants.js +28 -4
  4. package/dist/core/lb-constants.js.map +1 -1
  5. package/dist/core/lb-types.d.ts +11 -0
  6. package/dist/core/lb-types.d.ts.map +1 -1
  7. package/dist/core/lb-types.js +20 -0
  8. package/dist/core/lb-types.js.map +1 -1
  9. package/dist/hub/lb-apply.d.ts +10 -0
  10. package/dist/hub/lb-apply.d.ts.map +1 -1
  11. package/dist/hub/lb-apply.js +15 -1
  12. package/dist/hub/lb-apply.js.map +1 -1
  13. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  14. package/dist/hub/lb-hub.browser.js +161 -38
  15. package/dist/hub/lb-hub.browser.js.map +1 -1
  16. package/dist/server/lb-express.d.ts +22 -7
  17. package/dist/server/lb-express.d.ts.map +1 -1
  18. package/dist/server/lb-express.js +79 -31
  19. package/dist/server/lb-express.js.map +1 -1
  20. package/dist/server/lb-server.d.ts +26 -0
  21. package/dist/server/lb-server.d.ts.map +1 -1
  22. package/dist/server/lb-server.js +53 -3
  23. package/dist/server/lb-server.js.map +1 -1
  24. package/docs/TECHREF-1.0.md +154 -22
  25. package/docs/comparison.md +8 -7
  26. package/docs/reference/chrome.md +5 -3
  27. package/docs/reference/data-binding.md +33 -0
  28. package/docs/reference/page-files.md +39 -0
  29. package/docs/reference/server.md +17 -0
  30. package/docs/reference/widgets.md +6 -2
  31. package/docs/roadmap.md +28 -0
  32. package/docs/theory.md +8 -1
  33. package/docs/tutorials/010-pages-and-navigation.md +2 -1
  34. package/package.json +1 -1
  35. package/skills/loadbare-app/SKILL.md +36 -8
  36. package/skills/loadbare-app/references/TECHREF-1.0.md +154 -22
  37. package/skills/loadbare-app/references/chrome.md +5 -3
  38. package/skills/loadbare-app/references/data-binding.md +33 -0
  39. package/skills/loadbare-app/references/page-files.md +39 -0
  40. package/skills/loadbare-app/references/server.md +17 -0
  41. package/skills/loadbare-app/references/widgets.md +6 -2
@@ -106,14 +106,6 @@ in the row.
106
106
  stays a flag, since it is what locates the file. State the precedence
107
107
  between a flag and a key once. Its key names join the permanent surface,
108
108
  so this lands before 1.0 or not at all.
109
- - **Decide whether a URL carries view parameters.** A URL names a page and
110
- nothing finer — see [What a URL names](#what-a-url-names) — and a query
111
- takes no argument from the browser. So a page has nowhere to keep which
112
- record it shows, a filter, a sort, or a collapsed section: a reload or a
113
- shared link loses them, and a filter is lost whenever an action re-runs its
114
- query. Parameters on a page URL, as `<form method="get">` puts its fields
115
- in the query string, are one answer. Deciding against them, and leaving
116
- view state to the application, is another. Write whichever one down.
117
109
  - **Decide CSS pairing.** Naming a stylesheet `<tag>.css` beside its
118
110
  definition would let the builder ship only what survives expansion.
119
111
  Recommend deferring the mechanism and reserving the configuration key, so
@@ -507,8 +499,11 @@ A list nested in another list's rows receives the same rows in every outer
507
499
  row. That serves a picker offering the same choices on every row, and is
508
500
  not a way to show a different detail per row.
509
501
 
510
- Which master a page shows is server state reached through `ctx`, since a
511
- query takes no argument from the browser — see the blocker on view parameters.
502
+ Which master a page shows is a query parm, which a query reads off `ctx`
503
+ since it takes no argument from the browser — see [Query parms](#query-parms).
504
+ When a write creates or removes the master, the server response changes that
505
+ parm — see
506
+ [When a server response changes the query parms](#when-a-server-response-changes-the-query-parms).
512
507
 
513
508
  ### Requests
514
509
 
@@ -638,15 +633,16 @@ A failed insert resets nothing, so the entry can be corrected.
638
633
 
639
634
  A navigation that fails to load its data sets nothing. No element
640
635
  dispatched it, so there is nothing to stamp, and the hub reports it to the
641
- console.
636
+ console. A load started by a control writing a query parm stamps that
637
+ control, as a request stamps its origin.
642
638
 
643
639
  ### Links
644
640
 
645
641
  ---- UNEDITED ----
646
642
 
647
643
  An anchor carrying `lb-nav-link` navigates inside the application: the hub
648
- catches the click, pushes the anchor's path onto history, and swaps the page
649
- host in `<main>`. An anchor without it is left alone and behaves like any
644
+ catches the click, pushes the anchor's path and query string onto history,
645
+ and swaps the page host in `<main>`. An anchor without it is left alone and behaves like any
650
646
  other link, so leaving the application is the default and staying in it is
651
647
  the opt-in.
652
648
 
@@ -660,6 +656,11 @@ link to determine the path.
660
656
  Path space is flat. A path such as `/members` links to the `members.*` files
661
657
  on the server.
662
658
 
659
+ The query string is kept. `/transactions?date_begin=2026-09-01` opens the
660
+ transactions page narrowed to those dates, and a link to the page it is
661
+ already on loads that page again at the new URL without replacing its DOM —
662
+ see [Query parms](#query-parms).
663
+
663
664
  The bare path `/` resolves to `index`.
664
665
 
665
666
  A path that names no page is detected in the browser, after a successful
@@ -680,19 +681,122 @@ See also [Navigation Row lb-navigation](#lb-navigation).
680
681
 
681
682
  #### What a URL names
682
683
 
683
- A URL names a page, a place in the application. It never names a resource,
684
- and Loadbare/app does not reproduce REST: `/accounts/42` is not a way to
685
- reach account 42.
684
+ The path names a page, a place in the application. It never names a
685
+ resource, and Loadbare/app does not reproduce REST: `/accounts/42` is not a
686
+ way to reach account 42.
687
+
688
+ The query string describes what that page has on screen:
689
+ `/accounts?acct=23&from=2026-09-09`. It never commands, and the server
690
+ remembers nothing, so a URL is a reproducible view. Reload it, bookmark it,
691
+ or mail it to someone, and what they see is what the sender saw.
686
692
 
687
693
  A page declares what it shows, its queries, and what it allows, its actions.
688
694
  Loading a page runs its queries. A request performs an action at a position.
689
695
  A row is addressed by `list` and `key`, taken from where the element sits,
690
696
  which is how the database already names it. Nothing is fetched by URL, so an
691
- application designs no endpoints, and the browser reaches only what a page
692
- publishes.
697
+ application designs no endpoints.
698
+
699
+ That last is where query parms move Loadbare's position. A query still takes
700
+ no argument from the browser, and a parm arrives the way the session cookie
701
+ does, as part of the request the context is built from. But a user who
702
+ types `?acct=99999` now influences what a query returns. A query parm is
703
+ user input, validated like any other where the application reads it, and a
704
+ per-session database role means an id outside the caller's reach finds
705
+ nothing.
706
+
707
+ Loadbare is for applications, not sites. Every route is answered with the
708
+ same document, and a path that names no page is found out in the browser —
709
+ see [Links](#links).
710
+
711
+ #### Query parms
712
+
713
+ A control that narrows what a page shows writes its value into the query
714
+ string, rather than sending it:
715
+
716
+ ```html
717
+ <lb-options lb-list="teams" lb-query-parm="team" exp-label="Team:">
718
+ <option value="">Every team</option>
719
+ <template lb-key="id"><option lb-cell="name"></option></template>
720
+ </lb-options>
721
+ ```
722
+
723
+ | Attribute | Written by | Behavior |
724
+ | -------------------- | ---------- | --------------------------------------------------------------- |
725
+ | `lb-query-parm` | Developer | On a control: its `change` writes this parm and reloads the page |
726
+ | `lb-query-parm-push` | Developer | With `lb-query-parm`: the write pushes a history entry |
727
+
728
+ On `change`, the hub reads the control's value the way a form reads a cell,
729
+ and sets that one parm in the URL. Every other parm is left as it is, since
730
+ it may have arrived by link with no control on screen to say it again. An
731
+ empty value takes the parm out, so a URL is as long as the user has narrowed
732
+ the page. A value the URL already carries does nothing.
733
+
734
+ The write replaces the current history entry: changing what a page shows is
735
+ not going anywhere, so Back leaves the page rather than walking back through
736
+ every choice. `lb-query-parm-push` makes the write push an entry instead.
737
+
738
+ The page then loads at the new URL exactly as a cold load of that URL would:
739
+ `onPageEnter`, then every query. The page's DOM is kept, and the answer
740
+ lands by key, so a list that gets its rows back keeps them and its scroll
741
+ position.
742
+
743
+ After every load — cold, by link, by Back, by a write — the hub lands each
744
+ parm on the control that writes it, and an absent parm lands empty. A
745
+ control therefore shows what the address bar says.
746
+
747
+ A control that writes a query parm sends no request. One that also carries
748
+ `lb-action` has that request refused, since the choice would otherwise be
749
+ sent twice.
750
+
751
+ Every round trip carries the query string the browser is showing, a page
752
+ load and an action alike. The server hands its parms to `contextFor` — see
753
+ [The Express server](#the-express-server). Nothing in Loadbare assigns a
754
+ parm a meaning.
693
755
 
694
- Whether a page URL may carry parameters, so that a link can open the page on
695
- one record, is undecided — see the blockers.
756
+ #### When a server response changes the query parms
757
+
758
+ Some query parms can only be known once a write has run. After an insert,
759
+ the key of the new row exists only on the server. After a delete, only the
760
+ server knows that the row the page was showing is gone.
761
+
762
+ The usual web answer is Post/Redirect/Get: the server answers the write with
763
+ a redirect to a URL naming the result, and the browser makes a second request
764
+ to load it. Loadbare/app does the same work in one round trip, and never
765
+ changes the path.
766
+
767
+ A request's `run` returns `queryParms()`, naming the parms the write decided.
768
+ The server loads the page with those parms set, as a cold load of the
769
+ resulting URL would: `onPageEnter`, then every query. The server response
770
+ carries the parms and that load together. The hub sets the parms in the URL,
771
+ replacing the history entry, and then lands the load.
772
+
773
+ ```ts
774
+ rowInsert: {
775
+ run: async (ctx, { values }) => {
776
+ const id = await ctx.db.addAccount(values);
777
+ return queryParms({ acct: String(id) });
778
+ },
779
+ refresh: [],
780
+ },
781
+ ```
782
+
783
+ - Only query parms change. A server response cannot send the browser to
784
+ another page.
785
+ - Parms the response does not name are left as they are. An empty value
786
+ removes its parm.
787
+ - At least one parm is named, and every value is a string. Otherwise the
788
+ server warns, ignores the parms, and runs the refresh set as usual.
789
+ - The refresh set does not run, and anything else `run` returned is dropped.
790
+ Both were answers for the query string the page is leaving.
791
+ - If loading the page fails, the write has still happened. The response
792
+ carries the parms alone, and the hub loads the page itself. A failure
793
+ there is a page load's, and is not stamped on the element that sent the
794
+ request.
795
+ - A user who has left the page by the time the response arrives keeps the
796
+ URL they are on.
797
+
798
+ The page is loaded with a second context, built by `contextFor` from the new
799
+ parms — see [The Express server](#the-express-server).
696
800
 
697
801
  ### lb-navigation
698
802
 
@@ -706,10 +810,11 @@ like a server-produced result.
706
810
  | Cell | Holds |
707
811
  | ------------ | ------------------------------------------------------------------- |
708
812
  | `page-label` | The text of the link to the current page, empty if no link names it |
709
- | `page-uri` | The path as the browser has it, such as `/members` |
813
+ | `page-uri` | The path and query string as the browser has them, such as `/members?team=Engines` |
710
814
 
711
815
  The `page-uri` is taken from the current URL. The `page-label` is taken from the first
712
- `lb-nav-link` link in the document (presumably in a nav bar) that matches the URI.
816
+ `lb-nav-link` link in the document (presumably in a nav bar) whose path matches the
817
+ current path, so the query string does not change it.
713
818
 
714
819
  The row lands before the page is looked up, so a path that names no page has
715
820
  it too, and an `lb-unknown-page` dialog, where the chrome has one, displays
@@ -871,6 +976,24 @@ Explaining Express is beyond the scope of this technical reference. The
871
976
  only real requirement is that the catch-all for app.html is at the end,
872
977
  so it does not catch any other files.
873
978
 
979
+ `hubRoutes` answers `POST /lb/<page>`, with the query string the browser is
980
+ showing after it, verbatim. It hands `contextFor` that query string's parms
981
+ as a second argument, and `contextFor` puts on the context whatever a query
982
+ reads from them. They are user input:
983
+
984
+ ```ts
985
+ function contextFor(req: Request, parms: URLSearchParams): HubContext {
986
+ return { db: openDb(), acct: parms.get("acct") ?? "" };
987
+ }
988
+ ```
989
+
990
+ Read the parms from that argument, not from `req.query`. After a request
991
+ whose `run` returned `queryParms()`, `contextFor` is called a second time for
992
+ the same request, with the new parms, to load the page at them. So it must
993
+ be safe to call twice, and a write must be visible to the second context by
994
+ the time its `run` returns. A handle opened per request without a
995
+ transaction around it is both.
996
+
874
997
  Give the server the origin root. The hub reaches its own endpoints by
875
998
  absolute path, so an application cannot be hosted under a subpath such as
876
999
  `example.com/myapp/`, and anything proxying in front of the server passes
@@ -941,6 +1064,13 @@ interaction happened, as `list`, `row`, `key`, `cell` and `value`. Under
941
1064
  ones. That is how a delta reaches the browser: wrap it in `patch()`, naming
942
1065
  the rows that arrived or changed and the keys that went.
943
1066
 
1067
+ `run` may instead return `queryParms()`, naming query parms only the write
1068
+ can know, such as the key of a row it inserted. The page then loads at them
1069
+ in the same round trip, in place of the refresh set — see
1070
+ [When a server response changes the query parms](#when-a-server-response-changes-the-query-parms).
1071
+ A `rowDelete` that removes the row on screen returns `queryParms({ acct: "" })`
1072
+ and leaves any other delete to its refresh set.
1073
+
944
1074
  ```ts
945
1075
  // members.requests.ts
946
1076
  import { patch, type Requests } from "@loadbare/app/server";
@@ -1148,6 +1278,8 @@ Each attribute is defined in one section, and this table says which.
1148
1278
  | `lb-value` | Hub | [How a value lands](#how-a-value-lands) |
1149
1279
  | `lb-action` | Developer | [Requests](#requests) |
1150
1280
  | `lb-nav-link` | Developer | [Links](#links) |
1281
+ | `lb-query-parm` | Developer | [Query parms](#query-parms) |
1282
+ | `lb-query-parm-push` | Developer | [Query parms](#query-parms) |
1151
1283
  | `lb-pending` | Hub | [Request state](#request-state) |
1152
1284
  | `lb-error` | Hub | [Request state](#request-state) |
1153
1285
  | `lb-row-count` | Hub | [Request state](#request-state) |
@@ -361,7 +361,8 @@ Vuetify for Vue) are not usable in another without a wrapper.
361
361
 
362
362
  ### Loadbare/app
363
363
 
364
- Every request from the hub is `POST /lb?page=<name>` with a JSON body.
364
+ Every request from the hub is `POST /lb/<page>` with a JSON body, and carries
365
+ the query string the browser is showing.
365
366
 
366
367
  - An empty body asks for the page's whole query set. The server runs the
367
368
  page's `onPageEnter`, then every query the page declares.
@@ -679,12 +680,12 @@ blocker. Serving the static files from a separate origin is a roadmap item.
679
680
 
680
681
  First load of any path:
681
682
 
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 |
683
+ | Request | Returns |
684
+ |---------------------------|--------------------------------------------|
685
+ | `GET /<path>` | `app.html`: the chrome and every page as a `<template>` |
686
+ | `GET /client.js` | The hub and every used widget, one bundle |
687
+ | `GET /app.css` | Every stylesheet, one file |
688
+ | `POST /lb/<page>?<query>` | The page's query results as JSON |
688
689
 
689
690
  The three static files are fixed at release and can be compressed, cached,
690
691
  and served from a CDN. There are no per-route bundles, lazy chunks or module
@@ -73,7 +73,9 @@ sits directly inside `<body>`, with banner, nav, footer and `<main>` inside
73
73
  it, so Loadbare can act on all of them.
74
74
 
75
75
  A navigation anchor's `href` is a path, and the path names a page:
76
- `/members` shows `members.page.html`. A bare `/` resolves to `index`, so the
76
+ `/members` shows `members.page.html`. A query string on it is kept, so
77
+ `/members?team=Engines` opens the page narrowed; see
78
+ [Query parms](./data-binding.md#query-parms). A bare `/` resolves to `index`, so the
77
79
  landing page is the one named `index.page.html`. An anchor without
78
80
  `lb-nav-link` is left alone and behaves like any other link.
79
81
 
@@ -100,10 +102,10 @@ shows a value — so a chrome displays the current page with no code at all:
100
102
  | Cell | Holds |
101
103
  |--------------|-----------------------------------------------------------|
102
104
  | `page-label` | The text of the `lb-nav-link` anchor for the path, or empty if none |
103
- | `page-uri` | The path as the browser has it, such as `/members` |
105
+ | `page-uri` | The path and query string as the browser has them, such as `/members?team=Engines` |
104
106
 
105
107
  The label is the nav's. The hub takes it from the first `lb-nav-link`
106
- anchor whose `href` names the current page, so a click, a reload and the
108
+ anchor whose path names the current page, whatever its query string, so a click, a reload and the
107
109
  back button all land the same text, and a path no anchor names lands an
108
110
  empty label. A chrome that shows the label somewhere fixed should expect
109
111
  that case for a page reachable only by URL.
@@ -304,6 +304,39 @@ Leave `lb-action` off a widget inside an `lb-row-insert` or `lb-row-update`
304
304
  form. The form reads every `lb-cell` in it on submit, so a widget that also
305
305
  sent its own would write the same edit twice.
306
306
 
307
+ ## Query parms
308
+
309
+ A control that narrows what the page shows writes its value into the query
310
+ string instead of sending a request. `lb-query-parm` names the parm:
311
+
312
+ ```html
313
+ <select lb-query-parm="team">
314
+ <option value="">Every team</option>
315
+ <option value="Engines">Engines</option>
316
+ </select>
317
+ ```
318
+
319
+ On `change` the hub sets that one parm in the URL, leaving every other parm
320
+ alone, and takes it out when the value is empty. The write replaces the
321
+ current history entry; add `lb-query-parm-push` to push one instead. The page
322
+ then loads at the new URL, exactly as a cold load of it would, without
323
+ replacing its DOM.
324
+
325
+ After every load the hub lands each parm on the control that writes it, and
326
+ an absent parm lands empty, so the control shows what the address bar says.
327
+
328
+ A control that writes a query parm sends no request, and one that also
329
+ carries `lb-action` has that request refused.
330
+
331
+ A request can write parms too, when only the write knows their value, such as
332
+ the key of a row it inserted; see
333
+ [refresh and patch](./page-files.md#refresh-and-patch). The page loads at
334
+ them in the same round trip, and they land on their controls as above.
335
+
336
+ The server hands the parms to `contextFor`; see
337
+ [the Express server](./server.md#database-layer). See
338
+ [Query parms](../TECHREF-1.0.md#query-parms) for the whole rule.
339
+
307
340
  ## Conditional rendering
308
341
 
309
342
  Loadbare ships static HTML and hydrates elements that are already in the
@@ -192,3 +192,42 @@ resetRoster: {
192
192
  refresh: ["roster"],
193
193
  },
194
194
  ```
195
+
196
+ Return `queryParms()` instead when the server response changes the query
197
+ parms: after an insert, only the server knows the new key, and after a delete,
198
+ only the server knows the parm should go. The page loads at the query string
199
+ with those parms set, in the same round trip, and the hub writes them into the
200
+ URL, replacing the history entry. This is Post/Redirect/Get without the
201
+ redirect. The refresh set does not run, and nothing else `run` returned is
202
+ sent, since both answered for the query string the page left. Name at least
203
+ one parm, give each a string, and use an empty string to take one out:
204
+
205
+ ```ts
206
+ // src/pages/accounts.requests.ts
207
+ import { queryParms, type Requests } from "@loadbare/app/server";
208
+
209
+ export const requests: Requests = {
210
+ crud: {
211
+ accounts: {
212
+ rowInsert: {
213
+ run: async (ctx, { values }) => {
214
+ const id = await ctx.db.addAccount(values);
215
+ return queryParms({ acct: String(id) });
216
+ },
217
+ refresh: [],
218
+ },
219
+ rowDelete: {
220
+ run: async (ctx, { key }) => {
221
+ await ctx.db.deleteAccount(key);
222
+ if (key === ctx.acct) return queryParms({ acct: "" });
223
+ },
224
+ refresh: ["accounts"],
225
+ },
226
+ },
227
+ },
228
+ };
229
+ ```
230
+
231
+ Only parms: `run` cannot send the browser to another page. The loaded page
232
+ reads the new parms through `contextFor` like any others; see
233
+ [the Express server](./server.md#database-layer).
@@ -137,6 +137,23 @@ declare module "@loadbare/app/server" {
137
137
  }
138
138
  ```
139
139
 
140
+ The query string the browser is showing arrives on every data request, and
141
+ `contextFor` gets its parms as a second argument. Put on the context whatever
142
+ a query reads from them, and treat them as user input:
143
+
144
+ ```ts
145
+ function contextFor(req: Request, parms: URLSearchParams): HubContext {
146
+ return { db: openDb(), team: parms.get("team") ?? "" };
147
+ }
148
+ ```
149
+
150
+ Read the parms from that argument, not from `req.query`. When a server
151
+ response changes the query parms, `contextFor` is called a second time for
152
+ that request, with the new parms, to load the page at them; see [refresh and patch](./page-files.md#refresh-and-patch). Write it to
153
+ be safe to call twice, and make a write visible to the second context by the
154
+ time `run` returns: a handle opened per request, with no transaction held
155
+ open across the two, is both.
156
+
140
157
  Add a field for anything else a request needs — the authenticated user, a
141
158
  request id, a feature flag set. Queries and requests read them from `ctx`; see
142
159
  [page files](./page-files.md).
@@ -56,7 +56,8 @@ action carries a value.
56
56
  | ---------- | ----- |
57
57
  | `exp-label` | the visible `<label>` text |
58
58
 
59
- Requires `lb-action` — a change with none logs and sends nothing.
59
+ Requires `lb-action` or `lb-query-parm` — a change with neither logs and
60
+ sends nothing. With `lb-query-parm` the hub writes the choice into the URL.
60
61
 
61
62
  ## `lb-options`
62
63
 
@@ -81,8 +82,11 @@ hand. The author supplies the row template inside the widget (via
81
82
  once, with `lb-key`.
82
83
  - `data-group` on the row template sections the options into `<optgroup>`s,
83
84
  one per distinct value, created and removed as rows arrive and leave.
85
+ - `lb-value` selects the option with that key, including one that arrives
86
+ after the value did.
84
87
  - A `change` sends the action named by `lb-action`, value from the
85
- select's `.value`.
88
+ select's `.value`. With `lb-query-parm` instead, the hub writes the
89
+ choice into the URL.
86
90
 
87
91
  `lb-picker` is this same class with its row template supplied by the
88
92
  definition instead of the page — see below.
package/docs/roadmap.md CHANGED
@@ -34,6 +34,18 @@ A value that hasn't arrived yet is probably derivable from an absent
34
34
  `lb-value` rather than needing a signal of its own. Not yet needed because
35
35
  nothing currently produces that gap in practice — revisit if one does.
36
36
 
37
+ ### Events while a request is in flight
38
+
39
+ A native action, a button or a form, ignores being performed again while its
40
+ own round trip is in flight. Nothing holds back any other element, so two
41
+ round trips can be out at once and the one that answers last is what lands,
42
+ whichever the user started last.
43
+
44
+ Whether the hub should refuse every event while anything is in flight is
45
+ open. A widget is deliberately not held back today, because one that sends on
46
+ change must have its latest value sent rather than dropped, and a refusal of
47
+ everything would have to say what happens to that value.
48
+
37
49
  ### Validation placement
38
50
 
39
51
  Per-keystroke feedback cannot afford a round trip, so some validation will
@@ -103,6 +115,22 @@ If a dev team wishes to make their own widgets that identify `lb-list` or `lb-ro
103
115
  Perhaps a utility that can be called, like `getDataScope(el)`, to help
104
116
  clean up the code in these cases.
105
117
 
118
+ ### Refresh narrowed by query parm
119
+
120
+ A query parm write loads the whole page again, every query at once. Keyed
121
+ landing keeps the rows that came back, so nothing on screen is disturbed, but
122
+ the server does work for queries the parm never touched.
123
+
124
+ A query could declare the parms it reads, and the hub, which holds the old
125
+ URL and the new, could send the names that changed. The server would run
126
+ only the queries that read one of them, and still remember nothing.
127
+
128
+ ### Query parm history as a user preference
129
+
130
+ A query parm write replaces the history entry, and `lb-query-parm-push`
131
+ pushes one. Which of the two a user wants may be the user's to say, rather
132
+ than the page's.
133
+
106
134
  ### Build-time checking of `lb-action` against the declared requests
107
135
 
108
136
  Release 1.0 resolves every `lb-action` value at request time. A value naming
package/docs/theory.md CHANGED
@@ -210,6 +210,12 @@ empty slot and has no opinion about what goes in it. It does not:
210
210
  - require or prevent any authentication solution
211
211
  - expect or hinder the use of an ORM, or of `@loadbare/db`
212
212
 
213
+ Loadbare is for applications, not sites. Every route is answered with
214
+ the same document, and a path that names no page is found out in the
215
+ browser rather than answered with a 404. The path names a page, and the
216
+ query string describes what that page has on screen, so the address bar
217
+ is always true and a URL can be reloaded or mailed to someone.
218
+
213
219
  The rule for one-time chores, such as standing up an Express Server,
214
220
  is that they stay as close as possible to "set and forget", so that
215
221
  their cost is paid once and they are not a tax on
@@ -520,7 +526,8 @@ in the browser, providing an SPA is fairly simple.
520
526
  The hub catches anchor clicks, and checks if the anchor contains the
521
527
  attribute `lb-nav-link`. If so, the hub interprets it as in-app
522
528
  navigation, swaps the anchor's `href` into `<main>`, and sends a request
523
- to the server for the page data.
529
+ to the server for the page data. The query string rides along, so the
530
+ server always knows what URL the browser is showing.
524
531
 
525
532
  Anchors without the attribute behave as normal links.
526
533
 
@@ -77,7 +77,8 @@ If a `<dialog lb-unknown-page>` element is present in the HTML, it will be
77
77
  displayed to the user when a URL is entered that has no matching page in the app.
78
78
  The `lb-row` and `lb-cell` attributes will be explained when we get to data
79
79
  binding; for now, know that `lb-navigation` is a query the hub itself answers
80
- on every navigation, and `page-uri` is the path that was asked for. The
80
+ on every navigation, and `page-uri` is the path and query string that were
81
+ asked for. The
81
82
  widget library ships this dialog ready-made as `<lb-unknown-page>`, which
82
83
  [Using Widget Libraries](./090-using-widget-libraries.md) covers.
83
84
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@loadbare/app",
3
3
  "description": "High performance web app framework for server-bound applications",
4
- "version": "0.8.1",
4
+ "version": "0.9.0",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: loadbare-app
3
- description: Build server-bound web applications with Loadbare/app. Covers the file conventions the builder finds by name (`chrome.html`, `*.page.html`, `*.queries.ts`, `*.requests.ts`, `imports.ts`, `<tag>.html`, `<tag>.browser.ts`); the `lb-*` attribute vocabulary that binds HTML to server data and sends requests (`lb-list`, `lb-row`, `lb-cell`, `lb-key`, `lb-show`, `lb-action`); build-time widget expansion with `exp-*` parameters, `lb-slot` and `lb-template`; custom element code; the Express wiring with `hubRoutes`; and the `@loadbare/widgets` library. Use whenever a task involves `@loadbare/app`, `@loadbare/widgets`, `loadbare-app-build`, an `lb-` attribute, or a page, query, request or widget file in a Loadbare application. The model is not React, htmx or REST, and cannot be inferred from them.
3
+ description: Build server-bound web applications with Loadbare/app. Covers the file conventions the builder finds by name (`chrome.html`, `*.page.html`, `*.queries.ts`, `*.requests.ts`, `imports.ts`, `<tag>.html`, `<tag>.browser.ts`); the `lb-*` attribute vocabulary that binds HTML to server data and sends requests (`lb-list`, `lb-row`, `lb-cell`, `lb-key`, `lb-show`, `lb-action`); query parms in the URL with `lb-query-parm`; build-time widget expansion with `exp-*` parameters, `lb-slot` and `lb-template`; custom element code; the Express wiring with `hubRoutes`; and the `@loadbare/widgets` library. Use whenever a task involves `@loadbare/app`, `@loadbare/widgets`, `loadbare-app-build`, an `lb-` attribute, or a page, query, request or widget file in a Loadbare application. The model is not React, htmx or REST, and cannot be inferred from them.
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  package: "@loadbare/app"
@@ -159,10 +159,36 @@ for display by a widget's `lbPlaceRow`. A list nested inside another list's
159
159
  rows receives the same rows in every outer row; use it for a picker, never
160
160
  for per-row detail.
161
161
 
162
- **A URL names a page, never a resource.** Do not design `/accounts/42`. A
163
- row is addressed by `list` and `key`, taken from where the element sits.
164
- Queries take no arguments from the browser; which record a page shows is
165
- server state reached through `ctx`.
162
+ **A URL's path names a page, never a resource.** Do not design
163
+ `/accounts/42`. A row is addressed by `list` and `key`, taken from where the
164
+ element sits.
165
+
166
+ **The query string is what the page has on screen.** Which record a page
167
+ shows, a filter, a date range: each is a query parm,
168
+ `/accounts?acct=23&from=2026-09-09`, so a reload, a bookmark or a mailed link
169
+ shows the same thing. A control writes one with `lb-query-parm="acct"`, which
170
+ replaces the history entry, reloads the page at the new URL, and sends no
171
+ request. A link writes several with an ordinary `lb-nav-link` href. Queries
172
+ still take no arguments: `contextFor(req, parms)` puts parms onto `ctx`, and
173
+ a query reads them there. Read them from `parms`, never `req.query`, because
174
+ `contextFor` may be called twice for one request. A parm is user input, so
175
+ validate it where you read it. Do not keep a selection in server state set
176
+ by an action; that forces `refresh: []` and a hand-written re-answer of
177
+ everything the selection touches.
178
+
179
+ **When a server response changes the query parms.** After an insert, only
180
+ the server knows the new key. After a delete, only the server knows the parm
181
+ should go. Have `run` return `queryParms({ acct: String(id) })`, or
182
+ `queryParms({ acct: "" })` to remove the parm. The page loads at the new
183
+ query string in the same round trip, and the history entry is replaced to
184
+ match. This is Post/Redirect/Get without the redirect: the path never
185
+ changes, and there is no second request. Do not reach for a redirect, a
186
+ hand-written refresh of every query, or server state holding the selection.
187
+ See [refresh and patch](references/page-files.md#refresh-and-patch).
188
+
189
+ **Loadbare is for applications, not sites.** Every route is answered with
190
+ `app.html`, and a path that names no page is found in the browser, not
191
+ answered with a 404. That is the design, not a defect to report.
166
192
 
167
193
  **There are no endpoints to write.** `hubRoutes(hub, contextFor)` is the
168
194
  whole data channel. Declare every action under `actions` and every
@@ -238,8 +264,8 @@ with the package, so no network is needed.
238
264
  - [`references/overview.md`](references/overview.md) — the map of the
239
265
  reference, by part of the application.
240
266
  - [`references/data-binding.md`](references/data-binding.md) — every `lb-`
241
- attribute, requests, forms, conditions and request state. Start here for
242
- anything in a page.
267
+ attribute, requests, forms, query parms, conditions and request state.
268
+ Start here for anything in a page.
243
269
  - [`references/page-files.md`](references/page-files.md) — queries,
244
270
  `onPageEnter`, actions, CRUD, refresh and patch.
245
271
  - [`references/custom-elements.md`](references/custom-elements.md) —
@@ -254,5 +280,7 @@ with the package, so no network is needed.
254
280
  - [`references/widgets.md`](references/widgets.md) — the basic widget
255
281
  library.
256
282
  - [`references/TECHREF-1.0.md`](references/TECHREF-1.0.md) — the reserved
257
- names, and the open items that block 1.0. Read it before designing around
283
+ names, what a URL names and
284
+ [query parms](references/TECHREF-1.0.md#query-parms), and the open items
285
+ that block 1.0. Read it before designing around
258
286
  something the other references do not mention.