@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
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Data Binding
|
|
2
|
+
|
|
3
|
+
A page binds its elements to server data with four attributes, and asks the
|
|
4
|
+
server to change that data with four more. The page writes all eight into
|
|
5
|
+
its HTML; the server declares which queries it answers and which operations
|
|
6
|
+
it permits, and refuses anything it has not declared.
|
|
7
|
+
|
|
8
|
+
## A page that binds data
|
|
9
|
+
|
|
10
|
+
Here is a page that binds a scalar, a list, and three requests:
|
|
11
|
+
|
|
12
|
+
```html
|
|
13
|
+
<!-- src/pages/members.page.html -->
|
|
14
|
+
<h1>Members</h1>
|
|
15
|
+
|
|
16
|
+
<p lb-query="dues">Dues collected this year: <span lb-cell="total"></span></p>
|
|
17
|
+
|
|
18
|
+
<form lb-insert lb-query="roster">
|
|
19
|
+
<input lb-cell="name" placeholder="Name" />
|
|
20
|
+
<button type="submit">Add member</button>
|
|
21
|
+
</form>
|
|
22
|
+
|
|
23
|
+
<lb-list lb-query="roster">
|
|
24
|
+
<ul>
|
|
25
|
+
<template lb-key="id">
|
|
26
|
+
<li>
|
|
27
|
+
<lb-input lb-cell="name"></lb-input>
|
|
28
|
+
<button lb-delete>Remove</button>
|
|
29
|
+
</li>
|
|
30
|
+
</template>
|
|
31
|
+
</ul>
|
|
32
|
+
</lb-list>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The queries named here — `dues` and `roster` — and the operations the page
|
|
36
|
+
asks for are declared on the server; see [page files](./page-files.md).
|
|
37
|
+
|
|
38
|
+
## Binding
|
|
39
|
+
|
|
40
|
+
| Attribute | Names |
|
|
41
|
+
|------------|----------------------------------------------------------|
|
|
42
|
+
| `lb-query` | The query a subtree displays |
|
|
43
|
+
| `lb-key` | The cell that identifies a row, and a live row's own key |
|
|
44
|
+
| `lb-cell` | The cell an element displays |
|
|
45
|
+
| `lb-value` | Where a widget receives its value |
|
|
46
|
+
|
|
47
|
+
A binding is scoped by ancestry. An element binds to the query on its
|
|
48
|
+
nearest ancestor carrying `lb-query`, and to the row on its nearest ancestor
|
|
49
|
+
carrying `lb-key`. Nothing else establishes scope: an element outside every
|
|
50
|
+
`lb-query` is bound to nothing and displays nothing.
|
|
51
|
+
|
|
52
|
+
Bind an element to a cell by putting `lb-cell` on the element that shows the
|
|
53
|
+
value. The scope's own root counts as a cell if it carries one, which is how
|
|
54
|
+
an `<option>` — whose content model is text — displays the value it is.
|
|
55
|
+
|
|
56
|
+
Name the same query on more than one subtree to display it in more than one
|
|
57
|
+
place. Every subtree gets the result.
|
|
58
|
+
|
|
59
|
+
`lb-key` holds one name in two positions. On a row template it names the
|
|
60
|
+
cell that identifies a row; on a row that is showing, it carries that row's
|
|
61
|
+
key value. A template is never a row, so the two never collide.
|
|
62
|
+
|
|
63
|
+
### Where a bound value lands
|
|
64
|
+
|
|
65
|
+
A value lands on a bound element one of two ways.
|
|
66
|
+
|
|
67
|
+
| Element | Receives the value as |
|
|
68
|
+
|------------------|--------------------------|
|
|
69
|
+
| A native element | Its `textContent` |
|
|
70
|
+
| A custom element | Its `lb-value` attribute |
|
|
71
|
+
|
|
72
|
+
A native element has no behavior of its own, so its value is its text. A
|
|
73
|
+
widget owns whatever control it wraps, so it is handed the value and renders
|
|
74
|
+
it itself — see [Custom Elements](./custom-elements.md#code) for
|
|
75
|
+
observing `lb-value`.
|
|
76
|
+
|
|
77
|
+
Nothing an application writes ever sets `lb-value`. It is written by
|
|
78
|
+
Loadbare and read by the widget it is written on.
|
|
79
|
+
|
|
80
|
+
### Rows
|
|
81
|
+
|
|
82
|
+
A query that answers with many rows is delivered to a list widget, which
|
|
83
|
+
clones its row template once per row and binds each clone to one row. A
|
|
84
|
+
native element has one destination for a value and cannot acquire children,
|
|
85
|
+
so a query that answers with rows must be bound to a widget that accepts
|
|
86
|
+
them.
|
|
87
|
+
|
|
88
|
+
Bind the widget to the query with `lb-query`, and name the key cell on the
|
|
89
|
+
`<template>` inside it with `lb-key`. See
|
|
90
|
+
[The Basic Widget Library](./widgets.md) for the widgets that accept
|
|
91
|
+
rows, and for `lb-group` and `lb-sort`, which a list widget reads to decide
|
|
92
|
+
where a row goes.
|
|
93
|
+
|
|
94
|
+
## Requests
|
|
95
|
+
|
|
96
|
+
Four attributes turn an interaction into a request:
|
|
97
|
+
|
|
98
|
+
| Written | Asks for | Carries |
|
|
99
|
+
|---------------------------|------------------|------------------------------|
|
|
100
|
+
| `lb-action="name"` | The named action | `name`, and what is in scope |
|
|
101
|
+
| `lb-delete` | `tupleDelete` | `query`, `key` |
|
|
102
|
+
| `lb-insert` on a `<form>` | `tupleInsert` | `query`, `values` |
|
|
103
|
+
| `lb-update` on a `<form>` | `tupleUpdate` | `query`, `key`, `values` |
|
|
104
|
+
|
|
105
|
+
The shipped `<lb-input>` widget asks for a fifth, `cellChange`, carrying
|
|
106
|
+
`query`, `key`, `cell`, and `value`. It sends one when the page applies
|
|
107
|
+
`data-fire-on-change` to it, and stays quiet otherwise — an input inside an
|
|
108
|
+
`lb-insert` or `lb-update` form is read again by the form on submit, so a
|
|
109
|
+
widget that sent on its own would write the same edit twice.
|
|
110
|
+
|
|
111
|
+
Declare every one of these on the server. A name the page has not declared,
|
|
112
|
+
and an operation a query does not permit, are refused; see
|
|
113
|
+
[hooks, actions, CRUD](./page-files.md#hooks-actions-crud).
|
|
114
|
+
|
|
115
|
+
### Actions
|
|
116
|
+
|
|
117
|
+
Write `lb-action` on a button to ask the server to do something that is not
|
|
118
|
+
one of the four CRUD operations:
|
|
119
|
+
|
|
120
|
+
```html
|
|
121
|
+
<button lb-action="mailRoster">Mail the roster</button>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
An action carries whatever binding is in scope at the element — `query`,
|
|
125
|
+
`key`, `cell` — and nothing else. There is no argument list. A button
|
|
126
|
+
carries no value, so the server computes the whole of the new state and the
|
|
127
|
+
page displays only what came back.
|
|
128
|
+
|
|
129
|
+
Write `lb-action` on a widget to have the widget decide what performing the
|
|
130
|
+
action means. Loadbare turns a click into a request for a native element
|
|
131
|
+
only, and leaves a widget to send its own — a `<select>` performs its action
|
|
132
|
+
on change, not on click.
|
|
133
|
+
|
|
134
|
+
### Deleting a row
|
|
135
|
+
|
|
136
|
+
```html
|
|
137
|
+
<button lb-delete>Remove</button>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`lb-delete` needs no name. The row's `lb-query` and `lb-key` are already in
|
|
141
|
+
scope, and they are all the server needs to know which row is meant and
|
|
142
|
+
whether the query permits deleting it.
|
|
143
|
+
|
|
144
|
+
Write `lb-delete` on a native element, the same as `lb-action`. A widget
|
|
145
|
+
sends its own request.
|
|
146
|
+
|
|
147
|
+
### Forms
|
|
148
|
+
|
|
149
|
+
`lb-insert` and `lb-update` both gather every `lb-cell` inside the form into
|
|
150
|
+
one values map, read from the control each cell is or wraps. They differ in
|
|
151
|
+
one thing: `lb-update` also carries the key of the row it is inside, and
|
|
152
|
+
`lb-insert` carries none, because there is no row yet.
|
|
153
|
+
|
|
154
|
+
Put an `lb-update` form inside the row it edits, so it has that row's key
|
|
155
|
+
from the same ancestor a delete button reads:
|
|
156
|
+
|
|
157
|
+
```html
|
|
158
|
+
<template lb-key="id">
|
|
159
|
+
<li>
|
|
160
|
+
<form lb-update>
|
|
161
|
+
<input lb-cell="name" />
|
|
162
|
+
<button type="submit">Save</button>
|
|
163
|
+
</form>
|
|
164
|
+
</li>
|
|
165
|
+
</template>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Give every `lb-cell` in a form a control to read. A cell that is neither an
|
|
169
|
+
`<input>`, `<select>`, or `<textarea>` nor wraps one is left out of the
|
|
170
|
+
values map.
|
|
171
|
+
|
|
172
|
+
## Conditional rendering
|
|
173
|
+
|
|
174
|
+
Loadbare ships static HTML and hydrates elements that are already in the
|
|
175
|
+
document. There is no `if`, and none is needed: write every possibility into
|
|
176
|
+
the page, and control which of them is showing.
|
|
177
|
+
|
|
178
|
+
Use the standard `hidden` attribute, or CSS, and set it from a value the
|
|
179
|
+
server sent. A widget that receives a value in `lb-value` is the natural
|
|
180
|
+
place to do it — it is handed the current state and decides what to show:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
for (const step of steps) step.hidden = step.dataset.step !== value;
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Ship the case that is showing on arrival unhidden, and hide the rest in the
|
|
187
|
+
HTML.
|
|
188
|
+
|
|
189
|
+
### An empty list
|
|
190
|
+
|
|
191
|
+
A list widget stamps itself with `data-rows`, the number of rows it is
|
|
192
|
+
showing. It is the one conditional a page cannot be sent, because the server
|
|
193
|
+
answers with rows and says nothing about how many survived. It makes an
|
|
194
|
+
empty list a stylesheet rule rather than code in every list widget:
|
|
195
|
+
|
|
196
|
+
```html
|
|
197
|
+
<lb-list lb-query="roster">
|
|
198
|
+
<ul>
|
|
199
|
+
<template lb-key="id">
|
|
200
|
+
<li lb-cell="name"></li>
|
|
201
|
+
</template>
|
|
202
|
+
</ul>
|
|
203
|
+
<p class="roster-empty">No members yet.</p>
|
|
204
|
+
</lb-list>
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
```css
|
|
208
|
+
.roster-empty {
|
|
209
|
+
display: none;
|
|
210
|
+
}
|
|
211
|
+
lb-list[data-rows="0"] .roster-empty {
|
|
212
|
+
display: revert;
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Request state
|
|
217
|
+
|
|
218
|
+
Loadbare stamps two attributes on the element a request came from — the
|
|
219
|
+
button, the form, or the widget itself:
|
|
220
|
+
|
|
221
|
+
| Attribute | Means |
|
|
222
|
+
|-------------------|-------------------------------------------|
|
|
223
|
+
| `data-lb-pending` | The request is in flight |
|
|
224
|
+
| `data-lb-error` | The last request from this element failed |
|
|
225
|
+
|
|
226
|
+
`data-lb-pending` is set when the request goes out and removed when it
|
|
227
|
+
settles. `data-lb-error` is set on a failed response, a network failure, or
|
|
228
|
+
a timeout alike, and cleared when that element sends its next request.
|
|
229
|
+
|
|
230
|
+
Neither one carries any meaning beyond the fact it states. Dim a pending
|
|
231
|
+
button in a stylesheet, or have a widget watch its own attributes and
|
|
232
|
+
disable itself. An application that styles neither behaves correctly and
|
|
233
|
+
shows nothing.
|
|
234
|
+
|
|
235
|
+
## Sending a request from a widget
|
|
236
|
+
|
|
237
|
+
A widget can build and dispatch a request itself instead of carrying one of
|
|
238
|
+
the attributes above — which is what `<lb-input>` does, and what a widget
|
|
239
|
+
carrying `lb-action` must do. See
|
|
240
|
+
[Custom Elements](./custom-elements.md#code).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# The Elements of a Loadbare Application
|
|
2
|
+
|
|
3
|
+
This reference is organized into four groups that reflect how an application is
|
|
4
|
+
put together: the shell it lives in, the build that assembles it, the pages it
|
|
5
|
+
serves, and the widgets those pages are made of.
|
|
6
|
+
|
|
7
|
+
## The application shell
|
|
8
|
+
|
|
9
|
+
| Topic | Description |
|
|
10
|
+
|--------------------------------------------------|-----------------------------------------------------|
|
|
11
|
+
| [`chrome.html`](./chrome.md) | The HTML document w/head, body, banner, main, etc. |
|
|
12
|
+
| [The Express server](./server.md) | Serves static assets and handles data channel calls |
|
|
13
|
+
| [The database layer](./server.md#database-layer) | Where the app's database access fits |
|
|
14
|
+
|
|
15
|
+
## Building the app
|
|
16
|
+
|
|
17
|
+
| Topic | Description |
|
|
18
|
+
|----------------------------------------------------------|-------------------------------------------|
|
|
19
|
+
| [`imports.ts`](./builder.md#widgets-from-packages) | The packages this app takes widgets from |
|
|
20
|
+
| [`loadbare-app-build`](./builder.md#running-the-builder) | Configuring the build command |
|
|
21
|
+
| [CSS](./css.md) | How the builder packages css |
|
|
22
|
+
|
|
23
|
+
## Application Pages
|
|
24
|
+
|
|
25
|
+
| Topic | Description |
|
|
26
|
+
|---------------------------------------------------------|--------------------------------|
|
|
27
|
+
| [`<name>.page.html`](./page-files.md#html) | The page's HTML |
|
|
28
|
+
| [`<name>.queries.ts`](./page-files.md#queries) | The data the page displays |
|
|
29
|
+
| [`<name>.hooks.ts`](./page-files.md#hooks-actions-crud) | Data Channel handlers |
|
|
30
|
+
| [Data Binding](./data-binding.md) | Connecting HTML to server data |
|
|
31
|
+
|
|
32
|
+
## Widgets
|
|
33
|
+
|
|
34
|
+
| Topic | Description |
|
|
35
|
+
|------------------------------------------------|---------------------------------|
|
|
36
|
+
| [What a widget is](./custom-elements.md) | An HTML file, a script, or both |
|
|
37
|
+
| [`<tag-name>.html`](./custom-elements.md#html) | The markup the tag expands into |
|
|
38
|
+
| [`<tag-name>.ts`](./custom-elements.md#code) | The class the tag registers |
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Page Files
|
|
2
|
+
|
|
3
|
+
A page is a set of files sharing one base name. The application writes the
|
|
4
|
+
HTML, and adds queries and hooks when the page shows data.
|
|
5
|
+
|
|
6
|
+
| File | Holds |
|
|
7
|
+
|---------------------|-------------------------------|
|
|
8
|
+
| `<name>.page.html` | The page's HTML |
|
|
9
|
+
| `<name>.queries.ts` | The data the page displays |
|
|
10
|
+
| `<name>.hooks.ts` | What the page does when asked |
|
|
11
|
+
|
|
12
|
+
Name the landing page `index.page.html`. A bare `/` resolves to `index`, and
|
|
13
|
+
every other path names the page of the same name.
|
|
14
|
+
|
|
15
|
+
Put the files anywhere under `src/`. The builder pairs them by base name, not
|
|
16
|
+
by directory; `src/pages/` is the convention.
|
|
17
|
+
|
|
18
|
+
## HTML
|
|
19
|
+
|
|
20
|
+
Write the page as a fragment. The fragment will land in `<main>`, which
|
|
21
|
+
is supplied by [chrome.html](./chrome.md).
|
|
22
|
+
|
|
23
|
+
```html
|
|
24
|
+
<!-- src/pages/about.page.html -->
|
|
25
|
+
<h1>About</h1>
|
|
26
|
+
<p>This is the about page.</p>
|
|
27
|
+
|
|
28
|
+
<div lb-query="visits">
|
|
29
|
+
<p>This page has been visited <span lb-cell="count"></span> times.</p>
|
|
30
|
+
</div>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Bind elements to data with `lb-query`, `lb-cell`, and the rest of the
|
|
34
|
+
attribute vocabulary in [Data Binding](./data-binding.md).
|
|
35
|
+
|
|
36
|
+
A page that displays no data needs no other file.
|
|
37
|
+
|
|
38
|
+
## Queries
|
|
39
|
+
|
|
40
|
+
Export `queries` from `<name>.queries.ts`. Each key is a name the HTML binds
|
|
41
|
+
to with `lb-query`, and each value takes the request context and returns
|
|
42
|
+
that query's result:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// src/pages/about.queries.ts
|
|
46
|
+
import { type Queries } from "@loadbare/app/server";
|
|
47
|
+
|
|
48
|
+
export const queries: Queries = {
|
|
49
|
+
visits: async (ctx) => ({ count: String(await ctx.db.visitCount()) }),
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Return every cell as a string. Format numbers, dates, and money in the query,
|
|
54
|
+
so the browser displays a value it never computes.
|
|
55
|
+
|
|
56
|
+
Return many rows through `rows()`:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
// src/pages/directory.queries.ts
|
|
60
|
+
import { rows, type Queries } from "@loadbare/app/server";
|
|
61
|
+
|
|
62
|
+
export const queries: Queries = {
|
|
63
|
+
directory: async (ctx) => rows(await ctx.db.directory()),
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Give every row a cell that identifies it, and name that cell with `lb-key` in
|
|
68
|
+
the HTML. Return the rows in the order the page shows them.
|
|
69
|
+
|
|
70
|
+
Return the full result every time. Sending only what changed is a hook's job —
|
|
71
|
+
see [refresh and patch](#refresh-and-patch).
|
|
72
|
+
|
|
73
|
+
## Hooks, actions, CRUD
|
|
74
|
+
|
|
75
|
+
Export `hooks` from `<name>.hooks.ts`. It holds three keys, each optional:
|
|
76
|
+
|
|
77
|
+
| Key | Runs |
|
|
78
|
+
|-------------|------------------------------------------------------|
|
|
79
|
+
| `beforeGet` | Before the page's queries, on a request for the page |
|
|
80
|
+
| `actions` | What the page may be asked to do, by name |
|
|
81
|
+
| `crud` | The four operations a query permits on its rows |
|
|
82
|
+
|
|
83
|
+
### beforeGet
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// src/pages/about.hooks.ts
|
|
87
|
+
import { type Hooks } from "@loadbare/app/server";
|
|
88
|
+
|
|
89
|
+
export const hooks: Hooks = {
|
|
90
|
+
beforeGet: (ctx) => ctx.db.recordVisit(),
|
|
91
|
+
};
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Declare no refresh set here. The page's queries run afterward.
|
|
95
|
+
|
|
96
|
+
### actions
|
|
97
|
+
|
|
98
|
+
Declare an action under the name the HTML gives `lb-action`. Pair what it
|
|
99
|
+
does with the queries to re-run once it has:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
export const hooks: Hooks = {
|
|
103
|
+
actions: {
|
|
104
|
+
resetVisits: {
|
|
105
|
+
run: (ctx) => ctx.db.resetVisits(),
|
|
106
|
+
refresh: ["visits"],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Declare every action the page allows. A name the page does not declare is
|
|
113
|
+
refused.
|
|
114
|
+
|
|
115
|
+
Read where the interaction happened from `run`'s second argument, which
|
|
116
|
+
carries `query`, `key`, `cell`, and `value` when the element that dispatched
|
|
117
|
+
the request had them.
|
|
118
|
+
|
|
119
|
+
### crud
|
|
120
|
+
|
|
121
|
+
Declare CRUD operations under `crud`, keyed by the query they operate on. Each
|
|
122
|
+
one takes the binding its trigger supplies:
|
|
123
|
+
|
|
124
|
+
| Operation | The page writes | `run` receives |
|
|
125
|
+
|---------------|--------------------|------------------------|
|
|
126
|
+
| `cellChange` | `<lb-input data-fire-on-change>` | `key`, `cell`, `value` |
|
|
127
|
+
| `tupleDelete` | `lb-delete` | `key` |
|
|
128
|
+
| `tupleInsert` | `<form lb-insert>` | `values` |
|
|
129
|
+
| `tupleUpdate` | `<form lb-update>` | `key`, `values` |
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// src/pages/directory.hooks.ts
|
|
133
|
+
import { patch, type Hooks } from "@loadbare/app/server";
|
|
134
|
+
|
|
135
|
+
export const hooks: Hooks = {
|
|
136
|
+
crud: {
|
|
137
|
+
directory: {
|
|
138
|
+
tupleInsert: {
|
|
139
|
+
run: async (ctx, { values }) => {
|
|
140
|
+
const entry = await ctx.db.addDirectoryEntry(values);
|
|
141
|
+
return { directory: patch({ rows: [entry] }) };
|
|
142
|
+
},
|
|
143
|
+
refresh: [],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Declare every operation the query permits. An operation a query does not
|
|
151
|
+
declare is refused, and a query with no `crud` entry permits none.
|
|
152
|
+
|
|
153
|
+
### refresh and patch
|
|
154
|
+
|
|
155
|
+
List in `refresh` every query whose whole answer the operation changed.
|
|
156
|
+
|
|
157
|
+
Return a result from `run` to state a narrower change than re-running a query
|
|
158
|
+
would. What `run` returns is laid over the refreshed queries:
|
|
159
|
+
|
|
160
|
+
| Result | States |
|
|
161
|
+
|--------------------------|---------------------------------------------|
|
|
162
|
+
| `rows([...])` | The entire set, and its order |
|
|
163
|
+
| `patch({ rows: [...] })` | These rows arrive or change; the rest stand |
|
|
164
|
+
| `patch({ drop: [...] })` | These keys are gone; the rest stand |
|
|
165
|
+
|
|
166
|
+
Return a patch for a change the operation knows the extent of — one row added,
|
|
167
|
+
one row dropped, one cell edited — and leave `refresh` empty. Re-run the query
|
|
168
|
+
instead when membership or order changed in a way the operation cannot name:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
resetRoster: {
|
|
172
|
+
run: (ctx) => ctx.db.resetMembers(),
|
|
173
|
+
refresh: ["roster"],
|
|
174
|
+
},
|
|
175
|
+
```
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# The Express Server
|
|
2
|
+
|
|
3
|
+
Loadbare ships no server. The application writes an ordinary Express app and
|
|
4
|
+
serves four things from it: the client script, the stylesheet, the data
|
|
5
|
+
channel, and the one HTML document.
|
|
6
|
+
|
|
7
|
+
Express is a peer dependency. The application installs it.
|
|
8
|
+
|
|
9
|
+
## A complete server
|
|
10
|
+
|
|
11
|
+
Here is a minimal but complete server for a typical app:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// server.ts
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import express, { type Request } from "express";
|
|
18
|
+
import { hubRoutes } from "@loadbare/app/express";
|
|
19
|
+
import type { HubContext } from "@loadbare/app/server";
|
|
20
|
+
import { hub } from "./dist/pages";
|
|
21
|
+
import { openDb } from "./src/database";
|
|
22
|
+
|
|
23
|
+
const DIST = path.resolve("dist");
|
|
24
|
+
const app = express();
|
|
25
|
+
|
|
26
|
+
app.get("/client.js", (_req, res) => res.sendFile(path.join(DIST, "client.js")));
|
|
27
|
+
app.get("/app.css", (_req, res) => res.sendFile(path.join(DIST, "app.css")));
|
|
28
|
+
|
|
29
|
+
function contextFor(_req: Request): HubContext {
|
|
30
|
+
return { db: openDb() };
|
|
31
|
+
}
|
|
32
|
+
app.use(hubRoutes(hub, contextFor));
|
|
33
|
+
|
|
34
|
+
app.get(/.*/, (_req, res) =>
|
|
35
|
+
res.type("html").send(readFileSync(path.join(DIST, "app.html"), "utf-8")),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
app.listen(8787);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## What the server serves
|
|
42
|
+
|
|
43
|
+
| Required | Serves |
|
|
44
|
+
|------------------------------|-------------------------------|
|
|
45
|
+
| `/client.js` | `dist/client.js` |
|
|
46
|
+
| `/app.css` | `dist/app.css` |
|
|
47
|
+
| `hubRoutes(hub, contextFor)` | `GET /lb/data` and `POST /lb` |
|
|
48
|
+
| Every other GET | `dist/app.html` |
|
|
49
|
+
|
|
50
|
+
Everything else is optional:
|
|
51
|
+
|
|
52
|
+
| Optional | Description |
|
|
53
|
+
|-------------------------------|----------------------------------------------|
|
|
54
|
+
| Middleware before `hubRoutes` | Sessions, authentication, logging |
|
|
55
|
+
| The application's own routes | Uploads, webhooks, anything outside Loadbare |
|
|
56
|
+
| An error handler | Express sends its own 500 without one |
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
## Rules for writing the server
|
|
60
|
+
|
|
61
|
+
Register the static routes and `hubRoutes` before the catch-all.
|
|
62
|
+
|
|
63
|
+
Register authentication before `hubRoutes`.
|
|
64
|
+
|
|
65
|
+
Serve `dist/app.html` for every route the application does not claim,
|
|
66
|
+
including a path that names no page. See [`chrome.html`](./chrome.md) for the
|
|
67
|
+
`<dialog lb-unknown-page>` that announces that case to the user.
|
|
68
|
+
|
|
69
|
+
Give the server the origin root. A proxy in front of it passes `/lb/data` and
|
|
70
|
+
`/lb` through unchanged, and the application cannot be hosted under a subpath
|
|
71
|
+
such as `example.com/myapp/`.
|
|
72
|
+
|
|
73
|
+
Leave `express.json()` to `hubRoutes`, which mounts it on its own routes.
|
|
74
|
+
|
|
75
|
+
## Running the server
|
|
76
|
+
|
|
77
|
+
Run the server under a TypeScript-capable runner. The builder writes
|
|
78
|
+
`dist/pages.ts`, which exports `hub`, as TypeScript:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"scripts": {
|
|
83
|
+
"dev": "loadbare-app-build --watch & tsx server.ts"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Restart the server after adding or changing a `.hooks.ts` or `.queries.ts`
|
|
89
|
+
file.
|
|
90
|
+
|
|
91
|
+
## Database layer
|
|
92
|
+
|
|
93
|
+
Loadbare ships no data layer. The application opens its own database and hands
|
|
94
|
+
it to Loadbare as the request context.
|
|
95
|
+
|
|
96
|
+
The application writes `contextFor` and passes it to `hubRoutes`. Loadbare
|
|
97
|
+
calls it on every data request:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// server.ts
|
|
101
|
+
function contextFor(req: Request): HubContext {
|
|
102
|
+
return { db: openDb(req.session.userId) };
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Open the handle in `contextFor` rather than once at startup, so that each
|
|
107
|
+
request works through a database opened for the caller it authenticated.
|
|
108
|
+
|
|
109
|
+
Declare what the context holds, once, anywhere in the application's own
|
|
110
|
+
source. Next to the database module is the natural place:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
// src/database.ts
|
|
114
|
+
declare module "@loadbare/app/server" {
|
|
115
|
+
interface HubContext {
|
|
116
|
+
db: Db;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Add a field for anything else a request needs — the authenticated user, a
|
|
122
|
+
request id, a feature flag set. Queries and hooks read them from `ctx`; see
|
|
123
|
+
[page files](./page-files.md).
|