@formbox/htmx 0.3.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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +323 -0
  3. package/dist/index.d.ts +615 -0
  4. package/dist/index.js +62387 -0
  5. package/dist/templates/AnswerList.html.hbs +17 -0
  6. package/dist/templates/AnswerScaffold.html.hbs +19 -0
  7. package/dist/templates/Checkbox.html.hbs +14 -0
  8. package/dist/templates/CheckboxList.html.hbs +29 -0
  9. package/dist/templates/CustomOptionForm.html.hbs +16 -0
  10. package/dist/templates/DateInput.html.hbs +14 -0
  11. package/dist/templates/DateTimeInput.html.hbs +18 -0
  12. package/dist/templates/DisplayRenderer.html.hbs +9 -0
  13. package/dist/templates/Errors.html.hbs +15 -0
  14. package/dist/templates/FileInput.html.hbs +20 -0
  15. package/dist/templates/Flyover.html.hbs +12 -0
  16. package/dist/templates/Footer.html.hbs +8 -0
  17. package/dist/templates/Form.html.hbs +13 -0
  18. package/dist/templates/GroupList.html.hbs +22 -0
  19. package/dist/templates/GroupScaffold.html.hbs +36 -0
  20. package/dist/templates/Header.html.hbs +8 -0
  21. package/dist/templates/Help.html.hbs +12 -0
  22. package/dist/templates/InputGroup.html.hbs +10 -0
  23. package/dist/templates/Label.html.hbs +23 -0
  24. package/dist/templates/LanguageSelector.html.hbs +13 -0
  25. package/dist/templates/Legal.html.hbs +12 -0
  26. package/dist/templates/Link.html.hbs +9 -0
  27. package/dist/templates/MultiSelectInput.html.hbs +30 -0
  28. package/dist/templates/NumberInput.html.hbs +16 -0
  29. package/dist/templates/OptionDisplay.html.hbs +10 -0
  30. package/dist/templates/OptionsLoading.html.hbs +10 -0
  31. package/dist/templates/QuestionScaffold.html.hbs +30 -0
  32. package/dist/templates/RadioButton.html.hbs +14 -0
  33. package/dist/templates/RadioButtonList.html.hbs +27 -0
  34. package/dist/templates/SelectInput.html.hbs +32 -0
  35. package/dist/templates/SignatureInput.html.hbs +12 -0
  36. package/dist/templates/SliderInput.html.hbs +23 -0
  37. package/dist/templates/SpinnerInput.html.hbs +16 -0
  38. package/dist/templates/Stack.html.hbs +7 -0
  39. package/dist/templates/TabContainer.html.hbs +27 -0
  40. package/dist/templates/Table.html.hbs +39 -0
  41. package/dist/templates/TextArea.html.hbs +13 -0
  42. package/dist/templates/TextInput.html.hbs +14 -0
  43. package/dist/templates/TimeInput.html.hbs +12 -0
  44. package/package.json +57 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Health Samurai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,323 @@
1
+ # @formbox/htmx
2
+
3
+ Server-rendered HTML renderer for Formbox FHIR Questionnaires.
4
+
5
+ This package renders FHIR Questionnaire controls as plain HTML that can be
6
+ posted with normal browser form submission or HTMX. Consumers do not import
7
+ React, hydrate React, or manage MobX.
8
+
9
+ The main API is built around a short-lived renderer instance:
10
+
11
+ ```ts
12
+ import { QuestionnaireRenderer, loadNativeTemplates } from "@formbox/htmx";
13
+
14
+ const route = "/questionnaire";
15
+ const templates = await loadNativeTemplates();
16
+ const renderer = new QuestionnaireRenderer({
17
+ token: "encounter-questionnaire",
18
+ templates,
19
+ questionnaire,
20
+ fhirVersion: "r5",
21
+ questionnaireResponse: draftResponse,
22
+ action: route,
23
+ });
24
+
25
+ try {
26
+ const result =
27
+ request.method === "POST"
28
+ ? await renderer.process(await request.formData())
29
+ : { submitted: false as const };
30
+
31
+ const html = await renderer.render();
32
+ const response = renderer.getQuestionnaireResponse();
33
+
34
+ if (result.submitted && result.valid) {
35
+ await saveResponse(response);
36
+ }
37
+ } finally {
38
+ renderer.dispose();
39
+ }
40
+ ```
41
+
42
+ `renderer.render()` returns a complete questionnaire form by default. It is
43
+ async because renderer-owned ValueSet expansions can require terminology
44
+ requests before the final HTML is usable. Your
45
+ application owns the route, layout, authentication, CSRF handling, draft
46
+ persistence, and response storage. Pass the route as `action` so the renderer
47
+ can generate the form attributes.
48
+
49
+ Generated controls use normal bracket-style field names such as
50
+ `fb[answer][patient-name][value]`, plus `fb[count][...]`, `fb[action]`, and
51
+ `fb[page]` for repeat and pagination state. The core renderer passes raw
52
+ questionnaire paths into the theme layer; `@formbox/htmx` owns this submitted
53
+ field encoding and parses the submitted payload in `renderer.process()`.
54
+
55
+ ## Basic Integration
56
+
57
+ ```ts
58
+ import { QuestionnaireRenderer, loadNativeTemplates } from "@formbox/htmx";
59
+
60
+ const templates = await loadNativeTemplates();
61
+
62
+ async function renderQuestionnaire(request: Request): Promise<Response> {
63
+ const route = "/questionnaire";
64
+ const draftResponse = await loadDraftResponse();
65
+ const renderer = new QuestionnaireRenderer({
66
+ token: "encounter-questionnaire",
67
+ templates,
68
+ questionnaire,
69
+ fhirVersion: "r5",
70
+ questionnaireResponse: draftResponse,
71
+ action: route,
72
+ });
73
+
74
+ try {
75
+ const result =
76
+ request.method === "POST"
77
+ ? await renderer.process(await request.formData())
78
+ : { submitted: false as const };
79
+
80
+ const form = await renderer.render();
81
+
82
+ if (result.submitted && result.valid) {
83
+ await saveResponse(renderer.getQuestionnaireResponse());
84
+ }
85
+
86
+ return html(layout(form));
87
+ } finally {
88
+ renderer.dispose();
89
+ }
90
+ }
91
+ ```
92
+
93
+ ## Lifecycle
94
+
95
+ Create one renderer instance for one request/render cycle:
96
+
97
+ 1. `new QuestionnaireRenderer(options)` creates a request-local renderer.
98
+ 2. `await renderer.process(formData)` applies submitted form state and returns submit result.
99
+ 3. `await renderer.render()` returns HTML fields for the current renderer state.
100
+ 4. `renderer.getQuestionnaireResponse()` reads the current FHIR response.
101
+ 5. `renderer.dispose()` releases the underlying renderer store.
102
+
103
+ Do not keep a renderer instance in a session or reuse it between requests. Persist
104
+ the `QuestionnaireResponse` instead, then pass it to the next constructor call
105
+ as `questionnaireResponse`.
106
+
107
+ ## API
108
+
109
+ ### `new QuestionnaireRenderer(options)`
110
+
111
+ Creates a request-local renderer instance.
112
+
113
+ ```ts
114
+ const renderer = new QuestionnaireRenderer({
115
+ token: "encounter-questionnaire",
116
+ questionnaire,
117
+ fhirVersion: "r5",
118
+ action,
119
+ questionnaireResponse,
120
+ language,
121
+ strings,
122
+ terminologyServerUrl,
123
+ launchContext,
124
+ mode,
125
+ customExtensions,
126
+ templates,
127
+ });
128
+ ```
129
+
130
+ `questionnaireResponse` is optional initial state.
131
+ `token` is required and must be unique for each rendered form on the same page.
132
+ `templates` is required; call `await loadNativeTemplates()` for the built-in
133
+ file templates, or merge those templates with application overrides.
134
+
135
+ ### `renderer.process(formData)`
136
+
137
+ Applies a submitted form payload to the renderer.
138
+
139
+ It handles:
140
+
141
+ - scalar values and complex FHIR answer values
142
+ - repeated question and group add/remove actions
143
+ - pagination actions
144
+ - enableWhen and expression recomputation through the renderer store
145
+ - validation when the submitted action is `submit`
146
+
147
+ The generated fields include hidden state needed to preserve off-page and
148
+ hidden answers across full-form posts.
149
+
150
+ It returns a promise:
151
+
152
+ ```ts
153
+ type ProcessResult = { submitted: false } | { submitted: true; valid: boolean };
154
+ ```
155
+
156
+ `submitted` is true only for the final submit action. `valid` is present only
157
+ when submit validation ran.
158
+
159
+ ### `renderer.render()`
160
+
161
+ Returns a promise for rendered questionnaire HTML. The default Form template
162
+ includes a surrounding `<form>`, hidden state, rendered controls,
163
+ repeat/pagination action buttons, validation messages, and the default submit
164
+ button.
165
+
166
+ With the default Form template, pass `action` so the renderer can generate the
167
+ required form attributes:
168
+
169
+ ```ts
170
+ const renderer = new QuestionnaireRenderer({
171
+ token: "encounter-questionnaire",
172
+ templates,
173
+ questionnaire,
174
+ fhirVersion: "r5",
175
+ action: route,
176
+ });
177
+
178
+ const html = await renderer.render();
179
+ ```
180
+
181
+ Use a custom `Form` template when the application needs to add shell markup or
182
+ adjust attributes:
183
+
184
+ ```ts
185
+ import { compileTemplates, loadNativeTemplates } from "@formbox/htmx";
186
+
187
+ const templates = {
188
+ ...(await loadNativeTemplates()),
189
+ ...compileTemplates({
190
+ Form: `
191
+ <form{{{attrs attributes}}}>
192
+ ${csrf}
193
+ {{{fields}}}
194
+ </form>
195
+ `,
196
+ }),
197
+ };
198
+
199
+ const renderer = new QuestionnaireRenderer({
200
+ token: "encounter-questionnaire",
201
+ questionnaire,
202
+ fhirVersion: "r5",
203
+ action: route,
204
+ templates,
205
+ });
206
+
207
+ const html = await renderer.render();
208
+ ```
209
+
210
+ ### Templates
211
+
212
+ The renderer consumes callback templates:
213
+
214
+ ```ts
215
+ type Template<T> = (properties: T) => string;
216
+ ```
217
+
218
+ For simple markup customization, use Handlebars strings and compile them into
219
+ the same callback interface:
220
+
221
+ ```ts
222
+ import { compileTemplates } from "@formbox/htmx";
223
+
224
+ const templates = compileTemplates({
225
+ TextInput: `
226
+ <input
227
+ {{{fieldAttributes}}}
228
+ {{{attr "id" id}}}
229
+ {{{attr "type" type}}}
230
+ {{{attr "value" value}}}
231
+ {{#if disabled}}readonly{{/if}}
232
+ >
233
+ `,
234
+ });
235
+ ```
236
+
237
+ Callbacks and Handlebars strings can be mixed:
238
+
239
+ ```ts
240
+ const templates = compileTemplates({
241
+ TextInput: `<input{{{fieldAttributes}}}{{{attr "id" id}}}>`,
242
+ Form({ attributes, fields }) {
243
+ return `<form${htmlAttributes(attributes)}>${fields}</form>`;
244
+ },
245
+ });
246
+ ```
247
+
248
+ Load the native templates explicitly:
249
+
250
+ ```ts
251
+ import { loadNativeTemplates } from "@formbox/htmx";
252
+
253
+ const templates = await loadNativeTemplates();
254
+ ```
255
+
256
+ If you prefer overrides as files, load a directory of `*.html.hbs` files and
257
+ merge the result with the native templates:
258
+
259
+ ```ts
260
+ import { loadNativeTemplates, loadTemplates } from "@formbox/htmx";
261
+
262
+ const templates = {
263
+ ...(await loadNativeTemplates()),
264
+ ...(await loadTemplates("./questionnaire-templates")),
265
+ };
266
+ ```
267
+
268
+ File names map to template names:
269
+
270
+ ```txt
271
+ questionnaire-templates/
272
+ Form.html.hbs
273
+ TextInput.html.hbs
274
+ SelectInput.html.hbs
275
+ ```
276
+
277
+ Unknown template file names throw, so misspellings fail early.
278
+
279
+ Available Handlebars helpers:
280
+
281
+ - `{{{attr "name" value}}}` renders one escaped HTML attribute, including the leading space.
282
+ - `{{{attrs attributes}}}` renders an object of escaped HTML attributes.
283
+ - `{{{fieldAttributes}}}` renders `data-fb-link-id`, `data-fb-field`, `name`, and `hx-include` for the current field.
284
+
285
+ Use triple braces for renderer-provided HTML slots such as `fields`, `children`,
286
+ `label`, `errors`, and `customOptionForm`.
287
+
288
+ Native templates and user templates use the same data shape. The package does
289
+ not keep a separate JSX fallback path.
290
+
291
+ ### `renderer.getQuestionnaireResponse()`
292
+
293
+ Returns the current FHIR `QuestionnaireResponse`.
294
+
295
+ Disabled-by-enableWhen items are omitted from the response. Hidden-but-enabled
296
+ values and protected read-only values are preserved through generated hidden
297
+ fields where needed.
298
+
299
+ ### `renderer.dispose()`
300
+
301
+ Disposes the underlying renderer store. Call this in a `finally` block.
302
+
303
+ ## Bun Demo
304
+
305
+ Run the package demo with Bun:
306
+
307
+ ```sh
308
+ cd packages/htmx
309
+ bun run demo
310
+ ```
311
+
312
+ The demo server uses `Bun.serve`, returns an application-owned layout through
313
+ the shared render helper, loads the morphdom HTMX extension from a CDN, and
314
+ posts full form payloads back through `await renderer.process(formData)`.
315
+
316
+ ## Notes
317
+
318
+ - The server remains stateless. Recreate form state from a submitted
319
+ `QuestionnaireResponse` plus `FormData`.
320
+ - HTMX is only a browser transport. The server APIs use standard `Request`,
321
+ `FormData`, HTML strings, and FHIR resources.
322
+ - The package uses `@formbox/renderer` internally for store behavior,
323
+ expressions, enableWhen, visibility, validation, and response generation.