@loadbare/app 0.4.0 → 0.5.1
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/README.md +53 -82
- package/dist/build/assemble.d.ts +7 -5
- package/dist/build/assemble.d.ts.map +1 -1
- package/dist/build/assemble.js +29 -9
- package/dist/build/cli.d.ts +20 -12
- package/dist/build/cli.d.ts.map +1 -1
- package/dist/build/cli.js +34 -16
- package/dist/build/elements.d.ts +15 -29
- package/dist/build/elements.d.ts.map +1 -1
- package/dist/build/elements.js +25 -111
- package/dist/build/expand.d.ts +1 -1
- package/dist/build/expand.js +1 -1
- package/dist/build/format.d.ts +6 -3
- package/dist/build/format.d.ts.map +1 -1
- package/dist/build/format.js +6 -3
- package/dist/build/locations.d.ts +14 -37
- package/dist/build/locations.d.ts.map +1 -1
- package/dist/build/locations.js +31 -69
- package/dist/build/origins.d.ts +109 -0
- package/dist/build/origins.d.ts.map +1 -0
- package/dist/build/origins.js +270 -0
- package/dist/core/lb-constants.d.ts +1 -0
- package/dist/core/lb-constants.d.ts.map +1 -1
- package/dist/core/lb-constants.js +15 -8
- package/dist/core/lb-types.d.ts +2 -2
- package/dist/core/lb-types.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +1 -1
- package/dist/hub/lb-hub.d.ts.map +1 -1
- package/dist/hub/lb-hub.js +44 -17
- package/dist/hub/lb-rows.d.ts.map +1 -1
- package/dist/hub/lb-rows.js +3 -3
- package/dist/server/lb-express.d.ts.map +1 -1
- package/dist/server/lb-server.d.ts +5 -4
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/tests/assemble.test.js +11 -4
- package/dist/tests/elements.test.js +47 -51
- package/dist/tests/expand.test.d.ts +1 -1
- package/dist/tests/expand.test.js +2 -2
- package/dist/tests/fixtures/elements/collision/imports.d.ts +3 -0
- package/dist/tests/fixtures/elements/collision/imports.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/collision/imports.js +1 -0
- package/dist/tests/fixtures/elements/manifest/imports.d.ts +3 -0
- package/dist/tests/fixtures/elements/manifest/imports.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/manifest/imports.js +1 -0
- package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts +3 -0
- package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/manifest-bad-entry/imports.js +1 -0
- package/dist/tests/fixtures/elements/{collision/elements.d.ts → manifest-not-array/imports.d.ts} +1 -1
- package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/manifest-not-array/imports.js +1 -0
- package/dist/tests/fixtures/elements/pkg/acme-widget.d.ts +2 -0
- package/dist/tests/fixtures/elements/pkg/acme-widget.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/pkg/acme-widget.js +1 -0
- package/dist/tests/lb-express.test.js +1 -1
- package/dist/tests/origins.test.d.ts +10 -0
- package/dist/tests/origins.test.d.ts.map +1 -0
- package/dist/tests/origins.test.js +326 -0
- package/dist/tests/pages.test.js +3 -3
- package/dist/tests/styles.test.js +7 -4
- package/docs/reference/builder.md +128 -0
- package/docs/reference/chrome.md +75 -0
- package/docs/reference/css.md +44 -0
- package/docs/reference/custom-elements.md +327 -0
- package/docs/reference/data-binding.md +240 -0
- package/docs/reference/overview.md +38 -0
- package/docs/reference/page-files.md +175 -0
- package/docs/reference/server.md +123 -0
- package/docs/reference/widgets.md +163 -0
- package/docs/roadmap.md +130 -0
- package/docs/testing.md +228 -0
- package/docs/theory.md +344 -223
- package/docs/tutorials/000-getting-started.md +86 -0
- package/docs/tutorials/010-pages-and-navigation.md +129 -0
- package/docs/tutorials/020-css.md +103 -0
- package/docs/tutorials/030-html-decomposition.md +79 -0
- package/docs/tutorials/040-displaying-data.md +169 -0
- package/docs/tutorials/050-actions.md +77 -0
- package/docs/tutorials/060-custom-element-code.md +73 -0
- package/docs/tutorials/065-conditional-rendering.md +161 -0
- package/docs/tutorials/070-displaying-a-list.md +137 -0
- package/docs/tutorials/072-inserting-into-a-list.md +88 -0
- package/docs/tutorials/074-deleting-from-a-list.md +77 -0
- package/docs/tutorials/076-updating-a-list-item.md +86 -0
- package/docs/tutorials/080-widget-requests.md +124 -0
- package/docs/tutorials/090-using-widget-libraries.md +75 -0
- package/package.json +10 -18
- package/dist/client.js +0 -522
- package/dist/demo-static/src/widgets/app-box.d.ts +0 -15
- package/dist/demo-static/src/widgets/app-box.d.ts.map +0 -1
- package/dist/demo-static/src/widgets/app-box.js +0 -19
- package/dist/tests/fixtures/elements/collision/elements.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/collision/elements.js +0 -3
- package/dist/tests/fixtures/elements/manifest/elements.d.ts +0 -5
- package/dist/tests/fixtures/elements/manifest/elements.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/manifest/elements.js +0 -3
- package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts +0 -5
- package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/manifest-bad-tag/elements.js +0 -3
- package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts +0 -5
- package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts.map +0 -1
- package/dist/tests/fixtures/elements/manifest-bad-value/elements.js +0 -3
- package/dist/tests/golden.test.d.ts +0 -19
- package/dist/tests/golden.test.d.ts.map +0 -1
- package/dist/tests/golden.test.js +0 -60
- package/dist/tests/helpers/window.d.ts +0 -43
- package/dist/tests/helpers/window.d.ts.map +0 -1
- package/dist/tests/helpers/window.js +0 -78
- package/dist/tests/lb-input.test.d.ts +0 -9
- package/dist/tests/lb-input.test.d.ts.map +0 -1
- package/dist/tests/lb-input.test.js +0 -78
- package/dist/tests/lb-list.test.d.ts +0 -12
- package/dist/tests/lb-list.test.d.ts.map +0 -1
- package/dist/tests/lb-list.test.js +0 -44
- package/dist/tests/lb-options.test.d.ts +0 -10
- package/dist/tests/lb-options.test.d.ts.map +0 -1
- package/dist/tests/lb-options.test.js +0 -121
- package/dist/tests/lb-picker.test.d.ts +0 -14
- package/dist/tests/lb-picker.test.d.ts.map +0 -1
- package/dist/tests/lb-picker.test.js +0 -59
- package/dist/tests/lb-select.test.d.ts +0 -9
- package/dist/tests/lb-select.test.d.ts.map +0 -1
- package/dist/tests/lb-select.test.js +0 -71
- package/dist/tests/lb-table.test.d.ts +0 -15
- package/dist/tests/lb-table.test.d.ts.map +0 -1
- package/dist/tests/lb-table.test.js +0 -205
- package/dist/widgets/index.d.ts +0 -7
- package/dist/widgets/index.d.ts.map +0 -1
- package/dist/widgets/index.js +0 -6
- package/dist/widgets/lb-input.d.ts +0 -2
- package/dist/widgets/lb-input.d.ts.map +0 -1
- package/dist/widgets/lb-input.js +0 -48
- package/dist/widgets/lb-list.d.ts +0 -2
- package/dist/widgets/lb-list.d.ts.map +0 -1
- package/dist/widgets/lb-list.js +0 -17
- package/dist/widgets/lb-options.d.ts +0 -26
- package/dist/widgets/lb-options.d.ts.map +0 -1
- package/dist/widgets/lb-options.js +0 -72
- package/dist/widgets/lb-picker.d.ts +0 -2
- package/dist/widgets/lb-picker.d.ts.map +0 -1
- package/dist/widgets/lb-picker.js +0 -25
- package/dist/widgets/lb-select.d.ts +0 -2
- package/dist/widgets/lb-select.d.ts.map +0 -1
- package/dist/widgets/lb-select.js +0 -43
- package/dist/widgets/lb-table.d.ts +0 -2
- package/dist/widgets/lb-table.d.ts.map +0 -1
- package/dist/widgets/lb-table.js +0 -113
- package/docs/application-chrome.md +0 -36
- package/docs/building-html-pages.md +0 -130
- package/docs/getting-started.md +0 -120
- package/docs/guide.md +0 -1164
- package/docs/hosting.md +0 -218
- package/docs/latent-risks.md +0 -20
- package/widgets/index.ts +0 -6
- package/widgets/lb-input.html +0 -1
- package/widgets/lb-input.ts +0 -64
- package/widgets/lb-list.html +0 -1
- package/widgets/lb-list.ts +0 -21
- package/widgets/lb-options.html +0 -4
- package/widgets/lb-options.ts +0 -88
- package/widgets/lb-picker.html +0 -7
- package/widgets/lb-picker.ts +0 -27
- package/widgets/lb-select.html +0 -4
- package/widgets/lb-select.ts +0 -55
- package/widgets/lb-table.html +0 -8
- package/widgets/lb-table.ts +0 -126
package/docs/guide.md
DELETED
|
@@ -1,1164 +0,0 @@
|
|
|
1
|
-
# Programmer's Guide
|
|
2
|
-
|
|
3
|
-
Loadbare App is a web application framework for data-driven applications.
|
|
4
|
-
|
|
5
|
-
Loadbare App's "Axiom zero" is that both users and developers are best served
|
|
6
|
-
when static host code displays and allows user interactions with
|
|
7
|
-
fixed-schema data.
|
|
8
|
-
|
|
9
|
-
Most frameworks assume that the entire DOM is subject to change, and they
|
|
10
|
-
carry considerable plumbing to chase down the implications of that assumption.
|
|
11
|
-
Loadbare App assumes only data values change. When the data populates a SELECT,
|
|
12
|
-
TABLE, or some type of tree, there are necessary DOM changes, but they fit
|
|
13
|
-
within the model of a fixed static application, the application author does
|
|
14
|
-
not need to think about how the DOM changes happen.
|
|
15
|
-
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
## The Counter App But Server Persisted
|
|
19
|
-
|
|
20
|
-
Loadbare App assumes the critical path is server state. Here we will show the
|
|
21
|
-
universal example of a counter (two counters actually) where the value is
|
|
22
|
-
persisted on the server.
|
|
23
|
-
|
|
24
|
-
This section shows the files that make a page: the host, its
|
|
25
|
-
queries, its hooks, and the service they call. That is the whole of what you
|
|
26
|
-
write per page.
|
|
27
|
-
|
|
28
|
-
### The host: `demo/pages/hello.html`
|
|
29
|
-
|
|
30
|
-
The host declares the structure of the page and declares which elements
|
|
31
|
-
dynamically display data. Whenever a request is made to the server, the
|
|
32
|
-
response contains data updates. The hub matches the returned data
|
|
33
|
-
to the declarations and updates whatever matches.
|
|
34
|
-
|
|
35
|
-
A complete Loadbare App page includes named queries that retrieve data. In this
|
|
36
|
-
example there are two queries, each of which returns a tuple with one
|
|
37
|
-
keyed value:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
```html
|
|
41
|
-
<h2>Hello, World!</h2>
|
|
42
|
-
|
|
43
|
-
<div lb-query="pageStats">
|
|
44
|
-
<p>
|
|
45
|
-
This page has been visited
|
|
46
|
-
<span lb-cell="visit_count"></span> times!
|
|
47
|
-
</p>
|
|
48
|
-
</div>
|
|
49
|
-
|
|
50
|
-
<p>Reload, or navigate away and back, to increase the count.</p>
|
|
51
|
-
|
|
52
|
-
<div lb-query="counter">
|
|
53
|
-
<p>Button count: <span lb-cell="count"></span></p>
|
|
54
|
-
<button lb-action="increment">+1</button>
|
|
55
|
-
</div>
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
`lb-query` marks the region that displays one query. All DOM nodes
|
|
59
|
-
within the subtree are scoped to that query. The `lb-cell` attribute marks
|
|
60
|
-
the leaf that displays a single keyed value.
|
|
61
|
-
|
|
62
|
-
In this demo, both queries return a plain object with one value, and both
|
|
63
|
-
displays are simple text. When `lb-cell` is on an ordinary element, Loadbare App
|
|
64
|
-
assigns the value to the element's `textContent` property.
|
|
65
|
-
|
|
66
|
-
The attribute `lb-action` names something the page declares it can do on the server.
|
|
67
|
-
The hub turns the
|
|
68
|
-
click into a request and sends it. In this case there are no parameters
|
|
69
|
-
for the task, so the button carries no additional attributes.
|
|
70
|
-
|
|
71
|
-
### The request context in `demo/services.ts:19`
|
|
72
|
-
|
|
73
|
-
Server code consists of hooks and queries, which we will see shortly.
|
|
74
|
-
Each of those is an asynchronous routine that takes a context, `ctx` as
|
|
75
|
-
it only argument. Loadbare App declares it as an empty interface:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
export interface HubContext {}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
The application expands it using `declare module`. In the demo this code
|
|
83
|
-
is in `demo/services.ts`, and declares that the demo pages will recieve an
|
|
84
|
-
object providing database access. The internals of that object are not
|
|
85
|
-
discussed here, they can be found in `demo/services.ts`.
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
declare module "@loadbare/app/server" {
|
|
89
|
-
interface HubContext {
|
|
90
|
-
db: Db;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### The queries: `pages/hello.queries.ts`
|
|
96
|
-
|
|
97
|
-
A query is a named asynchronous routine that calls out to the
|
|
98
|
-
application data store and returns results that can be displayed
|
|
99
|
-
in the host HTML. Here are the two queries we declared in `pages/hello.html`.
|
|
100
|
-
|
|
101
|
-
```ts
|
|
102
|
-
import type { Queries } from "@loadbare/app/server";
|
|
103
|
-
|
|
104
|
-
export const queries: Queries = {
|
|
105
|
-
pageStats: async (ctx) => ({
|
|
106
|
-
visit_count: String(await ctx.db.visitCount()),
|
|
107
|
-
}),
|
|
108
|
-
counter: async (ctx) => ({ count: String(await ctx.db.buttonCount()) }),
|
|
109
|
-
};
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
The query namespace is local to the page, so query names must be unique within
|
|
113
|
-
a page but need not be unique across pages. The file is named for its page, so
|
|
114
|
-
nothing declares which page runs it.
|
|
115
|
-
|
|
116
|
-
### The hooks and actions: `pages/hello.hooks.ts`
|
|
117
|
-
|
|
118
|
-
When the app loads a page, a request is sent to the server to run all
|
|
119
|
-
queries and return the results. The `beforeGet` hook fires before
|
|
120
|
-
the queries run. In this case the `recordVisit()` implementation increases
|
|
121
|
-
the count by one.
|
|
122
|
-
|
|
123
|
-
```ts
|
|
124
|
-
import type { Hooks } from "@loadbare/app/server";
|
|
125
|
-
|
|
126
|
-
export const hooks: Hooks = {
|
|
127
|
-
beforeGet: (ctx) => ctx.db.recordVisit(),
|
|
128
|
-
actions: {
|
|
129
|
-
increment: {
|
|
130
|
-
run: (ctx) => ctx.db.raiseButtonCount(),
|
|
131
|
-
refresh: ["counter"],
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
};
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
The actions are declared with the hooks. In our host HTML we declared that
|
|
138
|
-
a button had `lb-action` named 'increment', so we must provide an implementation
|
|
139
|
-
of action 'increment'. The action names a routine to run, and which queries
|
|
140
|
-
should be refreshed afterward.
|
|
141
|
-
|
|
142
|
-
### Serving it
|
|
143
|
-
|
|
144
|
-
Those are all the files the page needed. Naming the page in a registry,
|
|
145
|
-
assembling the document that carries it, and answering the two endpoints are
|
|
146
|
-
done once for an application rather than once per page, so they are in
|
|
147
|
-
[Hosting a Loadbare App Application](./hosting.md). The demo does them in
|
|
148
|
-
`demo/pages.ts` and `demo/server.ts`.
|
|
149
|
-
|
|
150
|
-
### Run it
|
|
151
|
-
|
|
152
|
-
```
|
|
153
|
-
npm run dev
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
Open `http://localhost:8787/hello`. The visit count rises on every load and
|
|
157
|
-
on every navigation back to the page. The button raises its own count and
|
|
158
|
-
leaves the visit count alone.
|
|
159
|
-
|
|
160
|
-
---
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
## Widgets
|
|
164
|
-
|
|
165
|
-
A widget is the basic composable unit of the UI. A widget is an HTML
|
|
166
|
-
file and a Typescript containing the class.
|
|
167
|
-
|
|
168
|
-
A page author writes a widget's tag, and the attributes written on that tag
|
|
169
|
-
control how the definition expands. Expansion happens at compile time. It
|
|
170
|
-
is a build operation, not a runtime one: it runs once, before any request
|
|
171
|
-
exists, and what it produces is permanent. The expanded markup ships to the
|
|
172
|
-
browser and never changes there.
|
|
173
|
-
|
|
174
|
-
### Simple Example
|
|
175
|
-
|
|
176
|
-
A widget is two files with the same basename, in the same directory. The tag
|
|
177
|
-
name is the filename. Here is `lb-input`, which ships with the framework
|
|
178
|
-
and wraps a labeled `<input>`.
|
|
179
|
-
|
|
180
|
-
The definition, `widgets/lb-input.html`, is markup that will go inside of
|
|
181
|
-
the custom element.
|
|
182
|
-
|
|
183
|
-
```html
|
|
184
|
-
<label>{{label}} <input readonly="{{readonly}}" /></label>
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
A name in double braces is a placeholder. It stands for a parameter the page
|
|
188
|
-
author supplies on the tag, and it may occupy an entire attribute value or an
|
|
189
|
-
entire text node.
|
|
190
|
-
|
|
191
|
-
The element, `widgets/lb-input.ts`, is the class that gives the definition
|
|
192
|
-
behavior:
|
|
193
|
-
|
|
194
|
-
```ts
|
|
195
|
-
import { ATTR_VALUE } from "@loadbare/app/constants";
|
|
196
|
-
|
|
197
|
-
class LbInput extends HTMLElement {
|
|
198
|
-
static observedAttributes = [ATTR_VALUE];
|
|
199
|
-
|
|
200
|
-
attributeChangedCallback(_name: string, _old: string, value: string) {
|
|
201
|
-
this.querySelector("input")!.value = value;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
customElements.define("lb-input", LbInput);
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
When the hub receives a data payload from the server, and finds a
|
|
209
|
-
match on `lb-query` and `lb-cell`, it updates the attribute value
|
|
210
|
-
`lb-value`. The widget's job is to forward that value to whatever
|
|
211
|
-
control it owns. Here the `<input>` is not checked for before it is used,
|
|
212
|
-
because the definition beside this file is what places it.
|
|
213
|
-
|
|
214
|
-
A page author writes the tag, as `demo/pages/entity.html` does, supplying each
|
|
215
|
-
parameter as an `exp-` attribute:
|
|
216
|
-
|
|
217
|
-
```html
|
|
218
|
-
<lb-input lb-cell="name" exp-label="Name" exp-readonly></lb-input>
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
In Loadbare App terms, we say the `<lb-input>` is "expanded" at build time,
|
|
222
|
-
resulting in the following HTML going out to the page:
|
|
223
|
-
|
|
224
|
-
```html
|
|
225
|
-
<lb-input lb-cell="name" exp-label="Name" exp-readonly=""><label>Name <input readonly=""></label></lb-input>
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
The tag survives expansion exactly as written, and the definition became its
|
|
229
|
-
children. So the shipped page shows what was asked for beside what it
|
|
230
|
-
produced, and the class can find its `<input>` with an ordinary
|
|
231
|
-
`querySelector`.
|
|
232
|
-
|
|
233
|
-
Three namespaces share the tag, and each has one owner. `lb-` belongs to the
|
|
234
|
-
hub, which is how it addresses this widget at run time. `exp-` belongs to
|
|
235
|
-
expansion, and is gone by the time the browser matters. Everything unprefixed
|
|
236
|
-
belongs to HTML and means exactly what HTML says it means, so `class`,
|
|
237
|
-
`title`, `hidden` and their kind can be written on any widget, whatever that
|
|
238
|
-
widget's parameters happen to be called.
|
|
239
|
-
|
|
240
|
-
Marking the parameters buys an error message. A definition declares which
|
|
241
|
-
ones it has, so `exp-labl` is a name that resolves to nothing, and the build
|
|
242
|
-
says so rather than shipping a blank label:
|
|
243
|
-
|
|
244
|
-
```
|
|
245
|
-
expand: <lb-input> was given exp-labl, but its definition has no {{labl}}.
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
The reverse is not an error, because it is how an optional parameter works.
|
|
249
|
-
Written without `exp-readonly`, the placeholder has nothing behind it and the
|
|
250
|
-
attribute is dropped rather than emitted empty, which is HTML's own rule for
|
|
251
|
-
boolean attributes:
|
|
252
|
-
|
|
253
|
-
```html
|
|
254
|
-
<lb-input lb-cell="name" exp-label="Name"></lb-input>
|
|
255
|
-
<!-- ships as -->
|
|
256
|
-
<lb-input lb-cell="name" exp-label="Name"><label>Name <input></label></lb-input>
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
Because expansion runs before any request exists, it has no data to branch on,
|
|
260
|
-
and definitions have no control flow. If you find yourself wanting a
|
|
261
|
-
conditional in a definition, something data-shaped has leaked into the static
|
|
262
|
-
half; find the leak.
|
|
263
|
-
|
|
264
|
-
### Slots and Children
|
|
265
|
-
|
|
266
|
-
A parameter carries a word. Some widgets need to be given markup instead —
|
|
267
|
-
the options of a select, the cells of a row, the contents of a panel. A
|
|
268
|
-
widget that accepts markup marks one element in its definition with
|
|
269
|
-
`lb-slot`, and whatever the page author writes inside the tag becomes that
|
|
270
|
-
element's children.
|
|
271
|
-
|
|
272
|
-
`lb-select` is the case. Its definition, `widgets/lb-select.html`,
|
|
273
|
-
supplies the label and the control and says where the choices go:
|
|
274
|
-
|
|
275
|
-
```html
|
|
276
|
-
<label
|
|
277
|
-
>{{label}}
|
|
278
|
-
<select lb-slot></select
|
|
279
|
-
></label>
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
The page author writes them, as `demo/pages/entity.html` does:
|
|
283
|
-
|
|
284
|
-
```html
|
|
285
|
-
<lb-select lb-cell="id" lb-action="selectEntity" exp-label="Show:">
|
|
286
|
-
<option value="e1">Ada</option>
|
|
287
|
-
<option value="e2">Grace</option>
|
|
288
|
-
<option value="e3">Alan</option>
|
|
289
|
-
</lb-select>
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
And the two are joined at build time. The definition became the tag's
|
|
293
|
-
children, and the authored content landed inside the `<select>`, which no
|
|
294
|
-
longer carries the marker:
|
|
295
|
-
|
|
296
|
-
```html
|
|
297
|
-
<lb-select lb-cell="id" lb-action="selectEntity" exp-label="Show:"><label>Show:
|
|
298
|
-
<select>
|
|
299
|
-
<option value="e1">Ada</option>
|
|
300
|
-
<option value="e2">Grace</option>
|
|
301
|
-
<option value="e3">Alan</option>
|
|
302
|
-
</select></label></lb-select>
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
The content moves rather than copies, so each option appears once, in the
|
|
306
|
-
place the definition chose for it.
|
|
307
|
-
|
|
308
|
-
The slot is an attribute rather than an element because of where slots are
|
|
309
|
-
needed. HTML's content models discard foreign elements inside `<select>` and
|
|
310
|
-
`<table>`, so a `<lb-slot>` tag written in either would be dropped by the
|
|
311
|
-
parser before expansion ever saw it. An attribute rides on an element the
|
|
312
|
-
content model already accepts.
|
|
313
|
-
|
|
314
|
-
A definition may mark at most one element. Two is an error, because nothing
|
|
315
|
-
would say which one the content meant:
|
|
316
|
-
|
|
317
|
-
```
|
|
318
|
-
expand: <lb-panel> declares more than one lb-slot
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
Writing content inside a widget whose definition has no slot is also an error,
|
|
322
|
-
rather than content silently vanishing:
|
|
323
|
-
|
|
324
|
-
```
|
|
325
|
-
expand: content was written inside <lb-input>, whose definition has no lb-slot
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
A definition with a slot that nobody fills is not an error. The slot element
|
|
329
|
-
ships empty, exactly as an unsupplied parameter ships nothing.
|
|
330
|
-
|
|
331
|
-
Two things follow from the order the build works in. Placeholders are
|
|
332
|
-
substituted into the definition before the slot is filled, so `{{label}}`
|
|
333
|
-
written in authored content is literal text, not a parameter — parameters
|
|
334
|
-
belong to the tag, content belongs to the page. Expansion then descends into
|
|
335
|
-
what it just produced, so a widget written inside another widget's slot is
|
|
336
|
-
expanded in place.
|
|
337
|
-
|
|
338
|
-
Children are static, like everything else in the host. The three options
|
|
339
|
-
above are the three options the application has; they are not a list that came
|
|
340
|
-
from a query. The value that arrives from the server is the one that gets
|
|
341
|
-
selected, not the set that gets offered. Choices that come from data are list
|
|
342
|
-
processing, below.
|
|
343
|
-
|
|
344
|
-
### Markup the Parser Would Drop
|
|
345
|
-
|
|
346
|
-
A slot receives what the author wrote inside the tag. Some of what an author
|
|
347
|
-
needs to write cannot get there, and the obstacle is the parser rather than the
|
|
348
|
-
slot.
|
|
349
|
-
|
|
350
|
-
A `<thead>` written inside `<lb-table>` is not inside a table. The
|
|
351
|
-
tokenizer has nowhere to put the tag, so it drops it and keeps the text:
|
|
352
|
-
|
|
353
|
-
```html
|
|
354
|
-
<lb-table><thead><tr><th>Name</th></tr></thead></lb-table>
|
|
355
|
-
<!-- is parsed as -->
|
|
356
|
-
<lb-table>Name</lb-table>
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
That is the same fact as the one that keeps a custom element out of `<tbody>`,
|
|
360
|
-
seen from the other side. Table markup and a custom element cannot be written
|
|
361
|
-
adjacent, in either order.
|
|
362
|
-
|
|
363
|
-
A `<template>` survives anywhere, and its contents are parsed as though they
|
|
364
|
-
were already in the place they describe, so the wrapper is what gets the markup
|
|
365
|
-
through intact. The author names a destination on it, and the definition marks
|
|
366
|
-
the destination by the same name:
|
|
367
|
-
|
|
368
|
-
```html
|
|
369
|
-
<!-- widgets/lb-table.html -->
|
|
370
|
-
<table>
|
|
371
|
-
<caption>{{caption}}</caption>
|
|
372
|
-
<thead lb-template="head"></thead>
|
|
373
|
-
<tbody lb-slot></tbody>
|
|
374
|
-
<tfoot lb-template="foot"></tfoot>
|
|
375
|
-
</table>
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
```html
|
|
379
|
-
<!-- demo/pages/staff.html -->
|
|
380
|
-
<lb-table lb-query="staff" exp-caption="Everyone, by team">
|
|
381
|
-
<template lb-template="head">
|
|
382
|
-
<tr><th>Name</th><th>Role</th><th></th></tr>
|
|
383
|
-
</template>
|
|
384
|
-
|
|
385
|
-
<template lb-key="id" lb-group="team" lb-sort="name">
|
|
386
|
-
<tr>…</tr>
|
|
387
|
-
</template>
|
|
388
|
-
</lb-table>
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
`lb-template` is one name in two positions, like `lb-key`: in a definition it
|
|
392
|
-
marks a destination, on an authored `<template>` it names the destination that
|
|
393
|
-
template is for. It is never ambiguous, because a destination is not a
|
|
394
|
-
template.
|
|
395
|
-
|
|
396
|
-
The destination is filled with the template's contents rather than with the
|
|
397
|
-
template, so the wrapper is gone by the time anything ships and what the browser
|
|
398
|
-
holds is an ordinary `<thead>` with ordinary rows in it:
|
|
399
|
-
|
|
400
|
-
```html
|
|
401
|
-
<lb-table lb-query="staff" exp-caption="Everyone, by team"><table>
|
|
402
|
-
<caption>Everyone, by team</caption>
|
|
403
|
-
<thead><tr><th>Name</th><th>Role</th><th></th></tr></thead>
|
|
404
|
-
<tbody><template lb-key="id" lb-group="team" lb-sort="name">…</template></tbody>
|
|
405
|
-
<tfoot></tfoot>
|
|
406
|
-
</table></lb-table>
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
The `<tfoot>` is empty because this page named no template for it. A
|
|
410
|
-
destination nobody fills is an empty element rather than an absent one, which
|
|
411
|
-
is the same rule presence propagation follows for an attribute: what the
|
|
412
|
-
definition declares, the definition emits.
|
|
413
|
-
|
|
414
|
-
Named templates are taken first and the slot gets what is left, which is why
|
|
415
|
-
the row template above needs no name — it is content, and content goes where
|
|
416
|
-
content goes. Whitespace between named templates is not content, so a widget
|
|
417
|
-
with destinations and no slot is not accused of having been given any.
|
|
418
|
-
|
|
419
|
-
A definition may declare several destinations, and only one slot. They are
|
|
420
|
-
counted differently because the reason for each is different. One slot is a
|
|
421
|
-
statement that a widget has one place for what an author writes. Destinations
|
|
422
|
-
exist to carry markup past the parser, and how many are needed is a question
|
|
423
|
-
about the parser: a table has a head, a body and a foot, and each needs its own
|
|
424
|
-
way through.
|
|
425
|
-
|
|
426
|
-
Both directions of a mismatch are build errors, and a name is matched exactly
|
|
427
|
-
rather than approximately:
|
|
428
|
-
|
|
429
|
-
```
|
|
430
|
-
expand: a <template lb-template="footer"> was written inside <lb-table>, whose definition has no destination by that name.
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
The name may be empty, which is a name. A widget with one destination costs the
|
|
434
|
-
author nothing but the attribute.
|
|
435
|
-
|
|
436
|
-
### Conditional Rendering With Visibility Flags and CSS
|
|
437
|
-
|
|
438
|
-
A framework that mutates the DOM needs a conditional, and gets one cheaply. If
|
|
439
|
-
the tree is a function of the data, an `if` in the template is the natural way
|
|
440
|
-
to say that a panel is absent. Loadbare App hydrates attributes and text and does
|
|
441
|
-
nothing else, so there is no operation here that could make an element exist,
|
|
442
|
-
and no place to put the `if` even if there were.
|
|
443
|
-
|
|
444
|
-
What replaces it is stated in [data-binding.md](../docs-llm-slop/data-binding.md): visibility
|
|
445
|
-
is the boolean case, cells are the general one. Everything ships. A value
|
|
446
|
-
decides what is showing, never what is there. So the section that would be
|
|
447
|
-
about conditionals in another framework is, here, three examples and one line
|
|
448
|
-
of framework code.
|
|
449
|
-
|
|
450
|
-
#### Tabs
|
|
451
|
-
|
|
452
|
-
`demo/pages/tabs.html` ships all three panels, with two of them carrying
|
|
453
|
-
`hidden`, which is HTML's own visibility flag and needs nothing from us. The
|
|
454
|
-
widget is addressed like any leaf:
|
|
455
|
-
|
|
456
|
-
```html
|
|
457
|
-
<lb-tabs lb-query="prefs" lb-cell="active_tab" lb-action="selectTab">
|
|
458
|
-
<template lb-template="strip">
|
|
459
|
-
<button data-tab="summary" aria-selected="true">Summary</button>
|
|
460
|
-
<button data-tab="detail">Detail</button>
|
|
461
|
-
<button data-tab="history">History</button>
|
|
462
|
-
</template>
|
|
463
|
-
|
|
464
|
-
<section data-panel="summary">…</section>
|
|
465
|
-
<section data-panel="detail" hidden>…</section>
|
|
466
|
-
<section data-panel="history" hidden>…</section>
|
|
467
|
-
</lb-tabs>
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
Which tab is showing is a cell, so it arrives as `lb-value` and the widget
|
|
471
|
-
forwards it — to the `hidden` attribute of its panels rather than to the value
|
|
472
|
-
of an `<input>`, which is the whole of the difference between `lb-tabs` and
|
|
473
|
-
`lb-input`. Nothing in the vocabulary is new, and the framework was not
|
|
474
|
-
consulted.
|
|
475
|
-
|
|
476
|
-
A click is the widget's own interaction, so it switches the panel itself and
|
|
477
|
-
sends the choice afterward, and the action refreshes nothing — the browser is
|
|
478
|
-
already showing what it just asked for, and saying so again would spend a round
|
|
479
|
-
trip on nothing. Persistence rides along behind a paint that already happened.
|
|
480
|
-
|
|
481
|
-
That is what makes the returning user work. The choice is a stored value like
|
|
482
|
-
the visit count, so navigating away and back re-runs the query and the third
|
|
483
|
-
tab is what the server answers with. There is no blink, and no mechanism was
|
|
484
|
-
needed for that either: the hub inserts a page host and lands its data in one
|
|
485
|
-
synchronous block, and the browser does not paint mid-task, so the first tab is
|
|
486
|
-
never on screen. Hydration is early enough by construction rather than by
|
|
487
|
-
timing.
|
|
488
|
-
|
|
489
|
-
A value naming no panel changes nothing. That is deliberate, and it is what
|
|
490
|
-
makes the page's authored state the starting state: the host ships showing
|
|
491
|
-
something, and a value only ever moves it.
|
|
492
|
-
|
|
493
|
-
#### A wizard, decided by the server
|
|
494
|
-
|
|
495
|
-
`demo/pages/wizard.html` is the same shape with the decision on the other side.
|
|
496
|
-
Every step ships, one of them visible. Back and Next are ordinary buttons
|
|
497
|
-
carrying `lb-action`, so the hub sends the click, `wizard.hooks.ts` computes
|
|
498
|
-
the step and clamps it at the ends, and the step comes back as a value. The
|
|
499
|
-
widget owns no arithmetic, and the browser never displays a step the server has
|
|
500
|
-
not confirmed — the rule the counter follows for its number.
|
|
501
|
-
|
|
502
|
-
Tabs switch first and tell the server after; a wizard waits. A tab is a view
|
|
503
|
-
of what is already there, and a wizard step is a position the server is
|
|
504
|
-
entitled to refuse. Both are the widget's decision about its own interaction,
|
|
505
|
-
and neither is a framework feature.
|
|
506
|
-
|
|
507
|
-
The disabled ends are derived rather than sent. Which step is first is a fact
|
|
508
|
-
about the page's markup, so the widget reads it there rather than being told
|
|
509
|
-
twice — the same argument that gives a section heading its `colspan`.
|
|
510
|
-
|
|
511
|
-
#### The empty list
|
|
512
|
-
|
|
513
|
-
One conditional cannot be sent, and it is the one this framework owes a page.
|
|
514
|
-
The server answers a list query with rows and says nothing about how many
|
|
515
|
-
survived reconciliation, so after a delete the count exists only in the DOM.
|
|
516
|
-
Without help, every list widget would grow its own copy of the same three
|
|
517
|
-
lines.
|
|
518
|
-
|
|
519
|
-
So `applyRows` stamps it, once, for every list widget there will ever be:
|
|
520
|
-
|
|
521
|
-
```html
|
|
522
|
-
<lb-list lb-query="roster" data-rows="0">
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
It is `data-` rather than `lb-` for the reason the section heading in
|
|
526
|
-
`lb-table` is: it is derived from rows the hub already knows, and nothing
|
|
527
|
-
addresses it. The page then says what empty looks like in a stylesheet, and no
|
|
528
|
-
widget has a conditional in it:
|
|
529
|
-
|
|
530
|
-
```css
|
|
531
|
-
.empty {
|
|
532
|
-
display: none;
|
|
533
|
-
}
|
|
534
|
-
[data-rows="0"] .empty {
|
|
535
|
-
display: block;
|
|
536
|
-
}
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
```html
|
|
540
|
-
<lb-list lb-query="roster">
|
|
541
|
-
<table>…</table>
|
|
542
|
-
<p class="empty">No members yet.</p>
|
|
543
|
-
</lb-list>
|
|
544
|
-
```
|
|
545
|
-
|
|
546
|
-
Hidden is the default and shown is the rule, which is the ordering that matters:
|
|
547
|
-
the stamp is absent until a projection has landed, and absent is not zero. A
|
|
548
|
-
list still waiting for its first response says nothing, rather than announcing
|
|
549
|
-
that it is empty and then correcting itself.
|
|
550
|
-
|
|
551
|
-
#### Where a flag may land
|
|
552
|
-
|
|
553
|
-
A value lands on a widget as `lb-value` and on a native element as its
|
|
554
|
-
`textContent`. That is the whole of `land`, and two things follow for anyone
|
|
555
|
-
writing a visibility rule.
|
|
556
|
-
|
|
557
|
-
A flag cell must be a leaf. `<section lb-cell="state">` wrapping anything at
|
|
558
|
-
all replaces its children with a string, because assigning `textContent` is
|
|
559
|
-
what a native cell means. A cell names a place a value goes, and a container
|
|
560
|
-
is not one.
|
|
561
|
-
|
|
562
|
-
And CSS reaches an ancestor with `:has()`, which is how a leaf flag governs the
|
|
563
|
-
box around it:
|
|
564
|
-
|
|
565
|
-
```css
|
|
566
|
-
.panel:has([lb-cell="state"][lb-value="stale"]) { … }
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
Only a widget carries `lb-value`, so a page with no widget in it can branch on
|
|
570
|
-
a flag's text but not on an attribute. That is the limit, and it is not much of
|
|
571
|
-
one: by the time a page has a visibility rule worth writing it has behavior, and
|
|
572
|
-
behavior is a widget.
|
|
573
|
-
|
|
574
|
-
The flag is an attribute and the branching is a stylesheet. Neither an inline
|
|
575
|
-
`style` nor a placeholder that produces one is available — those are unsafe
|
|
576
|
-
sinks, and expansion has no data to fill them with anyway.
|
|
577
|
-
|
|
578
|
-
### List processing
|
|
579
|
-
|
|
580
|
-
Everything so far has been one tuple. A query may instead return many, and
|
|
581
|
-
what displays them is a `<template>` the page author wrote and a widget that
|
|
582
|
-
owns where each row goes.
|
|
583
|
-
|
|
584
|
-
#### The table
|
|
585
|
-
|
|
586
|
-
`demo/pages/roster.html` is the plain case. The page author writes the table,
|
|
587
|
-
and writes one row inside a `<template>`:
|
|
588
|
-
|
|
589
|
-
```html
|
|
590
|
-
<lb-list lb-query="roster">
|
|
591
|
-
<table>
|
|
592
|
-
<thead>
|
|
593
|
-
<tr><th>Name</th><th>Role</th><th>Team</th><th></th></tr>
|
|
594
|
-
</thead>
|
|
595
|
-
<tbody>
|
|
596
|
-
<template lb-key="id">
|
|
597
|
-
<tr>
|
|
598
|
-
<td lb-cell="name"></td>
|
|
599
|
-
<td lb-cell="role"></td>
|
|
600
|
-
<td lb-cell="team"></td>
|
|
601
|
-
<td><button lb-action="deleteMember">Delete</button></td>
|
|
602
|
-
</tr>
|
|
603
|
-
</template>
|
|
604
|
-
</tbody>
|
|
605
|
-
</table>
|
|
606
|
-
</lb-list>
|
|
607
|
-
```
|
|
608
|
-
|
|
609
|
-
Inside the template, nothing is new. `lb-cell` names a column and lands a
|
|
610
|
-
value exactly as it does anywhere else on the page, because a row is a scope
|
|
611
|
-
that happens to be small. One function fills both, and a list widget imports
|
|
612
|
-
it rather than reimplementing it:
|
|
613
|
-
|
|
614
|
-
```ts
|
|
615
|
-
import { applyTuple } from "@loadbare/app";
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
`lb-key` on the template names the column that identifies a row. On a live
|
|
619
|
-
row the same attribute carries that row's value, which is never ambiguous
|
|
620
|
-
because a template is not a row. The rows are the template's preceding
|
|
621
|
-
siblings, so the template stays put as the insertion marker and static markup
|
|
622
|
-
can sit on either side of the list.
|
|
623
|
-
|
|
624
|
-
The widget is around the table rather than in it. A custom element written
|
|
625
|
-
inside `<tbody>` is discarded by the parser before expansion could ever see
|
|
626
|
-
it, but a `<template>` is allowed there, which is why the template is the unit
|
|
627
|
-
the page author writes and the widget is the element that wraps it.
|
|
628
|
-
|
|
629
|
-
The delete button needs no wiring. The hub builds a request from the
|
|
630
|
-
attributes the element already sits under — `lb-query` from the widget,
|
|
631
|
-
`lb-key` from the row — so the action arrives knowing which row was clicked,
|
|
632
|
-
and the page never passed an argument.
|
|
633
|
-
|
|
634
|
-
#### Two results, because a widget must know the difference
|
|
635
|
-
|
|
636
|
-
A query returns the whole of its set. It cannot know why it was re-run, so it
|
|
637
|
-
never sends a delta:
|
|
638
|
-
|
|
639
|
-
```ts
|
|
640
|
-
import { rows, type Queries } from "@loadbare/app/server";
|
|
641
|
-
|
|
642
|
-
export const queries: Queries = {
|
|
643
|
-
roster: async (ctx) => rows(await ctx.db.members()),
|
|
644
|
-
};
|
|
645
|
-
```
|
|
646
|
-
|
|
647
|
-
An action does know what it changed, and can say only that. So an action may
|
|
648
|
-
return results of its own, laid over whatever its refresh set produced:
|
|
649
|
-
|
|
650
|
-
```ts
|
|
651
|
-
import { patch, type Hooks } from "@loadbare/app/server";
|
|
652
|
-
|
|
653
|
-
export const hooks: Hooks = {
|
|
654
|
-
actions: {
|
|
655
|
-
addMember: {
|
|
656
|
-
run: async (ctx) => ({ roster: patch({ rows: [await ctx.db.addMember()] }) }),
|
|
657
|
-
refresh: [],
|
|
658
|
-
},
|
|
659
|
-
deleteMember: {
|
|
660
|
-
run: async (ctx, where) => {
|
|
661
|
-
await ctx.db.deleteMember(where.key ?? "");
|
|
662
|
-
return { roster: patch({ drop: [where.key ?? ""] }) };
|
|
663
|
-
},
|
|
664
|
-
refresh: [],
|
|
665
|
-
},
|
|
666
|
-
resetRoster: {
|
|
667
|
-
run: (ctx) => ctx.db.resetMembers(),
|
|
668
|
-
refresh: ["roster"],
|
|
669
|
-
},
|
|
670
|
-
},
|
|
671
|
-
};
|
|
672
|
-
```
|
|
673
|
-
|
|
674
|
-
Those are the two results a list can receive:
|
|
675
|
-
|
|
676
|
-
`rows` is the entire set, and therefore also the order. Every row it names is
|
|
677
|
-
placed in the order given, and a row whose key it does not name is gone.
|
|
678
|
-
|
|
679
|
-
`patch` names only what changed. Rows it lists arrive or are updated, keys in
|
|
680
|
-
`drop` are gone, and a row it does not mention keeps both its contents and its
|
|
681
|
-
position.
|
|
682
|
-
|
|
683
|
-
Adding and removing are one result rather than two because the interesting
|
|
684
|
-
cases are both at once — a row whose sort key changed has to move, a swap is
|
|
685
|
-
one out and one in — and two messages would paint the state in between.
|
|
686
|
-
|
|
687
|
-
`resetRoster` could have been a patch too, and is not, because membership and
|
|
688
|
-
order both changed and the whole set is the honest answer. The other two
|
|
689
|
-
declare no refresh set at all. Re-running `roster` would have shown the same
|
|
690
|
-
thing, but it would be the server saying "here is everything" when it knows
|
|
691
|
-
the answer is "one more row" — and only the narrower statement is something a
|
|
692
|
-
widget that sorts or groups can act on.
|
|
693
|
-
|
|
694
|
-
#### Where a row goes is the widget's
|
|
695
|
-
|
|
696
|
-
`demo/pages/picker.html` puts the same rows in a `<select>`:
|
|
697
|
-
|
|
698
|
-
```html
|
|
699
|
-
<lb-options lb-query="choices" lb-action="pickMember" exp-label="Member:">
|
|
700
|
-
<template lb-key="id" lb-group="team">
|
|
701
|
-
<option lb-cell="name"></option>
|
|
702
|
-
</template>
|
|
703
|
-
</lb-options>
|
|
704
|
-
```
|
|
705
|
-
|
|
706
|
-
Two things are different, and both follow from `<option>`.
|
|
707
|
-
|
|
708
|
-
Its content model is text, so there is no element to put inside it and the row
|
|
709
|
-
itself carries `lb-cell`. A row root counts as a cell when it declares one.
|
|
710
|
-
|
|
711
|
-
It also needs a `value`, which is a second destination, and a native element
|
|
712
|
-
has only one. Rather than invent a way to aim a cell at an attribute, the
|
|
713
|
-
widget uses what it already has: the identity of the row is the value of the
|
|
714
|
-
option, so `lb-key` supplies both and the page declares it once.
|
|
715
|
-
|
|
716
|
-
Then the grouping. Nothing on the wire knows what an `<optgroup>` is — the
|
|
717
|
-
rows arrive flat, and `team` is a column like any other. `lb-group` names it
|
|
718
|
-
and `lb-options` builds one group per distinct value, reuses a group a
|
|
719
|
-
later row belongs to, and removes one when its last row leaves:
|
|
720
|
-
|
|
721
|
-
```html
|
|
722
|
-
<select>
|
|
723
|
-
<optgroup label="Engines">
|
|
724
|
-
<option lb-key="m1" value="m1" lb-cell="name">Ada Lovelace</option>
|
|
725
|
-
<option lb-key="m3" value="m3" lb-cell="name">Alan Turing</option>
|
|
726
|
-
</optgroup>
|
|
727
|
-
<optgroup label="Compilers">
|
|
728
|
-
<option lb-key="m2" value="m2" lb-cell="name">Grace Hopper</option>
|
|
729
|
-
</optgroup>
|
|
730
|
-
<template lb-key="id" lb-group="team">…</template>
|
|
731
|
-
</select>
|
|
732
|
-
```
|
|
733
|
-
|
|
734
|
-
This is the whole reason placement belongs to the widget. Sorting, grouping,
|
|
735
|
-
and section headings are one question — *where does this row go?* — and the
|
|
736
|
-
answer is always local. The hub delivers flat keyed rows and stops. A widget
|
|
737
|
-
that has nothing to say supplies nothing and rows accumulate in the order the
|
|
738
|
-
server sent, which is what `lb-list` does.
|
|
739
|
-
|
|
740
|
-
Because grouping is placement, it is derived and not addressed. An
|
|
741
|
-
`<optgroup>` carries no `lb-key` and the hub cannot see it; it is the
|
|
742
|
-
widget's own scaffolding around rows the hub does know.
|
|
743
|
-
|
|
744
|
-
#### How it reaches the widget
|
|
745
|
-
|
|
746
|
-
A projection has to land on a widget. A native element has one destination
|
|
747
|
-
for a value and no way to acquire children, so a list is not something it can
|
|
748
|
-
be asked to show, and the hub says so rather than doing nothing:
|
|
749
|
-
|
|
750
|
-
```
|
|
751
|
-
lb-hub: query 'oops' returned rows, but <div> is not a list widget
|
|
752
|
-
```
|
|
753
|
-
|
|
754
|
-
The hub calls one method and stops. It tests for the method, never for the
|
|
755
|
-
element, so it holds no table of tag names:
|
|
756
|
-
|
|
757
|
-
```ts
|
|
758
|
-
export interface HubRowHost {
|
|
759
|
-
acceptRows(result: Projection): void;
|
|
760
|
-
}
|
|
761
|
-
```
|
|
762
|
-
|
|
763
|
-
A list widget is small, because everything above the placement decision is
|
|
764
|
-
shared. `lb-list` in its entirety:
|
|
765
|
-
|
|
766
|
-
```ts
|
|
767
|
-
class LbList extends HTMLElement implements HubRowHost {
|
|
768
|
-
acceptRows(result: Projection) {
|
|
769
|
-
applyRows(this, result);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
```
|
|
773
|
-
|
|
774
|
-
`applyRows` clones the template, matches each tuple to the row already showing
|
|
775
|
-
it, fills that row with `applyTuple`, and drops what left. A widget that
|
|
776
|
-
places rows itself passes a third argument and writes nothing else:
|
|
777
|
-
|
|
778
|
-
```ts
|
|
779
|
-
applyRows(this, result, (row, tuple, template) => { … });
|
|
780
|
-
```
|
|
781
|
-
|
|
782
|
-
Rows are filled before they are inserted, so a widget inside a row has its
|
|
783
|
-
attributes already set when it upgrades. That is the same thing that makes
|
|
784
|
-
hydration and refresh one operation everywhere else on the page.
|
|
785
|
-
|
|
786
|
-
#### Widgets that own the scaffolding
|
|
787
|
-
|
|
788
|
-
`lb-list` and `lb-options` are the framework's two answers to *where does
|
|
789
|
-
a row go?* — nowhere in particular, and in a group. Both leave the rest of the
|
|
790
|
-
markup to the page. `demo/pages/staff.html` and `demo/pages/chooser.html` are
|
|
791
|
-
the same two lists again, in widgets that take the markup as well.
|
|
792
|
-
|
|
793
|
-
`lb-table` supplies the table and answers the placement question twice.
|
|
794
|
-
`lb-sort` names the column rows are ordered by, `lb-group` names the column
|
|
795
|
-
they are sectioned by, and the page writes only what the page knows:
|
|
796
|
-
|
|
797
|
-
```html
|
|
798
|
-
<lb-table lb-query="staff" exp-caption="Everyone, by team">
|
|
799
|
-
<template lb-template="head">
|
|
800
|
-
<tr><th>Name</th><th>Role</th><th></th></tr>
|
|
801
|
-
</template>
|
|
802
|
-
|
|
803
|
-
<template lb-key="id" lb-group="team" lb-sort="name">
|
|
804
|
-
<tr>
|
|
805
|
-
<td lb-cell="name"></td>
|
|
806
|
-
<td lb-cell="role"></td>
|
|
807
|
-
<td><button lb-action="dropMember">Delete</button></td>
|
|
808
|
-
</tr>
|
|
809
|
-
</template>
|
|
810
|
-
</lb-table>
|
|
811
|
-
```
|
|
812
|
-
|
|
813
|
-
Its `place` inserts a row before the first row in its section that sorts after
|
|
814
|
-
it, which keeps the section ordered whatever else is in it — so a whole set and
|
|
815
|
-
a single patched row take the same path and neither needs to know which it is.
|
|
816
|
-
Add one member and the server sends one row and says nothing about order; it
|
|
817
|
-
lands in its team's section, in name order, because that is the whole of what
|
|
818
|
-
placement decides.
|
|
819
|
-
|
|
820
|
-
The section heading is a `<tr>` the widget builds, exactly as `lb-options`
|
|
821
|
-
builds an `<optgroup>`, and it is scaffolding for the same reason: derived from
|
|
822
|
-
rows the hub knows, invisible to the hub itself, and gone when its last row
|
|
823
|
-
leaves. It carries `data-group` rather than a name from the vocabulary,
|
|
824
|
-
because nothing addresses it. Its `colspan` is the number of cells in the row
|
|
825
|
-
the page author wrote, which is the only place that number exists.
|
|
826
|
-
|
|
827
|
-
`lb-picker` goes the other way and supplies the row template. Every option
|
|
828
|
-
is one column of one row, so what is left to say is which columns — and those
|
|
829
|
-
are words, which is what a parameter carries:
|
|
830
|
-
|
|
831
|
-
```html
|
|
832
|
-
<lb-picker lb-query="choices" lb-action="chooseMember"
|
|
833
|
-
exp-label="Member:" exp-key="id" exp-cell="name" exp-group="team"></lb-picker>
|
|
834
|
-
```
|
|
835
|
-
|
|
836
|
-
```html
|
|
837
|
-
<!-- widgets/lb-picker.html -->
|
|
838
|
-
<label>{{label}}
|
|
839
|
-
<select>
|
|
840
|
-
<template lb-key="{{key}}" lb-group="{{group}}">
|
|
841
|
-
<option lb-cell="{{cell}}"></option>
|
|
842
|
-
</template>
|
|
843
|
-
</select>
|
|
844
|
-
</label>
|
|
845
|
-
```
|
|
846
|
-
|
|
847
|
-
What ships is the template the picker page wrote by hand, and the behavior is
|
|
848
|
-
`lb-options` inherited whole — a distinct class exists only because
|
|
849
|
-
`customElements.define` wants one constructor per name. Drop `exp-group` and
|
|
850
|
-
the placeholder has nothing behind it, so `lb-group` is dropped with it and
|
|
851
|
-
the options arrive ungrouped: presence propagation doing the work a conditional
|
|
852
|
-
would do elsewhere.
|
|
853
|
-
|
|
854
|
-
Neither widget is more capable than the pair it wraps, and that is the point.
|
|
855
|
-
A page that wants a second element in a row, or an option built from two
|
|
856
|
-
columns, writes the template itself and gets the plain repeater. These are for
|
|
857
|
-
when it does not.
|
|
858
|
-
|
|
859
|
-
#### Subtotals, and where a total goes
|
|
860
|
-
|
|
861
|
-
`demo/pages/ledger.html` is a report: detail lines, a subtotal under each
|
|
862
|
-
section, and a total under the table. It is the shape a balance sheet has, and
|
|
863
|
-
it is the one case that looks like it needs the browser to calculate something.
|
|
864
|
-
|
|
865
|
-
It does not, and the reason is that the calculation was already done by
|
|
866
|
-
something better at it. Every SQL database produces a set with subtotals in it
|
|
867
|
-
— `GROUP BY ROLLUP`, or `GROUPING SETS` for finer control — so a report can
|
|
868
|
-
leave the database finished, in report order, as flat keyed rows. That is the
|
|
869
|
-
shape the data channel already carries. Nothing new is on the wire, and the
|
|
870
|
-
page writes one row template:
|
|
871
|
-
|
|
872
|
-
```html
|
|
873
|
-
<lb-table class="report" lb-query="ledger" exp-caption="Expenses by department">
|
|
874
|
-
<template lb-template="head">
|
|
875
|
-
<tr><th>Account</th><th class="amount">Amount</th></tr>
|
|
876
|
-
</template>
|
|
877
|
-
|
|
878
|
-
<template lb-key="key" lb-group="dept">
|
|
879
|
-
<tr>
|
|
880
|
-
<td lb-cell="line"></td>
|
|
881
|
-
<td class="amount" lb-cell="amount"></td>
|
|
882
|
-
</tr>
|
|
883
|
-
</template>
|
|
884
|
-
|
|
885
|
-
<template lb-template="foot">
|
|
886
|
-
<tr lb-query="ledgerTotal">
|
|
887
|
-
<th scope="row" lb-cell="line"></th>
|
|
888
|
-
<td class="amount" lb-cell="amount"></td>
|
|
889
|
-
</tr>
|
|
890
|
-
</template>
|
|
891
|
-
</lb-table>
|
|
892
|
-
```
|
|
893
|
-
|
|
894
|
-
There is no `lb-sort`, and its absence is what makes this a report rather than
|
|
895
|
-
a table. A set is the order it arrived in, so the server decides where a
|
|
896
|
-
subtotal sits — which is the only place that decision can be correct, since
|
|
897
|
-
only the server knows what the subtotal is under. `lb-group` stays, and the
|
|
898
|
-
section headings are the scaffolding `lb-table` already builds. A subtotal
|
|
899
|
-
row carries its section like any other row, so it lands in that section, at the
|
|
900
|
-
end, because that is where it arrived.
|
|
901
|
-
|
|
902
|
-
**The key says which kind of row it is.** A subtotal has no `id` of its own
|
|
903
|
-
and needs a key anyway, so the server mints one, and the prefix it chooses is
|
|
904
|
-
the only discriminator the page needs:
|
|
905
|
-
|
|
906
|
-
```json
|
|
907
|
-
{ "key": "d:x1", "dept": "Engineering", "line": "Salaries", "amount": "184,500.00" },
|
|
908
|
-
{ "key": "s:Engineering", "dept": "Engineering", "line": "Total, Engineering", "amount": "213,650.00" }
|
|
909
|
-
```
|
|
910
|
-
|
|
911
|
-
`lb-key` lands on the row as an attribute, which is what makes this reachable.
|
|
912
|
-
A cell would not be: a cell lands as *text* on a native element, and no selector
|
|
913
|
-
matches text. So the stylesheet is the whole of the difference between a
|
|
914
|
-
subtotal and a detail line, and no widget carries a conditional:
|
|
915
|
-
|
|
916
|
-
```css
|
|
917
|
-
.report tr[lb-key^="d:"] td:first-child { padding-left: 1.5rem; }
|
|
918
|
-
.report tr[lb-key^="s:"] { font-weight: bold; }
|
|
919
|
-
.report tr[lb-key^="s:"] td { border-top: 1px solid; padding-top: 0.35rem; }
|
|
920
|
-
```
|
|
921
|
-
|
|
922
|
-
Blank lines are `padding` and rules are `border`. A spacer row would be a row
|
|
923
|
-
with a key, which is data standing in for whitespace, and a delete away from
|
|
924
|
-
being wrong.
|
|
925
|
-
|
|
926
|
-
**The total under the table is not a row.** It is one line that does not
|
|
927
|
-
repeat, so it is a tuple, from a second query, landing in a scope of its own —
|
|
928
|
-
`lb-query="ledgerTotal"` on the `<tfoot>` row. Two projections of one table
|
|
929
|
-
on one page is what the query name is for, and `applyData` resolves the two
|
|
930
|
-
independently: one selector finds the widget and delivers rows, another finds
|
|
931
|
-
the footer row and fills its cells, and neither knows the second is inside the
|
|
932
|
-
first.
|
|
933
|
-
|
|
934
|
-
A full `ROLLUP` would have produced that total as a row as well, with a null
|
|
935
|
-
department. It is a tuple here for two reasons, and the first is mechanical: a
|
|
936
|
-
row belonging to no section has nowhere to land in a grouped body. The second
|
|
937
|
-
is that a footer is often not a sum of what is above it at all — a prior
|
|
938
|
-
period, a budget, a check figure — and those are the same markup and a
|
|
939
|
-
different query.
|
|
940
|
-
|
|
941
|
-
**What the server spends and the page never sees.** `GROUPING(line)` is what
|
|
942
|
-
tells a subtotal from a detail row, and it is the query's own answer rather
|
|
943
|
-
than a column anyone invented. It is spent server-side, on the key prefix and
|
|
944
|
-
the label, and never reaches the browser. Amounts are integer cents until the
|
|
945
|
-
last moment and arrive formatted, because cells are strings and formatting
|
|
946
|
-
money is the same kind of decision as rounding it.
|
|
947
|
-
|
|
948
|
-
The whole of it generalizes downward without further mechanism. A third-order
|
|
949
|
-
subtotal is another grouping set and another key prefix; depth is a column like
|
|
950
|
-
any other. A report of any depth is still a flat ordered list of keyed rows,
|
|
951
|
-
because that is what a rendered report is.
|
|
952
|
-
|
|
953
|
-
Two things do not survive, and both are shape rather than value. A subtotal
|
|
954
|
-
label cannot span columns, since `colspan` is not something a value can
|
|
955
|
-
produce — the label goes in the first cell and the rest stay empty. And
|
|
956
|
-
indentation by arbitrary depth needs a `style` attribute, which is an unsafe
|
|
957
|
-
sink and closed: a fixed ladder of selectors covers a report whose depth is
|
|
958
|
-
known, and a deeper one indents in the label.
|
|
959
|
-
|
|
960
|
-
#### What a page still cannot do
|
|
961
|
-
|
|
962
|
-
A projection is a top-level query result and its values are strings. A tuple
|
|
963
|
-
may not contain one, so a page that wants master and detail declares two
|
|
964
|
-
queries, as `picker` does: `choices` returns the set, `picked` returns the one
|
|
965
|
-
that was chosen. A projection and a tuple cannot arrive on the same element,
|
|
966
|
-
so they arrive on two.
|
|
967
|
-
|
|
968
|
-
That limit is what a report has to be flattened past rather than a wall a
|
|
969
|
-
report stops at, and the section above is what paying it looks like: the
|
|
970
|
-
hierarchy is resolved by whatever computed it, and what arrives is flat. What
|
|
971
|
-
remains genuinely out of reach is structure whose *depth* is data — a comment
|
|
972
|
-
tree, an org chart — where the number of levels is not known when the template
|
|
973
|
-
is written.
|
|
974
|
-
|
|
975
|
-
An empty set is an empty list and nothing more. Saying "no members yet" is a
|
|
976
|
-
conditional rather than a repetition, so it is not here: `applyRows` stamps
|
|
977
|
-
`data-rows` and a stylesheet says the rest, in the section above.
|
|
978
|
-
|
|
979
|
-
---
|
|
980
|
-
|
|
981
|
-
## Reference
|
|
982
|
-
|
|
983
|
-
### Attribute vocabulary
|
|
984
|
-
|
|
985
|
-
Import every name from `core/lb-constants.ts`. Do not write one as a string
|
|
986
|
-
literal in a widget, a page, a test or a document.
|
|
987
|
-
|
|
988
|
-
| attribute | constant | goes on | names |
|
|
989
|
-
| -------------- | --------------- | ------------------- | --------------------------- |
|
|
990
|
-
| `lb-query` | `ATTR_QUERY` | scope element | a declared query |
|
|
991
|
-
| `lb-key` | `ATTR_KEY` | row template, row | key column, then its value |
|
|
992
|
-
| `lb-group` | `ATTR_GROUP` | a row template | column a widget groups by |
|
|
993
|
-
| `lb-cell` | `ATTR_CELL` | leaf widget | column within scope |
|
|
994
|
-
| `lb-value` | `ATTR_VALUE` | leaf widget | where the value lands |
|
|
995
|
-
| `lb-action` | `ATTR_ACTION` | any element | a declared action |
|
|
996
|
-
| `lb-sort` | `ATTR_SORT` | a row template | column a widget sorts by |
|
|
997
|
-
| `lb-slot` | `ATTR_SLOT` | inside a definition | where authored content goes |
|
|
998
|
-
| `lb-template` | `ATTR_TEMPLATE` | definition, template | a named destination |
|
|
999
|
-
| `lb-nav-link` | `ATTR_NAV_LINK` | an anchor | intercept this navigation |
|
|
1000
|
-
| `data-rows` | `ATTR_ROW_COUNT`| a list widget | how many rows are showing |
|
|
1001
|
-
|
|
1002
|
-
`data-rows` is the one name in the table that is not Loadbare App's namespace. The row
|
|
1003
|
-
machinery writes it and a stylesheet reads it; nothing addresses it, so it is
|
|
1004
|
-
`data-`, like the section heading `lb-table` builds. It is in the table
|
|
1005
|
-
because it is written by shared code and read by pages, which makes it
|
|
1006
|
-
published — and a published name is imported like any other.
|
|
1007
|
-
|
|
1008
|
-
`lb-group` and `lb-sort` are the two names in the table the hub never reads.
|
|
1009
|
-
Placement is the widget's, but the names are shared, because a grouped
|
|
1010
|
-
`<select>` and a table with section headings ask the same thing of the same
|
|
1011
|
-
data, and a table that sorts asks it of the same column a page author would
|
|
1012
|
-
name anywhere else. Loadbare App has no namespace for an attribute that is a
|
|
1013
|
-
widget's alone.
|
|
1014
|
-
|
|
1015
|
-
The `lb-` prefix is load bearing rather than decorative. `data-*` is the
|
|
1016
|
-
handler-prop namespace, so a prop named `cell` or `key` would collide with an
|
|
1017
|
-
addressing attribute.
|
|
1018
|
-
|
|
1019
|
-
Endpoints: `LB_DATA_ENDPOINT` is `/lb/data`, `LB_REQUEST_ENDPOINT` is
|
|
1020
|
-
`/lb`. Page hosts ship as `<template id="page-<name>">`.
|
|
1021
|
-
|
|
1022
|
-
### The definition language
|
|
1023
|
-
|
|
1024
|
-
The rules a definition obeys, beyond the ones the Widgets section shows by
|
|
1025
|
-
example.
|
|
1026
|
-
|
|
1027
|
-
Registry. Every `.html` file in a definition directory defines the tag matching
|
|
1028
|
-
its basename. Directories are searched in order and later ones win, so an
|
|
1029
|
-
application overrides a built-in widget by putting a file of the same name in
|
|
1030
|
-
its own directory.
|
|
1031
|
-
|
|
1032
|
-
Scope. Only tags matching `lb-*` are expanded, and every one of them must
|
|
1033
|
-
have a definition. An unknown widget tag fails the build by name.
|
|
1034
|
-
|
|
1035
|
-
Lowercase names. Placeholder names are lowercase, hyphenated if they need a
|
|
1036
|
-
break: `{{input-class}}`, never `{{inputClass}}`. HTML lowercases attribute
|
|
1037
|
-
names, so a capital is a name no tag could ever supply. A definition that
|
|
1038
|
-
declares one is rejected when definitions load, naming the file and the
|
|
1039
|
-
spelling to use.
|
|
1040
|
-
|
|
1041
|
-
Attribute survival. Every authored attribute stays on the tag after expansion,
|
|
1042
|
-
`exp-` ones included. Placing a parameter in the definition copies it inward;
|
|
1043
|
-
it does not move it.
|
|
1044
|
-
|
|
1045
|
-
Recursion. A definition may use other widgets. The graph is checked for cycles
|
|
1046
|
-
once at load and rejected by name. There is no depth cap.
|
|
1047
|
-
|
|
1048
|
-
Templates. Expansion descends into `<template>` content, for substitution as
|
|
1049
|
-
well as for expansion, so a row template ships already expanded — whether the
|
|
1050
|
-
page author wrote it or the definition did — and the browser only ever clones a
|
|
1051
|
-
finished tree.
|
|
1052
|
-
|
|
1053
|
-
Named destinations. A definition may mark elements with `lb-template`, and an
|
|
1054
|
-
authored `<template>` carrying the same name is unwrapped into the matching
|
|
1055
|
-
one. Named templates are taken before the slot, both directions of a mismatch
|
|
1056
|
-
fail the build, and there may be several, because each exists to carry markup
|
|
1057
|
-
past a parse context the slot cannot reach.
|
|
1058
|
-
|
|
1059
|
-
Substitution is applied to a parsed tree with `setAttribute` and node data, and
|
|
1060
|
-
serialized once through the DOM's own serializer. Write no escaping code.
|
|
1061
|
-
|
|
1062
|
-
Two known holes. A placeholder nobody supplies cannot be detected, because that
|
|
1063
|
-
is indistinguishable from presence propagation dropping an attribute. And a
|
|
1064
|
-
definition is parsed in a body context, so one whose root is `<tr>` or
|
|
1065
|
-
`<option>` is mangled before substitution runs.
|
|
1066
|
-
|
|
1067
|
-
### Applying a response
|
|
1068
|
-
|
|
1069
|
-
Every response is a set of query results, and every one lands through the same
|
|
1070
|
-
path, whether it is a cold start or the narrowest refresh. The hub calls
|
|
1071
|
-
`applyData(main, data)`, which for each query finds every element carrying that
|
|
1072
|
-
`lb-query`, and within it sets `lb-value` on every element carrying a matching
|
|
1073
|
-
`lb-cell`.
|
|
1074
|
-
|
|
1075
|
-
A query in the response with no scope on the page logs a warning and is skipped.
|
|
1076
|
-
A cell with no element is silently ignored.
|
|
1077
|
-
|
|
1078
|
-
Hydration and refresh are the same call. The browser runs
|
|
1079
|
-
`attributeChangedCallback` for attributes already present at upgrade, so a widget
|
|
1080
|
-
never asks when its data arrived.
|
|
1081
|
-
|
|
1082
|
-
### Requests
|
|
1083
|
-
|
|
1084
|
-
A widget has two options and no third. Handle the interaction itself, or send a
|
|
1085
|
-
request. It never asks the framework to change something on its behalf.
|
|
1086
|
-
|
|
1087
|
-
To send, dispatch one bubbling `CustomEvent` named `LB_EVENT_NAME` carrying a
|
|
1088
|
-
`HubRequest` as its detail:
|
|
1089
|
-
|
|
1090
|
-
```ts
|
|
1091
|
-
this.dispatchEvent(
|
|
1092
|
-
new CustomEvent(LB_EVENT_NAME, {
|
|
1093
|
-
bubbles: true,
|
|
1094
|
-
detail: { op: "action", name, value: select.value },
|
|
1095
|
-
}),
|
|
1096
|
-
);
|
|
1097
|
-
```
|
|
1098
|
-
|
|
1099
|
-
The hub sits above every widget, POSTs anything that reaches it to `/lb`, and
|
|
1100
|
-
applies the response through the path above. Build the request from the
|
|
1101
|
-
attributes the widget already carries — its `lb-cell`, the `lb-key` of its
|
|
1102
|
-
row, the `lb-query` of its scope. The vocabulary runs in both directions;
|
|
1103
|
-
declare nothing twice.
|
|
1104
|
-
|
|
1105
|
-
On a native element the hub turns a click into the request. On a widget it does
|
|
1106
|
-
not: the widget owns its own interaction and decides what counts as performing
|
|
1107
|
-
it, which for `lb-select` is a change rather than a click.
|
|
1108
|
-
|
|
1109
|
-
The operation set is closed:
|
|
1110
|
-
|
|
1111
|
-
```ts
|
|
1112
|
-
type HubRequest =
|
|
1113
|
-
| { op: "cell-change"; query: string; key: string; cell: string; value: string }
|
|
1114
|
-
| { op: "tuple-insert"; query: string; values: Record<string, string> }
|
|
1115
|
-
| { op: "tuple-update"; query: string; key: string; values: Record<string, string> }
|
|
1116
|
-
| { op: "tuple-delete"; query: string; key: string }
|
|
1117
|
-
| { op: "action"; name: string; query?: string; key?: string; cell?: string; value?: string };
|
|
1118
|
-
```
|
|
1119
|
-
|
|
1120
|
-
The four CRUD operations name a query, never a table. The fifth names something
|
|
1121
|
-
the page declared it can do, and what keeps it from being an RPC endpoint is
|
|
1122
|
-
that the name must already appear in the page's hooks — the wire cannot reach
|
|
1123
|
-
anything the page has not published.
|
|
1124
|
-
|
|
1125
|
-
An action carries at most one value, because a control has at most one. A
|
|
1126
|
-
button has none, so the server computes the whole of the result. A `<select>`
|
|
1127
|
-
has one, because the choice is the interaction. There is no argument list.
|
|
1128
|
-
|
|
1129
|
-
State the refresh set yourself, in `pages/<name>.hooks.ts`. There is no
|
|
1130
|
-
automatic mapping from an operation to the queries it affects.
|
|
1131
|
-
|
|
1132
|
-
### Widget author's contract
|
|
1133
|
-
|
|
1134
|
-
- Extend `HTMLElement`. No Shadow DOM.
|
|
1135
|
-
- Events travel up, method calls travel down.
|
|
1136
|
-
- List the attributes you read in `observedAttributes` and render in
|
|
1137
|
-
`attributeChangedCallback`. Do not ask when the data arrived.
|
|
1138
|
-
- Take your value from `lb-value` and forward it to whatever control you own.
|
|
1139
|
-
The hub writes one attribute name for every widget type and holds no table of
|
|
1140
|
-
native attribute names.
|
|
1141
|
-
- Find your control with `querySelector`. It is a child, placed by your
|
|
1142
|
-
definition.
|
|
1143
|
-
- Build no children on upgrade. Everything in the live DOM must appear in
|
|
1144
|
-
view-source, except values, control state, and clones whose count came from a
|
|
1145
|
-
query.
|
|
1146
|
-
- To show a list, implement `acceptRows` and call `applyRows`. Decide where a
|
|
1147
|
-
row goes and nothing else — cloning, matching and filling are shared, and a
|
|
1148
|
-
second implementation of them is a second set of bugs.
|
|
1149
|
-
- Attach listeners in `connectedCallback`. A parent may read its children there
|
|
1150
|
-
but may not call widget methods on them until insertion completes.
|
|
1151
|
-
- Ship no guard against your own definition. The control you `querySelector` is
|
|
1152
|
-
placed by the file beside you, so a check for its absence is unreachable code
|
|
1153
|
-
that a test should have caught. Write the straight line.
|
|
1154
|
-
- Guard what a page author supplied. A missing `lb-action` or an unset
|
|
1155
|
-
`lb-cell` comes from a hand-written page, and nothing rejects it today.
|
|
1156
|
-
`console.error` with the tag name, return, and disturb nothing else.
|
|
1157
|
-
|
|
1158
|
-
The line between the last two is who wrote the thing you are checking. Your
|
|
1159
|
-
definition is yours and is tested. A page host is the application's and is not.
|
|
1160
|
-
|
|
1161
|
-
A widget is for behavior. `lb-select` is one: it receives its value like any
|
|
1162
|
-
other cell, and on change it sends a request. A widget that only forwards a
|
|
1163
|
-
value to a control it wraps, or only sends a click, is doing work the hub
|
|
1164
|
-
already does.
|