@loadbare/app 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/README.md +53 -82
  2. package/dist/build/assemble.d.ts +7 -5
  3. package/dist/build/assemble.d.ts.map +1 -1
  4. package/dist/build/assemble.js +29 -9
  5. package/dist/build/cli.d.ts +20 -12
  6. package/dist/build/cli.d.ts.map +1 -1
  7. package/dist/build/cli.js +34 -16
  8. package/dist/build/elements.d.ts +15 -29
  9. package/dist/build/elements.d.ts.map +1 -1
  10. package/dist/build/elements.js +25 -111
  11. package/dist/build/expand.d.ts +1 -1
  12. package/dist/build/expand.js +1 -1
  13. package/dist/build/locations.d.ts +14 -37
  14. package/dist/build/locations.d.ts.map +1 -1
  15. package/dist/build/locations.js +24 -67
  16. package/dist/build/origins.d.ts +109 -0
  17. package/dist/build/origins.d.ts.map +1 -0
  18. package/dist/build/origins.js +270 -0
  19. package/dist/core/lb-constants.d.ts +1 -0
  20. package/dist/core/lb-constants.d.ts.map +1 -1
  21. package/dist/core/lb-constants.js +15 -8
  22. package/dist/core/lb-types.d.ts +2 -2
  23. package/dist/core/lb-types.d.ts.map +1 -1
  24. package/dist/hub/lb-apply.js +1 -1
  25. package/dist/hub/lb-hub.d.ts.map +1 -1
  26. package/dist/hub/lb-hub.js +44 -17
  27. package/dist/hub/lb-rows.js +3 -3
  28. package/dist/server/lb-server.d.ts +5 -4
  29. package/dist/server/lb-server.d.ts.map +1 -1
  30. package/dist/tests/assemble.test.js +11 -4
  31. package/dist/tests/elements.test.js +47 -51
  32. package/dist/tests/expand.test.d.ts +1 -1
  33. package/dist/tests/expand.test.js +2 -2
  34. package/dist/tests/fixtures/elements/collision/imports.d.ts +3 -0
  35. package/dist/tests/fixtures/elements/collision/imports.d.ts.map +1 -0
  36. package/dist/tests/fixtures/elements/collision/imports.js +1 -0
  37. package/dist/tests/fixtures/elements/manifest/imports.d.ts +3 -0
  38. package/dist/tests/fixtures/elements/manifest/imports.d.ts.map +1 -0
  39. package/dist/tests/fixtures/elements/manifest/imports.js +1 -0
  40. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts +3 -0
  41. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.d.ts.map +1 -0
  42. package/dist/tests/fixtures/elements/manifest-bad-entry/imports.js +1 -0
  43. package/dist/tests/fixtures/elements/{collision/elements.d.ts → manifest-not-array/imports.d.ts} +1 -1
  44. package/dist/tests/fixtures/elements/manifest-not-array/imports.d.ts.map +1 -0
  45. package/dist/tests/fixtures/elements/manifest-not-array/imports.js +1 -0
  46. package/dist/tests/fixtures/elements/pkg/acme-widget.d.ts +2 -0
  47. package/dist/tests/fixtures/elements/pkg/acme-widget.d.ts.map +1 -0
  48. package/dist/tests/fixtures/elements/pkg/acme-widget.js +1 -0
  49. package/dist/tests/lb-express.test.js +1 -1
  50. package/dist/tests/origins.test.d.ts +10 -0
  51. package/dist/tests/origins.test.d.ts.map +1 -0
  52. package/dist/tests/origins.test.js +326 -0
  53. package/dist/tests/pages.test.js +3 -3
  54. package/dist/tests/styles.test.js +7 -4
  55. package/docs/reference/builder.md +128 -0
  56. package/docs/reference/chrome.md +75 -0
  57. package/docs/reference/css.md +44 -0
  58. package/docs/reference/custom-elements.md +327 -0
  59. package/docs/reference/data-binding.md +240 -0
  60. package/docs/reference/overview.md +38 -0
  61. package/docs/reference/page-files.md +175 -0
  62. package/docs/reference/server.md +123 -0
  63. package/docs/reference/widgets.md +163 -0
  64. package/docs/roadmap.md +130 -0
  65. package/docs/testing.md +228 -0
  66. package/docs/theory.md +344 -223
  67. package/docs/tutorials/000-getting-started.md +86 -0
  68. package/docs/tutorials/010-pages-and-navigation.md +129 -0
  69. package/docs/tutorials/020-css.md +103 -0
  70. package/docs/tutorials/030-html-decomposition.md +79 -0
  71. package/docs/tutorials/040-displaying-data.md +169 -0
  72. package/docs/tutorials/050-actions.md +77 -0
  73. package/docs/tutorials/060-custom-element-code.md +73 -0
  74. package/docs/tutorials/065-conditional-rendering.md +161 -0
  75. package/docs/tutorials/070-displaying-a-list.md +137 -0
  76. package/docs/tutorials/072-inserting-into-a-list.md +88 -0
  77. package/docs/tutorials/074-deleting-from-a-list.md +77 -0
  78. package/docs/tutorials/076-updating-a-list-item.md +86 -0
  79. package/docs/tutorials/080-widget-requests.md +124 -0
  80. package/docs/tutorials/090-using-widget-libraries.md +75 -0
  81. package/package.json +4 -12
  82. package/dist/client.js +0 -522
  83. package/dist/demo-static/src/widgets/app-box.d.ts +0 -15
  84. package/dist/demo-static/src/widgets/app-box.d.ts.map +0 -1
  85. package/dist/demo-static/src/widgets/app-box.js +0 -19
  86. package/dist/tests/fixtures/elements/collision/elements.d.ts.map +0 -1
  87. package/dist/tests/fixtures/elements/collision/elements.js +0 -3
  88. package/dist/tests/fixtures/elements/manifest/elements.d.ts +0 -5
  89. package/dist/tests/fixtures/elements/manifest/elements.d.ts.map +0 -1
  90. package/dist/tests/fixtures/elements/manifest/elements.js +0 -3
  91. package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts +0 -5
  92. package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts.map +0 -1
  93. package/dist/tests/fixtures/elements/manifest-bad-tag/elements.js +0 -3
  94. package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts +0 -5
  95. package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts.map +0 -1
  96. package/dist/tests/fixtures/elements/manifest-bad-value/elements.js +0 -3
  97. package/dist/tests/golden.test.d.ts +0 -19
  98. package/dist/tests/golden.test.d.ts.map +0 -1
  99. package/dist/tests/golden.test.js +0 -60
  100. package/dist/tests/helpers/window.d.ts +0 -43
  101. package/dist/tests/helpers/window.d.ts.map +0 -1
  102. package/dist/tests/helpers/window.js +0 -78
  103. package/dist/tests/lb-input.test.d.ts +0 -9
  104. package/dist/tests/lb-input.test.d.ts.map +0 -1
  105. package/dist/tests/lb-input.test.js +0 -78
  106. package/dist/tests/lb-list.test.d.ts +0 -12
  107. package/dist/tests/lb-list.test.d.ts.map +0 -1
  108. package/dist/tests/lb-list.test.js +0 -44
  109. package/dist/tests/lb-options.test.d.ts +0 -10
  110. package/dist/tests/lb-options.test.d.ts.map +0 -1
  111. package/dist/tests/lb-options.test.js +0 -121
  112. package/dist/tests/lb-picker.test.d.ts +0 -14
  113. package/dist/tests/lb-picker.test.d.ts.map +0 -1
  114. package/dist/tests/lb-picker.test.js +0 -59
  115. package/dist/tests/lb-select.test.d.ts +0 -9
  116. package/dist/tests/lb-select.test.d.ts.map +0 -1
  117. package/dist/tests/lb-select.test.js +0 -71
  118. package/dist/tests/lb-table.test.d.ts +0 -15
  119. package/dist/tests/lb-table.test.d.ts.map +0 -1
  120. package/dist/tests/lb-table.test.js +0 -205
  121. package/dist/widgets/index.d.ts +0 -7
  122. package/dist/widgets/index.d.ts.map +0 -1
  123. package/dist/widgets/index.js +0 -6
  124. package/dist/widgets/lb-input.d.ts +0 -2
  125. package/dist/widgets/lb-input.d.ts.map +0 -1
  126. package/dist/widgets/lb-input.js +0 -48
  127. package/dist/widgets/lb-list.d.ts +0 -2
  128. package/dist/widgets/lb-list.d.ts.map +0 -1
  129. package/dist/widgets/lb-list.js +0 -17
  130. package/dist/widgets/lb-options.d.ts +0 -26
  131. package/dist/widgets/lb-options.d.ts.map +0 -1
  132. package/dist/widgets/lb-options.js +0 -72
  133. package/dist/widgets/lb-picker.d.ts +0 -2
  134. package/dist/widgets/lb-picker.d.ts.map +0 -1
  135. package/dist/widgets/lb-picker.js +0 -25
  136. package/dist/widgets/lb-select.d.ts +0 -2
  137. package/dist/widgets/lb-select.d.ts.map +0 -1
  138. package/dist/widgets/lb-select.js +0 -43
  139. package/dist/widgets/lb-table.d.ts +0 -2
  140. package/dist/widgets/lb-table.d.ts.map +0 -1
  141. package/dist/widgets/lb-table.js +0 -113
  142. package/docs/application-chrome.md +0 -36
  143. package/docs/building-html-pages.md +0 -130
  144. package/docs/getting-started.md +0 -120
  145. package/docs/guide.md +0 -1164
  146. package/docs/hosting.md +0 -218
  147. package/docs/latent-risks.md +0 -20
  148. package/widgets/index.ts +0 -6
  149. package/widgets/lb-input.html +0 -1
  150. package/widgets/lb-input.ts +0 -64
  151. package/widgets/lb-list.html +0 -1
  152. package/widgets/lb-list.ts +0 -21
  153. package/widgets/lb-options.html +0 -4
  154. package/widgets/lb-options.ts +0 -88
  155. package/widgets/lb-picker.html +0 -7
  156. package/widgets/lb-picker.ts +0 -27
  157. package/widgets/lb-select.html +0 -4
  158. package/widgets/lb-select.ts +0 -55
  159. package/widgets/lb-table.html +0 -8
  160. package/widgets/lb-table.ts +0 -126
@@ -0,0 +1,86 @@
1
+ # Getting Started
2
+
3
+ This first tutorial sets us up with an empty application shell
4
+ and a simple express server. This will be the foundation for
5
+ creating a full database application.
6
+
7
+ ## The chrome file
8
+
9
+ Write a file `src/chrome.html`. The location is convention, in
10
+ fact Loadbare only requires exactly one file named `chrome.html`
11
+ anywhere in the `src/` tree.
12
+
13
+ The chrome is plain old HTML. All Loadbare chrome, pages, and
14
+ widgets are plain old HTML and custom elements.
15
+
16
+ At minimum, the chrome must contain:
17
+
18
+ - a link to the `client.js` script
19
+ - The `<lb-hub>` custom element just inside `<body>`
20
+ - an empty `<main>` inside `<lb-hub>`
21
+
22
+ ```html
23
+ <!doctype html>
24
+ <html lang="en">
25
+ <head>
26
+ <meta charset="utf-8" />
27
+ <title>@Loadbare/app Tutorials</title>
28
+ <script src="/client.js" defer></script>
29
+ </head>
30
+ <body>
31
+ <lb-hub>
32
+ <main></main>
33
+ </lb-hub>
34
+ </body>
35
+ </html>
36
+ ```
37
+
38
+ The client script `client.js` is generated by the builder. In loadbare,
39
+ `client.js` is the combination of Javascript class definitions for all of
40
+ the custom elements used anywhere in `src/`. In this basic starting point,
41
+ we have `<lb-hub>` as the only custom element, so `client.js` will just
42
+ contain the Javascript class `LbHub`.
43
+
44
+ ## The dev script
45
+
46
+ Add a command to package.json that builds the app.
47
+
48
+ ```json
49
+ {
50
+ "scripts": {
51
+ "dev": "loadbare-app-build --watch & node server.js"
52
+ }
53
+ }
54
+ ```
55
+
56
+ By default the builder puts all output into `dist/`.
57
+
58
+ ## The express server
59
+
60
+ Create `server.js` as an express server:
61
+
62
+ ```js
63
+ import express from "express";
64
+ import { readFileSync } from "node:fs";
65
+
66
+ const app = express();
67
+
68
+ app.use("/client.js", express.static("dist/client.js"));
69
+
70
+ app.get("/", (_req, res) =>
71
+ res.type("html").send(readFileSync("dist/app.html", "utf-8")),
72
+ );
73
+
74
+ app.listen(8787);
75
+ ```
76
+
77
+ ## Run it
78
+
79
+ Try `npm run dev`, and navigate the browser to localhost:8787, you
80
+ should see an empty page, with a console message, and the view source
81
+ option will show exactly the chrome as it was written on disk
82
+ with its empty `<main>`.
83
+
84
+ ---
85
+
86
+ Next: [Pages and Navigation](./010-pages-and-navigation.md)
@@ -0,0 +1,129 @@
1
+ # Pages and Navigation
2
+
3
+ In our first tutorial, [Getting Started](./000-getting-started.md), we
4
+ create an empty
5
+ `<main>` with no content.
6
+
7
+ Now we will see how to make pages and navigate between them by creating
8
+ two simple static pages.
9
+
10
+ ## Two page files
11
+
12
+ Write two files ending `.page.html` for two pages,
13
+ `src/pages/index.page.html` and `src/pages/about.page.html`. Like the
14
+ chrome, a page is found by its filename, not its directory; we use
15
+ the `src/pages` directory by convention and for convenience.
16
+
17
+ The file `index.page.html` is special in Loadbare. Loadbare considers
18
+ this the landing page. If a user navigates to `myapp.example.com` without
19
+ a page specified, Loadbare displays `index.page.html`.
20
+
21
+
22
+ ```html
23
+ <!-- src/pages/index.page.html -->
24
+ <h1>Home</h1>
25
+ <p>This is the home page.</p>
26
+ ```
27
+
28
+ ```html
29
+ <!-- src/pages/about.page.html -->
30
+ <h1>About</h1>
31
+ <p>This is the about page.</p>
32
+ ```
33
+
34
+ ## Linking between them
35
+
36
+ Add a `<nav>` to the chrome, with one anchor per page.
37
+
38
+ Also add the `<dialog lb-unknown-page>` element, so the hub `<lb-hub>` can
39
+ display something to the user if they type a URL that has no matching page.
40
+
41
+ ```html
42
+ <!-- src/chrome.html -->
43
+ <!doctype html>
44
+ <html lang="en">
45
+ <head>
46
+ <meta charset="utf-8" />
47
+ <title>Pages and Navigation</title>
48
+ <script src="/client.js" defer></script>
49
+ </head>
50
+ <body>
51
+ <lb-hub>
52
+ <nav>
53
+ <!-- a bare link to "/" goes to page "index" -->
54
+ <a href="/" lb-nav-link>Home</a>
55
+ <a href="/about" lb-nav-link>About</a>
56
+ </nav>
57
+ <main></main>
58
+ <dialog lb-unknown-page>The page <span lb-cell="page"></span> is not in this app.</dialog>
59
+ </lb-hub>
60
+ </body>
61
+ </html>
62
+ ```
63
+
64
+ Here we introduce the attribute `lb-nav-link`, which indicates the link
65
+ should navigate within the app. When the user clicks, the hub `<lb-hub>`
66
+ intercepts the event and swaps the page's HTML into the `<main>` element.
67
+ The hub ensures the back button works by calling `history.pushState` on
68
+ navigation, and catching the browser's `popstate` event.
69
+
70
+ An anchor without `lb-nav-link` is left alone and behaves like an ordinary
71
+ link.
72
+
73
+ If a `<dialog lb-unknown-page>` element is present in the HTML, it will be
74
+ displayed to the user when a URL is entered that has no matching page in the app.
75
+ The `lb-cell` attribute will be explained when we get to data binding.
76
+
77
+ ## Serving every route
78
+
79
+ The server sends the same document for every route, which is a monolithic
80
+ assembly of chrome and all pages (as `<template>` elements). The hub
81
+ `<lb-hub>` picks which page to show from the URL.
82
+
83
+ With this in mind, we modify the code from the previous example to
84
+ serve the monolithic HTML for any get route.
85
+
86
+ ```js
87
+ // server.js
88
+ import express from "express";
89
+ import { readFileSync } from "node:fs";
90
+
91
+ const app = express();
92
+
93
+ app.use("/client.js", express.static("dist/client.js"));
94
+
95
+ // Getting Started had app.get("/", ...); this catches every path instead
96
+ app.get(/.*/, (_req, res) =>
97
+ res.type("html").send(readFileSync("dist/app.html", "utf-8")),
98
+ );
99
+
100
+ app.listen(8787);
101
+ ```
102
+
103
+ The static route for `/client.js` has to come before the catch-all, or the
104
+ catch-all answers that request too.
105
+
106
+ ## If you look at the console
107
+
108
+ On every navigation, the hub also asks the server for that page's data, at
109
+ `/lb/data?page=<name>`. Neither of these pages is serving any data, and our
110
+ Express server has no route handler for /lb/*, so the catch-all route is currently
111
+ returning the same `app.html` for data requests as it does for the inital
112
+ app load. This is expected in this early stage of the tutorial, we will fix
113
+ it when we get to data binding and handling.
114
+
115
+ ## Run it
116
+
117
+ ```
118
+ npm run dev
119
+ ```
120
+
121
+ Open `http://localhost:8787/`. Click between Home and About — `<main>`
122
+ swaps instantly, with no server request for markup. View source still shows
123
+ exactly what's on disk: the chrome, both page hosts (each inside its own
124
+ `<template>`), and the `<dialog>`. Then try a path neither page defines,
125
+ like `http://localhost:8787/badpagename`, and confirm the dialog announces it.
126
+
127
+ ---
128
+ Prev: [Getting Started](./000-getting-started.md)
129
+ Next: [Adding CSS](./020-css.md)
@@ -0,0 +1,103 @@
1
+ # Adding CSS
2
+
3
+ Loadbare has a precise precedence opinion for CSS — see [CSS](../reference/css.md)
4
+ for the full rule and why. For now we just want to load some styles onto
5
+ the two pages from [Pages and Navigation](./010-pages-and-navigation.md).
6
+
7
+ The Loadbare build concatenates every `.css` file anywhere under `src`
8
+ into one bundle, sorted alphabetically by filename; a
9
+ directory never affects that order, so a global stylesheet sorts first by
10
+ naming itself something like `00-reset.css`.
11
+
12
+ ## Adding CSS to our two static pages
13
+
14
+ We'll add two stylesheets: one global, one specific to a single page.
15
+
16
+ ```css
17
+ /* src/00-global.css */
18
+ body {
19
+ font-family: system-ui, sans-serif;
20
+ margin: 2rem;
21
+ }
22
+
23
+ nav a {
24
+ margin-right: 1rem;
25
+ }
26
+ ```
27
+
28
+ ```css
29
+ /* src/pages/about.css */
30
+ .about-note {
31
+ color: #555;
32
+ font-style: italic;
33
+ }
34
+ ```
35
+
36
+ Loadbare doesn't pair a stylesheet with a page.
37
+ The name `about.css` for the page `about.page.html` is only a convention we're
38
+ choosing for easy identification. Loadbare never enforces or
39
+ even notices that the file has the same stem as an HTML page.
40
+
41
+ Use the class in the page:
42
+
43
+ ```html
44
+ <!-- src/pages/about.page.html -->
45
+ <h1>About</h1>
46
+ <p class="about-note">This is the about page.</p>
47
+ ```
48
+
49
+ ## Linking the bundle
50
+
51
+ Add the stylesheet link to the chrome:
52
+
53
+ ```html
54
+ <!-- src/chrome.html -->
55
+ <!doctype html>
56
+ <html lang="en">
57
+ <head>
58
+ <meta charset="utf-8" />
59
+ <title>Pages and Navigation</title>
60
+ <script src="/client.js" defer></script>
61
+ <!-- Add a standard stylesheet link -->
62
+ <link rel="stylesheet" href="/app.css" />
63
+ </head>
64
+ <body>
65
+ <lb-hub>
66
+ <nav>
67
+ <a href="/" lb-nav-link>Home</a>
68
+ <a href="/about" lb-nav-link>About</a>
69
+ </nav>
70
+ <main></main>
71
+ <dialog lb-unknown-page>The page <span lb-cell="page"></span> is not in this app.</dialog>
72
+ </lb-hub>
73
+ </body>
74
+ </html>
75
+ ```
76
+
77
+ And a static route for it in the server, the same way `/client.js` already
78
+ has one:
79
+
80
+ ```js
81
+ // server.js
82
+ app.use("/client.js", express.static("dist/client.js"));
83
+ app.use("/app.css", express.static("dist/app.css"));
84
+ ```
85
+
86
+ Like `/client.js`, this has to come before the catch-all route, or the
87
+ catch-all answers that request too.
88
+
89
+ ## Run it
90
+
91
+ ```
92
+ npm run dev
93
+ ```
94
+
95
+ Open `http://localhost:8787/`. Both pages should now use the system font
96
+ and the wider margin from `00-global.css`; the About page's note should be
97
+ grey and italic. View `dist/app.css` — it's `00-global.css`'s contents
98
+ followed by `about.css`'s, in exactly that order, because `00-global.css`
99
+ sorts first by name.
100
+
101
+ ---
102
+ Prev: [Pages and Navigation](./010-pages-and-navigation.md)
103
+ Next: [HTML Decomposition](./030-html-decomposition.md)
@@ -0,0 +1,79 @@
1
+ # HTML Decomposition
2
+
3
+ So far we have been building a monolithic chrome, but this will soon
4
+ become unwieldy. We need to break it up so we can work with subtrees
5
+ of the HTML in isolation.
6
+
7
+ Loadbare interprets custom elements as server-side HTML
8
+ includes. When the HTML contains `<my-custom-widget>`, and there
9
+ is a file `my-custom-widget.html` anywhere in `src/`, the
10
+ Loadbare builder inserts the contents of `my-custom-widget.html` as the
11
+ children of `<my-custom-widget>`.
12
+
13
+ This HTML include feature has options for attributes and slots, but
14
+ for now we will just do a static example.
15
+
16
+ ## Static decomposition of the navigation bar
17
+
18
+ We begin by replacing the literal nav bar with an empty custom
19
+ element, not yet defined:
20
+
21
+ ```html
22
+ <!-- src/chrome.html -->
23
+ <!doctype html>
24
+ <html lang="en">
25
+ <head>
26
+ <meta charset="utf-8" />
27
+ <title>Pages and Navigation</title>
28
+ <script src="/client.js" defer></script>
29
+ <link rel="stylesheet" href="/app.css" />
30
+ </head>
31
+ <body>
32
+ <lb-hub>
33
+ <!-- replace this from the previous tutorial...
34
+ <nav>
35
+ <a href="/" lb-nav-link>Home</a>
36
+ <a href="/about" lb-nav-link>About</a>
37
+ </nav>
38
+ -->
39
+ <!-- ...with this empty custom element: -->
40
+ <app-nav></app-nav>
41
+ <main></main>
42
+ <dialog lb-unknown-page>The page <span lb-cell="page"></span> is not in this app.</dialog>
43
+ </lb-hub>
44
+ </body>
45
+ </html>
46
+ ```
47
+
48
+ Now we specify the HTML that should be inserted as children
49
+ of `<app-nav>` by writing `src/app-nav.html`.
50
+
51
+ ```html
52
+ <nav>
53
+ <a href="/" lb-nav-link>Home</a>
54
+ <a href="/about" lb-nav-link>About</a>
55
+ </nav>
56
+ ```
57
+
58
+ During the build, Loadbare locates `app-nav.html` and inserts its contents
59
+ into the `<app-nav>` custom element, so the actual result will look like this:
60
+
61
+ ```html
62
+ <app-nav>
63
+ <nav>
64
+ <a href="/" lb-nav-link>Home</a>
65
+ <a href="/about" lb-nav-link>About</a>
66
+ </nav>
67
+ </app-nav>
68
+ ```
69
+
70
+ ## Dynamic Composition
71
+
72
+ There is much more to HTML decomposition. Custom elements can have
73
+ slots and attributes that are resolved at build time. Read all about
74
+ them in [Custom Elements](../reference/custom-elements.md#html).
75
+
76
+ ---
77
+ Prev: [CSS](./020-css.md)
78
+ Next: [Displaying Data](040-displaying-data.md)
79
+
@@ -0,0 +1,169 @@
1
+ # Displaying Data
2
+
3
+ We now have the basics of a Loadbare app: the chrome, the express server,
4
+ some CSS, and two pages. But they are static, and the entire point of
5
+ Loadbare is to build data-centric apps. So now we add the first half of
6
+ data-centric apps, displaying data.
7
+
8
+ ## Data Binding
9
+
10
+ Loadbare can understand and process scalar cells, entities, and
11
+ collections. We begin with a simple scalar cell.
12
+
13
+ All data that is displayed in the app is scoped to a named query that
14
+ is implemented on the server. In the code below we scope the div
15
+ to query `visits`, then specify that the span will display
16
+ the cell `count`.
17
+
18
+ ```html
19
+ <!-- src/pages/about.page.html -->
20
+ <h1>About</h1>
21
+ <p class="about-note">This is the about page.</p>
22
+
23
+ <div lb-query="visits">
24
+ <p>This page has been visited <span lb-cell="count"></span> times.</p>
25
+ </div>
26
+ ```
27
+
28
+ ## Write the Query
29
+
30
+ Put the query into `about.queries.ts`, which uses the same page stem,
31
+ `about`, as `about.page.html`.
32
+
33
+ ```ts
34
+ // src/pages/about.queries.ts
35
+ export const queries = {
36
+ visits: async (ctx) => ({ count: String(await ctx.db.visitCount()) }),
37
+ };
38
+ ```
39
+
40
+ We have not yet implemented `ctx.db`, we will do that shortly.
41
+
42
+ ## Write the Before Get Hook
43
+
44
+ When the user navigates to a page, the loadbare hub `<lb-hub>` swaps
45
+ the contents of `<main>` and sends a request to the server for the
46
+ query results defined for that page.
47
+
48
+ For a visit counter, we need to write a `beforeGet` hook to fire first
49
+ and increment the counter:
50
+
51
+
52
+ ```ts
53
+ // src/pages/about.hooks.ts
54
+ export const hooks = {
55
+ beforeGet: (ctx) => ctx.db.recordVisit(),
56
+ };
57
+ ```
58
+
59
+ Once again we are referring to a method on `ctx.db`, which we must now
60
+ implement.
61
+
62
+ ## Implement a simple database
63
+
64
+ In a real application we would be connecting to some database server,
65
+ but we want to keep the tutorial simple and step-by-step, so we will mock
66
+ up a file-based database server that only does what the tutorial needs.
67
+
68
+
69
+ ```ts
70
+ // src/database.ts
71
+ import { readFile, writeFile } from "node:fs/promises";
72
+
73
+ const DATA_FILE = new URL("./.lb-data.json", import.meta.url);
74
+
75
+ async function read() {
76
+ try {
77
+ return JSON.parse(await readFile(DATA_FILE, "utf-8"));
78
+ } catch {
79
+ return { visitCount: 0 };
80
+ }
81
+ }
82
+
83
+ export function openDb() {
84
+ return {
85
+ async visitCount() {
86
+ return (await read()).visitCount;
87
+ },
88
+ async recordVisit() {
89
+ const current = await read();
90
+ await writeFile(
91
+ DATA_FILE,
92
+ JSON.stringify({ visitCount: current.visitCount + 1 }),
93
+ "utf-8",
94
+ );
95
+ },
96
+ };
97
+ }
98
+ ```
99
+
100
+ Nothing here is Loadbare's concern — it never looks inside `ctx`. A real
101
+ application would put a real database behind `openDb`, and would open it for
102
+ the authenticated caller instead of the same file for everyone.
103
+
104
+ ## Wiring the server
105
+
106
+ The Loadbare builder compiles a list of all hooks and queries.
107
+ Add the four
108
+ lines below, and our server will correctly route all data channel
109
+ requests to the correct page code.
110
+
111
+ ```js
112
+ // server.js
113
+ import express from "express";
114
+ import { readFileSync } from "node:fs";
115
+ import { hubRoutes } from "@loadbare/app/express";
116
+ import { hub } from "./dist/pages";
117
+ import { openDb } from "./src/database";
118
+
119
+ const app = express();
120
+
121
+ app.use("/client.js", express.static("dist/client.js"));
122
+ app.use("/app.css", express.static("dist/app.css"));
123
+
124
+ // New: answers GET /lb/data?page=<name> and POST /lb?page=<name>.
125
+ // Must come before the catch-all, or the catch-all answers these too.
126
+ function contextFor(_req) {
127
+ return { db: openDb() };
128
+ }
129
+ app.use(hubRoutes(hub, contextFor));
130
+
131
+ app.get(/.*/, (_req, res) =>
132
+ res.type("html").send(readFileSync("dist/app.html", "utf-8")),
133
+ );
134
+
135
+ app.listen(8787);
136
+ ```
137
+
138
+ `dist/pages.ts` and our own `.ts` files mean `server.js` is no longer
139
+ runnable by plain `node`. Swap the dev script to a TypeScript-capable
140
+ runner:
141
+
142
+ ```json
143
+ {
144
+ "scripts": {
145
+ "dev": "loadbare-app-build --watch & tsx server.js"
146
+ }
147
+ }
148
+ ```
149
+
150
+ ## Run it
151
+
152
+ ```
153
+ npm run dev
154
+ ```
155
+
156
+ Open `http://localhost:8787/` and navigate to About a few times, using the
157
+ nav links so `<main>` swaps in place. The count rises by one on every visit
158
+ to About.
159
+
160
+ View source on either page: the host HTML is exactly what's on disk. Only
161
+ the `<span>`'s text changes, never the markup around it.
162
+
163
+ This tutorial only used `lb-query` and `lb-cell` to display one value.
164
+ The full attribute vocabulary — actions, CRUD operations, lists, and
165
+ navigation — is in [Data Binding](../reference/data-binding.md).
166
+
167
+ ---
168
+ Prev: [HTML Decomposition](./030-html-decomposition.md)
169
+ Next: [Actions](./050-actions.md)
@@ -0,0 +1,77 @@
1
+ # Actions
2
+
3
+ So far we have used data binding to display a single cell of a named
4
+ query. Before we go on to CRUD operations, we will show an "action",
5
+ which is a named routine on the server that cna be invoked from the
6
+ client.
7
+
8
+ ## An action in the client
9
+
10
+ Modify the about page to include a button that requests the server
11
+ to reset the visit count.
12
+
13
+ ```html
14
+ <!-- src/pages/about.page.html -->
15
+ <h1>About</h1>
16
+ <p class="about-note">This is the about page.</p>
17
+
18
+ <div lb-query="visits">
19
+ <p>This page has been visited <span lb-cell="count"></span> times.</p>
20
+ <button lb-action="resetVisits">Reset count</button>
21
+ </div>
22
+ ```
23
+
24
+ ## Implement the action on the server
25
+
26
+ Actions go into the page's hooks file:
27
+
28
+ ```ts
29
+ // src/pages/about.hooks.ts
30
+ export const hooks = {
31
+ beforeGet: (ctx) => ctx.db.recordVisit(),
32
+ actions: {
33
+ resetVisits: {
34
+ run: (ctx) => ctx.db.resetVisits(),
35
+ refresh: ["visits"],
36
+ },
37
+ },
38
+ };
39
+ ```
40
+
41
+ An action is a `run` paired with a `refresh` list. After the `run` code
42
+ is executed, all of the named queries in `refresh` are rerun and their
43
+ results go to the browser, where the Loadbare hub `<lb-hub>` refreshes
44
+ the bound elements.
45
+
46
+ ## Extending the database
47
+
48
+ We have to extend our bespoke database with `resetVisits`.
49
+
50
+ ```ts
51
+ // src/database.ts
52
+ export function openDb() {
53
+ return {
54
+ // ...visitCount() and recordVisit() unchanged...
55
+ async resetVisits() {
56
+ await write({ visitCount: 0 });
57
+ },
58
+ };
59
+ }
60
+ ```
61
+
62
+ `write` already merges over whatever's stored, so this only ever touches
63
+ `visitCount`.
64
+
65
+ ## Run it
66
+
67
+ ```
68
+ npm run dev
69
+ ```
70
+
71
+ Open the About page a few times to raise the count, then click "Reset
72
+ count" — it drops to zero in place, no reload. Reload anyway: it stays at
73
+ zero, because the reset happened on the server, not just in the browser.
74
+
75
+ ---
76
+ Prev: [Displaying Data](./040-displaying-data.md)
77
+ Next: [Custom Element Code](./060-custom-element-code.md)