Using forms in applications
Once you’ve built a form using the Form Builder or converted an existing HTML Form Entry form, you’ll need to integrate it into your application. The React Form Engine provides flexible ways to render forms in different contexts, from patient chart workspaces to custom applications.
Overview
The React Form Engine can be used in two main ways:
- Form Renderer Extension - A pre-built component that handles form loading, rendering, and workspace integration. This is the easiest way to render forms in workspaces.
- FormEngine Component - The core component that provides full control over form rendering, submission, and behavior.
Rendering forms in workspaces
The patient chart already provides a form entry workspace through @openmrs/esm-patient-forms-app. For most patient-chart workflows, launch patient-form-entry-workspace with a form object and optional encounter details instead of registering your own workspace:
import { launchWorkspace2 } from '@openmrs/esm-framework';
import { type Form } from '@openmrs/esm-patient-common-lib';
function launchMyForm(form: Form, encounterUuid?: string) {
launchWorkspace2('patient-form-entry-workspace', {
form,
encounterUuid,
additionalProps: {
mode: encounterUuid ? 'edit' : 'enter',
},
});
}patient-form-entry-workspace reads patient, patientUuid, visitContext, and mutateVisitContext from the patient chart workspace group. Launch it from inside the patient chart, or build your own workspace wrapper if your form needs to run outside that context.
The workspace wraps the form in Workspace2, renders the visit-context-header-slot, and then renders either an HTML Form Entry iframe or the React Form Engine through the form-widget-slot.
Creating a custom form workspace
The react-form-engine-widget extension is registered by @openmrs/esm-form-engine-app in the form-widget-slot. You only need a custom workspace when you want a different workspace shell, custom data refresh behavior, or a non-patient-chart rendering context.
If you do create your own workspace for the patient chart, register it with Workspace v2 routes and put its window in the patient-chart group:
{
"workspaces2": [
{
"name": "my-form-workspace",
"component": "myFormWorkspace",
"window": "my-form-window"
}
],
"workspaceWindows2": [
{
"name": "my-form-window",
"group": "patient-chart",
"width": "extra-wide",
"canMaximize": true
}
]
}Then render form-widget-slot from a Workspace2 shell and pass the state expected by the form renderer:
import React, { useState } from 'react';
import { ExtensionSlot, Workspace2 } from '@openmrs/esm-framework';
import { useSWRConfig } from 'swr';
import {
invalidateVisitAndEncounterData,
type PatientWorkspace2DefinitionProps,
type FormRendererProps,
} from '@openmrs/esm-patient-common-lib';
interface MyFormWorkspaceProps {
formUuid: string;
encounterUuid?: string;
}
export function MyFormWorkspace({
closeWorkspace,
workspaceProps: { formUuid, encounterUuid },
groupProps: { patient, patientUuid, visitContext, mutateVisitContext },
}: PatientWorkspace2DefinitionProps<MyFormWorkspaceProps, object>) {
const { mutate: globalMutate } = useSWRConfig();
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const state: FormRendererProps = {
formUuid,
patient,
patientUuid,
encounterUuid: encounterUuid ?? '',
visit: visitContext,
visitUuid: visitContext?.uuid,
closeWorkspace,
closeWorkspaceWithSavedChanges: () => {
mutateVisitContext?.();
invalidateVisitAndEncounterData(globalMutate, patientUuid);
return closeWorkspace({ discardUnsavedChanges: true });
},
setHasUnsavedChanges,
additionalProps: {
mode: encounterUuid ? 'edit' : 'enter',
},
};
return (
<Workspace2 title="My form" hasUnsavedChanges={hasUnsavedChanges}>
<ExtensionSlot name="form-widget-slot" state={state} />
</Workspace2>
);
}closeWorkspaceWithSavedChanges is part of the FormRendererProps state passed to form-widget-slot. Use it for post-save refresh work before closing the workspace. Workspace v2’s closeWorkspace still handles ordinary cancel or close behavior.
Launch the custom workspace with Workspace v2:
import { launchWorkspace2 } from '@openmrs/esm-framework';
function launchMyCustomWorkspace(formUuid: string) {
launchWorkspace2('my-form-workspace', {
formUuid,
});
}This launch example assumes it runs from a patient-chart extension or page, so the patient-chart group props are available to the workspace.
The form renderer extension automatically handles form schema loading, error states, and workspace lifecycle management. It’s the recommended approach for most use cases.
Using FormEngine directly
For more control over form rendering and behavior, you can use the FormEngine component directly from @openmrs/esm-form-engine-lib. This is useful when you need custom submission handling, want to render forms outside of workspaces, or need fine-grained control over form behavior.
Basic usage
import React from 'react';
import { type Encounter } from '@openmrs/esm-framework';
import { FormEngine, type FormSchema } from '@openmrs/esm-form-engine-lib';
interface MyFormComponentProps {
patientUuid: string;
formSchema: FormSchema;
}
export function MyFormComponent({ patientUuid, formSchema }: MyFormComponentProps) {
const handleSubmit = (encounters: Array<Encounter>) => {
console.log('Form submitted:', encounters);
// Handle form submission
};
return (
<FormEngine
patientUUID={patientUuid}
formJson={formSchema}
onSubmit={handleSubmit}
/>
);
}Loading form schema from UUID
If you have a form UUID instead of the schema, simply pass it to the formUUID prop. The FormEngine will automatically load the schema:
import React from 'react';
import { FormEngine } from '@openmrs/esm-form-engine-lib';
interface MyFormComponentProps {
patientUuid: string;
formUuid: string;
}
export function MyFormComponent({ patientUuid, formUuid }: MyFormComponentProps) {
return (
<FormEngine
patientUUID={patientUuid}
formUUID={formUuid}
/>
);
}The FormEngine handles loading states and errors internally. You don’t need to manually fetch the form schema unless you have a specific use case that requires it.
Form props and configuration
The FormEngine component accepts the following props:
Required props
patientUUID(string) - The UUID of the patient for whom the form is being filled
Optional props
formUUID(string) - The UUID of the form to load from the server. If provided, the form schema will be loaded automatically. Cannot be provided together withformJson- you must use eitherformUUIDorformJson, not both.formJson(FormSchema) - The form schema object. Use this when you already have the schema loaded or want to use a custom schema.encounterUUID(string) - The UUID of an existing encounter to edit. When provided, the form will be pre-filled with encounter data and rendered in edit mode.visit(Visit) - The visit object to associate with a submitted encounter. The patient chart form workspace supplies the active visit for new forms, or the encounter’s own visit when editing an existing encounter.mode('enter' | 'edit' | 'view' | 'embedded-view') - The rendering mode for the form. See Form rendering modes below.formSessionIntent(string) - The form intent used while refining the schema. It applies intent-specific defaults and behaviours such asavailableIntents.defaultPage, fieldreadonly, fieldhide, fielddefaultValue, and subform intent selection. The form renderer extension passes'*'by default; when usingFormEnginedirectly, pass'*'explicitly if you want wildcard intent behavior.onSubmit(function) - Callback function called when the form is successfully submitted. Receives an array of processor results. For forms that useEncounterFormProcessor, those results are created or updated encounters.onCancel(function) - Callback function called when the user cancels the form.handleClose(function) - Callback function called when the form should be closed.handleConfirmQuestionDeletion(function) - Custom handler for confirming deletion of repeatable questions. Should return a Promise that resolves when deletion is confirmed.markFormAsDirty(function) - Callback function called when the form’s dirty state changes. Useful for workspace integration to prompt before closing.hideControls(boolean) - Hide form action buttons (Save, Cancel). Useful for embedded views or custom UI.hidePatientBanner(boolean) - Hide the patient banner that appears in ultra-wide workspaces.preFilledQuestions(Record<string, string | number | Date | boolean | Array<string>>) - Pre-fill specific questions with values. Keys are question IDs, values are the pre-filled values.
Common prop combinations
Basic form with UUID:
<FormEngine
patientUUID={patientUuid}
formUUID={formUuid}
/>Editing an existing encounter:
<FormEngine
patientUUID={patientUuid}
formUUID={formUuid}
encounterUUID={encounterUuid}
mode="edit"
/>Form with workspace integration:
import React, { useState } from 'react';
import { Workspace2, type Workspace2DefinitionProps } from '@openmrs/esm-framework';
import { FormEngine } from '@openmrs/esm-form-engine-lib';
function MyDirectFormWorkspace({
closeWorkspace,
workspaceProps: { patientUuid, formUuid },
}: Workspace2DefinitionProps<{ patientUuid: string; formUuid: string }>) {
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
return (
<Workspace2 title="My form" hasUnsavedChanges={hasUnsavedChanges}>
<FormEngine
patientUUID={patientUuid}
formUUID={formUuid}
onSubmit={() => closeWorkspace({ discardUnsavedChanges: true })}
handleClose={() => closeWorkspace()}
markFormAsDirty={setHasUnsavedChanges}
/>
</Workspace2>
);
}Form rendering modes
The React Form Engine supports four rendering modes, each suited for different use cases:
Enter mode (default)
Mode: 'enter' or undefined
Use case: Creating new form entries
Behavior:
- Form is rendered in read-write mode
- All fields are editable
- Form can be submitted to create a new encounter
- Automatically selected when no
encounterUUIDis provided andmodeis not specified
<FormEngine
patientUUID={patientUuid}
formUUID={formUuid}
mode="enter" // or omit for default
/>Edit mode
Mode: 'edit'
Use case: Editing existing form entries
Behavior:
- Form is rendered in read-write mode
- Form is pre-filled with data from the existing encounter
- Form can be submitted to update the encounter
- Automatically selected when
encounterUUIDis provided andmodeis not specified
<FormEngine
patientUUID={patientUuid}
formUUID={formUuid}
encounterUUID={encounterUuid}
mode="edit" // or omit when encounterUUID is provided
/>View mode
Mode: 'view'
Use case: Displaying form data in read-only mode
Behavior:
- Form is rendered in read-only mode
- All fields are disabled
- Form cannot be submitted
- Save is disabled or hidden, and the close/cancel control closes the form
<FormEngine
patientUUID={patientUuid}
formUUID={formUuid}
encounterUUID={encounterUuid}
mode="view"
/>Embedded view mode
Mode: 'embedded-view'
Use case: Displaying form data in a condensed widget or card
Behavior:
- Form is rendered in read-only mode
- Sidebar navigation and bottom action buttons are hidden
- Patient banner is hidden
- Empty transient fields, and empty unanswered fields configured to be hidden in read-only forms, are omitted
- Compact layout suitable for embedding in other components
<FormEngine
patientUUID={patientUuid}
formUUID={formUuid}
encounterUUID={encounterUuid}
mode="embedded-view"
hideControls={true}
/>Form submission and response handling
When a form is submitted, the onSubmit callback receives an array of processor results. For forms that use EncounterFormProcessor, each result is an encounter that was created or updated.
Handling form submission
import { type Encounter } from '@openmrs/esm-framework';
function MyFormWorkspace({ patientUuid, formUuid }: Props) {
const handleSubmit = (encounters: Array<Encounter>) => {
const encounter = encounters[0];
// Handle successful submission
console.log('Encounter created:', encounter.uuid);
console.log('Observations:', encounter.obs);
// Close workspace or navigate
closeWorkspace({ discardUnsavedChanges: true });
// Refresh related encounter, visit, or workspace data here.
};
return (
<FormEngine
patientUUID={patientUuid}
formUUID={formUuid}
onSubmit={handleSubmit}
/>
);
}Error handling
The Form Engine handles validation errors automatically and displays them inline. The onSubmit callback is only called after successful form submission. If submission fails, the Form Engine handles the error internally and displays it to the user - your onSubmit callback will not be called.
import { type Encounter } from '@openmrs/esm-framework';
function MyFormWorkspace({ patientUuid, formUuid }: Props) {
const handleSubmit = (encounters: Array<Encounter>) => {
// This callback is only called if submission succeeds
// The Form Engine handles errors internally
console.log('Form submitted successfully:', encounters);
closeWorkspace({ discardUnsavedChanges: true });
};
return (
<FormEngine
patientUUID={patientUuid}
formUUID={formUuid}
onSubmit={handleSubmit}
/>
);
}Complete example: Form workspace
Here’s a complete example of a custom form workspace that integrates with the patient chart:
import React, { useMemo, useState } from 'react';
import { ExtensionSlot, Workspace2 } from '@openmrs/esm-framework';
import { useSWRConfig } from 'swr';
import {
invalidateVisitAndEncounterData,
type FormRendererProps,
type PatientWorkspace2DefinitionProps,
} from '@openmrs/esm-patient-common-lib';
interface MyFormWorkspaceProps {
formUuid: string;
encounterUuid?: string;
}
export function MyFormWorkspace({
closeWorkspace,
workspaceProps: { formUuid, encounterUuid },
groupProps: { patient, patientUuid, visitContext, mutateVisitContext },
}: PatientWorkspace2DefinitionProps<MyFormWorkspaceProps, object>) {
const { mutate: globalMutate } = useSWRConfig();
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const state = useMemo(
(): FormRendererProps => ({
formUuid,
patient,
patientUuid,
visit: visitContext,
visitUuid: visitContext?.uuid,
encounterUuid: encounterUuid ?? '',
closeWorkspace,
closeWorkspaceWithSavedChanges: () => {
mutateVisitContext?.();
invalidateVisitAndEncounterData(globalMutate, patientUuid);
return closeWorkspace({ discardUnsavedChanges: true });
},
setHasUnsavedChanges,
additionalProps: {
mode: encounterUuid ? 'edit' : 'enter',
},
}),
[
formUuid,
globalMutate,
patient,
patientUuid,
visitContext,
encounterUuid,
closeWorkspace,
mutateVisitContext,
setHasUnsavedChanges,
]
);
return (
<Workspace2 title="My form" hasUnsavedChanges={hasUnsavedChanges}>
<ExtensionSlot name="form-widget-slot" state={state} />
</Workspace2>
);
}Pre-filling form questions
You can pre-fill specific questions in a form using the preFilledQuestions prop:
<FormEngine
patientUUID={patientUuid}
formUUID={formUuid}
preFilledQuestions={{
'vitals-weight': 70,
'vitals-height': 175,
'chief-complaint': 'Patient reports headache'
}}
/>The keys in preFilledQuestions should match the id property of the questions in your form schema. When you pass values through the patient chart form renderer extension, keep them as strings because its shared FormRendererProps type currently narrows preFilledQuestions to Record<string, string>.
Form session intent
The formSessionIntent prop selects the intent-specific behaviours defined in the form schema. Use it when a schema has different defaults, readonly or hidden fields, default pages, or subform behaviours for different workflows. The patient chart form renderer extension defaults this to '*', but direct FormEngine usage leaves it unset unless you pass a value:
<FormEngine
patientUUID={patientUuid}
formUUID={formUuid}
formSessionIntent="VITALS" // Apply schema behaviours for the VITALS intent
/>Next steps
- Learn about building forms with the Form Builder
- Read the React Form Engine documentation for advanced features
- Check out the workspace documentation for more on creating workspaces