@loadbare/app 0.8.0 → 0.8.2
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/skills-cli.d.ts +12 -0
- package/dist/build/skills-cli.d.ts.map +1 -0
- package/dist/build/skills-cli.js +81 -0
- package/dist/build/skills-cli.js.map +1 -0
- package/dist/build/skills.d.ts +47 -0
- package/dist/build/skills.d.ts.map +1 -0
- package/dist/build/skills.js +124 -0
- package/dist/build/skills.js.map +1 -0
- package/dist/core/lb-constants.d.ts +2 -0
- package/dist/core/lb-constants.d.ts.map +1 -1
- package/dist/core/lb-constants.js +18 -4
- package/dist/core/lb-constants.js.map +1 -1
- package/dist/hub/lb-apply.d.ts +10 -0
- package/dist/hub/lb-apply.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +15 -1
- 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 +116 -35
- package/dist/hub/lb-hub.browser.js.map +1 -1
- package/dist/server/lb-express.d.ts +13 -5
- package/dist/server/lb-express.d.ts.map +1 -1
- package/dist/server/lb-express.js +26 -7
- package/dist/server/lb-express.js.map +1 -1
- package/dist/server/lb-server.d.ts +3 -0
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/server/lb-server.js.map +1 -1
- package/docs/TECHREF-1.0.md +93 -22
- package/docs/comparison.md +8 -7
- package/docs/reference/chrome.md +5 -3
- package/docs/reference/data-binding.md +28 -0
- package/docs/reference/server.md +11 -0
- package/docs/reference/widgets.md +8 -4
- package/docs/roadmap.md +16 -0
- package/docs/theory.md +8 -1
- package/docs/tutorials/010-pages-and-navigation.md +2 -1
- package/package.json +8 -4
- package/skills/loadbare-app/SKILL.md +275 -0
- package/skills/loadbare-app/references/TECHREF-1.0.md +1260 -0
- package/skills/loadbare-app/references/builder.md +134 -0
- package/skills/loadbare-app/references/chrome.md +160 -0
- package/skills/loadbare-app/references/css.md +44 -0
- package/skills/loadbare-app/references/custom-elements.md +397 -0
- package/skills/loadbare-app/references/data-binding.md +485 -0
- package/skills/loadbare-app/references/overview.md +38 -0
- package/skills/loadbare-app/references/page-files.md +194 -0
- package/skills/loadbare-app/references/server.md +153 -0
- package/skills/loadbare-app/references/widgets.md +178 -0
|
@@ -8,14 +8,18 @@
|
|
|
8
8
|
*
|
|
9
9
|
* One endpoint, and no second:
|
|
10
10
|
*
|
|
11
|
-
* POST /lb
|
|
11
|
+
* POST /lb/<page>?<query string>
|
|
12
|
+
* an empty body asks for the page's whole query
|
|
12
13
|
* set; a body carrying an action runs that action
|
|
13
14
|
* and answers with its refresh set
|
|
14
15
|
*
|
|
15
|
-
* The page rides on the
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* The page rides on the path rather than in the body, which is what lets the
|
|
17
|
+
* operation set stay closed. The query string is the one the browser is
|
|
18
|
+
* showing, verbatim, so `req.query` holds the page's query parms and
|
|
19
|
+
* `contextFor` reads them there like any other part of the request. What a
|
|
20
|
+
* parm means is the application's: nothing here reads one. Every hub call is
|
|
21
|
+
* a POST, so an application's own GET routes never collide with this one and
|
|
22
|
+
* no page name is reserved.
|
|
19
23
|
*/
|
|
20
24
|
import express from "express";
|
|
21
25
|
import { ACTION_ROW_DELETE, ACTION_ROW_INSERT, ACTION_ROW_UPDATE, LB_ENDPOINT, } from "../core/lb-constants.js";
|
|
@@ -25,8 +29,10 @@ export function hubRoutes(hub, contextFor) {
|
|
|
25
29
|
// Mounted here rather than on the app so that Hub's need for a parsed
|
|
26
30
|
// body is stated where the routes that need it are.
|
|
27
31
|
router.use(express.json());
|
|
28
|
-
router.post(LB_ENDPOINT
|
|
29
|
-
|
|
32
|
+
router.post(`${LB_ENDPOINT}/*page`, async (req, res) => {
|
|
33
|
+
// A page name may hold a slash, so the path below the endpoint is
|
|
34
|
+
// gathered whole.
|
|
35
|
+
const page = req.params.page.join("/");
|
|
30
36
|
const request = req.body;
|
|
31
37
|
const ctx = contextFor(req);
|
|
32
38
|
// No body at all is the page load: the browser is asking for this
|
|
@@ -80,6 +86,19 @@ export function hubRoutes(hub, contextFor) {
|
|
|
80
86
|
res.status(500).json({});
|
|
81
87
|
}
|
|
82
88
|
});
|
|
89
|
+
// A body that never parsed is refused like any other malformed request,
|
|
90
|
+
// rather than falling through to the application's error handler, which
|
|
91
|
+
// would print a stack and call it a server failure. Only for the hub's own
|
|
92
|
+
// path: express.json() above sees every request, and a bad body sent to one
|
|
93
|
+
// of the application's routes is the application's to answer.
|
|
94
|
+
router.use(`${LB_ENDPOINT}/*page`, (err, _req, res, next) => {
|
|
95
|
+
if (err.type !== "entity.parse.failed") {
|
|
96
|
+
next(err);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
console.warn(`loadbare: request body is not JSON`);
|
|
100
|
+
res.status(400).json({});
|
|
101
|
+
});
|
|
83
102
|
return router;
|
|
84
103
|
}
|
|
85
104
|
//# sourceMappingURL=lb-express.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lb-express.js","sourceRoot":"","sources":["../../server/lb-express.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"lb-express.js","sourceRoot":"","sources":["../../server/lb-express.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The Express adapter — the standard hand-off from an Express router to a\n * Hub built by createHub(). Every Hub application wires this same router,\n * which is why it ships from here rather than being copied into each one.\n *\n * Express itself is a peer dependency: nothing outside this file imports it,\n * so an application that hosts Hub some other way never pays for it.\n *\n * One endpoint, and no second:\n *\n * POST /lb/<page>?<query string>\n * an empty body asks for the page's whole query\n * set; a body carrying an action runs that action\n * and answers with its refresh set\n *\n * The page rides on the path rather than in the body, which is what lets the\n * operation set stay closed. The query string is the one the browser is\n * showing, verbatim, so `req.query` holds the page's query parms and\n * `contextFor` reads them there like any other part of the request. What a\n * parm means is the application's: nothing here reads one. Every hub call is\n * a POST, so an application's own GET routes never collide with this one and\n * no page name is reserved.\n */\n\nimport express, {\n type NextFunction,\n type Request,\n type Response,\n type Router,\n} from \"express\";\nimport {\n ACTION_ROW_DELETE,\n ACTION_ROW_INSERT,\n ACTION_ROW_UPDATE,\n LB_ENDPOINT,\n} from \"../core/lb-constants.js\";\nimport { isOperation, type HubRequest } from \"../core/lb-types.js\";\nimport type { Hub, HubContext } from \"./lb-server.js\";\n\n/**\n * Building the context is the application's job — it is where an\n * authenticated database handle comes from — so it is injected rather than\n * assumed. Whatever established identity has already run by the time this\n * router is reached; where that happens is the application's ordering\n * decision, not Hub's.\n *\n * The query parms arrive here too, on `req.query`. They are user input like\n * any other, typed into an address bar or mailed in a link, and are\n * validated where they are read.\n */\nexport type ContextFor = (req: Request) => HubContext;\n\nexport function hubRoutes(hub: Hub, contextFor: ContextFor): Router {\n const router = express.Router();\n\n // Mounted here rather than on the app so that Hub's need for a parsed\n // body is stated where the routes that need it are.\n router.use(express.json());\n\n router.post(`${LB_ENDPOINT}/*page`, async (req, res) => {\n // A page name may hold a slash, so the path below the endpoint is\n // gathered whole.\n const page = (req.params.page as string[]).join(\"/\");\n const request = req.body as HubRequest | undefined;\n const ctx = contextFor(req);\n\n // No body at all is the page load: the browser is asking for this\n // page's whole query set, which is every request it makes that names no\n // action. A body that carries something but not an action is malformed.\n if (request === undefined || Object.keys(request).length === 0) {\n res.json(await hub.dataForPage(page, ctx));\n return;\n }\n\n if (!request.action) {\n console.warn(`loadbare: request without an action`);\n res.status(400).json({});\n return;\n }\n\n // A thrown error is how application code (a query, an action, a CRUD\n // op) says \"this request is invalid\" or hits an unexpected failure —\n // it resolves to a real response rather than an unhandled rejection, so\n // the browser side has something to catch.\n try {\n // The reserved prefix is the whole of the discriminant: a name so\n // prefixed is one of the three operations, and anything else is a name\n // the page declared. Nothing else tells them apart, here or anywhere.\n if (!isOperation(request)) {\n const { action, list, row, key, cell, value } = request;\n res.json(\n await hub.runAction(\n page,\n action,\n { list, row, key, cell, value },\n ctx,\n ),\n );\n return;\n }\n switch (request.action) {\n case ACTION_ROW_DELETE: {\n const { list, key } = request;\n res.json(await hub.runRowDelete(page, { list, key }, ctx));\n return;\n }\n case ACTION_ROW_INSERT: {\n const { list, values } = request;\n res.json(await hub.runRowInsert(page, { list, values }, ctx));\n return;\n }\n case ACTION_ROW_UPDATE: {\n const { list, key, values } = request;\n res.json(await hub.runRowUpdate(page, { list, key, values }, ctx));\n return;\n }\n default:\n console.warn(\n `loadbare: operation '${(request as HubRequest).action}' not implemented`,\n );\n res.status(400).json({});\n }\n } catch (err) {\n console.error(`loadbare: '${request.action}' threw`, err);\n res.status(500).json({});\n }\n });\n\n // A body that never parsed is refused like any other malformed request,\n // rather than falling through to the application's error handler, which\n // would print a stack and call it a server failure. Only for the hub's own\n // path: express.json() above sees every request, and a bad body sent to one\n // of the application's routes is the application's to answer.\n router.use(\n `${LB_ENDPOINT}/*page`,\n (err: unknown, _req: Request, res: Response, next: NextFunction) => {\n if ((err as { type?: string }).type !== \"entity.parse.failed\") {\n next(err);\n return;\n }\n console.warn(`loadbare: request body is not JSON`);\n res.status(400).json({});\n },\n );\n\n return router;\n}\n"]}
|
|
@@ -14,6 +14,9 @@ import type { Patch, Row, HubData, HubResult } from "../core/lb-types.js";
|
|
|
14
14
|
* That is why no type on this page takes a type parameter. The context is a
|
|
15
15
|
* request-scoped handle — an authenticated database connection is the
|
|
16
16
|
* expected case — so it is passed per call rather than held by the engine.
|
|
17
|
+
* The page's query parms are the other expected member: a query that reads
|
|
18
|
+
* one off the context takes no argument, and re-runs under `refresh` like
|
|
19
|
+
* any other.
|
|
17
20
|
*/
|
|
18
21
|
export interface HubContext {
|
|
19
22
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lb-server.d.ts","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAG1E
|
|
1
|
+
{"version":3,"file":"lb-server.d.ts","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lb-server.js","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"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","sourcesContent":["// The server-side contract and the engine that runs it.\n//\n// This is the counterpart to core/lb-constants.ts: that file is the\n// vocabulary the browser writes, this one is the vocabulary the application\n// writes. The engine below is the whole of Hub on the server. It ships no\n// HTTP server, no router, and no data layer.\n\nimport type { Patch, Row, HubData, HubResult } from \"../core/lb-types.js\";\nimport { LB_RESERVED_PREFIX } from \"../core/lb-constants.js\";\n\n/**\n * Whatever the application hands the engine for the duration of one request.\n *\n * Hub declares it empty and never reads it. An application fills it in by\n * declaration merging, once, anywhere in its own source:\n *\n * declare module \"@loadbare/app/server\" {\n * interface HubContext {\n * db: Db;\n * }\n * }\n *\n * That is why no type on this page takes a type parameter. The context is a\n * request-scoped handle — an authenticated database connection is the\n * expected case — so it is passed per call rather than held by the engine.\n */\nexport interface HubContext {}\n\n/**\n * A declared query: the request context in, one result out.\n *\n * Cardinality is a property of the name rather than of any one answer, so it\n * is declared here and never inferred from what comes back. One name answers\n * with one shape, always. A page that wants the roster once as a single row\n * and once as a set declares two queries.\n *\n * Write one with `row()` or `list()` below; nothing else builds one.\n */\nexport interface Query {\n readonly kind: \"row\" | \"list\";\n readonly run: (ctx: HubContext) => HubResult | Promise<HubResult>;\n}\n\n/** A query that answers with one row. */\nexport function row(run: (ctx: HubContext) => Row | Promise<Row>): Query {\n return { kind: \"row\", run };\n}\n\n/**\n * A query that answers with the entire set, and therefore also the order. A\n * list reconciles to exactly this: a row whose key is not here is gone.\n *\n * There is no wrapper around the array, because the declaration already said\n * this name answers with rows.\n */\nexport function list(run: (ctx: HubContext) => Row[] | Promise<Row[]>): Query {\n return { kind: \"list\", run };\n}\n\n/**\n * Only what changed, returned from a `crud` run rather than from a query.\n * Rows named here arrive or are updated, keys in `drop` are gone, and\n * everything unnamed is left alone — its contents, and its place in whatever\n * order the widget is keeping.\n *\n * `Array.isArray` is what tells a patch from a whole set, so a patch needs no\n * marker of its own and no column name is reserved to carry one.\n */\nexport function patch(change: { rows?: Row[]; drop?: string[] }): Patch {\n return { ...change };\n}\n\n/** The shape of a `<name>.queries.ts` module. */\nexport type Queries = Record<string, Query>;\n\n/**\n * Where the interaction happened, in the binding vocabulary, plus the one\n * value a control may carry.\n *\n * The browser fills these from attributes it already has. It never names a\n * function — only a name the page declared — which is what keeps this from\n * being an RPC endpoint. A value may ride along because a `<select>` has one\n * and there is nowhere else to put it; it is a string from a control, not an\n * argument list.\n */\nexport interface Where {\n list?: string;\n row?: string;\n key?: string;\n cell?: string;\n value?: string;\n}\n\n/**\n * What an action does, and which queries must re-run once it has.\n *\n * `run` may also return results of its own, which are laid over the refreshed\n * ones. That is how a patch reaches the browser: a query answers for its\n * whole set and cannot know why it was re-run, but the action knows exactly\n * what it changed and can say only that.\n */\nexport interface Action {\n run: (\n ctx: HubContext,\n where: Where,\n ) => void | HubData | Promise<void | HubData>;\n refresh: string[];\n}\n\n/**\n * One CRUD operation on a declared query. Same shape as `Action` — run, then\n * refresh — but `where` carries only what that operation is typed to carry\n * on the wire, rather than the general `Where`.\n */\nexport interface CrudOp<W> {\n run: (ctx: HubContext, where: W) => void | HubData | Promise<void | HubData>;\n refresh: string[];\n}\n\nexport type RowDeleteOp = CrudOp<{ key: string }>;\nexport type RowInsertOp = CrudOp<{ values: Record<string, string> }>;\n/** `values` holds only the columns being set; see `Hub.runRowUpdate`. */\nexport type RowUpdateOp = CrudOp<{\n key: string;\n values: Record<string, string>;\n}>;\n\n/**\n * The CRUD operations declared for one list, keyed by its name in\n * `Requests.crud`. A name with no entry here permits none of them — the wire\n * cannot reach anything the page has not published, exactly as for a named\n * action.\n *\n * Every key here is the reserved `lb-action` value with the prefix stripped\n * and the rest camel-cased, so the attribute, the wire field and this key\n * are one vocabulary. All three are list operations: each needs a key, and a\n * key exists only on a live row inside a list.\n */\nexport interface Crud {\n rowDelete?: RowDeleteOp;\n rowInsert?: RowInsertOp;\n rowUpdate?: RowUpdateOp;\n}\n\n/**\n * The shape of a `<name>.requests.ts` module.\n *\n * `onPageEnter` runs once when the page is entered, before any query. It\n * declares no refresh set: entering the page runs the whole query set\n * afterward, so whatever the hook changed is already in the response.\n *\n * `actions` names what this page may be asked to do. A name the page did not\n * declare is refused, so the wire cannot reach anything the page has not\n * published.\n *\n * `crud` is the same rule for the typed CRUD operations, keyed by the list\n * they operate on rather than by a declared name — there is nothing to name,\n * since the row's own binding says what it is.\n */\nexport interface Requests {\n onPageEnter?: (ctx: HubContext) => void | Promise<void>;\n actions?: Record<string, Action>;\n crud?: Record<string, Crud>;\n}\n\n/** A page is three files sharing a basename; two of them are these. */\nexport interface Page {\n queries: Queries;\n requests: Requests;\n}\n\n/**\n * The page registry. The builder populates this automatically from every\n * `.requests.ts`/`.queries.ts` pair it discovers — see docs/reference/builder.md,\n * \"Generating the server-side page registry\".\n */\nexport type Pages = Record<string, Page>;\n\nexport interface Hub {\n /** Entering a page: run its onPageEnter hook, then all of its queries. */\n dataForPage(page: string, ctx: HubContext): Promise<HubData>;\n\n /**\n * An action: run what the page declared under that name, then the refresh\n * set declared with it.\n */\n runAction(\n page: string,\n name: string,\n where: Where,\n ctx: HubContext,\n ): Promise<HubData>;\n\n /** Drop one row: run the list's declared `rowDelete`, then its refresh set. */\n runRowDelete(\n page: string,\n where: { list: string; key: string },\n ctx: HubContext,\n ): Promise<HubData>;\n\n /** Add one row: run the list's declared `rowInsert`, then its refresh set. */\n runRowInsert(\n page: string,\n where: { list: string; values: Record<string, string> },\n ctx: HubContext,\n ): Promise<HubData>;\n\n /**\n * Edit a row: run the list's declared `rowUpdate`, then its refresh set.\n * `values` holds only the columns being set — one from a widget that\n * commits a cell, several from a form — and a column absent from it is left\n * as it is, as an SQL UPDATE leaves it.\n */\n runRowUpdate(\n page: string,\n where: { list: string; key: string; values: Record<string, string> },\n ctx: HubContext,\n ): Promise<HubData>;\n}\n\n/**\n * Build the engine over a set of pages. The pages are fixed at startup; the\n * context is not, and arrives with each call.\n */\nexport function createHub(pages: Pages): Hub {\n // Names beginning with the reserved prefix are Loadbare's — the hub's\n // own query, the CRUD operations — so an application cannot declare one.\n // Refused at startup, because a name is a fact about the page and not\n // about any request.\n for (const [page, entry] of Object.entries(pages)) {\n const declared = [\n ...Object.keys(entry.queries),\n ...Object.keys(entry.requests.actions ?? {}),\n ];\n for (const name of declared) {\n if (name.startsWith(LB_RESERVED_PREFIX)) {\n throw new Error(\n `loadbare: page '${page}' declares '${name}', but names ` +\n `beginning with '${LB_RESERVED_PREFIX}' are reserved`,\n );\n }\n }\n }\n\n async function run(\n page: string,\n names: string[],\n ctx: HubContext,\n ): Promise<HubData> {\n const data: HubData = {};\n for (const name of names) {\n const query = pages[page]?.queries[name];\n if (!query) {\n console.warn(`loadbare: page '${page}' declares no query '${name}'`);\n continue;\n }\n const result = await query.run(ctx);\n // Cardinality is declared, so an answer that disagrees is a mistake in\n // the query rather than a case to handle. Refused here, because a page\n // is better off missing one name than showing the wrong shape for it.\n if (Array.isArray(result) !== (query.kind === \"list\")) {\n console.warn(\n `loadbare: query '${name}' is declared ${query.kind} but answered ` +\n `with ${Array.isArray(result) ? \"rows\" : \"one row\"}`,\n );\n continue;\n }\n data[name] = result;\n }\n return data;\n }\n\n /**\n * Shared by every operation kind: run what was declared, then its refresh\n * set against the same context, laying what the operation itself stated\n * over the refreshed queries — the narrower answer wins because it is the\n * one that knows what actually changed.\n */\n async function settle<W>(\n page: string,\n declared: CrudOp<W> | Action | undefined,\n where: W,\n ctx: HubContext,\n notFound: string,\n ): Promise<HubData> {\n if (!declared) {\n console.warn(notFound);\n return {};\n }\n const stated = await declared.run(ctx, where as never);\n return { ...(await run(page, declared.refresh, ctx)), ...(stated ?? {}) };\n }\n\n return {\n async dataForPage(page, ctx) {\n const entry = pages[page];\n if (!entry) {\n console.warn(`loadbare: no page '${page}'`);\n return {};\n }\n await entry.requests.onPageEnter?.(ctx);\n return run(page, Object.keys(entry.queries), ctx);\n },\n\n runAction(page, name, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.actions?.[name],\n where,\n ctx,\n `loadbare: page '${page}' declares no action '${name}'`,\n );\n },\n\n runRowDelete(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.rowDelete,\n { key: where.key },\n ctx,\n `loadbare: page '${page}' declares no rowDelete for '${where.list}'`,\n );\n },\n\n runRowInsert(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.rowInsert,\n { values: where.values },\n ctx,\n `loadbare: page '${page}' declares no rowInsert for '${where.list}'`,\n );\n },\n\n runRowUpdate(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.rowUpdate,\n { key: where.key, values: where.values },\n ctx,\n `loadbare: page '${page}' declares no rowUpdate for '${where.list}'`,\n );\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"lb-server.js","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"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","sourcesContent":["// The server-side contract and the engine that runs it.\n//\n// This is the counterpart to core/lb-constants.ts: that file is the\n// vocabulary the browser writes, this one is the vocabulary the application\n// writes. The engine below is the whole of Hub on the server. It ships no\n// HTTP server, no router, and no data layer.\n\nimport type { Patch, Row, HubData, HubResult } from \"../core/lb-types.js\";\nimport { LB_RESERVED_PREFIX } from \"../core/lb-constants.js\";\n\n/**\n * Whatever the application hands the engine for the duration of one request.\n *\n * Hub declares it empty and never reads it. An application fills it in by\n * declaration merging, once, anywhere in its own source:\n *\n * declare module \"@loadbare/app/server\" {\n * interface HubContext {\n * db: Db;\n * }\n * }\n *\n * That is why no type on this page takes a type parameter. The context is a\n * request-scoped handle — an authenticated database connection is the\n * expected case — so it is passed per call rather than held by the engine.\n * The page's query parms are the other expected member: a query that reads\n * one off the context takes no argument, and re-runs under `refresh` like\n * any other.\n */\nexport interface HubContext {}\n\n/**\n * A declared query: the request context in, one result out.\n *\n * Cardinality is a property of the name rather than of any one answer, so it\n * is declared here and never inferred from what comes back. One name answers\n * with one shape, always. A page that wants the roster once as a single row\n * and once as a set declares two queries.\n *\n * Write one with `row()` or `list()` below; nothing else builds one.\n */\nexport interface Query {\n readonly kind: \"row\" | \"list\";\n readonly run: (ctx: HubContext) => HubResult | Promise<HubResult>;\n}\n\n/** A query that answers with one row. */\nexport function row(run: (ctx: HubContext) => Row | Promise<Row>): Query {\n return { kind: \"row\", run };\n}\n\n/**\n * A query that answers with the entire set, and therefore also the order. A\n * list reconciles to exactly this: a row whose key is not here is gone.\n *\n * There is no wrapper around the array, because the declaration already said\n * this name answers with rows.\n */\nexport function list(run: (ctx: HubContext) => Row[] | Promise<Row[]>): Query {\n return { kind: \"list\", run };\n}\n\n/**\n * Only what changed, returned from a `crud` run rather than from a query.\n * Rows named here arrive or are updated, keys in `drop` are gone, and\n * everything unnamed is left alone — its contents, and its place in whatever\n * order the widget is keeping.\n *\n * `Array.isArray` is what tells a patch from a whole set, so a patch needs no\n * marker of its own and no column name is reserved to carry one.\n */\nexport function patch(change: { rows?: Row[]; drop?: string[] }): Patch {\n return { ...change };\n}\n\n/** The shape of a `<name>.queries.ts` module. */\nexport type Queries = Record<string, Query>;\n\n/**\n * Where the interaction happened, in the binding vocabulary, plus the one\n * value a control may carry.\n *\n * The browser fills these from attributes it already has. It never names a\n * function — only a name the page declared — which is what keeps this from\n * being an RPC endpoint. A value may ride along because a `<select>` has one\n * and there is nowhere else to put it; it is a string from a control, not an\n * argument list.\n */\nexport interface Where {\n list?: string;\n row?: string;\n key?: string;\n cell?: string;\n value?: string;\n}\n\n/**\n * What an action does, and which queries must re-run once it has.\n *\n * `run` may also return results of its own, which are laid over the refreshed\n * ones. That is how a patch reaches the browser: a query answers for its\n * whole set and cannot know why it was re-run, but the action knows exactly\n * what it changed and can say only that.\n */\nexport interface Action {\n run: (\n ctx: HubContext,\n where: Where,\n ) => void | HubData | Promise<void | HubData>;\n refresh: string[];\n}\n\n/**\n * One CRUD operation on a declared query. Same shape as `Action` — run, then\n * refresh — but `where` carries only what that operation is typed to carry\n * on the wire, rather than the general `Where`.\n */\nexport interface CrudOp<W> {\n run: (ctx: HubContext, where: W) => void | HubData | Promise<void | HubData>;\n refresh: string[];\n}\n\nexport type RowDeleteOp = CrudOp<{ key: string }>;\nexport type RowInsertOp = CrudOp<{ values: Record<string, string> }>;\n/** `values` holds only the columns being set; see `Hub.runRowUpdate`. */\nexport type RowUpdateOp = CrudOp<{\n key: string;\n values: Record<string, string>;\n}>;\n\n/**\n * The CRUD operations declared for one list, keyed by its name in\n * `Requests.crud`. A name with no entry here permits none of them — the wire\n * cannot reach anything the page has not published, exactly as for a named\n * action.\n *\n * Every key here is the reserved `lb-action` value with the prefix stripped\n * and the rest camel-cased, so the attribute, the wire field and this key\n * are one vocabulary. All three are list operations: each needs a key, and a\n * key exists only on a live row inside a list.\n */\nexport interface Crud {\n rowDelete?: RowDeleteOp;\n rowInsert?: RowInsertOp;\n rowUpdate?: RowUpdateOp;\n}\n\n/**\n * The shape of a `<name>.requests.ts` module.\n *\n * `onPageEnter` runs once when the page is entered, before any query. It\n * declares no refresh set: entering the page runs the whole query set\n * afterward, so whatever the hook changed is already in the response.\n *\n * `actions` names what this page may be asked to do. A name the page did not\n * declare is refused, so the wire cannot reach anything the page has not\n * published.\n *\n * `crud` is the same rule for the typed CRUD operations, keyed by the list\n * they operate on rather than by a declared name — there is nothing to name,\n * since the row's own binding says what it is.\n */\nexport interface Requests {\n onPageEnter?: (ctx: HubContext) => void | Promise<void>;\n actions?: Record<string, Action>;\n crud?: Record<string, Crud>;\n}\n\n/** A page is three files sharing a basename; two of them are these. */\nexport interface Page {\n queries: Queries;\n requests: Requests;\n}\n\n/**\n * The page registry. The builder populates this automatically from every\n * `.requests.ts`/`.queries.ts` pair it discovers — see docs/reference/builder.md,\n * \"Generating the server-side page registry\".\n */\nexport type Pages = Record<string, Page>;\n\nexport interface Hub {\n /** Entering a page: run its onPageEnter hook, then all of its queries. */\n dataForPage(page: string, ctx: HubContext): Promise<HubData>;\n\n /**\n * An action: run what the page declared under that name, then the refresh\n * set declared with it.\n */\n runAction(\n page: string,\n name: string,\n where: Where,\n ctx: HubContext,\n ): Promise<HubData>;\n\n /** Drop one row: run the list's declared `rowDelete`, then its refresh set. */\n runRowDelete(\n page: string,\n where: { list: string; key: string },\n ctx: HubContext,\n ): Promise<HubData>;\n\n /** Add one row: run the list's declared `rowInsert`, then its refresh set. */\n runRowInsert(\n page: string,\n where: { list: string; values: Record<string, string> },\n ctx: HubContext,\n ): Promise<HubData>;\n\n /**\n * Edit a row: run the list's declared `rowUpdate`, then its refresh set.\n * `values` holds only the columns being set — one from a widget that\n * commits a cell, several from a form — and a column absent from it is left\n * as it is, as an SQL UPDATE leaves it.\n */\n runRowUpdate(\n page: string,\n where: { list: string; key: string; values: Record<string, string> },\n ctx: HubContext,\n ): Promise<HubData>;\n}\n\n/**\n * Build the engine over a set of pages. The pages are fixed at startup; the\n * context is not, and arrives with each call.\n */\nexport function createHub(pages: Pages): Hub {\n // Names beginning with the reserved prefix are Loadbare's — the hub's\n // own query, the CRUD operations — so an application cannot declare one.\n // Refused at startup, because a name is a fact about the page and not\n // about any request.\n for (const [page, entry] of Object.entries(pages)) {\n const declared = [\n ...Object.keys(entry.queries),\n ...Object.keys(entry.requests.actions ?? {}),\n ];\n for (const name of declared) {\n if (name.startsWith(LB_RESERVED_PREFIX)) {\n throw new Error(\n `loadbare: page '${page}' declares '${name}', but names ` +\n `beginning with '${LB_RESERVED_PREFIX}' are reserved`,\n );\n }\n }\n }\n\n async function run(\n page: string,\n names: string[],\n ctx: HubContext,\n ): Promise<HubData> {\n const data: HubData = {};\n for (const name of names) {\n const query = pages[page]?.queries[name];\n if (!query) {\n console.warn(`loadbare: page '${page}' declares no query '${name}'`);\n continue;\n }\n const result = await query.run(ctx);\n // Cardinality is declared, so an answer that disagrees is a mistake in\n // the query rather than a case to handle. Refused here, because a page\n // is better off missing one name than showing the wrong shape for it.\n if (Array.isArray(result) !== (query.kind === \"list\")) {\n console.warn(\n `loadbare: query '${name}' is declared ${query.kind} but answered ` +\n `with ${Array.isArray(result) ? \"rows\" : \"one row\"}`,\n );\n continue;\n }\n data[name] = result;\n }\n return data;\n }\n\n /**\n * Shared by every operation kind: run what was declared, then its refresh\n * set against the same context, laying what the operation itself stated\n * over the refreshed queries — the narrower answer wins because it is the\n * one that knows what actually changed.\n */\n async function settle<W>(\n page: string,\n declared: CrudOp<W> | Action | undefined,\n where: W,\n ctx: HubContext,\n notFound: string,\n ): Promise<HubData> {\n if (!declared) {\n console.warn(notFound);\n return {};\n }\n const stated = await declared.run(ctx, where as never);\n return { ...(await run(page, declared.refresh, ctx)), ...(stated ?? {}) };\n }\n\n return {\n async dataForPage(page, ctx) {\n const entry = pages[page];\n if (!entry) {\n console.warn(`loadbare: no page '${page}'`);\n return {};\n }\n await entry.requests.onPageEnter?.(ctx);\n return run(page, Object.keys(entry.queries), ctx);\n },\n\n runAction(page, name, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.actions?.[name],\n where,\n ctx,\n `loadbare: page '${page}' declares no action '${name}'`,\n );\n },\n\n runRowDelete(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.rowDelete,\n { key: where.key },\n ctx,\n `loadbare: page '${page}' declares no rowDelete for '${where.list}'`,\n );\n },\n\n runRowInsert(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.rowInsert,\n { values: where.values },\n ctx,\n `loadbare: page '${page}' declares no rowInsert for '${where.list}'`,\n );\n },\n\n runRowUpdate(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.rowUpdate,\n { key: where.key, values: where.values },\n ctx,\n `loadbare: page '${page}' declares no rowUpdate for '${where.list}'`,\n );\n },\n };\n}\n"]}
|
package/docs/TECHREF-1.0.md
CHANGED
|
@@ -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,8 @@ 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
|
|
511
|
-
|
|
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).
|
|
512
504
|
|
|
513
505
|
### Requests
|
|
514
506
|
|
|
@@ -638,15 +630,16 @@ A failed insert resets nothing, so the entry can be corrected.
|
|
|
638
630
|
|
|
639
631
|
A navigation that fails to load its data sets nothing. No element
|
|
640
632
|
dispatched it, so there is nothing to stamp, and the hub reports it to the
|
|
641
|
-
console.
|
|
633
|
+
console. A load started by a control writing a query parm stamps that
|
|
634
|
+
control, as a request stamps its origin.
|
|
642
635
|
|
|
643
636
|
### Links
|
|
644
637
|
|
|
645
638
|
---- UNEDITED ----
|
|
646
639
|
|
|
647
640
|
An anchor carrying `lb-nav-link` navigates inside the application: the hub
|
|
648
|
-
catches the click, pushes the anchor's path onto history,
|
|
649
|
-
host in `<main>`. An anchor without it is left alone and behaves like any
|
|
641
|
+
catches the click, pushes the anchor's path and query string onto history,
|
|
642
|
+
and swaps the page host in `<main>`. An anchor without it is left alone and behaves like any
|
|
650
643
|
other link, so leaving the application is the default and staying in it is
|
|
651
644
|
the opt-in.
|
|
652
645
|
|
|
@@ -660,6 +653,11 @@ link to determine the path.
|
|
|
660
653
|
Path space is flat. A path such as `/members` links to the `members.*` files
|
|
661
654
|
on the server.
|
|
662
655
|
|
|
656
|
+
The query string is kept. `/transactions?date_begin=2026-09-01` opens the
|
|
657
|
+
transactions page narrowed to those dates, and a link to the page it is
|
|
658
|
+
already on loads that page again at the new URL without replacing its DOM —
|
|
659
|
+
see [Query parms](#query-parms).
|
|
660
|
+
|
|
663
661
|
The bare path `/` resolves to `index`.
|
|
664
662
|
|
|
665
663
|
A path that names no page is detected in the browser, after a successful
|
|
@@ -680,19 +678,77 @@ See also [Navigation Row lb-navigation](#lb-navigation).
|
|
|
680
678
|
|
|
681
679
|
#### What a URL names
|
|
682
680
|
|
|
683
|
-
|
|
684
|
-
and Loadbare/app does not reproduce REST: `/accounts/42` is not a
|
|
685
|
-
reach account 42.
|
|
681
|
+
The path names a page, a place in the application. It never names a
|
|
682
|
+
resource, and Loadbare/app does not reproduce REST: `/accounts/42` is not a
|
|
683
|
+
way to reach account 42.
|
|
684
|
+
|
|
685
|
+
The query string describes what that page has on screen:
|
|
686
|
+
`/accounts?acct=23&from=2026-09-09`. It never commands, and the server
|
|
687
|
+
remembers nothing, so a URL is a reproducible view. Reload it, bookmark it,
|
|
688
|
+
or mail it to someone, and what they see is what the sender saw.
|
|
686
689
|
|
|
687
690
|
A page declares what it shows, its queries, and what it allows, its actions.
|
|
688
691
|
Loading a page runs its queries. A request performs an action at a position.
|
|
689
692
|
A row is addressed by `list` and `key`, taken from where the element sits,
|
|
690
693
|
which is how the database already names it. Nothing is fetched by URL, so an
|
|
691
|
-
application designs no endpoints
|
|
692
|
-
|
|
694
|
+
application designs no endpoints.
|
|
695
|
+
|
|
696
|
+
That last is where query parms move Loadbare's position. A query still takes
|
|
697
|
+
no argument from the browser, and a parm arrives the way the session cookie
|
|
698
|
+
does, as part of the request the context is built from. But a user who
|
|
699
|
+
types `?acct=99999` now influences what a query returns. A query parm is
|
|
700
|
+
user input, validated like any other where the application reads it, and a
|
|
701
|
+
per-session database role means an id outside the caller's reach finds
|
|
702
|
+
nothing.
|
|
703
|
+
|
|
704
|
+
Loadbare is for applications, not sites. Every route is answered with the
|
|
705
|
+
same document, and a path that names no page is found out in the browser —
|
|
706
|
+
see [Links](#links).
|
|
707
|
+
|
|
708
|
+
#### Query parms
|
|
709
|
+
|
|
710
|
+
A control that narrows what a page shows writes its value into the query
|
|
711
|
+
string, rather than sending it:
|
|
712
|
+
|
|
713
|
+
```html
|
|
714
|
+
<lb-options lb-list="teams" lb-query-parm="team" exp-label="Team:">
|
|
715
|
+
<option value="">Every team</option>
|
|
716
|
+
<template lb-key="id"><option lb-cell="name"></option></template>
|
|
717
|
+
</lb-options>
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
| Attribute | Written by | Behavior |
|
|
721
|
+
| -------------------- | ---------- | --------------------------------------------------------------- |
|
|
722
|
+
| `lb-query-parm` | Developer | On a control: its `change` writes this parm and reloads the page |
|
|
723
|
+
| `lb-query-parm-push` | Developer | With `lb-query-parm`: the write pushes a history entry |
|
|
693
724
|
|
|
694
|
-
|
|
695
|
-
one
|
|
725
|
+
On `change`, the hub reads the control's value the way a form reads a cell,
|
|
726
|
+
and sets that one parm in the URL. Every other parm is left as it is, since
|
|
727
|
+
it may have arrived by link with no control on screen to say it again. An
|
|
728
|
+
empty value takes the parm out, so a URL is as long as the user has narrowed
|
|
729
|
+
the page. A value the URL already carries does nothing.
|
|
730
|
+
|
|
731
|
+
The write replaces the current history entry: changing what a page shows is
|
|
732
|
+
not going anywhere, so Back leaves the page rather than walking back through
|
|
733
|
+
every choice. `lb-query-parm-push` makes the write push an entry instead.
|
|
734
|
+
|
|
735
|
+
The page then loads at the new URL exactly as a cold load of that URL would:
|
|
736
|
+
`onPageEnter`, then every query. The page's DOM is kept, and the answer
|
|
737
|
+
lands by key, so a list that gets its rows back keeps them and its scroll
|
|
738
|
+
position.
|
|
739
|
+
|
|
740
|
+
After every load — cold, by link, by Back, by a write — the hub lands each
|
|
741
|
+
parm on the control that writes it, and an absent parm lands empty. A
|
|
742
|
+
control therefore shows what the address bar says.
|
|
743
|
+
|
|
744
|
+
A control that writes a query parm sends no request. One that also carries
|
|
745
|
+
`lb-action` has that request refused, since the choice would otherwise be
|
|
746
|
+
sent twice.
|
|
747
|
+
|
|
748
|
+
Every round trip carries the query string the browser is showing, a page
|
|
749
|
+
load and an action alike. The server reads the parms off `req.query` in
|
|
750
|
+
`contextFor` — see [The Express server](#the-express-server). Nothing in
|
|
751
|
+
Loadbare assigns a parm a meaning.
|
|
696
752
|
|
|
697
753
|
### lb-navigation
|
|
698
754
|
|
|
@@ -706,10 +762,11 @@ like a server-produced result.
|
|
|
706
762
|
| Cell | Holds |
|
|
707
763
|
| ------------ | ------------------------------------------------------------------- |
|
|
708
764
|
| `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
|
|
765
|
+
| `page-uri` | The path and query string as the browser has them, such as `/members?team=Engines` |
|
|
710
766
|
|
|
711
767
|
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)
|
|
768
|
+
`lb-nav-link` link in the document (presumably in a nav bar) whose path matches the
|
|
769
|
+
current path, so the query string does not change it.
|
|
713
770
|
|
|
714
771
|
The row lands before the page is looked up, so a path that names no page has
|
|
715
772
|
it too, and an `lb-unknown-page` dialog, where the chrome has one, displays
|
|
@@ -871,6 +928,18 @@ Explaining Express is beyond the scope of this technical reference. The
|
|
|
871
928
|
only real requirement is that the catch-all for app.html is at the end,
|
|
872
929
|
so it does not catch any other files.
|
|
873
930
|
|
|
931
|
+
`hubRoutes` answers `POST /lb/<page>`, with the query string the browser is
|
|
932
|
+
showing after it, verbatim. So `req.query` holds the page's query parms, and
|
|
933
|
+
`contextFor` puts on the context whatever a query reads from them. They are
|
|
934
|
+
user input:
|
|
935
|
+
|
|
936
|
+
```ts
|
|
937
|
+
function contextFor(req: Request): HubContext {
|
|
938
|
+
const { acct } = req.query;
|
|
939
|
+
return { db: openDb(), acct: typeof acct === "string" ? acct : "" };
|
|
940
|
+
}
|
|
941
|
+
```
|
|
942
|
+
|
|
874
943
|
Give the server the origin root. The hub reaches its own endpoints by
|
|
875
944
|
absolute path, so an application cannot be hosted under a subpath such as
|
|
876
945
|
`example.com/myapp/`, and anything proxying in front of the server passes
|
|
@@ -1148,6 +1217,8 @@ Each attribute is defined in one section, and this table says which.
|
|
|
1148
1217
|
| `lb-value` | Hub | [How a value lands](#how-a-value-lands) |
|
|
1149
1218
|
| `lb-action` | Developer | [Requests](#requests) |
|
|
1150
1219
|
| `lb-nav-link` | Developer | [Links](#links) |
|
|
1220
|
+
| `lb-query-parm` | Developer | [Query parms](#query-parms) |
|
|
1221
|
+
| `lb-query-parm-push` | Developer | [Query parms](#query-parms) |
|
|
1151
1222
|
| `lb-pending` | Hub | [Request state](#request-state) |
|
|
1152
1223
|
| `lb-error` | Hub | [Request state](#request-state) |
|
|
1153
1224
|
| `lb-row-count` | Hub | [Request state](#request-state) |
|
package/docs/comparison.md
CHANGED
|
@@ -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
|
|
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
|
|
683
|
-
|
|
684
|
-
| `GET /<path>`
|
|
685
|
-
| `GET /client.js`
|
|
686
|
-
| `GET /app.css`
|
|
687
|
-
| `POST /lb
|
|
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
|
package/docs/reference/chrome.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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,34 @@ 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
|
+
The server reads the parms off `req.query` in `contextFor`; see
|
|
332
|
+
[the Express server](./server.md#database-layer). See
|
|
333
|
+
[Query parms](../TECHREF-1.0.md#query-parms) for the whole rule.
|
|
334
|
+
|
|
307
335
|
## Conditional rendering
|
|
308
336
|
|
|
309
337
|
Loadbare ships static HTML and hydrates elements that are already in the
|
package/docs/reference/server.md
CHANGED
|
@@ -137,6 +137,17 @@ declare module "@loadbare/app/server" {
|
|
|
137
137
|
}
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
+
The query string the browser is showing arrives on every data request, so
|
|
141
|
+
`req.query` holds the page's query parms. Put on the context whatever a query
|
|
142
|
+
reads from them, and treat them as user input:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
function contextFor(req: Request): HubContext {
|
|
146
|
+
const { team } = req.query;
|
|
147
|
+
return { db: openDb(), team: typeof team === "string" ? team : "" };
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
140
151
|
Add a field for anything else a request needs — the authenticated user, a
|
|
141
152
|
request id, a feature flag set. Queries and requests read them from `ctx`; see
|
|
142
153
|
[page files](./page-files.md).
|
|
@@ -19,8 +19,8 @@ npm install @loadbare/widgets
|
|
|
19
19
|
export default ["@loadbare/widgets"];
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
See [
|
|
23
|
-
|
|
22
|
+
See [Widgets from packages](./builder.md#widgets-from-packages) for what
|
|
23
|
+
listing a package does, and [The Builder](./builder.md#where-the-builder-looks)
|
|
24
24
|
for where a listed package sits in the cascade.
|
|
25
25
|
|
|
26
26
|
## `lb-input`
|
|
@@ -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
|
|
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
|
@@ -103,6 +103,22 @@ If a dev team wishes to make their own widgets that identify `lb-list` or `lb-ro
|
|
|
103
103
|
Perhaps a utility that can be called, like `getDataScope(el)`, to help
|
|
104
104
|
clean up the code in these cases.
|
|
105
105
|
|
|
106
|
+
### Refresh narrowed by query parm
|
|
107
|
+
|
|
108
|
+
A query parm write loads the whole page again, every query at once. Keyed
|
|
109
|
+
landing keeps the rows that came back, so nothing on screen is disturbed, but
|
|
110
|
+
the server does work for queries the parm never touched.
|
|
111
|
+
|
|
112
|
+
A query could declare the parms it reads, and the hub, which holds the old
|
|
113
|
+
URL and the new, could send the names that changed. The server would run
|
|
114
|
+
only the queries that read one of them, and still remember nothing.
|
|
115
|
+
|
|
116
|
+
### Query parm history as a user preference
|
|
117
|
+
|
|
118
|
+
A query parm write replaces the history entry, and `lb-query-parm-push`
|
|
119
|
+
pushes one. Which of the two a user wants may be the user's to say, rather
|
|
120
|
+
than the page's.
|
|
121
|
+
|
|
106
122
|
### Build-time checking of `lb-action` against the declared requests
|
|
107
123
|
|
|
108
124
|
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
|
|
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,18 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loadbare/app",
|
|
3
3
|
"description": "High performance web app framework for server-bound applications",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
8
8
|
"docs",
|
|
9
|
+
"skills/loadbare-app/SKILL.md",
|
|
10
|
+
"skills/loadbare-app/references",
|
|
9
11
|
"README.md"
|
|
10
12
|
],
|
|
11
13
|
"publishConfig": {
|
|
12
14
|
"access": "public"
|
|
13
15
|
},
|
|
14
16
|
"bin": {
|
|
15
|
-
"loadbare-app-build": "dist/build/cli.js"
|
|
17
|
+
"loadbare-app-build": "dist/build/cli.js",
|
|
18
|
+
"loadbare-app": "dist/build/skills-cli.js"
|
|
16
19
|
},
|
|
17
20
|
"exports": {
|
|
18
21
|
".": "./dist/hub/lb-hub.browser.js",
|
|
@@ -31,8 +34,9 @@
|
|
|
31
34
|
"release:major": "node ../../scripts/bump-release.mjs major",
|
|
32
35
|
"prebuild": "node --eval \"fs.rmSync('dist',{recursive:true,force:true})\" --input-type=module",
|
|
33
36
|
"build:server": "tsc --project tsconfig.build.json",
|
|
34
|
-
"build": "
|
|
35
|
-
"
|
|
37
|
+
"build:skills": "node scripts/build-skills.mjs",
|
|
38
|
+
"build": "npm run build:server && npm run build:skills",
|
|
39
|
+
"postbuild": "node --eval \"for (const f of ['dist/build/cli.js','dist/build/skills-cli.js']) fs.chmodSync(f, 0o755)\" --input-type=module && node ../../scripts/check-dist-imports.mjs",
|
|
36
40
|
"test": "tsx --test \"tests/**/*.test.ts\"",
|
|
37
41
|
"typecheck": "tsc --noEmit",
|
|
38
42
|
"format": "prettier --write .",
|