@loadbare/app 0.7.4 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -94,17 +94,13 @@ export interface CrudOp<W> {
94
94
  run: (ctx: HubContext, where: W) => void | HubData | Promise<void | HubData>;
95
95
  refresh: string[];
96
96
  }
97
- export type CellChangeOp = CrudOp<{
98
- key: string;
99
- cell: string;
100
- value: string;
101
- }>;
102
97
  export type RowDeleteOp = CrudOp<{
103
98
  key: string;
104
99
  }>;
105
100
  export type RowInsertOp = CrudOp<{
106
101
  values: Record<string, string>;
107
102
  }>;
103
+ /** `values` holds only the columns being set; see `Hub.runRowUpdate`. */
108
104
  export type RowUpdateOp = CrudOp<{
109
105
  key: string;
110
106
  values: Record<string, string>;
@@ -117,11 +113,10 @@ export type RowUpdateOp = CrudOp<{
117
113
  *
118
114
  * Every key here is the reserved `lb-action` value with the prefix stripped
119
115
  * and the rest camel-cased, so the attribute, the wire field and this key
120
- * are one vocabulary. All four are list operations: each needs a key, and a
116
+ * are one vocabulary. All three are list operations: each needs a key, and a
121
117
  * key exists only on a live row inside a list.
122
118
  */
123
119
  export interface Crud {
124
- cellChange?: CellChangeOp;
125
120
  rowDelete?: RowDeleteOp;
126
121
  rowInsert?: RowInsertOp;
127
122
  rowUpdate?: RowUpdateOp;
@@ -165,13 +160,6 @@ export interface Hub {
165
160
  * set declared with it.
166
161
  */
167
162
  runAction(page: string, name: string, where: Where, ctx: HubContext): Promise<HubData>;
168
- /** Edit one cell: run the list's declared `cellChange`, then its refresh set. */
169
- runCellChange(page: string, where: {
170
- list: string;
171
- key: string;
172
- cell: string;
173
- value: string;
174
- }, ctx: HubContext): Promise<HubData>;
175
163
  /** Drop one row: run the list's declared `rowDelete`, then its refresh set. */
176
164
  runRowDelete(page: string, where: {
177
165
  list: string;
@@ -182,7 +170,12 @@ export interface Hub {
182
170
  list: string;
183
171
  values: Record<string, string>;
184
172
  }, ctx: HubContext): Promise<HubData>;
185
- /** Edit several cells at once: run the list's declared `rowUpdate`, then its refresh set. */
173
+ /**
174
+ * Edit a row: run the list's declared `rowUpdate`, then its refresh set.
175
+ * `values` holds only the columns being set — one from a widget that
176
+ * commits a cell, several from a form — and a column absent from it is left
177
+ * as it is, as an SQL UPDATE leaves it.
178
+ */
186
179
  runRowUpdate(page: string, where: {
187
180
  list: string;
188
181
  key: string;
@@ -1 +1 @@
1
- {"version":3,"file":"lb-server.d.ts","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"lb-server.d.ts","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"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"}
@@ -100,9 +100,6 @@ export function createHub(pages) {
100
100
  runAction(page, name, where, ctx) {
101
101
  return settle(page, pages[page]?.requests.actions?.[name], where, ctx, `loadbare: page '${page}' declares no action '${name}'`);
102
102
  },
103
- runCellChange(page, where, ctx) {
104
- return settle(page, pages[page]?.requests.crud?.[where.list]?.cellChange, { key: where.key, cell: where.cell, value: where.value }, ctx, `loadbare: page '${page}' declares no cellChange for '${where.list}'`);
105
- },
106
103
  runRowDelete(page, where, ctx) {
107
104
  return settle(page, pages[page]?.requests.crud?.[where.list]?.rowDelete, { key: where.key }, ctx, `loadbare: page '${page}' declares no rowDelete for '${where.list}'`);
108
105
  },
@@ -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 CellChangeOp = CrudOp<{ key: string; cell: string; value: string }>;\nexport type RowDeleteOp = CrudOp<{ key: string }>;\nexport type RowInsertOp = CrudOp<{ values: Record<string, string> }>;\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 four 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 cellChange?: CellChangeOp;\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 /** Edit one cell: run the list's declared `cellChange`, then its refresh set. */\n runCellChange(\n page: string,\n where: { list: string; key: string; cell: string; value: string },\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 /** Edit several cells at once: run the list's declared `rowUpdate`, then its refresh set. */\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 runCellChange(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.cellChange,\n { key: where.key, cell: where.cell, value: where.value },\n ctx,\n `loadbare: page '${page}' declares no cellChange for '${where.list}'`,\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 */\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"]}
@@ -529,8 +529,7 @@ name, the same test that decides how a value lands — see
529
529
  | -------------- | ------------------------------------------------- |
530
530
  | lb-row-insert | a `<form>`, or a button in a `<tr>`, in a list |
531
531
  | lb-row-delete | anything inside a live row |
532
- | lb-row-update | a `<form>`, or a button in a live row |
533
- | lb-cell-change | a widget wrapping one control |
532
+ | lb-row-update | a `<form>`, a button in a live row, or a widget cell |
534
533
  | anything else | must be a named routine in the page's server code |
535
534
 
536
535
  The wire format is not visible to the user, but uses the same lb-*
@@ -545,7 +544,6 @@ carries.
545
544
  | `action` | Filled from scope | Required |
546
545
  | ---------------- | ------------------------------ | ---------------------- |
547
546
  | a declared name | `list` or `row`, `key`, `cell` | nothing |
548
- | `lb-cell-change` | `list`, `key`, `cell` | all three, and `value` |
549
547
  | `lb-row-insert` | `list` | `list`, and `values` |
550
548
  | `lb-row-delete` | `list`, `key` | both |
551
549
  | `lb-row-update` | `list`, `key` | both, and `values` |
@@ -556,7 +554,13 @@ way a control belongs to the form around it. `list` or `row` comes from the
556
554
  nearest ancestor scope, `key` from the nearest live row inside that scope,
557
555
  and `cell` from the dispatching element's own `lb-cell`.
558
556
  A request missing a required field is not sent. The hub never fills
559
- `value`. An `lb-row-insert` or `lb-row-update` gathers the row it belongs
557
+ `value`. An `lb-row-insert` or `lb-row-update` from an element carrying
558
+ `lb-cell` is a record of one cell, the way a control has a value and a form
559
+ has values: `values` holds that cell alone, taken from the `value` the widget
560
+ sent or else read from the control it is or wraps, and `value` is not sent
561
+ beside it. A widget that sends a `value` from an element carrying no
562
+ `lb-cell` is refused, since nothing names the column. Any other
563
+ `lb-row-insert` or `lb-row-update` gathers the row it belongs
560
564
  to: `values` holds every `lb-cell` with a control to read in the nearest
561
565
  `<form>`, `<tr>` or live row around the dispatching element, itself
562
566
  included, inside its scope. The cells are found the way a row lands, so a
@@ -569,10 +573,11 @@ own form; a live row counts because it is the row the key names. The scope
569
573
  itself is never the row, since its cells belong to other rows. A request
570
574
  from an element in no form or row is not sent, and the hub reports that its
571
575
  cells belong in a `<form>`. A request that already carries `values` keeps
572
- them, and one whose row has no cell to read is not sent. A click inside a
573
- native element that carries either operation and
574
- holds cells itself is not sent, since clicking into one of its controls
575
- would send the row: put the action on a form or on a button.
576
+ them, and one whose row has no cell to read is not sent. A click on a
577
+ native element that carries either operation and is a cell or
578
+ holds cells is not sent, since clicking into one of its controls
579
+ would send the row: put the action on a form, on a button, or on a widget
580
+ that decides when its cell has changed.
576
581
 
577
582
  A widget dispatches the action and, where it wraps a control, that control's
578
583
  value.
@@ -581,7 +586,7 @@ value.
581
586
  { action: "lb-row-insert", list: "rosterList", values: {...} }
582
587
  { action: "lb-row-delete", list: "rosterList", key: "42" }
583
588
  { action: "lb-row-update", list: "rosterList", key: "42", values: {...} }
584
- { action: "lb-cell-change", list: "rosterList", key: "42", cell: "name", value: "Ann" }
589
+ { action: "lb-row-update", list: "rosterList", key: "42", values: { name: "Ann" } }
585
590
  { action: "selectTab", row: "prefs", cell: "active_tab", value: "two" }
586
591
  ```
587
592
 
@@ -912,7 +917,7 @@ It has three optional keys.
912
917
  | Key | Keyed by | Answers |
913
918
  | ------------- | --------------------- | -------------------------------------------------- |
914
919
  | `actions` | The `lb-action` value | Anything the page chooses to declare |
915
- | `crud` | A query name | The four reserved `lb-action` values |
920
+ | `crud` | A query name | The three reserved `lb-action` values |
916
921
  | `onPageEnter` | Nothing | Runs once on entering the page, before its queries |
917
922
 
918
923
 
@@ -963,16 +968,19 @@ export const requests: Requests = {
963
968
 
964
969
  Each key under a `crud` entry is a reserved `lb-action` value with its prefix
965
970
  stripped and the rest camel-cased, so the attribute, the wire and this key
966
- are one vocabulary. All four operate on a list, because each needs a key and
971
+ are one vocabulary. All three operate on a list, because each needs a key and
967
972
  a key exists only on a live row.
968
973
 
969
974
  | `lb-action` | Key under `crud` | `where` carries |
970
975
  | ---------------- | ---------------- | ---------------------- |
971
- | `lb-cell-change` | `cellChange` | `key`, `cell`, `value` |
972
976
  | `lb-row-delete` | `rowDelete` | `key` |
973
977
  | `lb-row-insert` | `rowInsert` | `values` |
974
978
  | `lb-row-update` | `rowUpdate` | `key`, `values` |
975
979
 
980
+ A `rowUpdate` sets the columns `values` names and leaves the rest as they
981
+ are, as an SQL `UPDATE` does. A form sends the cells it holds and a widget
982
+ cell sends itself, so one handler answers both.
983
+
976
984
  ## Widgets
977
985
 
978
986
  ---- UNEDITED ----
@@ -1124,7 +1132,7 @@ application writes the ones this table names and invents none of its own,
1124
1132
  because a name Loadbare has not defined today it may define tomorrow.
1125
1133
 
1126
1134
  The prefix reaches past HTML. A query name cannot begin with `lb-` either,
1127
- which is what keeps the four operations apart from an application's own
1135
+ which is what keeps the three operations apart from an application's own
1128
1136
  actions on the wire — see [Page Queries](#page-queries).
1129
1137
 
1130
1138
  Each attribute is defined in one section, and this table says which.
@@ -71,17 +71,23 @@ closed.
71
71
  | Operation | Takes from position | Carries | Answers |
72
72
  | ---------------- | ------------------- | -------------------------------------------- | ------------ |
73
73
  | `lb-row-insert` | list | values, gathered from the record | patch rows |
74
- | `lb-row-update` | list, key | values, gathered or supplied by a widget | patch rows |
74
+ | `lb-row-update` | list, key | values, gathered, or one cell from a widget | patch rows |
75
75
  | `lb-row-delete` | list, key | nothing | patch drop |
76
76
  | a declared name | list or row, key, cell | value | any names |
77
77
 
78
- ### Proposed: fold `lb-cell-change` into `lb-row-update`
78
+ ### Done: `lb-cell-change` folded into `lb-row-update`
79
79
 
80
- A cell change is an update whose `values` holds one entry, and the hub
81
- already keeps `values` a widget supplies. SQL has one UPDATE whether it sets
82
- one column or many. On the server, `cellChange` and `rowUpdate` merge into
83
- one `update(key, values)`, where a column absent from `values` is left as it
84
- is.
80
+ A cell change is an update whose `values` holds one entry. SQL has one
81
+ UPDATE whether it sets one column or many. On the server, `cellChange` and
82
+ `rowUpdate` merged into `rowUpdate(key, values)`, where a column absent from
83
+ `values` is left as it is.
84
+
85
+ The fold needed one rule beyond this proposal. Without it, a widget sending
86
+ `lb-row-update` gathers its whole record, and every other editable cell in
87
+ the row goes with it. The rule: an element carrying `lb-cell` is a record
88
+ of one cell, the way a control has a value and a form has values. Its
89
+ `values` holds that cell alone, from the `value` the widget sent or else its
90
+ control, so a widget never writes its own column name into the request.
85
91
 
86
92
  ### Reordering is not an operation
87
93
 
@@ -177,8 +183,9 @@ overwrites that in 0.7.3, so the rule introduces nothing new.
177
183
 
178
184
  1. The hub holds each name's current value and fills scopes that appear
179
185
  late.
180
- 2. `lb-cell-change` folds into `lb-row-update`, with widget-supplied
181
- `values`, and the server's `cellChange` and `rowUpdate` merge.
186
+ 2. `lb-cell-change` folds into `lb-row-update`, with an element carrying
187
+ `lb-cell` sending itself alone, and the server's `cellChange` and
188
+ `rowUpdate` merge. Done.
182
189
  3. The reference states the patch row rule: upsert by key, unnamed columns
183
190
  unchanged.
184
191
 
@@ -411,25 +411,26 @@ Data types are an open blocker in TECHREF-1.0.
411
411
  ### Loadbare/app
412
412
 
413
413
  `lb-action` names what an interaction asks for. A value beginning with `lb-`
414
- is one of four reserved operations; any other value is an action the page
414
+ is one of three reserved operations; any other value is an action the page
415
415
  declares.
416
416
 
417
417
  | `lb-action` | Written on | Carries | Server key under `crud` |
418
418
  |------------------|---------------------------------------------|---------------------------------|-------------------------|
419
419
  | `lb-row-insert` | A `<form>`, or a button in a `<tr>`, in a list | `list`, `values` | `rowInsert` |
420
- | `lb-row-update` | A `<form>` or a button in a live row | `list`, `key`, `values` | `rowUpdate` |
420
+ | `lb-row-update` | A `<form>`, a button in a live row, or a widget cell | `list`, `key`, `values` | `rowUpdate` |
421
421
  | `lb-row-delete` | Anything inside a live row | `list`, `key` | `rowDelete` |
422
- | `lb-cell-change` | A widget wrapping one control | `list`, `key`, `cell`, `value` | `cellChange` |
423
422
  | Any other name | Any element | Scope in effect, and a widget's `value` | Under `actions` |
424
423
 
425
424
  - Position comes from the document. `list` or `row` comes from the nearest
426
425
  ancestor scope, `key` from the nearest live row, `cell` from the element's
427
426
  own `lb-cell`. The author writes the action and not the position.
428
427
  - Values are gathered from the nearest `<form>`, `<tr>` or live row around
429
- the element, the way a button submits its form owner.
428
+ the element, the way a button submits its form owner. A widget carrying
429
+ `lb-cell` sends its own cell alone, so one `rowUpdate` answers a form and a
430
+ per-field edit.
430
431
  - A native element sends on click, a form on submit. A widget sends its own.
431
432
  - A declared action carries no argument list.
432
- - A single-row scope is read-only for the four operations, because each needs
433
+ - A single-row scope is read-only for the three operations, because each needs
433
434
  a key and a key exists only on a live row in a list.
434
435
 
435
436
  On the server, each entry has `run` and `refresh`. `run` does the work and
@@ -728,7 +729,7 @@ TECHREF-1.0 lists every name Loadbare/app owns in one cross-reference.
728
729
  | Owned | Count | Names |
729
730
  |-------------------------------------|-------|---------------------------------------------------------------|
730
731
  | `lb-*` HTML attributes | 16 | `lb-list`, `lb-row`, `lb-cell`, `lb-show`, `lb-key`, `lb-key-value`, `lb-value`, `lb-action`, `lb-nav-link`, `lb-pending`, `lb-error`, `lb-row-count`, `lb-unknown-page`, `lb-slot`, `lb-template`, `lb-page` |
731
- | Reserved `lb-action` values | 4 | `lb-row-insert`, `lb-row-update`, `lb-row-delete`, `lb-cell-change` |
732
+ | Reserved `lb-action` values | 3 | `lb-row-insert`, `lb-row-update`, `lb-row-delete` |
732
733
  | `lb*` methods on custom elements | 2 | `lbPlaceRow`, `lbRowsLanded` |
733
734
  | Attribute namespace for expansion | 1 | `exp-*` |
734
735
  | Reserved tags | 1 | `<lb-hub>` |
@@ -229,8 +229,8 @@ as a string literal.
229
229
  | `ATTR_KEY` | `lb-key` |
230
230
  | `ATTR_KEY_VALUE` | `lb-key-value` |
231
231
  | `ATTR_ACTION` | `lb-action`|
232
- | `ACTION_ROW_INSERT`, `ACTION_ROW_DELETE`, `ACTION_ROW_UPDATE`, `ACTION_CELL_CHANGE` | the reserved `lb-action` values |
233
- | `LB_ACTIONS` | all four of them, in one array |
232
+ | `ACTION_ROW_INSERT`, `ACTION_ROW_DELETE`, `ACTION_ROW_UPDATE` | the reserved `lb-action` values |
233
+ | `LB_ACTIONS` | all three of them, in one array |
234
234
  | `LB_RESERVED_PREFIX` | `lb-`, the prefix every reserved name begins with |
235
235
  | `ATTR_ROW_COUNT` | `lb-row-count`|
236
236
  | `LB_EVENT_NAME` | `lb-request` |
@@ -287,7 +287,7 @@ control's value as its `detail`:
287
287
  import { LB_EVENT_NAME } from "@loadbare/app/constants";
288
288
  import type { HubRequest } from "@loadbare/app/types";
289
289
 
290
- const detail: HubRequest = { action: "lb-cell-change", value: input.value };
290
+ const detail: HubRequest = { action: "lb-row-update", value: input.value };
291
291
  this.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
292
292
  ```
293
293
 
@@ -296,9 +296,14 @@ event, and does not send a request missing a field its operation requires.
296
296
  See [TECHREF-1.0](../TECHREF-1.0.md#requests) for what each operation is
297
297
  filled with.
298
298
 
299
- A widget sending `lb-row-insert` or `lb-row-update` doesn't read its own
300
- controls. It dispatches the bare action from the row, or from anything
301
- inside it, and the hub gathers `values` from the row the event came from:
299
+ A widget that carries `lb-cell` and sends `lb-row-insert` or `lb-row-update`
300
+ sends that one cell. The hub builds `values` from the cell's name and the
301
+ `value` the widget sent, or reads the control the widget wraps when it sent
302
+ none.
303
+
304
+ A widget that carries no `lb-cell` doesn't read its own controls. It
305
+ dispatches the bare action from the row, or from anything inside it, and the
306
+ hub gathers `values` from the row the event came from:
302
307
  the nearest `<form>`, `<tr>` or live row around the dispatching element,
303
308
  itself included, inside its scope. Every readable `lb-cell` in that row is
304
309
  gathered, wherever in the row it sits, except the cells of a scope nested in
@@ -152,9 +152,9 @@ the prefix stripped and the rest camel-cased.
152
152
  | `lb-action="lb-row-delete"` | `rowDelete` | `list`, `key` |
153
153
  | `lb-action="lb-row-insert"` on a `<form>` | `rowInsert` | `list`, `values` |
154
154
  | `lb-action="lb-row-update"` on a `<form>` | `rowUpdate` | `list`, `key`, `values` |
155
- | `lb-action="lb-cell-change"` on a widget | `cellChange` | `list`, `key`, `cell`, `value`|
155
+ | `lb-action="lb-row-update"` on a widget cell | `rowUpdate` | `list`, `key`, `values` of one cell |
156
156
 
157
- All four operations are list operations. Each needs a key, and a key exists
157
+ All three operations are list operations. Each needs a key, and a key exists
158
158
  only on a live row the hub stamped inside a list, so a single-row scope is
159
159
  read-only and a declared action is the only thing it can send. An application
160
160
  that wants a writable single row declares a list that answers with one row.
@@ -166,15 +166,11 @@ with one an application already uses. That reservation is also the whole of
166
166
  the wire discriminant: a value beginning with `lb-` is an operation, and
167
167
  anything else is a name the page declared.
168
168
 
169
- The hub sends the first four from a native element on the element's own
170
- event: a form on submit, anything else on click. An insert or update clicked
171
- from a button reads the form or row the button is in (see [Forms](#forms)). A
172
- cell change needs a
173
- widget to say what a change is, so only a widget sends it: the shipped
174
- `<lb-input>` does when it carries `lb-action="lb-cell-change"`, and stays
175
- quiet otherwise — an input inside an `lb-row-insert` or `lb-row-update` form is read
176
- again by the form on submit, so a widget that sent on its own would write
177
- the same edit twice.
169
+ The hub sends a request from a native element on the element's own event: a
170
+ form on submit, anything else on click. An insert or update clicked from a
171
+ button reads the form or row the button is in (see [Forms](#forms)). To
172
+ commit one cell as it changes, put `lb-row-update` on a widget cell instead
173
+ (see [Committing one cell](#committing-one-cell)).
178
174
 
179
175
  Declare every action on the server, and permit every operation on its list. A
180
176
  name the page has not declared, and an operation a list does not permit, are
@@ -184,7 +180,7 @@ refused; see
184
180
  ### Actions
185
181
 
186
182
  Write `lb-action` on a button to ask the server to do something that is not
187
- one of the four CRUD operations:
183
+ one of the three CRUD operations:
188
184
 
189
185
  ```html
190
186
  <button lb-action="mailRoster">Mail the roster</button>
@@ -280,6 +276,34 @@ refuses it. A widget may dispatch either from any element, and the hub
280
276
  gathers the same way; see
281
277
  [Sending a request](./custom-elements.md#sending-a-request).
282
278
 
279
+ ### Committing one cell
280
+
281
+ Put `lb-row-update` on a widget that carries `lb-cell` to save that one cell
282
+ whenever it changes. The shipped `<lb-input>` sends it on `change`:
283
+
284
+ ```html
285
+ <template lb-key="id">
286
+ <tr>
287
+ <td><lb-input lb-cell="name" lb-action="lb-row-update"></lb-input></td>
288
+ <td><lb-input lb-cell="note" lb-action="lb-row-update"></lb-input></td>
289
+ </tr>
290
+ </template>
291
+ ```
292
+
293
+ An element carrying `lb-cell` is a record of one cell, the way a control has
294
+ a value and a form has values. Its update carries `values` holding that cell
295
+ alone, so an edit in one input never sends the other. It reaches the same
296
+ `rowUpdate` a form does. SQL has one UPDATE whether it sets one column or
297
+ many, and the application writes one handler for both.
298
+
299
+ The widget decides when the cell has changed. A native `<input>` carrying
300
+ `lb-cell` and `lb-row-update` has no such moment, since a click into it
301
+ would send it, so the hub refuses it and says so.
302
+
303
+ Leave `lb-action` off a widget inside an `lb-row-insert` or `lb-row-update`
304
+ form. The form reads every `lb-cell` in it on submit, so a widget that also
305
+ sent its own would write the same edit twice.
306
+
283
307
  ## Conditional rendering
284
308
 
285
309
  Loadbare ships static HTML and hydrates elements that are already in the
@@ -86,7 +86,7 @@ Export `requests` from `<name>.requests.ts`. It holds three keys, each optional:
86
86
  |---------------|------------------------------------------------------|
87
87
  | `onPageEnter` | Before the page's queries, on entering the page |
88
88
  | `actions` | What the page may be asked to do, by name |
89
- | `crud` | The four operations a list permits on its rows |
89
+ | `crud` | The three operations a list permits on its rows |
90
90
 
91
91
  ### onPageEnter
92
92
 
@@ -127,17 +127,21 @@ carries `list` or `row`, whichever attribute scoped the element, plus `key`,
127
127
  ### crud
128
128
 
129
129
  Declare CRUD operations under `crud`, keyed by the list they operate on. All
130
- four are list operations: each needs a key, and a key exists only on a live
130
+ three are list operations: each needs a key, and a key exists only on a live
131
131
  row inside a list, so a single-row scope is read-only and a declared action is
132
132
  the only thing it can send. Each operation takes the binding its trigger
133
133
  supplies:
134
134
 
135
- | Operation | The page writes | `run` receives |
136
- |---------------|------------------------------------------|------------------------|
137
- | `cellChange` | `<lb-input lb-action="lb-cell-change">` | `key`, `cell`, `value` |
138
- | `rowDelete` | `lb-action="lb-row-delete"` | `key` |
139
- | `rowInsert` | `<form lb-action="lb-row-insert">` | `values` |
140
- | `rowUpdate` | `<form lb-action="lb-row-update">` | `key`, `values` |
135
+ | Operation | The page writes | `run` receives |
136
+ |-------------|------------------------------------------------------------------------|-----------------|
137
+ | `rowDelete` | `lb-action="lb-row-delete"` | `key` |
138
+ | `rowInsert` | `<form lb-action="lb-row-insert">` | `values` |
139
+ | `rowUpdate` | `<form lb-action="lb-row-update">` or `<lb-input lb-action="lb-row-update">` | `key`, `values` |
140
+
141
+ Write `rowUpdate` to set the columns `values` names and leave every other
142
+ column as it is. A form sends the cells it holds, and a widget cell sends
143
+ itself alone. Check the names in `values` against the columns the list lets
144
+ the page edit.
141
145
 
142
146
  The operation names are reserved: a name beginning with `lb-` cannot be
143
147
  declared under `actions` or as a query, and `createHub` refuses a page that
@@ -179,7 +183,7 @@ would. What `run` returns is laid over the refreshed queries:
179
183
  | `patch({ drop: [...] })` | These keys are gone; the rest stand |
180
184
 
181
185
  Return a patch for a change the operation knows the extent of — one row added,
182
- one row dropped, one cell edited — and leave `refresh` empty. Re-run the query
186
+ one row dropped, one row edited — and leave `refresh` empty. Re-run the query
183
187
  instead when membership or order changed in a way the operation cannot name:
184
188
 
185
189
  ```ts
@@ -27,7 +27,7 @@ for where a listed package sits in the cascade.
27
27
 
28
28
  Wraps an `<input>`. `lb-value` sets the input's `.value`. The widget sends
29
29
  nothing on its own: `lb-action` names what the input's `change` sends. The
30
- reserved `lb-cell-change` sends `lb-cell-change`; any other name sends that
30
+ reserved `lb-row-update` saves the input's own cell; any other name sends that
31
31
  action. Either carries the input's value, and the hub adds the scope the
32
32
  input sits in.
33
33
 
@@ -42,7 +42,7 @@ of them, so an input that also sent its own would write the same edit twice.
42
42
 
43
43
  | Attribute | Asks for |
44
44
  | ---------- | ----- |
45
- | `lb-action` | what to send on `change`; `lb-cell-change` for the cell's own edit |
45
+ | `lb-action` | what to send on `change`; `lb-row-update` to save the cell's own edit |
46
46
 
47
47
  ## `lb-select`
48
48
 
package/docs/roadmap.md CHANGED
@@ -140,7 +140,7 @@ machinery would then also let `lb-list` and `lb-row` names be checked against
140
140
  the declared queries.
141
141
 
142
142
  One piece is separable and needs none of the above: an `lb-action` beginning
143
- with `lb-` that names none of the four operations is a typo the builder can
143
+ with `lb-` that names none of the three operations is a typo the builder can
144
144
  refuse from markup alone. `LB_ACTIONS` in `core/lb-constants.ts` exists for
145
145
  this, and the builder does not yet import it.
146
146
 
package/docs/testing.md CHANGED
@@ -188,10 +188,14 @@ What the hub is tested for here:
188
188
  its scope, whatever shares the button's cell: one ghost row among its
189
189
  neighbours, a whole live row, a form nested in a row, a button's form
190
190
  owner; and refuses a `<div>` holding cells, pointing to `<form>`
191
+ - an insert or update from an element carrying `lb-cell` sends that cell
192
+ alone, from the value a widget sent or else its control; refuses a value
193
+ with no `lb-cell` to name its column; and refuses a click on a native
194
+ element that is a cell
191
195
  - gathering skips the cells of a nested scope, so a picker in a row sends
192
196
  its own cell and nothing about its options
193
197
  - a request arriving with no action, or with a reserved name that is not one
194
- of the four, is refused before it reaches the wire
198
+ of the three, is refused before it reaches the wire
195
199
  - `lb-pending` lands on the element that dispatched, `lb-error` replaces it
196
200
  on failure, and the next request clears it
197
201
  - `aria-busy` comes and goes with `lb-pending`; a native button or form
package/docs/theory.md CHANGED
@@ -460,7 +460,7 @@ that "knows what to do", assembling a request driven purely from
460
460
  attributes.
461
461
 
462
462
  This requirement is satisfied with a single new attribute, `lb-action`.
463
- Four values are reserved, and any other value is interpreted as the
463
+ Three values are reserved, and any other value is interpreted as the
464
464
  name of a routine on the server.
465
465
 
466
466
  A request is an action and a position. The author writes the action,
@@ -37,7 +37,7 @@ class NoteInput extends HTMLElement {
37
37
  this.dispatchEvent(
38
38
  new CustomEvent(LB_EVENT_NAME, {
39
39
  bubbles: true,
40
- detail: { action: "lb-cell-change", value: input.value },
40
+ detail: { action: "lb-row-update", value: input.value },
41
41
  }),
42
42
  );
43
43
  });
@@ -49,9 +49,9 @@ customElements.define("note-input", NoteInput);
49
49
 
50
50
  A widget with both a `.html` and a `.browser.ts` file shares one tag name; the
51
51
  build finds each half independently. On change, it dispatches a
52
- `lb-cell-change` carrying the input's value. The hub adds the list, the key
53
- of the row it is in and its own `lb-cell`, the same way it does for a
54
- `lb-row-update` form.
52
+ `lb-row-update` carrying the input's value. The hub adds the list and the key
53
+ of the row it is in, and turns the value into a `values` map holding only
54
+ the widget's own `lb-cell`.
55
55
 
56
56
  ## Using it
57
57
 
@@ -72,30 +72,11 @@ every change, so there's nothing left to batch.
72
72
 
73
73
  ## Answering it server-side
74
74
 
75
- ```ts
76
- // src/pages/about.requests.ts
77
- import { patch } from "@loadbare/app/server";
78
-
79
- export const requests = {
80
- // ...onPageEnter and actions unchanged...
81
- crud: {
82
- notes: {
83
- // ...rowInsert, rowDelete, rowUpdate unchanged...
84
- cellChange: {
85
- run: async (ctx, { key, value }) => {
86
- const note = await ctx.db.updateNote(key, value);
87
- return { notes: patch({ rows: [note] }) };
88
- },
89
- refresh: [],
90
- },
91
- },
92
- },
93
- };
94
- ```
95
-
96
- `cellChange` reuses `updateNote` from [Updating a List Item](./076-updating-a-list-item.md)
97
- — the database doesn't care whether the edit came from a form's values
98
- map or one widget's value.
75
+ There is nothing to add. The request is the same `lb-row-update` the form
76
+ sent in [Updating a List Item](./076-updating-a-list-item.md), with `values`
77
+ holding only `text`, so the `rowUpdate` written there answers it unchanged.
78
+ A `rowUpdate` sets the columns `values` names and leaves the rest alone, as
79
+ an SQL `UPDATE` does, so one handler serves a form and a widget alike.
99
80
 
100
81
  ## Run it
101
82