Embed the renderer
The right-hand preview — the part that renders a questionnaire as a fillable form, runs the logic, validates answers and shows the FHIR JSON — can be embedded in your own web page as a self-contained widget, without the builder shell around it. Host apps use it to let people fill in a questionnaire you designed here, in-place, with no iframe.
You can put several independent widgets on one page — each keeps its own answers, calculations and validation state, with no cross-talk.
Install
Pick whichever fits your project; the full guide with every option is in WIDGET.md.
- CDN (no install): load the bundle straight from jsDelivr, pinned to a release tag.
- Download: the ready-made bundle is attached to every GitHub Release.
- npm:
npm install @sergeymosyakov/questionnaire-widgetfor bundler projects. - NuGet: the
FhirQuestionnaireWidgetpackage for .NET web apps.
Quick start
The snippet below uses the files from a copy of this repository; with the CDN or npm bundle you import the widget from there instead.
<link rel="stylesheet" href="css/widget.css">
<div id="form"></div>
<script type="module">
import { QuestionnaireRenderer } from './js/renderer/index.js';
const questionnaire = await (await fetch('my-questionnaire.json')).json();
const widget = new QuestionnaireRenderer(document.getElementById('form'), {
questionnaire,
config: { previewMode: 'patient' },
});
widget.on('response-changed', qr => console.log('answers now:', qr));
</script>
That renders the questionnaire as a patient-facing form. Open the
live demo (widget-demo.html in the repository)
for a complete three-widget example.
Modes
config.previewMode picks how the form looks — the same three views as the
builder's preview panel (see Preview, Patient View & FHIR JSON):
patient— the clean, fillable form a respondent sees.preview— the design view with link IDs, prefixes and status badges.json— the liveQuestionnaireResponse/QuestionnaireJSON.
Turning features on
The widget has no menus — the host page decides what to show through
config. Everything is off unless you opt in:
| Option | What it adds |
|---|---|
search: true |
A search box that highlights matching rows (or JSON). |
validation: true |
A live PASS / FAIL badge with a dropdown listing the items that still need attention; clicking one scrolls to it. |
explain: true |
Makes calculated values and FHIRPath conditions clickable to open an Explain popup that shows why a value or a visibility rule evaluates the way it does. |
tooltips: true |
Rich hover tooltips describing each field and its FHIR mapping. |
navButton: true |
A “go to builder node” arrow on each row (only useful when a builder is present). |
fhirBaseUrl, corsProxy |
A FHIR server for reference search and server-side $populate. |
viewPrefs: { showLinkId, showPrefix, showBadges, showHiddenItems } |
Fine-grained toggles for the design view. |
language: 'fr' |
Show a translated language if the questionnaire carries translations. |
getAuthToken |
A function returning a sign-in token for your FHIR server. The widget never opens its own login window; without this, requests go out with no authorization. |
terminology: { server, corsProxy, nlmApiBase } |
This widget's own terminology server for looking up the choices of value-set questions. If omitted, the public HL7 server (tx.fhir.org) is used, so opening a form with an external value set contacts it. |
onProgress |
A function called with a message while a long operation runs, and with null when it finishes. |
Reading and setting answers
const qr = widget.getResponse(); // current answers as a QuestionnaireResponse
widget.setResponse(existingQR); // load answers back in
widget.setLanguage('es'); // switch language
widget.setConfig({ language: 'es' }); // after creation only the language can change
widget.populate('Patient/123'); // fill answers from a patient on your FHIR server
widget.structureMapPopulate('Patient/123'); // same, using a StructureMap
widget.destroy(); // remove it and free listeners
populate() and structureMapPopulate() need fhirBaseUrl; the other display
options (search, validation, tooltips and so on) are fixed when the widget is
created.
Events you can subscribe to with widget.on(name, cb): ready,
response-changed, language-changed, render, info (a populate call
succeeded) and error (a populate call failed — nothing is shown unless you
listen).
Why it stays isolated
Each widget runs on its own event channel and its own answer store, so two forms on the same page never affect each other. The host owns the surrounding UI; the widget only renders and reports back through the API and events.
Next: Validation.