@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.
- package/LICENSE +21 -0
- package/README.md +323 -0
- package/dist/index.d.ts +615 -0
- package/dist/index.js +62387 -0
- package/dist/templates/AnswerList.html.hbs +17 -0
- package/dist/templates/AnswerScaffold.html.hbs +19 -0
- package/dist/templates/Checkbox.html.hbs +14 -0
- package/dist/templates/CheckboxList.html.hbs +29 -0
- package/dist/templates/CustomOptionForm.html.hbs +16 -0
- package/dist/templates/DateInput.html.hbs +14 -0
- package/dist/templates/DateTimeInput.html.hbs +18 -0
- package/dist/templates/DisplayRenderer.html.hbs +9 -0
- package/dist/templates/Errors.html.hbs +15 -0
- package/dist/templates/FileInput.html.hbs +20 -0
- package/dist/templates/Flyover.html.hbs +12 -0
- package/dist/templates/Footer.html.hbs +8 -0
- package/dist/templates/Form.html.hbs +13 -0
- package/dist/templates/GroupList.html.hbs +22 -0
- package/dist/templates/GroupScaffold.html.hbs +36 -0
- package/dist/templates/Header.html.hbs +8 -0
- package/dist/templates/Help.html.hbs +12 -0
- package/dist/templates/InputGroup.html.hbs +10 -0
- package/dist/templates/Label.html.hbs +23 -0
- package/dist/templates/LanguageSelector.html.hbs +13 -0
- package/dist/templates/Legal.html.hbs +12 -0
- package/dist/templates/Link.html.hbs +9 -0
- package/dist/templates/MultiSelectInput.html.hbs +30 -0
- package/dist/templates/NumberInput.html.hbs +16 -0
- package/dist/templates/OptionDisplay.html.hbs +10 -0
- package/dist/templates/OptionsLoading.html.hbs +10 -0
- package/dist/templates/QuestionScaffold.html.hbs +30 -0
- package/dist/templates/RadioButton.html.hbs +14 -0
- package/dist/templates/RadioButtonList.html.hbs +27 -0
- package/dist/templates/SelectInput.html.hbs +32 -0
- package/dist/templates/SignatureInput.html.hbs +12 -0
- package/dist/templates/SliderInput.html.hbs +23 -0
- package/dist/templates/SpinnerInput.html.hbs +16 -0
- package/dist/templates/Stack.html.hbs +7 -0
- package/dist/templates/TabContainer.html.hbs +27 -0
- package/dist/templates/Table.html.hbs +39 -0
- package/dist/templates/TextArea.html.hbs +13 -0
- package/dist/templates/TextInput.html.hbs +14 -0
- package/dist/templates/TimeInput.html.hbs +12 -0
- 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.
|