Renderer Overview
Installation
Prerequisites:
- Node.js 20 or later
- React 17.x or 18.x
- Have @tanstack/react-query v5.x.x installed in your React project
To install, run this command:
npm install @aehrc/smart-forms-renderer
Usage
Basic Usage
The BaseRenderer component is the main component that renders the form.
The most basic way to use it is to wrap it with a parent component which contains the useBuildForm() hook to initialise it.
import React from 'react';
import {
BaseRenderer,
RendererThemeProvider,
useBuildForm,
useRendererQueryClient
} from '@aehrc/smart-forms-renderer';
import { QueryClientProvider } from '@tanstack/react-query';
import { Questionnaire } from 'fhir/r4';
// Pass in your FHIR R4.Questionnaire
interface YourBaseRendererWrapperProps {
questionnaire: Questionnaire;
}
function YourBaseRendererWrapper(props: YourBaseRendererWrapperProps) {
const { questionnaire } = props;
// The renderer needs a query client to make API calls
const queryClient = useRendererQueryClient();
// This hook builds the form based on the questionnaire
const isBuilding = useBuildForm({ questionnaire });
if (isBuilding) {
return <div>Loading...</div>;
}
return (
// The RendererThemeProvider provides the default renderer theme based on Material UI
<RendererThemeProvider>
<QueryClientProvider client={queryClient}>
<BaseRenderer />
</QueryClientProvider>
</RendererThemeProvider>
);
}
Here's the output of the above code. Click on the "Input Questionnaire" tab to see the JSON representation of the questionnaire.
- Output
- Input Questionnaire
Now let's say you have a pre-populated or pre-filled QuestionnaireResponse resource that you want to render alongside your Questionnaire. You can modify your wrapper to pass in your QuestionnaireResponse as a prop, and subsequently into useBuildForm().
import React from 'react';
import {
BaseRenderer,
RendererThemeProvider,
useBuildForm,
useRendererQueryClient
} from '@aehrc/smart-forms-renderer';
import { QueryClientProvider } from '@tanstack/react-query';
import { Questionnaire, QuestionnaireResponse } from 'fhir/r4';
// Pass in your FHIR R4.Questionnaire
interface YourBaseRendererWrapperProps {
questionnaire: Questionnaire;
questionnaireResponse?: QuestionnaireResponse;
}
function YourBaseRendererWrapper(props: YourBaseRendererWrapperProps) {
const { questionnaire, questionnaireResponse } = props;
// The renderer needs a query client to make API calls
const queryClient = useRendererQueryClient();
// This hook builds the form based on the questionnaire
const isBuilding = useBuildForm({ questionnaire, questionnaireResponse });
if (isBuilding) {
return <div>Loading...</div>;
}
return (
// The RendererThemeProvider provides the default renderer theme based on Material UI
<RendererThemeProvider>
<QueryClientProvider client={queryClient}>
<BaseRenderer />
</QueryClientProvider>
</RendererThemeProvider>
);
}
Here's the output of the above code after adding the QuestionnaireResponse prop.
- Output
- Input Questionnaire
- Input QuestionnaireResponse
{
"resourceType": "Questionnaire",
"status": "draft",
"url": "https://smartforms.csiro.au/docs/components/boolean/basic",
"item": [
{
"linkId": "eaten",
"type": "boolean",
"repeats": false,
"text": "Have you eaten yet?"
}
]
}
{
"resourceType": "QuestionnaireResponse",
"status": "in-progress",
"item": [
{
"linkId": "eaten",
"text": "Have you eaten yet?",
"answer": [
{
"valueBoolean": true
}
]
}
]
}
Source code used in Storybook: BuildFormWrapperForStorybook.tsx
Basic Button Usage
The useBuildForm() hook used above is a wrapper around the buildForm() function.
You can actually use the buildForm() function directly via a button click or any other event.
Below shows an example of the above code modified to contain a BuildFormButton component that initialises the form on click.
import React from 'react';
import {
BaseRenderer,
RendererThemeProvider,
useBuildForm,
useRendererQueryClient
} from '@aehrc/smart-forms-renderer';
import { QueryClientProvider } from '@tanstack/react-query';
import { Questionnaire, QuestionnaireResponse } from 'fhir/r4';
import BuildFormButton from './BuildFormButton';
// Pass in your FHIR R4.Questionnaire and optional FHIR R4.QuestionnaireResponse as props
interface YourBaseRendererWrapperWithButtonProps {
questionnaire: Questionnaire;
questionnaireResponse?: QuestionnaireResponse;
}
function YourBaseRendererWrapperWithButton(props: YourBaseRendererWrapperWithButtonProps) {
const { questionnaire, questionnaireResponse } = props;
const queryClient = useRendererQueryClient();
// This time, we are passing only the questionnaire in here to demonstrate the use of the buildForm function
const isBuilding = useBuildForm({ questionnaire });
if (isBuilding) {
return <div>Loading...</div>;
}
return (
<RendererThemeProvider>
<QueryClientProvider client={queryClient}>
<>
{/* Additional button here for building the form*/}
<BuildFormButton
questionnaire={questionnaire}
questionnaireResponse={questionnaireResponse}
/>
<BaseRenderer />
</>
</QueryClientProvider>
</RendererThemeProvider>
);
}
import React from 'react';
import { buildForm } from '@aehrc/smart-forms-renderer';
import { Questionnaire, QuestionnaireResponse } from 'fhir/r4';
interface BuildFormButtonProps {
questionnaire: Questionnaire;
questionnaireResponse?: QuestionnaireResponse;
}
function BuildFormButton(props: BuildFormButtonProps) {
const { questionnaire, questionnaireResponse } = props;
async function handleBuildForm() {
await buildForm({ questionnaire, questionnaireResponse });
}
return <button onClick={handleBuildForm}>Build Form</button>;
}
export default BuildFormButton;
This example is actually a bit counterintuitive since we are deliberately not passing the questionnaireResponse to useBuildForm() hook.
It exists mainly to demonstrate how you can use the buildForm() function. In a real-world scenario, your button might be somewhere else within your application.
Below is the output of the above modified code. This time, you need to click on the "Build Form" button to see the questionnaireResponse rendered. The button will look different in the output, but it still functions the same way.
Source code used in Storybook: BuildFormButtonTesterWrapperForStorybook.tsx and BuildFormButtonForStorybook.tsx
Pre-Population Usage
You can bring the pre-population capabilities of @aehrc/sdc-populate into the mix by further modifying the above code.
import React, { useState } from 'react';
import {
BaseRenderer,
buildForm,
RendererThemeProvider,
useBuildForm,
useRendererQueryClient
} from '@aehrc/smart-forms-renderer';
import { QueryClientProvider } from '@tanstack/react-query';
import { Encounter, Patient, Practitioner, Questionnaire, QuestionnaireResponse } from 'fhir/r4';
import Client from 'fhirclient/lib/Client';
import { populateQuestionnaire } from '@aehrc/sdc-populate';
import { fetchResourceCallback } from './populateCallback';
// Pass in your FHIR R4.Questionnaire, FHIRClient, FHIR R4.Patient, optional FHIR R4.Practitioner, and optional FHIR R4.Encounter as props
interface YourBaseRendererWrapperWithPrePopProps {
questionnaire: Questionnaire;
fhirClient: Client;
patient: Patient;
user?: Practitioner;
encounter?: Encounter;
}
function YourBaseRendererWrapperWithPrePop(props: YourBaseRendererWrapperWithPrePopProps) {
const { questionnaire, fhirClient, patient, user, encounter } = props;
const [isPopulating, setIsPopulating] = useState(false);
const isBuilding = useBuildForm({ questionnaire });
const queryClient = useRendererQueryClient();
// Event handler for the pre-population
function handlePrepopulate() {
setIsPopulating(true);
populateQuestionnaire({
questionnaire: questionnaire,
fetchResourceCallback: fetchResourceCallback,
requestConfig: {
clientEndpoint: fhirClient.state.serverUrl
},
patient: patient,
user: user
}).then(async ({ populateSuccess, populateResult }) => {
if (!populateSuccess || !populateResult) {
setIsPopulating(false);
return;
}
const { populatedResponse, populatedContext } = populateResult;
// Call to buildForm to pre-populate the QR which repaints the entire BaseRenderer view
// Include additionalContext if any expressions use values from x-fhir-query variables
await buildForm({
questionnaire: questionnaire,
questionnaireResponse: populatedResponse,
additionalContext: {
...populatedContext
}
});
setIsPopulating(false);
});
}
if (isBuilding) {
return <div>Building form...</div>;
}
if (isPopulating) {
return <div>Pre-populating form...</div>;
}
return (
<RendererThemeProvider>
<QueryClientProvider client={queryClient}>
<>
<PrePopButton isPopulating={isPopulating} onPopulate={handlePrepopulate} />
<BaseRenderer />
</>
</QueryClientProvider>
</RendererThemeProvider>
);
}
You would need to further define fetchResourceCallback and PrePopButton but we will skip those for brevity.
-
fetchResourceCallbackis a callback function to fetch resources from your FHIR server defined as an argument to thepopulateQuestionnaire()function. -
PrePopButtonis a button component similar to the BuildFormButton above, but it uses a callback tohandlePrepopulate()defined in the wrapper component.
Again, this example is only for demo purposes. Your wrapper component props might be entirely different from the ones used here, or you might have your own custom pre-population logic.
If you are interested if using the @aehrc/sdc-populate library, you can refer to the API for more details.
Below is the output of the above modified code.
- Output
- Input Questionnaire
{
"resourceType": "Questionnaire",
"status": "draft",
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/variable",
"valueExpression": {
"name": "ObsBodyHeight",
"language": "application/x-fhir-query",
"expression": "Observation?code=8302-2&_count=1&_sort=-date&patient={{%patient.id}}"
}
},
{
"url": "http://hl7.org/fhir/StructureDefinition/variable",
"valueExpression": {
"name": "ObsBodyWeight",
"language": "application/x-fhir-query",
"expression": "Observation?code=29463-7&_count=1&_sort=-date&patient={{%patient.id}}"
}
},
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext",
"extension": [
{
"url": "name",
"valueCoding": {
"system": "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext",
"code": "patient"
}
},
{
"url": "type",
"valueCode": "Patient"
},
{
"url": "description",
"valueString": "The patient that is to be used to pre-populate the form"
}
]
}
],
"item": [
{
"linkId": "bmi-calculation",
"text": "BMI Calculation",
"type": "group",
"repeats": false,
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/variable",
"valueExpression": {
"name": "height",
"language": "text/fhirpath",
"expression": "item.where(linkId='patient-height').answer.value"
}
},
{
"url": "http://hl7.org/fhir/StructureDefinition/variable",
"valueExpression": {
"name": "weight",
"language": "text/fhirpath",
"expression": "item.where(linkId='patient-weight').answer.value"
}
}
],
"item": [
{
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression",
"valueExpression": {
"language": "text/fhirpath",
"expression": "%ObsBodyHeight.entry.resource.value.value"
}
},
{
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-unit",
"valueCoding": {
"system": "http://unitsofmeasure.org",
"code": "cm",
"display": "cm"
}
}
],
"linkId": "patient-height",
"text": "Height",
"type": "decimal",
"repeats": false,
"readOnly": false
},
{
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression",
"valueExpression": {
"language": "text/fhirpath",
"expression": "%ObsBodyWeight.entry.resource.value.value"
}
},
{
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-unit",
"valueCoding": {
"system": "http://unitsofmeasure.org",
"code": "kg",
"display": "kg"
}
}
],
"linkId": "patient-weight",
"text": "Weight",
"type": "decimal",
"repeats": false,
"readOnly": false
},
{
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression",
"valueExpression": {
"description": "BMI calculation",
"language": "text/fhirpath",
"expression": "(%weight/((%height/100).power(2))).round(1)"
}
},
{
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-unit",
"valueCoding": {
"system": "http://unitsofmeasure.org",
"code": "kg/m2",
"display": "kg/m2"
}
}
],
"linkId": "bmi-result",
"text": "Value",
"type": "decimal",
"repeats": false,
"readOnly": true
}
]
}
]
}
Source code used in Storybook: PrePopWrapperForStorybook.tsx, PrePopButtonForStorybook.tsx and populateCallbackForStorybook.ts
Retrieving the QuestionnaireResponse
Now that you have built and pre-populated the form, the next step is to edit the form and retrieve the filled QuestionnaireResponse resource from the renderer.
function YourBaseRendererWrapper() { const [response, setResponse] = useState<QuestionnaireResponse | null>(null); const questionnaire = qBooleanBasic; const queryClient = useRendererQueryClient(); const isBuilding = useBuildForm({ questionnaire }); if (isBuilding) { return <div>Loading...</div>; } return ( <> <QueryClientProvider client={queryClient}> <BaseRenderer /> </QueryClientProvider> <button onClick={() => { // A getResponse() function is exposed by the renderer const questionnaireResponse = getResponse(); setResponse(questionnaireResponse); }}> Retrieve QuestionnaireResponse </button> {/* Retrieved questionnaireResponse will be printed here */} {response ? ( <div style={{ border: `1px solid #EBEDF0`, borderRadius: '8px' }}> <pre style={{ fontSize: '11px' }}>{JSON.stringify(response, null, 2)}</pre> </div> ) : null} </> ); }
Click on the "Retrieve QuestionnaireResponse" button to see the output QuestionnaireResponse resource.
Alternatively, you can use store hooks exposed by the renderer to dynamically retrieve the QuestionnaireResponse resource as it updates. For more information on store hooks or stores in general, refer to the Renderer Store Hooks section.
function YourBaseRendererWrapper() { const updatableResponse = useQuestionnaireResponseStore.use.updatableResponse(); const questionnaire = qBooleanBasic; const queryClient = useRendererQueryClient(); const isBuilding = useBuildForm({ questionnaire }); if (isBuilding) { return <div>Loading...</div>; } return ( <> <QueryClientProvider client={queryClient}> <BaseRenderer /> </QueryClientProvider> {/* Dynamically retrieved questionnaireResponse will be printed here */} {updatableResponse ? ( <div style={{ border: `1px solid #EBEDF0`, borderRadius: '8px' }}> <pre style={{ fontSize: '11px' }}>{JSON.stringify(updatableResponse, null, 2)}</pre> </div> ) : null} </> ); }
Data Extraction Usage
You can extract data from the filled QuestionnaireResponse using the inAppExtract() library function from @aehrc/sdc-template-extract.
This uses a template-based approach so the source questionnaire should have the necessary extensions and templates defined for extraction.
import React, { useState } from 'react';
import {
BaseRenderer,
RendererThemeProvider,
useBuildForm,
useQuestionnaireResponseStore,
useRendererQueryClient
} from '@aehrc/smart-forms-renderer';
import { QueryClientProvider } from '@tanstack/react-query';
import { Encounter, Patient, Practitioner, Questionnaire } from 'fhir/r4';
import Client from 'fhirclient/lib/Client';
import { extractResultIsOperationOutcome, inAppExtract } from '@aehrc/sdc-template-extract';
// Pass in your FHIR R4.Questionnaire, FHIRClient, FHIR R4.Patient, optional FHIR R4.Practitioner, and optional FHIR R4.Encounter as props
interface YourBaseRendererWrapperWithExtractProps {
questionnaire: Questionnaire;
fhirClient: Client;
patient: Patient;
user?: Practitioner;
encounter?: Encounter;
}
function YourBaseRendererWrapperWithExtract(props: YourBaseRendererWrapperWithExtractProps) {
const { questionnaire, fhirClient, patient, user, encounter } = props;
const updatableResponse = useQuestionnaireResponseStore.use.updatableResponse();
const [extractedResource, setExtractedResource] = useState(null);
const isBuilding = useBuildForm({ questionnaire });
const queryClient = useRendererQueryClient();
// Event handler for extraction
async function handleExtract() {
const responseToExtract = updatableResponse;
inAppExtract(responseToExtract, questionnaire, null).then((inAppExtractOutput) => {
const extractResult = inAppExtractOutput.extractResult;
if (extractResultIsOperationOutcome(extractResult)) {
setExtractedResource(extractResult);
} else {
setExtractedResource(extractResult.extractedBundle);
}
});
}
if (isBuilding) {
return <div>Building form...</div>;
}
return (
<RendererThemeProvider>
<QueryClientProvider client={queryClient}>
<>
<ExtractButton onExtract={handleExtract} />
<BaseRenderer />
</>
</QueryClientProvider>
</RendererThemeProvider>
);
}
You would need to further define ExtractButton but we will skip those for brevity.
ExtractButtonis a button component similar to the BuildFormButton above, but it uses a callback tohandleExtract()defined in the wrapper component.
Again, this example is only for demo purposes. Your wrapper component props might be entirely different from the ones used here, or you might have your own custom extraction logic.
If you are interested if using the @aehrc/sdc-extract library, you can refer to the API for more details.
Below is the output of the above modified code.
- Output
- Template
{
...
"contained": [
{
"resourceType": "Observation",
"id": "bmi-obs",
"status": "final",
"category": [
{
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/observation-category",
"code": "vital-signs"
}
]
}
],
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "39156-5",
"display": "Body mass index"
}
]
},
"subject": {
"_reference": {
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-templateExtractValue",
"valueString": "%resource.subject.reference"
}
]
}
},
"effectiveDateTime": "1900-01-01",
"_effectiveDateTime": {
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-templateExtractValue",
"valueString": "now()"
}
]
},
"valueQuantity": {
"_value": {
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-templateExtractValue",
"valueString": "answer.value.value"
}
]
},
"unit": "kg/m2",
"system": "http://unitsofmeasure.org",
"code": "kg/m2"
}
}
],
...
}
Source code used in Storybook: ExtractWrapperForStorybook.tsx, ExtractButtonForStorybook.tsx