Skip to main content

Renderer Overview

Installation

Prerequisites:

  • Node.js 16 or later
  • React 17 or later
  • Have @tanstack/react-query v4.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.

YourBaseRendererWrapper.tsx
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.

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().

YourBaseRendererWrapper.tsx
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.

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.

YourBaseRendererWrapperWithButton.tsx
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>
);
}
BuildFormButton.tsx
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.

YourBaseRendererWrapperWithPrePop.tsx
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 } = populateResult;

// Call to buildForm to pre-populate the QR which repaints the entire BaseRenderer view
await buildForm(questionnaire, populatedResponse);

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.

  • fetchResourceCallback is a callback function to fetch resources from your FHIR server defined as an argument to the populateQuestionnaire() function.

  • PrePopButton is a button component similar to the BuildFormButton above, but it uses a callback to handlePrepopulate() 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.

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.

Live Editor
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}
    </>
  );
}
Result
Loading...

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.

Live Editor
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}
    </>
  );
}
Result
Loading...