@loadbare/app 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build/assemble.d.ts.map +1 -1
- package/dist/build/assemble.js +5 -1
- package/dist/build/assemble.js.map +1 -1
- 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 +8 -0
- package/dist/core/lb-constants.js.map +1 -1
- package/dist/core/lb-types.d.ts +11 -0
- package/dist/core/lb-types.d.ts.map +1 -1
- package/dist/core/lb-types.js.map +1 -1
- package/dist/hub/lb-apply.d.ts +7 -1
- package/dist/hub/lb-apply.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +106 -3
- 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 -31
- package/dist/hub/lb-hub.browser.js.map +1 -1
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/server/lb-server.js +17 -0
- package/dist/server/lb-server.js.map +1 -1
- package/docs/TECHREF-1.0.md +101 -9
- package/docs/comparison.md +5 -1
- package/docs/reference/chrome.md +15 -0
- package/docs/reference/custom-elements.md +62 -0
- package/docs/reference/data-binding.md +36 -0
- package/docs/reference/page-files.md +30 -2
- package/package.json +1 -1
- package/skills/loadbare-app/SKILL.md +58 -4
- package/skills/loadbare-app/references/TECHREF-1.0.md +101 -9
- package/skills/loadbare-app/references/chrome.md +15 -0
- package/skills/loadbare-app/references/custom-elements.md +62 -0
- package/skills/loadbare-app/references/data-binding.md +36 -0
- package/skills/loadbare-app/references/page-files.md +30 -2
package/dist/server/lb-server.js
CHANGED
|
@@ -81,6 +81,23 @@ export function createHub(pages) {
|
|
|
81
81
|
`beginning with '${LB_RESERVED_PREFIX}' are reserved`);
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
// An update or a delete names its row by key, so its handler always
|
|
85
|
+
// knows which rows changed and can answer with a patch; removing a row
|
|
86
|
+
// never reorders the rest. Refreshing the whole set instead sends every
|
|
87
|
+
// row and places every row again, which moves the element the user is
|
|
88
|
+
// in. There is no case where that is the answer to either, so it is
|
|
89
|
+
// refused rather than warned about. An insert is not: where its row
|
|
90
|
+
// goes depends on the markup, which the server cannot see.
|
|
91
|
+
for (const [name, crud] of Object.entries(entry.requests.crud ?? {})) {
|
|
92
|
+
if (entry.queries[name]?.kind !== KIND_ROWS)
|
|
93
|
+
continue;
|
|
94
|
+
for (const request of ["rowUpdate", "rowDelete"]) {
|
|
95
|
+
if (crud[request]?.refresh.includes(name)) {
|
|
96
|
+
throw new Error(`loadbare: page '${page}' refreshes '${name}' after its own ` +
|
|
97
|
+
`${request}; return the changed rows in a patch instead`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
84
101
|
}
|
|
85
102
|
/**
|
|
86
103
|
* Answers into response items, adding the kind and key each query
|
|
@@ -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 Loadbare on the server. It ships\n// no HTTP server, no router, and no data layer.\n\nimport type {\n HubData,\n HubRequest,\n HubResponse,\n HubResult,\n Kind,\n Patch,\n RequestFields,\n ResponseItem,\n Row,\n} from \"../core/lb-types.js\";\nimport { isRowRequest } from \"../core/lb-types.js\";\nimport {\n KIND_ROW,\n KIND_ROWS,\n LB_RESERVED_PREFIX,\n REQUEST_ROW_DELETE,\n REQUEST_ROW_INSERT,\n REQUEST_ROW_UPDATE,\n URL_COLUMN_PATH,\n URL_QUERY,\n} from \"../core/lb-constants.js\";\n\n/**\n * Whatever the application hands the engine for the duration of one request.\n *\n * Loadbare declares it empty and never reads it. An application fills it in\n * by 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: its kind, its key, and the run that answers it.\n *\n * Kind and key are properties of the name rather than of any one answer, so\n * they are declared here and never inferred from what comes back. The engine\n * sends both with every answer, so the markup repeats neither. Every query\n * has a key; an aggregate row answers with a constant one.\n *\n * Write one with `row()` or `rows()` below; nothing else builds one.\n */\nexport interface Query {\n readonly kind: Kind;\n readonly key: string;\n readonly run: (ctx: HubContext) => HubResult | Promise<HubResult>;\n}\n\n/** A query that answers with one row, identified by its `key` column. */\nexport function row(\n key: string,\n run: (ctx: HubContext) => Row | Promise<Row>,\n): Query {\n return { kind: KIND_ROW, key, run };\n}\n\n/**\n * A query that answers with all its rows, and therefore also their order.\n * Rows land by their `key` column: a row whose key is not in the answer is\n * gone.\n */\nexport function rows(\n key: string,\n run: (ctx: HubContext) => Row[] | Promise<Row[]>,\n): Query {\n return { kind: KIND_ROWS, key, run };\n}\n\n/**\n * Only what changed, returned from a handler for a `rows` query. Rows named\n * here are added or updated, keys in `drop` are removed, and everything\n * unnamed is left alone — its contents, and its place in whatever order the\n * page is keeping.\n *\n * `Array.isArray` is what tells a patch from all rows, 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?: unknown[] }): Patch {\n const made = { ...change };\n patches.add(made);\n return made;\n}\n\n/**\n * Every object `patch()` made. A patch and a row are both plain objects, so\n * this is how the engine refuses a patch answering a `row` query.\n */\nconst patches = new WeakSet<object>();\n\n/**\n * A new row for `lb-url`, returned from a handler when only the handler can\n * know where the page belongs: the key of a row it inserted, or the absence\n * of one it deleted.\n *\n * A column other than `lb-path` is a query parm: set, or taken out when its\n * value is empty. With `lb-path` naming another page, the page is entered\n * with only the parms named here; otherwise every other parm is kept.\n *\n * The page then loads at the new URL in the same round trip, as a cold load\n * of it would, so the refresh set is not run and whatever else the handler\n * returned is dropped: both were answers for the URL the page is leaving.\n * This is Post/Redirect/Get without the redirect.\n */\nexport function url(columns: Record<string, string>): HubData {\n return { [URL_QUERY]: { ...columns } };\n}\n\n/** The shape of a `<stub>.queries.ts` module. */\nexport type Queries = Record<string, Query>;\n\n/**\n * What a handler does, and which queries re-run once it has.\n *\n * `run` may also return answers of its own, which are laid over the\n * refreshed ones. That is how a patch reaches the browser: a query answers\n * for all its rows and cannot know why it re-ran, but the handler knows\n * exactly what it changed and can say only that.\n */\nexport interface Handler<R = RequestFields> {\n run: (\n ctx: HubContext,\n request: R,\n ) => void | HubData | Promise<void | HubData>;\n refresh: string[];\n}\n\nexport type RowInsertHandler = Handler<{\n key?: string;\n values: Record<string, string>;\n}>;\n/** `values` holds only the columns being set, as an SQL UPDATE sets them. */\nexport type RowUpdateHandler = Handler<{\n key: string;\n values: Record<string, string>;\n}>;\nexport type RowDeleteHandler = Handler<{ key: string }>;\n\n/**\n * What the requests Loadbare provides do to one query, keyed by its name in\n * `Requests.crud`. A query with no entry here permits none of them: the wire\n * cannot reach anything the page has not published.\n *\n * Every key is the request name with the prefix stripped and the rest\n * camel-cased: `lb-row-insert` runs `rowInsert`.\n */\nexport interface Crud {\n rowInsert?: RowInsertHandler;\n rowUpdate?: RowUpdateHandler;\n rowDelete?: RowDeleteHandler;\n}\n\n/**\n * The shape of a `<stub>.requests.ts` module.\n *\n * `onPageEnter` runs when the page loads, before its queries. It declares no\n * refresh set: the page's queries all run afterward, so whatever it changed\n * is already in the response.\n *\n * `handlers` holds the page's declared requests, by name. A name the page\n * did not declare is refused, so the wire cannot reach anything the page has\n * not published.\n *\n * `crud` holds what the requests Loadbare provides run, by query name.\n */\nexport interface Requests {\n onPageEnter?: (ctx: HubContext) => void | Promise<void>;\n handlers?: Record<string, Handler>;\n crud?: Record<string, Crud>;\n}\n\n/** A page is three files sharing a stub; two of them are these. */\nexport interface Page {\n queries: Queries;\n requests: Requests;\n}\n\n/**\n * The page registry. The builder writes it from every `.requests.ts` and\n * `.queries.ts` it discovers — see docs/reference/builder.md.\n */\nexport type Pages = Record<string, Page>;\n\n/**\n * The engine. Both calls answer with response items. A request whose\n * handler returned `url()` answers with the `lb-url` item alone: the caller\n * builds a context at the new URL, calls `dataForPage` with it, and sends\n * the load beside the item. `hubRoutes` does exactly that.\n */\nexport interface Hub {\n /** A page load: its `onPageEnter`, then all of its queries. */\n dataForPage(page: string, ctx: HubContext): Promise<HubResponse>;\n\n /** A request: run the handler its name picks, then the refresh set. */\n runRequest(\n page: string,\n request: HubRequest,\n ctx: HubContext,\n ): Promise<HubResponse>;\n}\n\n/** The camel-cased `Crud` key of a request Loadbare provides. */\nconst CRUD_KEYS: Record<string, keyof Crud> = {\n [REQUEST_ROW_INSERT]: \"rowInsert\",\n [REQUEST_ROW_UPDATE]: \"rowUpdate\",\n [REQUEST_ROW_DELETE]: \"rowDelete\",\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 — its own query,\n // the requests it provides — 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.handlers ?? {}),\n ...Object.keys(entry.requests.crud ?? {}),\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 /**\n * Answers into response items, adding the kind and key each query\n * declared. An answer that disagrees with its declaration is a mistake in\n * the application rather than a case to handle, and is left out: a page is\n * better off missing one query than showing the wrong shape for it.\n */\n function items(page: string, data: HubData): HubResponse {\n const out: ResponseItem[] = [];\n for (const [name, result] of Object.entries(data)) {\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 item: ResponseItem = {\n query: name,\n kind: query.kind,\n key: query.key,\n };\n if (query.kind === KIND_ROW) {\n if (Array.isArray(result) || patches.has(result)) {\n console.warn(\n `loadbare: query '${name}' is declared row but answered with ` +\n `${Array.isArray(result) ? \"rows\" : \"a patch\"}`,\n );\n continue;\n }\n item.row = result as Row;\n } else if (Array.isArray(result)) {\n item.rows = result;\n } else {\n item.patch = result as Patch;\n }\n out.push(item);\n }\n return out;\n }\n\n /** Run queries by name, each answering with all its rows or its row. */\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 if (Array.isArray(result) !== (query.kind === KIND_ROWS)) {\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 * Run a handler, then its refresh set against the same context, laying\n * what the handler itself returned over the refreshed queries — the\n * narrower answer wins because it knows what actually changed.\n *\n * A handler that returned `url()` answers with the `lb-url` item alone.\n * Loading the page there needs a context built at the new URL, and\n * building one is the caller's.\n */\n async function settle(\n page: string,\n handler: Handler<never> | undefined,\n request: RequestFields,\n ctx: HubContext,\n notFound: string,\n ): Promise<HubResponse> {\n if (!handler) {\n console.warn(notFound);\n return [];\n }\n const { [URL_QUERY]: moved, ...stated } =\n (await handler.run(ctx, request as never)) ?? {};\n if (moved !== undefined) {\n const refused = refusal(moved);\n if (refused === undefined) {\n return [\n {\n query: URL_QUERY,\n kind: KIND_ROW,\n key: URL_COLUMN_PATH,\n row: moved as Row,\n },\n ];\n }\n // The handler has already run, so the page still hears about it, by\n // its refresh set, as if no row for lb-url had been returned.\n console.warn(\n `loadbare: page '${page}' returned ${URL_QUERY} ${refused}, ignoring it`,\n );\n }\n return items(page, {\n ...(await run(page, handler.refresh, ctx)),\n ...stated,\n });\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 items(page, await run(page, Object.keys(entry.queries), ctx));\n },\n\n runRequest(page, request, ctx) {\n const { name, ...fields } = request;\n if (isRowRequest(name)) {\n const query = fields.query ?? \"\";\n return settle(\n page,\n pages[page]?.requests.crud?.[query]?.[CRUD_KEYS[name]!] as\n Handler<never> | undefined,\n fields,\n ctx,\n `loadbare: page '${page}' declares no ${CRUD_KEYS[name]} for '${query}'`,\n );\n }\n return settle(\n page,\n pages[page]?.requests.handlers?.[name] as Handler<never> | undefined,\n fields,\n ctx,\n `loadbare: page '${page}' declares no request '${name}'`,\n );\n },\n };\n}\n\n/**\n * Why a returned row for `lb-url` cannot be used, or nothing when it can.\n *\n * A value is a string, as a control's is and a URL's is. At least one column\n * is named: a row naming none changes nothing.\n */\nfunction refusal(columns: unknown): string | undefined {\n if (\n typeof columns !== \"object\" ||\n columns === null ||\n Array.isArray(columns)\n ) {\n return \"that is not a row\";\n }\n const values = Object.values(columns);\n if (values.length === 0) return \"naming no column\";\n if (!values.every((v) => typeof v === \"string\")) {\n return `holding a value that is not a string; write url({ name: \"...\" })`;\n }\n return undefined;\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 Loadbare on the server. It ships\n// no HTTP server, no router, and no data layer.\n\nimport type {\n HubData,\n HubRequest,\n HubResponse,\n HubResult,\n Kind,\n Patch,\n RequestFields,\n ResponseItem,\n Row,\n} from \"../core/lb-types.js\";\nimport { isRowRequest } from \"../core/lb-types.js\";\nimport {\n KIND_ROW,\n KIND_ROWS,\n LB_RESERVED_PREFIX,\n REQUEST_ROW_DELETE,\n REQUEST_ROW_INSERT,\n REQUEST_ROW_UPDATE,\n URL_COLUMN_PATH,\n URL_QUERY,\n} from \"../core/lb-constants.js\";\n\n/**\n * Whatever the application hands the engine for the duration of one request.\n *\n * Loadbare declares it empty and never reads it. An application fills it in\n * by 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: its kind, its key, and the run that answers it.\n *\n * Kind and key are properties of the name rather than of any one answer, so\n * they are declared here and never inferred from what comes back. The engine\n * sends both with every answer, so the markup repeats neither. Every query\n * has a key; an aggregate row answers with a constant one.\n *\n * Write one with `row()` or `rows()` below; nothing else builds one.\n */\nexport interface Query {\n readonly kind: Kind;\n readonly key: string;\n readonly run: (ctx: HubContext) => HubResult | Promise<HubResult>;\n}\n\n/** A query that answers with one row, identified by its `key` column. */\nexport function row(\n key: string,\n run: (ctx: HubContext) => Row | Promise<Row>,\n): Query {\n return { kind: KIND_ROW, key, run };\n}\n\n/**\n * A query that answers with all its rows, and therefore also their order.\n * Rows land by their `key` column: a row whose key is not in the answer is\n * gone.\n */\nexport function rows(\n key: string,\n run: (ctx: HubContext) => Row[] | Promise<Row[]>,\n): Query {\n return { kind: KIND_ROWS, key, run };\n}\n\n/**\n * Only what changed, returned from a handler for a `rows` query. Rows named\n * here are added or updated, keys in `drop` are removed, and everything\n * unnamed is left alone — its contents, and its place in whatever order the\n * page is keeping.\n *\n * `Array.isArray` is what tells a patch from all rows, 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?: unknown[] }): Patch {\n const made = { ...change };\n patches.add(made);\n return made;\n}\n\n/**\n * Every object `patch()` made. A patch and a row are both plain objects, so\n * this is how the engine refuses a patch answering a `row` query.\n */\nconst patches = new WeakSet<object>();\n\n/**\n * A new row for `lb-url`, returned from a handler when only the handler can\n * know where the page belongs: the key of a row it inserted, or the absence\n * of one it deleted.\n *\n * A column other than `lb-path` is a query parm: set, or taken out when its\n * value is empty. With `lb-path` naming another page, the page is entered\n * with only the parms named here; otherwise every other parm is kept.\n *\n * The page then loads at the new URL in the same round trip, as a cold load\n * of it would, so the refresh set is not run and whatever else the handler\n * returned is dropped: both were answers for the URL the page is leaving.\n * This is Post/Redirect/Get without the redirect.\n */\nexport function url(columns: Record<string, string>): HubData {\n return { [URL_QUERY]: { ...columns } };\n}\n\n/** The shape of a `<stub>.queries.ts` module. */\nexport type Queries = Record<string, Query>;\n\n/**\n * What a handler does, and which queries re-run once it has.\n *\n * `run` may also return answers of its own, which are laid over the\n * refreshed ones. That is how a patch reaches the browser: a query answers\n * for all its rows and cannot know why it re-ran, but the handler knows\n * exactly what it changed and can say only that.\n */\nexport interface Handler<R = RequestFields> {\n run: (\n ctx: HubContext,\n request: R,\n ) => void | HubData | Promise<void | HubData>;\n refresh: string[];\n}\n\nexport type RowInsertHandler = Handler<{\n key?: string;\n values: Record<string, string>;\n}>;\n/** `values` holds only the columns being set, as an SQL UPDATE sets them. */\nexport type RowUpdateHandler = Handler<{\n key: string;\n values: Record<string, string>;\n}>;\nexport type RowDeleteHandler = Handler<{ key: string }>;\n\n/**\n * What the requests Loadbare provides do to one query, keyed by its name in\n * `Requests.crud`. A query with no entry here permits none of them: the wire\n * cannot reach anything the page has not published.\n *\n * Every key is the request name with the prefix stripped and the rest\n * camel-cased: `lb-row-insert` runs `rowInsert`.\n */\nexport interface Crud {\n rowInsert?: RowInsertHandler;\n rowUpdate?: RowUpdateHandler;\n rowDelete?: RowDeleteHandler;\n}\n\n/**\n * The shape of a `<stub>.requests.ts` module.\n *\n * `onPageEnter` runs when the page loads, before its queries. It declares no\n * refresh set: the page's queries all run afterward, so whatever it changed\n * is already in the response.\n *\n * `handlers` holds the page's declared requests, by name. A name the page\n * did not declare is refused, so the wire cannot reach anything the page has\n * not published.\n *\n * `crud` holds what the requests Loadbare provides run, by query name.\n */\nexport interface Requests {\n onPageEnter?: (ctx: HubContext) => void | Promise<void>;\n handlers?: Record<string, Handler>;\n crud?: Record<string, Crud>;\n}\n\n/** A page is three files sharing a stub; two of them are these. */\nexport interface Page {\n queries: Queries;\n requests: Requests;\n}\n\n/**\n * The page registry. The builder writes it from every `.requests.ts` and\n * `.queries.ts` it discovers — see docs/reference/builder.md.\n */\nexport type Pages = Record<string, Page>;\n\n/**\n * The engine. Both calls answer with response items. A request whose\n * handler returned `url()` answers with the `lb-url` item alone: the caller\n * builds a context at the new URL, calls `dataForPage` with it, and sends\n * the load beside the item. `hubRoutes` does exactly that.\n */\nexport interface Hub {\n /** A page load: its `onPageEnter`, then all of its queries. */\n dataForPage(page: string, ctx: HubContext): Promise<HubResponse>;\n\n /** A request: run the handler its name picks, then the refresh set. */\n runRequest(\n page: string,\n request: HubRequest,\n ctx: HubContext,\n ): Promise<HubResponse>;\n}\n\n/** The camel-cased `Crud` key of a request Loadbare provides. */\nconst CRUD_KEYS: Record<string, keyof Crud> = {\n [REQUEST_ROW_INSERT]: \"rowInsert\",\n [REQUEST_ROW_UPDATE]: \"rowUpdate\",\n [REQUEST_ROW_DELETE]: \"rowDelete\",\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 — its own query,\n // the requests it provides — 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.handlers ?? {}),\n ...Object.keys(entry.requests.crud ?? {}),\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 // An update or a delete names its row by key, so its handler always\n // knows which rows changed and can answer with a patch; removing a row\n // never reorders the rest. Refreshing the whole set instead sends every\n // row and places every row again, which moves the element the user is\n // in. There is no case where that is the answer to either, so it is\n // refused rather than warned about. An insert is not: where its row\n // goes depends on the markup, which the server cannot see.\n for (const [name, crud] of Object.entries(entry.requests.crud ?? {})) {\n if (entry.queries[name]?.kind !== KIND_ROWS) continue;\n for (const request of [\"rowUpdate\", \"rowDelete\"] as const) {\n if (crud[request]?.refresh.includes(name)) {\n throw new Error(\n `loadbare: page '${page}' refreshes '${name}' after its own ` +\n `${request}; return the changed rows in a patch instead`,\n );\n }\n }\n }\n }\n\n /**\n * Answers into response items, adding the kind and key each query\n * declared. An answer that disagrees with its declaration is a mistake in\n * the application rather than a case to handle, and is left out: a page is\n * better off missing one query than showing the wrong shape for it.\n */\n function items(page: string, data: HubData): HubResponse {\n const out: ResponseItem[] = [];\n for (const [name, result] of Object.entries(data)) {\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 item: ResponseItem = {\n query: name,\n kind: query.kind,\n key: query.key,\n };\n if (query.kind === KIND_ROW) {\n if (Array.isArray(result) || patches.has(result)) {\n console.warn(\n `loadbare: query '${name}' is declared row but answered with ` +\n `${Array.isArray(result) ? \"rows\" : \"a patch\"}`,\n );\n continue;\n }\n item.row = result as Row;\n } else if (Array.isArray(result)) {\n item.rows = result;\n } else {\n item.patch = result as Patch;\n }\n out.push(item);\n }\n return out;\n }\n\n /** Run queries by name, each answering with all its rows or its row. */\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 if (Array.isArray(result) !== (query.kind === KIND_ROWS)) {\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 * Run a handler, then its refresh set against the same context, laying\n * what the handler itself returned over the refreshed queries — the\n * narrower answer wins because it knows what actually changed.\n *\n * A handler that returned `url()` answers with the `lb-url` item alone.\n * Loading the page there needs a context built at the new URL, and\n * building one is the caller's.\n */\n async function settle(\n page: string,\n handler: Handler<never> | undefined,\n request: RequestFields,\n ctx: HubContext,\n notFound: string,\n ): Promise<HubResponse> {\n if (!handler) {\n console.warn(notFound);\n return [];\n }\n const { [URL_QUERY]: moved, ...stated } =\n (await handler.run(ctx, request as never)) ?? {};\n if (moved !== undefined) {\n const refused = refusal(moved);\n if (refused === undefined) {\n return [\n {\n query: URL_QUERY,\n kind: KIND_ROW,\n key: URL_COLUMN_PATH,\n row: moved as Row,\n },\n ];\n }\n // The handler has already run, so the page still hears about it, by\n // its refresh set, as if no row for lb-url had been returned.\n console.warn(\n `loadbare: page '${page}' returned ${URL_QUERY} ${refused}, ignoring it`,\n );\n }\n return items(page, {\n ...(await run(page, handler.refresh, ctx)),\n ...stated,\n });\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 items(page, await run(page, Object.keys(entry.queries), ctx));\n },\n\n runRequest(page, request, ctx) {\n const { name, ...fields } = request;\n if (isRowRequest(name)) {\n const query = fields.query ?? \"\";\n return settle(\n page,\n pages[page]?.requests.crud?.[query]?.[CRUD_KEYS[name]!] as\n Handler<never> | undefined,\n fields,\n ctx,\n `loadbare: page '${page}' declares no ${CRUD_KEYS[name]} for '${query}'`,\n );\n }\n return settle(\n page,\n pages[page]?.requests.handlers?.[name] as Handler<never> | undefined,\n fields,\n ctx,\n `loadbare: page '${page}' declares no request '${name}'`,\n );\n },\n };\n}\n\n/**\n * Why a returned row for `lb-url` cannot be used, or nothing when it can.\n *\n * A value is a string, as a control's is and a URL's is. At least one column\n * is named: a row naming none changes nothing.\n */\nfunction refusal(columns: unknown): string | undefined {\n if (\n typeof columns !== \"object\" ||\n columns === null ||\n Array.isArray(columns)\n ) {\n return \"that is not a row\";\n }\n const values = Object.values(columns);\n if (values.length === 0) return \"naming no column\";\n if (!values.every((v) => typeof v === \"string\")) {\n return `holding a value that is not a string; write url({ name: \"...\" })`;\n }\n return undefined;\n}\n"]}
|
package/docs/TECHREF-1.0.md
CHANGED
|
@@ -64,10 +64,9 @@ fine, because every `<select>` receives the same rows.
|
|
|
64
64
|
|
|
65
65
|
But the allowed values may depend on other values in the row.
|
|
66
66
|
|
|
67
|
-
- **Decide
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
what a nested query is for.
|
|
67
|
+
- **Decide whether a nested query's rows may depend on its row.** A nested
|
|
68
|
+
query has one answer, which every live row receives. See
|
|
69
|
+
[Master-detail](#master-detail) for what a nested query is for.
|
|
71
70
|
|
|
72
71
|
```html
|
|
73
72
|
<tbody lb-query="accounts">
|
|
@@ -78,6 +77,24 @@ But the allowed values may depend on other values in the row.
|
|
|
78
77
|
</tbody>
|
|
79
78
|
```
|
|
80
79
|
|
|
80
|
+
### Who creates and orders rows
|
|
81
|
+
|
|
82
|
+
The hub creates every live row from a row template and places it in the
|
|
83
|
+
query's order, unless the element holding the rows has `lbPlaceRow`, in
|
|
84
|
+
which case the custom element decides where each row goes. Custom elements
|
|
85
|
+
also create rows of their own: `lb-table` clones a ghost row per section and
|
|
86
|
+
builds each section's heading. Neither side owns creation or order.
|
|
87
|
+
|
|
88
|
+
- **Decide who creates rows and who orders them.** A patch cannot say where
|
|
89
|
+
a new row goes, so it lands where the host puts it: in order under
|
|
90
|
+
`lb-table` with `data-sort`, last in plain markup and in `lb-options`.
|
|
91
|
+
Whether an insert may answer with a patch therefore depends on markup the
|
|
92
|
+
server cannot see, and the server cannot refuse an insert that refreshes
|
|
93
|
+
its own query the way it refuses an update or a delete.
|
|
94
|
+
- **Decide whether landing all rows moves rows already in place.** It moves
|
|
95
|
+
every row today, which takes focus from the control the user is in, and
|
|
96
|
+
`lbPlaceRow` does the same.
|
|
97
|
+
|
|
81
98
|
### The server API
|
|
82
99
|
|
|
83
100
|
- **Give the chrome a way to state its own queries.** A custom element in
|
|
@@ -425,7 +442,7 @@ query.
|
|
|
425
442
|
| ----------- | -------- | -------------------------------------------------- |
|
|
426
443
|
| `lb-query` | a query | Puts its rows in the element's content |
|
|
427
444
|
| `lb-column` | a column | Sets the element from that column |
|
|
428
|
-
| `lb-show` | a column | Removes it while the named column is null or false |
|
|
445
|
+
| `lb-show` | a column | Removes it while the named column is null or false; `!column` reverses it |
|
|
429
446
|
|
|
430
447
|
| Stamp | The hub stamps it with |
|
|
431
448
|
| -------------------- | -------------------------------- |
|
|
@@ -558,6 +575,10 @@ so `"false"` is on, and a query spells a condition as a boolean or a null.
|
|
|
558
575
|
</template>
|
|
559
576
|
```
|
|
560
577
|
|
|
578
|
+
Write `!` before the column to reverse it: `lb-show="!chosen"` is present
|
|
579
|
+
while `chosen` is null or false. One column then decides both of two
|
|
580
|
+
elements, rather than a column and its opposite, which could disagree.
|
|
581
|
+
|
|
561
582
|
It reads from the nearest ancestor row, as `lb-column` does, and on an
|
|
562
583
|
element that carries `lb-query` the column belongs to the row around it. A
|
|
563
584
|
row that does not carry the column leaves the element as it is.
|
|
@@ -578,7 +599,8 @@ nothing conditional shows until its row lands. An absent element's template
|
|
|
578
599
|
keeps its place among its siblings, and a position selector counts it.
|
|
579
600
|
|
|
580
601
|
These are build errors: `lb-show` on a row template's root, `lb-show` with
|
|
581
|
-
no `lb-query` around it,
|
|
602
|
+
no `lb-query` around it, `lb-show` on a `<template>`, and `lb-show="!"` or
|
|
603
|
+
`lb-show="!!column"`.
|
|
582
604
|
|
|
583
605
|
Hiding is presentation, and the server still refuses what a request may not
|
|
584
606
|
do.
|
|
@@ -608,7 +630,9 @@ each detail row carries its master's columns. A custom element's
|
|
|
608
630
|
|
|
609
631
|
A query nested in another query's row template receives the same rows in
|
|
610
632
|
every live row. That serves a picker offering the same choices on every
|
|
611
|
-
row, and is not a way to show a different detail per row.
|
|
633
|
+
row, and is not a way to show a different detail per row. A live row added
|
|
634
|
+
later is filled from the nested query's last answer, so a request that adds
|
|
635
|
+
one does not refresh the nested query to fill it.
|
|
612
636
|
|
|
613
637
|
Which master a page shows is a query parm, which a query reads off `ctx`
|
|
614
638
|
since it takes no argument from the browser — see [Query parms](#query-parms).
|
|
@@ -767,6 +791,18 @@ interceptor calls `stopPropagation`, not `preventDefault`. This is what
|
|
|
767
791
|
makes a confirmation wrapper possible without the wrapped element knowing
|
|
768
792
|
about it.
|
|
769
793
|
|
|
794
|
+
Once what the request brought back has landed, or its round trip has failed,
|
|
795
|
+
the hub dispatches the bubbling `lb-request-done` event from the same
|
|
796
|
+
element. Its `detail`, `HubRequestDone` in `@loadbare/app/types`, holds
|
|
797
|
+
the request as the hub sent it and `items`, every response item that landed
|
|
798
|
+
because of it, in order. An answer that moved the URL contributes its
|
|
799
|
+
`lb-url` item and the page load. `error` is set instead when the round trip
|
|
800
|
+
failed. By then `lb-request-pending` is gone, and a listener sees the page
|
|
801
|
+
as the answer left it. A request the hub or an ancestor stopped was never
|
|
802
|
+
sent, and gets no `lb-request-done`. An element the answer removed from the
|
|
803
|
+
document, such as the row a delete took away, dispatches the event where it
|
|
804
|
+
now is, and no ancestor it had on the page hears it.
|
|
805
|
+
|
|
770
806
|
#### Request state
|
|
771
807
|
|
|
772
808
|
The hub stamps `lb-request-pending` on the element that issued a request
|
|
@@ -804,6 +840,14 @@ A page load that fails lands nothing and is reported to the console. A load
|
|
|
804
840
|
started by a request for `lb-url` stamps the element that issued it, as any
|
|
805
841
|
request stamps its element.
|
|
806
842
|
|
|
843
|
+
A response answers for the URL its request was sent from, path and query
|
|
844
|
+
string both. One that returns after the URL has moved lands nothing, since
|
|
845
|
+
it describes what the user is no longer looking at: a request's answer,
|
|
846
|
+
including any `url()` it carries, and a page load overtaken by a newer one.
|
|
847
|
+
A write whose answer is dropped this way has still happened. Its
|
|
848
|
+
`lb-request-done` carries no items and no error, and an insert still resets
|
|
849
|
+
its form.
|
|
850
|
+
|
|
807
851
|
### The URL
|
|
808
852
|
|
|
809
853
|
---- UNEDITED ----
|
|
@@ -834,6 +878,14 @@ server runs its `onPageEnter`, then its queries. When a query parm takes a
|
|
|
834
878
|
new value, the hub reloads the page's queries, and keeps the page's DOM, so
|
|
835
879
|
live rows that come back keep their place.
|
|
836
880
|
|
|
881
|
+
Once an entered page's queries have landed, the hub focuses the first
|
|
882
|
+
element in `<main>` the user can operate: not disabled, not in a closed
|
|
883
|
+
`<dialog>`, not `inert`, not `hidden`, not in an absent `lb-show` branch,
|
|
884
|
+
and one that takes the focus. Entering is a cold load, a new `lb-path`,
|
|
885
|
+
and Back or Forward to another page. A change of query parm enters no
|
|
886
|
+
page, and focus stays where it is, as it does when it is already in
|
|
887
|
+
`<main>`.
|
|
888
|
+
|
|
837
889
|
The page's title is the text of the page file's `<title>`, which the builder
|
|
838
890
|
stamps on the page as `lb-page-title`. The hub sets the document title to
|
|
839
891
|
`lb-page-label` when it is not null.
|
|
@@ -1003,8 +1055,8 @@ rowInsert: {
|
|
|
1003
1055
|
carries the `lb-url` item alone, and the hub loads the page itself. A
|
|
1004
1056
|
failure there is a page load's, and is not stamped on the element that
|
|
1005
1057
|
issued the request.
|
|
1006
|
-
- A user who has
|
|
1007
|
-
URL they are on.
|
|
1058
|
+
- A user who has moved by the time the response arrives, to another page or
|
|
1059
|
+
to other query parms, keeps the URL they are on.
|
|
1008
1060
|
|
|
1009
1061
|
The page is loaded with a second context, built by `contextFor` from the new
|
|
1010
1062
|
parms — see [The Express server](#the-express-server).
|
|
@@ -1027,6 +1079,7 @@ to build on any of these:
|
|
|
1027
1079
|
`<lb-hub>`.
|
|
1028
1080
|
- `lb-show` on a `<template>`, on a row template's root, or with no
|
|
1029
1081
|
`lb-query` around it.
|
|
1082
|
+
- `lb-show="!"` or `lb-show="!!column"`, which reverse no column.
|
|
1030
1083
|
|
|
1031
1084
|
`lb-column` with no ancestor row is allowed: the hub gathers from it.
|
|
1032
1085
|
|
|
@@ -1276,6 +1329,9 @@ request carrying no name answers 400, and a `run` that throws answers 500.
|
|
|
1276
1329
|
|
|
1277
1330
|
Every handler under `handlers` and `crud` has the same two members. `run`
|
|
1278
1331
|
performs the work, and `refresh` names the queries to re-run once it has.
|
|
1332
|
+
A query belongs there when its answer changed, never to fill an element the
|
|
1333
|
+
request's answer creates: that element is filled from the last answer its
|
|
1334
|
+
query landed.
|
|
1279
1335
|
|
|
1280
1336
|
`run` receives the same `ctx` and the request less its name: `query`, `key`
|
|
1281
1337
|
and `values`, as present.
|
|
@@ -1284,6 +1340,15 @@ and `values`, as present.
|
|
|
1284
1340
|
the refreshed ones. That is how a delta reaches the browser: wrap it in
|
|
1285
1341
|
`patch()`, naming the rows that arrived or changed and the keys that went.
|
|
1286
1342
|
|
|
1343
|
+
A refreshed `rows` query sends every row, and the hub places every row
|
|
1344
|
+
again, which moves each element and takes focus from the control the user
|
|
1345
|
+
is in. An update or a delete names its row by key, and removing a row
|
|
1346
|
+
never reorders the rest, so its handler always knows what changed:
|
|
1347
|
+
`createHub` refuses at startup a `crud` `rowUpdate` or `rowDelete` on a
|
|
1348
|
+
`rows` query whose `refresh` names that same query. A `row` query sends one
|
|
1349
|
+
row and may refresh itself. An insert may refresh its own query; see
|
|
1350
|
+
[Who creates and orders rows](#who-creates-and-orders-rows).
|
|
1351
|
+
|
|
1287
1352
|
`run` may instead return `url()`, a new row for `lb-url`, naming query parms
|
|
1288
1353
|
only the write can know, such as the key of a row it inserted. The page
|
|
1289
1354
|
then loads at them in the same round trip, in place of the refresh set — see
|
|
@@ -1397,6 +1462,25 @@ export default ["@scope/library-name"];
|
|
|
1397
1462
|
Import every attribute name from `@loadbare/app/constants` — see
|
|
1398
1463
|
[Constants](#constants). Never write one as a string literal.
|
|
1399
1464
|
|
|
1465
|
+
### What the hub and an element say to each other
|
|
1466
|
+
|
|
1467
|
+
Each direction has one mechanism for each kind of message:
|
|
1468
|
+
|
|
1469
|
+
| Direction | What | How | Today |
|
|
1470
|
+
| -------------- | ------------------------------------------- | ---------------------------- | ------------------------------------------------ |
|
|
1471
|
+
| Hub to element | State that lasts | An attribute the hub stamps | `lb-column-value`, `lb-key-value`, `lb-query-row-count`, `lb-request-pending`, `lb-request-error` |
|
|
1472
|
+
| Hub to element | State the platform already names | A property | A control's `value` |
|
|
1473
|
+
| Hub to element | Work the hub needs done now, while landing | An optional `lb` method | `lbPlaceRow`, `lbRowsLanded` |
|
|
1474
|
+
| Hub to element | A moment an element started | A bubbling event | `lb-request-done` |
|
|
1475
|
+
| Element to hub | A moment | A bubbling event | `lb-request`, and `change`, `click` and `submit` |
|
|
1476
|
+
|
|
1477
|
+
State is an attribute, because a stylesheet can select on it and an element
|
|
1478
|
+
that upgrades late still finds it. A method is for work the hub cannot go
|
|
1479
|
+
on without, since it acts on one element at one point in landing and nothing
|
|
1480
|
+
else can do it. A moment is an event, because the element that cares is
|
|
1481
|
+
often an ancestor of the one it concerns, and an event reaches it with no
|
|
1482
|
+
knowledge of either.
|
|
1483
|
+
|
|
1400
1484
|
### Controls
|
|
1401
1485
|
|
|
1402
1486
|
A form-associated custom element with a `value` property that fires
|
|
@@ -1450,6 +1534,11 @@ The hub calls both on a custom element with `lb-query` and a row template.
|
|
|
1450
1534
|
Both are optional. The `RowsHost` interface in `@loadbare/app/types`
|
|
1451
1535
|
declares them.
|
|
1452
1536
|
|
|
1537
|
+
A custom element the builder shipped absent under `lb-show` has not
|
|
1538
|
+
upgraded while its column is off, and rows that land on it then are placed
|
|
1539
|
+
without it. When the column first turns on and it upgrades, the hub lands
|
|
1540
|
+
the query's last answer on it again, as all rows, so both hooks run.
|
|
1541
|
+
|
|
1453
1542
|
`applyRow(root, row)`, exported by `@loadbare/app`, fills `root` from one
|
|
1454
1543
|
row, the same operation that fills a live row.
|
|
1455
1544
|
|
|
@@ -1593,6 +1682,7 @@ Loadbare/app owns every DOM event in this table, both the name and what its
|
|
|
1593
1682
|
| Event | Dispatched from | Bubbles | Cancelable | Defined in |
|
|
1594
1683
|
| ------------ | -------------------------------- | ------- | ---------- | --------------------------------------- |
|
|
1595
1684
|
| `lb-request` | The element that committed | Yes | No | [The request event](#the-request-event) |
|
|
1685
|
+
| `lb-request-done` | The element that committed | Yes | No | [The request event](#the-request-event) |
|
|
1596
1686
|
|
|
1597
1687
|
### Reserved methods
|
|
1598
1688
|
|
|
@@ -1628,6 +1718,8 @@ imports them rather than writing a string.
|
|
|
1628
1718
|
| `ATTR_PAGE_TITLE` | `lb-page-title` |
|
|
1629
1719
|
| `DEVELOPER_ATTRIBUTES` | The seven attributes a developer writes |
|
|
1630
1720
|
| `LB_EVENT_NAME` | `lb-request` |
|
|
1721
|
+
| `LB_DONE_EVENT_NAME` | `lb-request-done` |
|
|
1722
|
+
| `SHOW_NOT` | `!`, written before a column in `lb-show` |
|
|
1631
1723
|
| `LB_RESERVED_PREFIX` | `lb-` |
|
|
1632
1724
|
| `REQUEST_ROW_INSERT` | `lb-row-insert` |
|
|
1633
1725
|
| `REQUEST_ROW_UPDATE` | `lb-row-update` |
|
package/docs/comparison.md
CHANGED
|
@@ -128,7 +128,7 @@ shape of the data an application works with.
|
|
|
128
128
|
### Loadbare/app
|
|
129
129
|
|
|
130
130
|
The hub holds no application data. There is no store, no signal, no
|
|
131
|
-
observable, and no client cache
|
|
131
|
+
observable, and no client cache that answers a request.
|
|
132
132
|
|
|
133
133
|
- A value that has landed exists in the DOM: as `textContent` or `value`, and
|
|
134
134
|
as the `lb-column-value` stamp.
|
|
@@ -138,6 +138,10 @@ observable, and no client cache of query results.
|
|
|
138
138
|
the path, the page label and the query parms.
|
|
139
139
|
- An element hidden by `lb-show` exists inside a `<template>` standing where
|
|
140
140
|
it stood.
|
|
141
|
+
- The hub keeps each query's last answer until the page changes, only to
|
|
142
|
+
fill an element that names the query and arrives after it, such as a
|
|
143
|
+
picker in a new live row. Nothing is read from it in place of asking the
|
|
144
|
+
server.
|
|
141
145
|
- The hub keeps two pieces of state of its own: the name of the current page,
|
|
142
146
|
and, until an insert completes, the form it gathered from.
|
|
143
147
|
|
package/docs/reference/chrome.md
CHANGED
|
@@ -108,6 +108,21 @@ tab and the browser history show the page.
|
|
|
108
108
|
`lb-url` is the one query a page may use that the server does not declare. A
|
|
109
109
|
page names it the same way, anywhere inside the hub.
|
|
110
110
|
|
|
111
|
+
### Where focus starts
|
|
112
|
+
|
|
113
|
+
On entering a page, once its queries have landed, the hub focuses the first
|
|
114
|
+
element in `<main>` the user can operate, so a keyboard user starts in the
|
|
115
|
+
page rather than on the document. The chrome comes first in the document,
|
|
116
|
+
and is passed over. So is anything disabled, in a closed `<dialog>`,
|
|
117
|
+
`inert`, `hidden`, in an absent `lb-show` branch, or that does not take
|
|
118
|
+
the focus when asked. A widget's native control counts, so write no
|
|
119
|
+
`autofocus` to restate this.
|
|
120
|
+
|
|
121
|
+
A page is entered on a cold load, a new `lb-path` from a link or from a
|
|
122
|
+
handler's `url()`, and Back or Forward to another page. A change of query
|
|
123
|
+
parm enters no page: the user who chose a record in a picker stays in the
|
|
124
|
+
picker. Focus the user has already put in `<main>` is left there.
|
|
125
|
+
|
|
111
126
|
### Links
|
|
112
127
|
|
|
113
128
|
Write `lb-url-link` on an `<a>` to move between pages:
|
|
@@ -268,6 +268,8 @@ one as a string literal:
|
|
|
268
268
|
| `REQUEST_ROW_INSERT`, `REQUEST_ROW_UPDATE`, `REQUEST_ROW_DELETE` | The requests Loadbare provides |
|
|
269
269
|
| `LB_ROW_REQUESTS` | All three, in one array |
|
|
270
270
|
| `LB_EVENT_NAME` | `lb-request`, the event every request travels as |
|
|
271
|
+
| `LB_DONE_EVENT_NAME` | `lb-request-done`, the event that says how it turned out |
|
|
272
|
+
| `SHOW_NOT` | `!`, written before a column in `lb-show` |
|
|
271
273
|
| `URL_QUERY` | `lb-url` |
|
|
272
274
|
| `URL_COLUMN_PATH`, `URL_COLUMN_PAGE_LABEL`, `URL_COLUMN_PAGE_UNKNOWN` | `lb-path`, `lb-page-label`, `lb-page-unknown` |
|
|
273
275
|
| `LB_RESERVED_PREFIX` | `lb-` |
|
|
@@ -278,6 +280,24 @@ writes them in markup, and the hub and the builder write their stamps.
|
|
|
278
280
|
Loadbare reserves method names beginning with `lb` on a custom element, for
|
|
279
281
|
the methods it calls; see [Holding rows](#holding-rows).
|
|
280
282
|
|
|
283
|
+
### What the hub and an element say to each other
|
|
284
|
+
|
|
285
|
+
Each direction has one mechanism for each kind of message:
|
|
286
|
+
|
|
287
|
+
| Direction | What | How | Today |
|
|
288
|
+
|----------------|--------------------------------------------|-----------------------------|-------|
|
|
289
|
+
| Hub to element | State that lasts | An attribute the hub stamps | `lb-column-value`, `lb-key-value`, `lb-query-row-count`, `lb-request-pending`, `lb-request-error` |
|
|
290
|
+
| Hub to element | State the platform already names | A property | A control's `value` |
|
|
291
|
+
| Hub to element | Work the hub needs done now, while landing | An optional `lb` method | `lbPlaceRow`, `lbRowsLanded` |
|
|
292
|
+
| Hub to element | A moment an element started | A bubbling event | `lb-request-done` |
|
|
293
|
+
| Element to hub | A moment | A bubbling event | `lb-request`, and `change`, `click` and `submit` |
|
|
294
|
+
|
|
295
|
+
State is an attribute: a stylesheet can select on it, and an element that
|
|
296
|
+
upgrades late still finds it. A method is for work the hub cannot go on
|
|
297
|
+
without, done by one element at one point in landing. A moment is an event,
|
|
298
|
+
because the element that cares is often an ancestor of the one it concerns,
|
|
299
|
+
and an event reaches it knowing neither.
|
|
300
|
+
|
|
281
301
|
### When its code runs
|
|
282
302
|
|
|
283
303
|
The hub creates, places and parks elements, and each of those reaches a
|
|
@@ -450,6 +470,39 @@ missing a field its name needs, and stamps the dispatching element with
|
|
|
450
470
|
|
|
451
471
|
Let the event bubble, so an ancestor can stop it before the hub sends it.
|
|
452
472
|
|
|
473
|
+
### Hearing how it turned out
|
|
474
|
+
|
|
475
|
+
Once what a request brought back has landed, or its round trip has failed,
|
|
476
|
+
the hub dispatches `lb-request-done` from the element that committed. It
|
|
477
|
+
bubbles, so the element that cares need not be the one that committed: a
|
|
478
|
+
dialog hears it from the form inside it.
|
|
479
|
+
|
|
480
|
+
```ts
|
|
481
|
+
import { LB_DONE_EVENT_NAME } from "@loadbare/app/constants";
|
|
482
|
+
import type { HubRequestDone } from "@loadbare/app/types";
|
|
483
|
+
|
|
484
|
+
this.addEventListener(LB_DONE_EVENT_NAME, (e) => {
|
|
485
|
+
const { request, items, error } = (e as CustomEvent<HubRequestDone>).detail;
|
|
486
|
+
if (error) return;
|
|
487
|
+
const item = items.find((i) => i.query === request.query);
|
|
488
|
+
const created = item?.patch?.rows?.[0];
|
|
489
|
+
if (created) this.choose(String(created[item!.key]));
|
|
490
|
+
});
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
`items` is every response item that landed because of the request, in
|
|
494
|
+
order. An answer that moved the URL contributes its `lb-url` item, which
|
|
495
|
+
carries a key only the server knew, and the page load. `error` is set when
|
|
496
|
+
the round trip failed, and then nothing landed. A new row's key is in the
|
|
497
|
+
answer only when the handler returned it in a patch: a refresh sends all
|
|
498
|
+
rows, and nothing marks which one is new.
|
|
499
|
+
|
|
500
|
+
By the time the event is dispatched, `lb-request-pending` is gone and the
|
|
501
|
+
page is as the answer left it. A request that was never sent, because the
|
|
502
|
+
hub or an ancestor stopped it, gets no `lb-request-done`. An element the
|
|
503
|
+
answer removed, such as the row a delete took away, dispatches the event
|
|
504
|
+
outside the document, and no ancestor it had there hears it.
|
|
505
|
+
|
|
453
506
|
### Holding rows
|
|
454
507
|
|
|
455
508
|
The hub lands every row template itself. A plain element carrying `lb-query`
|
|
@@ -498,6 +551,15 @@ template, in arrival order. `lb-options.browser.ts` and
|
|
|
498
551
|
`lb-table.browser.ts` in [`@loadbare/widgets`](./widgets.md) are two
|
|
499
552
|
different placements over the same landing.
|
|
500
553
|
|
|
554
|
+
A custom element the builder shipped absent under `lb-show` has not
|
|
555
|
+
upgraded while its column is off, so it has no `lbPlaceRow` when rows land
|
|
556
|
+
on it then, and the hub places them before the template. When the column
|
|
557
|
+
first turns on and the element upgrades, the hub lands the query's last
|
|
558
|
+
answer on it again, as all rows, so every row goes through `lbPlaceRow` and
|
|
559
|
+
`lbRowsLanded` runs. Once upgraded it stays so: when its branch goes away and
|
|
560
|
+
comes back, it has placed every row that landed meanwhile, and nothing is
|
|
561
|
+
landed again.
|
|
562
|
+
|
|
501
563
|
A custom element that walks its own rows finds them with `LIVE_ROW` from
|
|
502
564
|
`@loadbare/app/constants`, never by `lb-key-value`: an element a `row` lands
|
|
503
565
|
on carries a key too, and may sit among the rows, as a total in a table's
|
|
@@ -168,6 +168,28 @@ a row goes and add scaffolding around the rows; see
|
|
|
168
168
|
[Holding rows](./custom-elements.md#holding-rows) and
|
|
169
169
|
[The Basic Widget Library](./widgets.md).
|
|
170
170
|
|
|
171
|
+
### What arrives later
|
|
172
|
+
|
|
173
|
+
The hub keeps each query's last answer until the page changes, with any
|
|
174
|
+
patch since applied to it. An element that names a query and arrives after
|
|
175
|
+
that query's answer is filled from what was kept, at the end of the landing
|
|
176
|
+
that follows its arrival. Such an element is a picker in a new live row, or
|
|
177
|
+
scaffolding a custom element builds around its rows, such as a ghost row. So
|
|
178
|
+
a query nested in another query's rows need not land again, or land after
|
|
179
|
+
the outer query, for a new outer row to show its choices.
|
|
180
|
+
|
|
181
|
+
A custom element the builder shipped absent has not upgraded while its
|
|
182
|
+
`lb-show` column is off, so rows that land on it then are not placed by it.
|
|
183
|
+
When the column first turns on and the element upgrades, the hub lands the
|
|
184
|
+
kept answer on it again, and every row goes through its `lbPlaceRow`. So a
|
|
185
|
+
table that groups its rows can sit under `lb-show`. See
|
|
186
|
+
[Holding rows](./custom-elements.md#holding-rows).
|
|
187
|
+
|
|
188
|
+
Nothing is answered from what the hub kept: a request always goes to the
|
|
189
|
+
server, and a query re-runs only when a response names it. A patch that
|
|
190
|
+
arrives before all of a query's rows is not kept, since it is not the whole
|
|
191
|
+
of anything.
|
|
192
|
+
|
|
171
193
|
A `rows` query on an element with no row template lands nothing. That is the
|
|
172
194
|
insert form above: it sits inside `lb-query="roster"` so its request is for
|
|
173
195
|
`roster`, and it shows none of its rows.
|
|
@@ -225,6 +247,14 @@ have the query answer with a boolean or a null. A row that does not carry
|
|
|
225
247
|
the column leaves the element as it is, so a query answers with the column
|
|
226
248
|
in every row.
|
|
227
249
|
|
|
250
|
+
Write `!` before the column to reverse it. One column then decides between
|
|
251
|
+
two elements, rather than a column and its opposite, which could disagree:
|
|
252
|
+
|
|
253
|
+
```html
|
|
254
|
+
<h2 lb-show="chosen" lb-column="title"></h2>
|
|
255
|
+
<p lb-show="!chosen">Choose a batch.</p>
|
|
256
|
+
```
|
|
257
|
+
|
|
228
258
|
`lb-show` reads from the nearest ancestor row, as `lb-column` does. On an
|
|
229
259
|
element that also carries `lb-query`, the column belongs to the row around
|
|
230
260
|
it, so this picker takes its choices from `groups` and whether it is present
|
|
@@ -463,6 +493,11 @@ An ancestor may stop the event, and the request is not sent. A custom
|
|
|
463
493
|
element may dispatch the event itself; see
|
|
464
494
|
[Sending a request](./custom-elements.md#sending-a-request).
|
|
465
495
|
|
|
496
|
+
Once the answer has landed, or the round trip has failed, the hub dispatches
|
|
497
|
+
a bubbling `lb-request-done` event from the same element, carrying the
|
|
498
|
+
request and what landed because of it, or the error; see
|
|
499
|
+
[Hearing how it turned out](./custom-elements.md#hearing-how-it-turned-out).
|
|
500
|
+
|
|
466
501
|
## The URL
|
|
467
502
|
|
|
468
503
|
The hub serves one query of its own, `lb-url`: one row holding the path, the
|
|
@@ -495,3 +530,4 @@ expansion, and refuses to build:
|
|
|
495
530
|
- `lb-url-unknown` on anything but a `<dialog>`, or outside `<lb-hub>`
|
|
496
531
|
- `lb-show` on a `<template>`, or on a row template's root
|
|
497
532
|
- `lb-show` with no `lb-query` around it
|
|
533
|
+
- `lb-show="!"` or `lb-show="!!column"`, which reverse no column
|
|
@@ -223,6 +223,9 @@ Every value in `values` is a string, as a control's value is.
|
|
|
223
223
|
### refresh and patch
|
|
224
224
|
|
|
225
225
|
List in `refresh` every query whose whole answer the request changed.
|
|
226
|
+
Never list one only to fill what the request's answer creates, such as the
|
|
227
|
+
picker in a new row: the hub fills that from the query's last answer. See
|
|
228
|
+
[What arrives later](./data-binding.md#what-arrives-later).
|
|
226
229
|
|
|
227
230
|
Return answers from `run` to state a narrower change than re-running a query
|
|
228
231
|
would. What `run` returns is keyed by query name and laid over the refreshed
|
|
@@ -236,8 +239,33 @@ queries:
|
|
|
236
239
|
| `patch({ drop: [...] })` | These keys are gone; the rest stand |
|
|
237
240
|
|
|
238
241
|
Return a patch for a change the request knows the extent of — one row added,
|
|
239
|
-
one row dropped, one row edited — and leave `refresh` empty.
|
|
240
|
-
|
|
242
|
+
one row dropped, one row edited — and leave `refresh` empty. A refreshed
|
|
243
|
+
`rows` query sends every row, and the hub places every row again, which moves
|
|
244
|
+
each element and takes focus from the control the user is in.
|
|
245
|
+
|
|
246
|
+
A change that seems to need a refresh usually has a patch:
|
|
247
|
+
|
|
248
|
+
- A placeholder row standing in for an empty set or section: drop its key in
|
|
249
|
+
the patch that adds the first real row, and add it back in the patch that
|
|
250
|
+
drops the last.
|
|
251
|
+
- A row whose position changes: the rows host places it, as
|
|
252
|
+
[`lb-table`](./widgets.md#lb-table) does by `data-sort`.
|
|
253
|
+
- A row whose other columns change with the edit: re-read the row and patch
|
|
254
|
+
it.
|
|
255
|
+
- Rows the database removes with a deleted one: select their keys before the
|
|
256
|
+
delete and drop them too.
|
|
257
|
+
|
|
258
|
+
An update or a delete always has a patch, since it names its row by key and
|
|
259
|
+
removing a row never reorders the rest. `createHub` refuses at startup a
|
|
260
|
+
`crud` `rowUpdate` or `rowDelete` on a `rows` query whose `refresh` names
|
|
261
|
+
that same query. An insert may refresh its own query: where a new row goes
|
|
262
|
+
depends on whether its host places it, which the server cannot see.
|
|
263
|
+
|
|
264
|
+
Each request's `refresh` is its own, and every query in it needs its own
|
|
265
|
+
reason. A list shared by several requests is a warning sign.
|
|
266
|
+
|
|
267
|
+
Re-run the query when membership or order changed in a way the request
|
|
268
|
+
cannot name:
|
|
241
269
|
|
|
242
270
|
```ts
|
|
243
271
|
resetRoster: {
|