Modal Forms & Pop-up Dialogs - User Interactions
Modal windows in FormEngine Core allow you to display nested forms in overlay dialogs that appear above the main form content. Modals are perfect for confirmation dialogs, detailed edit forms, additional information panels, or any secondary interaction that should temporarily interrupt the user's workflow.
In this guide, you'll learn:
- How modal windows work with the
Modalcomponent andmodalTypeconfiguration - How to open and close modals using
openModalandcloseModalactions - How to pass data between the main form and modal windows
- How to use built-in modal components and create custom ones
- Practical examples for common modal use cases
How modal windows work
Modal windows in FormEngine Core are built on top of embedded forms. A modal displays a nested form inside a dialog overlay, with the following architecture:
- Modal component: The
Modalcomponent serves as a placeholder in your form definition - Modal type: The
modalTypesetting specifies which UI component renders the actual dialog (e.g.,RsModal,MuiDialog) - Modal properties: The
modalfield in the component's JSON defines properties for the dialog component - Nested form: The modal displays a form template loaded via the
getFormcallback
Configuring modal windows
Setting the modal type
The modalType property in your form JSON specifies which component library should render modal dialogs. This is typically set at the root level of your form definition:
{
"modalType": "MuiDialog",
"form": {
"key": "Screen",
"type": "Screen",
"children": [
// Form components...
]
}
}
Common modal type values include:
- "MuiDialog" for Material UI components
- "RsModal" for React Suite components
- Your custom modal component type
Defining a modal component
Use the Modal component type to create a modal placeholder in your form. The modalTemplate property specifies which form template to
load inside the modal:
{
"key": "confirmationModal",
"type": "Modal",
"props": {
"modalTemplate": {
"value": "Template:confirmation-dialog"
}
},
"modal": {
"props": {
"size": {
"value": "sm"
},
"backdrop": {
"value": true
}
},
"events": {}
}
}
Modal component properties
The modal field defines properties for the actual dialog component (specified by modalType). These properties vary depending on your
component library:
{
"key": "confirmationModal",
"type": "Modal",
"props": {
"modalTemplate": {
"value": "Template:confirmation-dialog"
}
},
"modal": {
"props": {
"size": {
"value": "md"
},
"backdrop": {
"value": true
},
"keyboard": {
"value": true
},
"enforceFocus": {
"value": true
}
}
}
}
Opening and closing modals
The openModal action
Use the openModal action to display a modal window. This action accepts several parameters:
{
"name": "openModal",
"type": "common",
"args": {
"modalKey": "confirmationModal",
"useFormData": true,
"beforeShow": {
"type": "fn",
"body": " // Optional pre-show logic\n if (e.data.requiresConfirmation === false) return false\n return {initialValue: e.data.userInput}"
},
"beforeHide": {
"type": "fn",
"body": " // Optional pre-close logic\n if (args.result === 'confirmed') {\n return {confirmedData: e.data.modalInput}\n }\n return {cancelled: true}"
}
}
}
openModal parameters
| Parameter | Type | Description |
|---|---|---|
modalKey | string | Required. The key of the Modal component to open |
useFormData | boolean | If true, passes the current form data to the modal as initial data |
beforeShow | function | Optional function executed before the modal opens. Return false to cancel opening, or return an object to pass as initial data |
beforeHide | function | Optional function executed before the modal closes. Return value is passed to the main form when modal closes |
The closeModal action
Use the closeModal action to programmatically close the currently open modal:
{
"name": "closeModal",
"type": "common",
"args": {
"result": "userConfirmed"
}
}
The result parameter is optional and can be any value that will be available to the beforeHide function and main form after closure.
Data flow between forms and modals
Passing data to modals
You can pass initial data to a modal in two ways:
- Using
useFormData: true: Passes the entire form data to the modal - Using
beforeShowfunction: Allows selective data passing or transformation
{
"events": {
"onClick": [
{
"name": "openModal",
"type": "common",
"args": {
"modalKey": "editModal",
"useFormData": true
}
}
]
}
}
{
"events": {
"onClick": [
{
"name": "openModal",
"type": "common",
"args": {
"modalKey": "editModal",
"beforeShow": {
"type": "fn",
"body": " return {userId: e.data.selectedUserId, editMode: true}"
}
}
}
]
}
}
Receiving data from modals
When a modal closes, data can be returned to the main form through:
beforeHidefunction: Processes data before returning to main form- Modal close with result: The
closeModalaction'sresultparameter
{
"events": {
"onClick": [
{
"name": "openModal",
"type": "common",
"args": {
"modalKey": "settingsModal",
"beforeHide": {
"type": "fn",
"body": " if (args.result === 'save') {\n // Update main form with modal data\n return {\n updatedSettings: e.data,\n lastUpdated: new Date().toISOString()\n }\n }\n return {cancelled: true}"
}
}
}
]
}
}
Live examples
Simple confirmation dialog
A basic modal that asks for user confirmation:
function App() { const mainForm = useMemo(() => ({ modalType: 'MuiDialog', form: { key: 'Screen', type: 'Screen', children: [ { key: 'deleteButton', type: 'MuiButton', props: { variant: {value: 'contained'}, color: {value: 'error'}, children: {value: 'Delete Item'} }, events: { onClick: [ { name: 'openModal', type: 'common', args: { modalKey: 'confirmDeleteModal', beforeHide: { type: "fn", body: 'if (args.result === "confirmed") { alert("The item has been deleted"); }' } } } ] } }, { key: 'confirmDeleteModal', type: 'Modal', props: { modalTemplate: {value: 'Template:confirmation-dialog'} }, modal: { props: { maxWidth: {value: 'sm'} } } } ] } }), []) const confirmationForm = useMemo(() => ({ form: { key: 'Screen', type: 'Screen', children: [ { key: 'message', type: 'MuiTypography', props: { children: {value: 'Are you sure you want to delete this item?'}, variant: {value: 'h6'} } }, { key: 'buttonContainer', type: 'MuiBox', css: { any: { object: { display: 'flex', justifyContent: 'flex-end', gap: '10px', marginTop: '20px' } } }, children: [ { key: 'cancelButton', type: 'MuiButton', props: { children: {value: 'Cancel'} }, events: { onClick: [ { name: 'closeModal', type: 'common', args: {result: 'cancelled'} } ] } }, { key: 'confirmButton', type: 'MuiButton', props: { variant: {value: 'contained'}, color: {value: 'error'}, children: {value: 'Delete'} }, events: { onClick: [ { name: 'closeModal', type: 'common', args: {result: 'confirmed'} } ] } } ] } ] } }), []) const getForm = useCallback((formName, options) => { if (formName === 'confirmation-dialog') { return JSON.stringify(confirmationForm) } return JSON.stringify(mainForm) }, [mainForm, confirmationForm]) return <FormViewer view={muiView} getForm={getForm}/> }
Data editing modal
A modal that allows editing form data with two-way data transfer:
function App() { const mainForm = useMemo(() => ({ modalType: 'MuiDialog', form: { key: 'Screen', type: 'Screen', children: [ { key: 'userInfo', type: 'MuiBox', css: { any: { object: { padding: '20px', border: '1px solid #e5e5e5', borderRadius: '6px', marginBottom: '20px' } } }, children: [ { key: 'userName', type: 'MuiTextField', props: { value: {value: 'John Doe'}, readOnly: {value: true}, label: {value: 'Current Name'}, helperText: { computeType: "function", fnSource: "return form.data.lastUpdated ? `Last updated: ${form.data.lastUpdated}` : 'Not modified yet'" } } } ] }, { key: 'editButton', type: 'MuiButton', props: { variant: {value: 'contained'}, children: {value: 'Edit Profile'} }, events: { onClick: [ { name: 'openModal', type: 'common', args: { modalKey: 'editProfileModal', useFormData: true, beforeHide: { type: 'fn', body: ` if (args.result === 'save') { // Return updated data to main form return { userName: e.data.userName, lastUpdated: new Date().toLocaleTimeString() } } return null` } } } ] } }, { key: 'editProfileModal', type: 'Modal', props: { modalTemplate: {value: 'Template:edit-profile'} }, modal: { props: { maxWidth: {value: 'md'}, fullWidth: {value: true} } } } ] } }), []) const editForm = useMemo(() => ({ form: { key: 'Screen', type: 'Screen', children: [ { key: 'formTitle', type: 'MuiTypography', props: { children: {value: 'Edit Profile'}, variant: {value: 'h4'} } }, { key: 'userName', type: 'MuiTextField', props: { label: {value: 'Full Name'}, placeholder: {value: 'Enter your name'} } }, { key: 'buttonContainer', type: 'MuiBox', css: { any: { object: { display: 'flex', justifyContent: 'flex-end', gap: '10px', marginTop: '30px' } } }, children: [ { key: 'cancelButton', type: 'MuiButton', props: { children: {value: 'Cancel'} }, events: { onClick: [ { name: 'closeModal', type: 'common', args: {result: 'cancel'} } ] } }, { key: 'saveButton', type: 'MuiButton', props: { variant: {value: 'contained'}, children: {value: 'Save Changes'} }, events: { onClick: [ { name: 'validate', type: 'common', args: {failOnError: true} }, { name: 'closeModal', type: 'common', args: {result: 'save'} } ] } } ] } ] } }), []) const getForm = useCallback((formName, options) => { if (formName === 'edit-profile') { return JSON.stringify(editForm) } return JSON.stringify(mainForm) }, [mainForm, editForm]) return <FormViewer view={muiView} getForm={getForm} /> }
Conditional modal with validation
A modal that only opens when certain conditions are met, with form validation:
function App() { const mainForm = useMemo(() => ({ modalType: 'MuiDialog', form: { key: 'Screen', type: 'Screen', children: [ { key: 'agreementCheckbox', type: 'MuiCheckbox', props: { label: {value: 'I agree to the terms and conditions'} } }, { key: 'viewTermsButton', type: 'MuiButton', props: { children: {value: 'View Terms and Conditions'}, }, events: { onClick: [ { name: 'openModal', type: 'common', args: { modalKey: 'termsModal', beforeShow: { type: 'fn', body: ` // Only show if checkbox is checked if (e.data.agreementCheckbox !== true) { alert('Please agree to terms first') return false } return null` } } } ] } }, { key: 'termsModal', type: 'Modal', props: { modalTemplate: {value: 'Template:terms-and-conditions'} }, modal: { props: { maxWidth: {value: 'lg'}, fullWidth: {value: true} } } } ] } }), []) const termsForm = useMemo(() => ({ form: { key: 'Screen', type: 'Screen', children: [ { key: 'termsContent', type: 'MuiBox', children: [ { key: "termsText", type: "MuiBox", children: [ { key: "muiTypography2", type: "MuiTypography", props: { children: {value: "Terms and Conditions"}, variant: {value: "h4"}, gutterBottom: {value: true} } }, { key: "muiTypography3", type: "MuiTypography", props: { children: {value: "Last Updated: January 1, 2024"}, variant: {value: "overline"}, gutterBottom: {value: true} } }, { key: "muiTypography4", type: "MuiTypography", props: { children: {value: "1. Agreement to Terms"}, variant: {value: "h5"} } }, { key: "muiTypography9", type: "MuiTypography", props: { children: {value: "By accessing or using our services, you agree to be bound by these Terms and Conditions."}, variant: {value: "body2"}, gutterBottom: {value: true} } }, { key: "muiTypography6", type: "MuiTypography", props: { children: {value: "2. User Responsibilities"}, variant: {value: "h5"} } }, { key: "muiTypography10", type: "MuiTypography", props: { children: {value: "You are responsible for maintaining the confidentiality of your account and password."}, variant: {value: "body2"}, gutterBottom: {value: true} } }, { key: "muiTypography7", type: "MuiTypography", props: { children: {value: "3. Intellectual Property"}, variant: {value: "h5"} } }, { key: "muiTypography13", type: "MuiTypography", props: { children: {value: "All content included on this site is the property of the company and protected by copyright laws."}, variant: {value: "body2"}, gutterBottom: {value: true} } }, { key: "muiTypography8", type: "MuiTypography", props: { children: {value: "4. Limitation of Liability"}, variant: {value: "h5"} } }, { key: "muiTypography11", type: "MuiTypography", props: { children: {value: "We shall not be liable for any indirect, incidental, special, consequential, or punitive damages."}, variant: {value: "body2"}, gutterBottom: {value: true} } }, { key: "muiTypography5", type: "MuiTypography", props: { children: {value: "5. Governing Law"}, variant: {value: "h5"} } }, { key: "muiTypography12", type: "MuiTypography", props: { children: {value: "These terms shall be governed by the laws of the State of California."}, variant: {value: "body2"}, gutterBottom: {value: true} } } ] } ] }, { key: 'closeButton', type: 'MuiButton', props: { variant: {value: 'contained'}, children: {value: 'Close'} }, events: { onClick: [ { name: 'closeModal', type: 'common' } ] } } ] } }), []) const getForm = useCallback((formName, options) => { if (formName === 'terms-and-conditions') { return JSON.stringify(termsForm) } return JSON.stringify(mainForm) }, [mainForm, termsForm]) return <FormViewer view={muiView} getForm={getForm}/> }
Custom modal components
FormEngine Core allows you to use custom modal components from your component library.
📄️ How to add your own modal component
Modal components in FormEngine Core are special UI elements that display content in a dialog window on top of the current form
Best practices
1. Keep modal forms focused
- Design modal forms to perform a single, focused task
- Limit the number of fields in modal forms
- Use clear, action-oriented titles (e.g., "Edit Profile", "Confirm Delete")
2. Handle modal state properly
- Always provide a way to close the modal (close button, backdrop click, Escape key)
- Consider disabling the main form while modal is open (backdrop helps with this)
- Clear modal data when closed to avoid stale data on next open
3. Use appropriate modal sizes
sm(small): Simple confirmations, alertsmd(medium): Forms with a few fieldslg(large): Complex forms, data tables, detailed informationfull(fullscreen): Maximum content, complex workflows
4. Implement proper data flow
- Use
beforeShowto validate conditions before opening - Use
beforeHideto process and validate data before closing - Return meaningful results from modals to main forms
- Consider using
useFormData: truefor simple data passing
5. Accessibility considerations
- Ensure modals are properly announced to screen readers
- Trap focus within the modal when open
- Provide clear close mechanisms
- Support keyboard navigation (Escape to close, Tab to navigate)
Troubleshooting
Modal doesn't open
- Check
modalKey: Ensure themodalKeyinopenModalaction matches the Modal component's key - Verify
modalType: ConfirmmodalTypeis set in form JSON and matches a registered component with 'modal' role - Check
beforeShow: IfbeforeShowreturnsfalse, the modal won't open
Modal data not passing correctly
useFormDatascope:useFormData: truepasses the entire form data objectbeforeShowreturn value: Data returned frombeforeShowoverridesuseFormData- Data structure: Ensure returned data matches the nested form's expected structure
Modal doesn't close
closeModalaction: VerifycloseModalaction is properly bound to a button or event- Modal context: Ensure modal has access to the close function through context
- Event handlers: Check for errors in event handlers that might prevent execution
Multiple modals interference
- Unique keys: Each Modal component must have a unique key
- Sequential opening: Avoid opening multiple modals simultaneously unless designed for it
Summary
- Modal windows display nested forms in overlay dialogs using the
Modalcomponent - The
modalTypesetting determines which UI component renders the dialog - Use
openModalandcloseModalactions to control modal visibility - Pass data between forms and modals using
useFormData,beforeShow, andbeforeHide - Follow best practices for modal design, accessibility, and data flow
For more information:
- Embedded forms - Learn about nested forms that power modals
- Actions and events - Detailed guide on action system
- Form JSON structure - Complete form definition reference
- Custom components - Guide to creating custom components