Examples
Setup
We will set up an application wrapper with all the form components mapped. We will also configure the typescript types (optional).
Entry point
import { FormComponentMappingsProvider } from 'react-flexyform'
import * as FormComponents from './your-project/form-components'
const fieldComponentMappings = {
text: FormComponents.FormIntegratedTextField,
password: FormComponents.FormIntegratedPasswordField,
select: FormComponents.FormIntegratedSelectField,
toggle: FormComponents.FormIntegratedToggleField,
date: FormComponents.FormIntegratedDateField,
nestedArray: FormComponents.FormIntegratedNestedArrayField,
}
const uiComponentMappings = {
button: FormComponents.FormIntegratedButton,
submitButton: FormComponents.FormIntegratedSubmitButton,
goToNextStepButton: FormComponents.FormIntegratedGoToNextStepButton,
goToPreviousStepButton: FormComponents.FormIntegratedGoToPreviousStepButton,
removeNestedArrayItemButton:
FormComponents.FormIntegratedRemoveNestedArrayItemButton,
stepProgress: FormComponents.FormIntegratedStepProgress,
errorMessage: FormComponents.FormIntegratedErrorMessage,
title: FormComponents.FormIntegratedTitle,
paragraph: FormComponents.FormIntegratedParagraph,
}
const wrapperComponentMappings = {
borderedSection: FormComponents.FormIntegratedBorderedSection,
}
const internalComponentMappings = {
formWrapper: FormComponents.FormWrapper,
componentWrapper: FormComponents.ComponentWrapper,
initialDataLoadingIndicator: FormComponents.InitialDataLoadingIndicator,
initialDataLoadingError: FormComponents.InitialDataLoadingError,
}
export const CodeExampleApp = ({ children }) => {
return (
<FormComponentMappingsProvider
internalComponentMappings={internalComponentMappings}
fieldComponentMappings={fieldComponentMappings}
uiComponentMappings={uiComponentMappings}
wrapperComponentMappings={wrapperComponentMappings}
>
{children}
</FormComponentMappingsProvider>
)
}
Internal components
import { Grid, MantineProvider, Modal } from '@mantine/core'
import { FormStore, useParentFormStore } from 'react-flexyform'
import { ReactNode, useMemo } from 'react'
import { MantineFormComponentsContext } from '../../mappings'
import { useTheme } from 'next-themes'
type Props = { children: ReactNode }
export const FormWrapper = (props: Props) => {
const { theme } = useTheme()
const context = useParentFormStore(
(formStore: FormStore<MantineFormComponentsContext>) => formStore.context
)
const formContent = useMemo(
() => (
<Grid
columns={12}
gutter={context.formGridGutter || 'xl'}
mx="auto"
maw="500px"
{...(context.formGridStyleProps || {})}
>
{props.children}
</Grid>
),
[context.formGridGutter, context.formGridStyleProps, props.children]
)
if (context.isFormInModal) {
return (
<MantineProvider>
<Modal opened onClose={() => context.onFormModalClose?.()}>
{formContent}
</Modal>
</MantineProvider>
)
}
return <MantineProvider>{formContent}</MantineProvider>
}
Field components
import {
useFormComponentParams,
useField,
useParentFormStore,
} from 'react-flexyform'
import { CheckIcon, Loader, TextInput, TextInputProps } from '@mantine/core'
import { useMemo } from 'react'
export type Params = TextInputProps
export const FormIntegratedTextField = () => {
const field = useField()
const params = useFormComponentParams<Params>().value
const isDisabled = useParentFormStore(
(formStore) =>
formStore.isSubmitting || formStore.isChangingStep || formStore.isSaving
)
const rightSectionIcon = useMemo(() => {
if (field.state.isValidating) {
return <Loader size="sm" />
}
if (!field.state.validationError && field.state.didPassAsyncValidation) {
return <CheckIcon size="sm" />
}
return undefined
}, [
field.state.isValidating,
field.state.validationError,
field.state.didPassAsyncValidation,
])
const fieldControls = useMemo(
() => ({
id: field.state.id,
value: field.state.value,
name: field.configuration.name,
onChange: field.methods.handleChange,
onBlur: field.methods.handleBlur,
error: field.state.validationError?.[0],
required: Boolean(field.configuration.validationRules?.required),
disabled: isDisabled,
rightSection: rightSectionIcon,
}),
[field, isDisabled, rightSectionIcon]
)
return <TextInput {...fieldControls} {...params} />
}
UI components
import { useFormComponentParams, useParentFormStore } from 'react-flexyform'
import { Button, ButtonProps } from '@mantine/core'
export type Params = ButtonProps
export const FormIntegratedSubmitButton = () => {
const params = useFormComponentParams<Params>().value
const triggerSubmit = useParentFormStore(
(formStore) => formStore.triggerSubmit
)
const isSubmitting = useParentFormStore((formStore) => formStore.isSubmitting)
const isChangingStep = useParentFormStore(
(formStore) => formStore.isChangingStep
)
const onClick = () => {
triggerSubmit()
}
return (
<Button
children="Submit"
w="100%"
{...params}
onClick={onClick}
disabled={isChangingStep}
loading={isSubmitting}
/>
)
}
Wrapper components
import { useFormComponentParams } from 'react-flexyform'
import { Grid, GridCol, GridColProps, GridProps } from '@mantine/core'
import { ReactNode } from 'react'
export type Params = { wrapperParams?: GridColProps; gridParams?: GridProps }
export const FormIntegratedBorderedSection = (props: {
children: ReactNode
}) => {
const params = useFormComponentParams<Params>().value
return (
<GridCol
span={12}
bd="1px solid gray.7"
p="lg"
style={{
borderRadius: 6,
}}
{...(params.wrapperParams || {})}
>
<Grid columns={12} gutter="xl" {...(params.gridParams || {})}>
{props.children}
</Grid>
</GridCol>
)
}
Typescript (optional)
To enable the typeSafeFieldComponent
, typeSafeUiComponent
, typeSafeWrapperComponent
(learn more here) you need to define the global types for the form component mappings:
// Import the types from your components
import {
TextFieldParams,
PasswordFieldParams,
TextareaFieldParams,
NumberFieldParams,
SelectFieldParams,
ToggleFieldParams,
DateFieldParams,
NestedArrayFieldParams,
} from '@your-project/components/types'
// ...rest of the imports
declare global {
interface FormFieldComponentMappings {
text: TextFieldParams
password: PasswordFieldParams
textarea: TextareaFieldParams
number: NumberFieldParams
select: SelectFieldParams
toggle: ToggleFieldParams
date: DateFieldParams
nestedArray: NestedArrayFieldParams
}
interface FormUiComponentMappings {
stepProgress: StepProgressParams
// Does not accept any params
errorMessage: Record<string, never>
title: TypographyParams
paragraph: TypographyParams
prose: TypographyParams
button: ButtonParams
saveButton: ButtonParams
submitButton: ButtonParams
goToNextStepButton: ButtonParams
goToPreviousStepButton: ButtonParams
// Does not accept any params
goBackConfirmationDialog: Record<string, never>
removeNestedArrayItemButton: ButtonParams
}
interface FormWrapperComponentMappings {
borderedSection: BorderedSectionParams
}
interface FormContext {
formGridGutter?: StyleProp<MantineSpacing>
formGridClassName?: string
formGridStyleProps?: MantineStyleProps
isFormInModal?: boolean
onFormModalClose?: () => void
isConfirmGoBackModalOpen?: boolean
}
interface ComponentWrapperParams {
wrapperParams?: GridColProps
}
}
Single step form
Sign up form
Let's create a simple sign-up form with multiple types of field validation rules: required, min length, max length, email, strong password, matching passwords, async validation (email field).
We can also see a simple implementation of responsive layout using a grid system: the first and last name fields will be on the same row on larger screens and separate rows on smaller ones.
- Try including the word "test" in the Email field to get a field level async validation error
- Try submitting the form with the word "test" being included the First name or Last name field to see the submission error message
shouldSubmitOnEnter
being set to true will trigger theevents.onSubmit
function when the user presses the enter key while the focus is in a field
Sign up
import { Form, useCreateFormStore } from 'react-flexyform'
import { CodeExampleWrapper } from '../ui/code-example-wrapper'
export const SignUpFormExample = () => {
const signUpFormStore = useCreateFormStore('signUpForm', (getStoreState) => ({
events: {
onSubmit: async () => {
const formValues = getStoreState().getAllFieldValues()
await signUpUser(formValues)
},
},
shouldSubmitOnEnter: true,
components: [
{
type: 'ui',
formComponentMappingKey: 'title',
componentParams: {
children: 'Sign up',
size: 'xl',
},
},
{
name: 'firstName',
type: 'field',
formComponentMappingKey: 'text',
componentParams: {
label: 'First name',
wrapperParams: {
span: {
xs: 12,
sm: 6,
},
},
},
validationRules: {
required: {
message: 'This field is required',
},
},
},
{
name: 'lastName',
type: 'field',
formComponentMappingKey: 'text',
componentParams: {
label: 'Last name',
wrapperParams: {
span: {
xs: 12,
sm: 6,
},
},
},
validationRules: {
required: {
message: 'This field is required',
},
},
},
{
name: 'email',
type: 'field',
formComponentMappingKey: 'text',
componentParams: {
label: 'Email',
},
validationRules: {
required: {
message: 'This field is required',
},
email: {
message: 'Must be a valid email',
},
customAsyncValidation: async (abortController) => {
const email = getStoreState().getFieldValue('email')
if (!email) {
return ''
}
const isEmailAlreadyTaken = await checkIfEmailIsAlreadyTaken(
email,
// Pass in abort controller to cancel the request if necessary in case of race conditions
abortController
)
if (isEmailAlreadyTaken) {
return 'This email is already taken'
}
return ''
},
},
},
{
name: 'password',
type: 'field',
formComponentMappingKey: 'password',
componentParams: {
label: 'Password',
},
validationRules: {
required: {
message: 'This field is required',
},
minLength: {
value: 8,
message: () => {
const password = getStoreState().getFieldValue(
'password'
) as string
return `Must be at least 8 characters long (current length ${password.length})`
},
},
maxLength: {
value: 20,
message: () => {
const password = getStoreState().getFieldValue(
'password'
) as string
return `Must be maximum 20 characters long (current length ${password.length})`
},
},
onlyStrongPasswordCharacters: {
message:
'Must contain at least one uppercase letter, one lowercase letter, one number, and one special character',
},
},
},
{
name: 'passwordConfirmation',
validationRules: {
required: {
message: 'This field is required',
},
matchAnotherField: {
value: 'password',
message: 'Must match the previous password',
},
},
type: 'field',
formComponentMappingKey: 'password',
componentParams: {
label: 'Confirm password',
},
},
{
type: 'ui',
formComponentMappingKey: 'errorMessage',
},
{
type: 'ui',
formComponentMappingKey: 'submitButton',
shouldShowOnlyIf: {
value: () => !getStoreState().didSubmitSuccessfully,
dependencies: () => [getStoreState().didSubmitSuccessfully],
},
},
{
type: 'ui',
formComponentMappingKey: 'paragraph',
componentParams: {
children: 'Sign up successful!',
},
shouldShowOnlyIf: {
value: () => getStoreState().didSubmitSuccessfully,
dependencies: () => [getStoreState().didSubmitSuccessfully],
},
},
],
}))
return <Form formStore={signUpFormStore} />
}
Multi step form
Booking form
The following multi-step booking form showcases some important features:
- Field validations
- Next/back buttons and step progress (save state to server only if step was modified)
- Conditional field: if checking the I want a guide, a new field will appear
- Dynamic component params: the Booking time options are fetched dynamically based on the Booking date
- Nested array field: the Other participants field allows adding a dynamic number of participants
- Confirmation modal when trying to go back if the step was modified
- Initial data loading and error handling with retry
- After initial data loading is successful, the correct step is loaded
Service
import React from 'react'
import {
CreateStoreMultiStepConfiguration,
Form,
FormStore,
useCreateFormStore,
} from 'react-flexyform'
import { format } from 'date-fns'
export type FormStoreFormFields = {
service: string
withGuide: boolean
guide?: string
bookingDate: Date
bookingTime: string
fullName: string
email: string
phone: string
otherParticipants: { fullName: string; email: string; phone: string }[]
}
export const getBookingFormStoreBase = (
getFormStore: () => FormStore<FormStoreFormFields>
): CreateStoreMultiStepConfiguration<FormStoreFormFields> => ({
events: {
onGoToNextStep: async () => {
// In case of going to the previous step, not modifying anything and going to the next step, there will be no need to send a request with the unmodified values
if (getFormStore().getIsStepDirty()) {
// Send request to save the state on the server when going to the next step
// Access field values with getFormStore().getAllFieldValues() or getFormStore().getStepFieldValues()
await saveBookingStep(
getFormStore().currentStepName,
getFormStore().getStepFieldValues()
)
}
},
},
steps: [
{
name: 'Service',
components: [
{
type: 'ui',
formComponentMappingKey: 'stepProgress',
componentParams: {
steps: [
{ label: 'Service' },
{ label: 'Date & time' },
{ label: 'Personal info' },
],
},
},
{
type: 'ui',
formComponentMappingKey: 'title',
componentParams: {
children: 'Service',
},
},
{
name: 'service',
type: 'field',
formComponentMappingKey: 'select',
componentParams: {
label: 'Choose a service for your booking',
data: [
{
value: 'mountainClimbing',
label: 'Mountain climbing',
},
{
value: 'kayaking',
label: 'Kayaking',
},
],
},
validationRules: {
required: {
message: 'Required',
},
},
},
{
name: 'withGuide',
type: 'field',
formComponentMappingKey: 'toggle',
componentParams: {
label: 'I want a guide',
},
defaultValue: false,
},
{
name: 'guide',
shouldShowOnlyIf: { withGuide: true },
type: 'field',
formComponentMappingKey: 'select',
componentParams: {
label: 'Choose a guide (optional)',
placeholder: 'Any guide is okay for me',
clearable: true,
data: [
{
value: 'johnWood',
label: 'John Wood',
},
{
value: 'christopherRock',
label: 'Christopher Rock',
},
{
value: 'annaWater',
label: 'Anna Water',
},
],
},
},
{
type: 'ui',
formComponentMappingKey: 'goToPreviousStepButton',
componentParams: {
wrapperParams: {
span: 6,
},
},
},
{
type: 'ui',
formComponentMappingKey: 'goToNextStepButton',
componentParams: {
wrapperParams: {
span: 6,
},
},
},
],
},
{
name: 'bookingDetails',
components: [
{
type: 'ui',
formComponentMappingKey: 'stepProgress',
componentParams: {
steps: [
{ label: 'Service' },
{ label: 'Date & time' },
{ label: 'Personal info' },
],
},
},
{
type: 'ui',
formComponentMappingKey: 'title',
componentParams: {
children: 'Date & time',
},
},
{
name: 'bookingDate',
type: 'field',
formComponentMappingKey: 'date',
componentParams: {
label: 'Choose a day for your booking',
},
validationRules: {
required: {
message: 'Required',
},
},
},
{
name: 'bookingTime',
type: 'field',
formComponentMappingKey: 'select',
componentParams: {
value: async () => {
const bookingDate = getFormStore().getFieldValue('bookingDate')
if (!bookingDate) {
return {
label: 'Choose a time for your booking',
description:
'Depends on the date chosen, please choose a date first',
data: [],
disabled: true,
}
}
return {
label: 'Choose a time for your booking',
description:
'Depends on the date chosen, please choose a date first',
data: await getAvailableTimeslotsForDate(bookingDate),
disabled: false,
}
},
dependencies: () => [getFormStore().getFieldValue('bookingDate')],
},
reactToChanges: {
functionToRun: () => {
getFormStore().setFieldValue('bookingTime', '')
},
dependencies: () => [getFormStore().getFieldValue('bookingDate')],
},
validationRules: {
required: {
message: 'Required',
},
},
},
{
type: 'ui',
formComponentMappingKey: 'goToPreviousStepButton',
componentParams: {
variant: 'outline',
wrapperParams: {
span: 6,
},
},
},
{
type: 'ui',
formComponentMappingKey: 'goToNextStepButton',
componentParams: {
wrapperParams: {
span: 6,
},
},
},
],
},
{
name: 'personalDetails',
shouldGoToNextStepOnEnter: true,
components: [
{
type: 'ui',
formComponentMappingKey: 'stepProgress',
componentParams: {
steps: [
{ label: 'Service' },
{ label: 'Date & time' },
{ label: 'Personal info' },
],
},
},
{
type: 'ui',
formComponentMappingKey: 'title',
componentParams: {
children: 'Personal info',
},
},
{
name: 'fullName',
type: 'field',
formComponentMappingKey: 'text',
componentParams: {
label: 'Full name',
},
validationRules: {
required: {
message: 'Required',
},
},
},
{
name: 'email',
type: 'field',
formComponentMappingKey: 'text',
componentParams: {
label: 'Email',
},
validationRules: {
required: {
message: 'Required',
},
email: {
message: 'Must be a valid email',
},
},
},
{
name: 'phone',
type: 'field',
formComponentMappingKey: 'text',
componentParams: {
label: 'Phone',
description: 'Include country code too',
},
validationRules: {
required: {
message: 'Required',
},
phoneNumber: {
message: 'Must be a valid phone number',
},
},
},
{
name: 'otherParticipants',
type: 'field',
formComponentMappingKey: 'nestedArray',
componentParams: {
label: 'Other participants (3 max)',
emptyStateText: 'No other participants added yet',
},
validationRules: {
maxLength: {
message: 'Cannot add more than 3 participants',
value: 3,
},
},
nestedArrayComponents: [
{
name: 'fullName',
type: 'field',
formComponentMappingKey: 'text',
componentParams: {
label: 'Full name',
},
validationRules: {
required: {
message: 'Required',
},
},
},
{
name: 'email',
type: 'field',
formComponentMappingKey: 'text',
componentParams: {
label: 'Email',
},
validationRules: {
required: {
message: 'Required',
},
email: {
message: 'Must be a valid email',
},
},
},
{
name: 'phone',
type: 'field',
formComponentMappingKey: 'text',
componentParams: {
label: 'Phone',
description: 'Include country code too',
},
validationRules: {
required: {
message: 'Required',
},
phoneNumber: {
message: 'Must be a valid phone number',
},
},
},
{
type: 'ui',
formComponentMappingKey: 'removeNestedArrayItemButton',
},
],
},
{
type: 'ui',
formComponentMappingKey: 'goToPreviousStepButton',
componentParams: {
wrapperParams: {
span: 6,
},
},
},
{
type: 'ui',
formComponentMappingKey: 'goToNextStepButton',
componentParams: {
wrapperParams: {
span: 6,
},
},
},
],
},
{
name: 'success',
components: [
{
type: 'ui',
formComponentMappingKey: 'stepProgress',
componentParams: {
steps: [
{ label: 'Service' },
{ label: 'Date & time' },
{ label: 'Personal info' },
],
},
},
{
type: 'ui',
formComponentMappingKey: 'title',
componentParams: {
children: 'Successful booking',
size: 'xl',
},
},
{
name: 'successMessage',
type: 'ui',
formComponentMappingKey: 'paragraph',
componentParams: {
value: () => {
const fieldValues = getFormStore().getAllFieldValues()
return {
children: `Your booking has been confirmed for ${format(fieldValues?.bookingDate, 'yyyy-MM-dd')} at ${fieldValues?.bookingTime}. We look forward to seeing you then!`,
}
},
dependencies: () => [
getFormStore().getFieldValue('bookingDate'),
getFormStore().getFieldValue('bookingTime'),
],
},
},
{
type: 'ui',
formComponentMappingKey: 'button',
componentParams: {
onClick: () => {
getFormStore().resetFormState()
},
children: 'New booking',
},
},
],
},
],
})
export const BookingFormExample = () => {
const bookingFormStore = useCreateFormStore<FormStoreFormFields>(
'bookingForm',
getBookingFormStoreBase
)
return <Form formStore={bookingFormStore} />
}
Loan application form
This example illustrates some additional features:
- Step level validation that depends on multiple fields that prevents proceeding if the applicant is not eligible (first step if score is smaller than 40)
- Conditional step (when Self-employed is selected, the Business details step will be available)
- Custom field validation that depends on the value of other fields (loan amount cannot be greater than the maximum loan amount based on the applicant's score)
- Wrapper component usage (first step bordered section)
import React from 'react'
import { useCreateFormStore, Form } from 'react-flexyform'
import { CodeExampleWrapper } from '../ui/code-example-wrapper'
type LoanApplicationFields = {
employmentStatus: string
annualIncome: number
savings: number
outstandingLoans: number
businessFoundedAt?: Date
businessType?: string
businessDescription?: string
numberOfEmployees?: number
lastYearRevenue?: number
requestedAmount: number
loanPurpose: string
loanTerm: number
}
const calculateLoanScore = (formData: Partial<LoanApplicationFields>) => {
let score = 0
if (formData.savings) {
if (formData.savings >= 200000) {
score += 50
} else if (formData.savings >= 100000) {
score += 30
} else if (formData.savings >= 50000) {
score += 20
} else if (formData.savings >= 30000) {
score += 10
} else {
score += 0
}
}
if (formData.annualIncome) {
if (formData.annualIncome >= 100000) {
score += 30
} else if (formData.annualIncome >= 75000) {
score += 25
} else if (formData.annualIncome >= 50000) {
score += 20
} else if (formData.annualIncome >= 30000) {
score += 15
} else {
score += 5
}
}
if (formData.employmentStatus) {
if (formData.employmentStatus === 'employed') {
score += 25
} else if (formData.employmentStatus === 'self-employed') {
score += 10
} else {
score += 0
}
}
if (formData.outstandingLoans !== undefined) {
if (formData.outstandingLoans === 0) {
score += 10
} else if (formData.outstandingLoans <= 1) {
score += 5
} else if (formData.outstandingLoans <= 2) {
score += 0
} else {
score += -5
}
}
const isEligible = score >= 40
const maxLoanAmount = Math.min(formData.annualIncome! * 0.8, 500000)
const interestRate = Math.max(5 + (70 - score) * 0.2, 5)
return {
score,
maxLoanAmount,
interestRate,
isEligible,
}
}
export const LoanApplicationFormExample = () => {
const loanApplicationFormStore = useCreateFormStore<LoanApplicationFields>(
'loanApplicationForm',
(getFormStore) => ({
events: {
onSubmit: async () => {
const formData = getFormStore().getAllFieldValues()
await submitLoanApplication(formData)
},
onSubmitSuccess: () => {
// Go to the success step
getFormStore().triggerGoToNextStep()
},
},
steps: [
{
name: 'financialDetails',
shouldGoToNextStepOnEnter: true,
components: [
{
type: 'ui',
formComponentMappingKey: 'stepProgress',
componentParams: {
value: () => {
const isSelfEmployed =
getFormStore().getFieldValue('employmentStatus') ===
'self-employed'
return {
steps: [
{ label: 'Financial' },
{ label: isSelfEmployed ? 'Business' : '' },
{ label: 'Loan terms' },
{ label: 'Review' },
].filter((l) => Boolean(l.label)),
}
},
dependencies: () => [
getFormStore().getFieldValue('employmentStatus'),
],
},
},
{
type: 'wrapper',
name: 'financialDetailsFieldsWrapper',
formComponentMappingKey: 'borderedSection',
wrapping: 'start',
},
{
type: 'field',
name: 'annualIncome',
formComponentMappingKey: 'number',
defaultValue: 0,
componentParams: {
label: 'Annual income',
leftSection: '$',
},
validationRules: {
required: {
message: 'Required',
},
},
},
{
type: 'field',
name: 'employmentStatus',
formComponentMappingKey: 'select',
componentParams: {
label: 'Employment status',
data: [
{ value: 'employed', label: 'Employed' },
{ value: 'self-employed', label: 'Self-employed' },
{ value: 'unemployed', label: 'Unemployed' },
],
},
validationRules: {
required: {
message: 'Required',
},
},
},
{
type: 'field',
name: 'outstandingLoans',
formComponentMappingKey: 'number',
defaultValue: 0,
componentParams: {
label: 'Number of outstanding loans',
},
validationRules: {
required: {
message: 'Required',
},
},
},
{
type: 'wrapper',
name: 'financialDetailsFieldsWrapper',
formComponentMappingKey: 'borderedSection',
wrapping: 'end',
},
{
type: 'ui',
formComponentMappingKey: 'paragraph',
componentParams: {
value: () => {
const formData = getFormStore().getAllFieldValues()
const loanScore = calculateLoanScore(formData)
return {
children: `Your current credit score is ${loanScore.score}`,
}
},
dependencies: () => [
calculateLoanScore(getFormStore().getAllFieldValues()).score,
],
},
},
{
type: 'ui',
formComponentMappingKey: 'errorMessage',
},
{
type: 'ui',
formComponentMappingKey: 'goToPreviousStepButton',
componentParams: {
wrapperParams: {
span: 6,
},
},
},
{
type: 'ui',
formComponentMappingKey: 'goToNextStepButton',
componentParams: {
wrapperParams: {
span: 6,
},
},
},
],
validate: () => {
const formData = getFormStore().getAllFieldValues()
const loanScore = calculateLoanScore(formData)
if (!loanScore.isEligible) {
return 'You need to score at least 40 points to be eligible.'
}
return ''
},
},
{
name: 'businessDetails',
shouldGoToNextStepOnEnter: true,
shouldSkip: () => {
const isSelfEmployed =
getFormStore().getFieldValue('employmentStatus') ===
'self-employed'
return !isSelfEmployed
},
components: [
{
type: 'ui',
formComponentMappingKey: 'stepProgress',
componentParams: {
value: () => {
const isSelfEmployed =
getFormStore().getFieldValue('employmentStatus') ===
'self-employed'
return {
steps: [
{ label: 'Financial' },
{ label: isSelfEmployed ? 'Business' : '' },
{ label: 'Loan terms' },
{ label: 'Review' },
].filter((l) => Boolean(l.label)),
}
},
dependencies: () => [
getFormStore().getFieldValue('employmentStatus'),
],
},
},
{
type: 'ui',
formComponentMappingKey: 'title',
componentParams: {
children: 'Business details',
},
},
{
type: 'field',
name: 'businessFoundedAt',
formComponentMappingKey: 'date',
componentParams: {
label: 'Founded at',
},
validationRules: {
required: {
message: 'Required',
},
},
},
{
type: 'field',
name: 'businessType',
formComponentMappingKey: 'select',
componentParams: {
label: 'Type',
data: [
{ value: 'LLC', label: 'LLC' },
{
value: 'sole-proprietorship',
label: 'Sole Proprietorship',
},
{ value: 'corporation', label: 'Corporation' },
{ value: 'partnership', label: 'Partnership' },
{ value: 'non-profit', label: 'Non-profit' },
{ value: 'other', label: 'Other' },
],
},
validationRules: {
required: {
message: 'Required',
},
},
},
{
type: 'field',
name: 'businessDescription',
formComponentMappingKey: 'textarea',
componentParams: {
label: 'Describe your business',
},
validationRules: {
required: {
message: 'Required',
},
},
},
{
type: 'field',
name: 'numberOfEmployees',
formComponentMappingKey: 'number',
componentParams: {
label: 'Number of employees',
defaultValue: 1,
},
validationRules: {
required: {
message: 'Required',
},
},
},
{
type: 'field',
name: 'lastYearRevenue',
formComponentMappingKey: 'number',
componentParams: {
label: "Last year's revenue",
leftSection: '$',
},
validationRules: {
required: {
message: 'Required',
},
},
},
{
type: 'ui',
formComponentMappingKey: 'errorMessage',
},
{
type: 'ui',
formComponentMappingKey: 'goToPreviousStepButton',
componentParams: {
wrapperParams: {
span: 6,
},
},
},
{
type: 'ui',
formComponentMappingKey: 'goToNextStepButton',
componentParams: {
wrapperParams: {
span: 6,
},
},
},
],
},
{
name: 'loanTerms',
shouldGoToNextStepOnEnter: true,
components: [
{
type: 'ui',
formComponentMappingKey: 'stepProgress',
componentParams: {
value: () => {
const isSelfEmployed =
getFormStore().getFieldValue('employmentStatus') ===
'self-employed'
return {
steps: [
{ label: 'Financial' },
{ label: isSelfEmployed ? 'Business' : '' },
{ label: 'Loan terms' },
{ label: 'Review' },
].filter((l) => Boolean(l.label)),
}
},
dependencies: () => [
getFormStore().getFieldValue('employmentStatus'),
],
},
},
{
type: 'field',
name: 'requestedAmount',
formComponentMappingKey: 'number',
defaultValue: 0,
componentParams: {
value: () => {
const formData = getFormStore().getAllFieldValues()
const loanScore = calculateLoanScore(formData)
return {
label: 'Requested loan amount',
leftSection: '$',
description: `Max loan amount based on your credit score: $${loanScore.maxLoanAmount.toFixed(2)}`,
}
},
dependencies: () => [
calculateLoanScore(getFormStore().getAllFieldValues())
.maxLoanAmount,
],
},
validationRules: {
required: {
message: 'Required',
priority: 1,
},
customValidation: {
validate: () => {
const formData = getFormStore().getAllFieldValues()
const loanScore = calculateLoanScore(formData)
if (formData.requestedAmount > loanScore.maxLoanAmount) {
return `The requested amount exceeds your maximum eligible amount of $${loanScore.maxLoanAmount.toFixed(2)}`
}
return ''
},
priority: 2,
},
dependencies: () => [
calculateLoanScore(getFormStore().getAllFieldValues())
.maxLoanAmount,
getFormStore().getFieldValue('requestedAmount'),
],
},
},
{
type: 'field',
name: 'loanPurpose',
formComponentMappingKey: 'textarea',
componentParams: {
label: 'Loan purpose',
},
validationRules: {
required: {
message: 'Required',
},
},
},
{
type: 'field',
name: 'loanTerm',
formComponentMappingKey: 'number',
componentParams: {
label: 'Loan term (months)',
},
validationRules: {
required: {
message: 'Required',
},
minValue: {
message: 'Loan term must be at least 6 months',
value: 6,
},
},
},
{
type: 'ui',
formComponentMappingKey: 'errorMessage',
},
{
type: 'ui',
formComponentMappingKey: 'goToPreviousStepButton',
componentParams: {
wrapperParams: {
span: 6,
},
},
},
{
type: 'ui',
formComponentMappingKey: 'goToNextStepButton',
componentParams: {
wrapperParams: {
span: 6,
},
},
},
],
},
{
name: 'review',
components: [
{
type: 'ui',
formComponentMappingKey: 'stepProgress',
componentParams: {
value: () => {
const isSelfEmployed =
getFormStore().getFieldValue('employmentStatus') ===
'self-employed'
return {
steps: [
{ label: 'Financial' },
{ label: isSelfEmployed ? 'Business' : '' },
{ label: 'Loan terms' },
{ label: 'Review' },
].filter((l) => Boolean(l.label)),
}
},
dependencies: () => [
getFormStore().getFieldValue('employmentStatus'),
],
},
},
{
type: 'ui',
formComponentMappingKey: 'title',
componentParams: {
children: 'Loan application summary',
},
},
{
type: 'ui',
formComponentMappingKey: 'prose',
componentParams: {
value: () => {
return {
children: (
<>
<b>Financial Information:</b>
<p>
Annual Income: $
{getFormStore().getFieldValue('annualIncome')}
</p>
<p>
Employment Status:{' '}
{getFormStore().getFieldValue('employmentStatus')}
</p>
<p>
Outstanding Loans:
{getFormStore().getFieldValue('outstandingLoans')}
</p>
<br />
<b>Eligibility:</b>
<p>
Credit Score:{' '}
{
calculateLoanScore(
getFormStore().getAllFieldValues()
).score
}{' '}
points
</p>
<p>
Interest Rate:{' '}
{
calculateLoanScore(
getFormStore().getAllFieldValues()
).interestRate
}
%
</p>
<p>
Max Loan Amount: $
{calculateLoanScore(
getFormStore().getAllFieldValues()
).maxLoanAmount.toFixed(2)}
</p>
<br />
<b>Loan Details:</b>
<p>
Requested Amount: $
{getFormStore().getFieldValue('requestedAmount')}
</p>
<p>
Purpose: {getFormStore().getFieldValue('loanPurpose')}
</p>
<p>
Term: {getFormStore().getFieldValue('loanTerm')}{' '}
months
</p>
</>
),
}
},
},
},
{
type: 'ui',
formComponentMappingKey: 'errorMessage',
},
{
type: 'ui',
formComponentMappingKey: 'goToPreviousStepButton',
componentParams: {
wrapperParams: {
span: 6,
},
},
},
{
type: 'ui',
formComponentMappingKey: 'submitButton',
componentParams: {
wrapperParams: {
span: 6,
},
},
},
],
},
{
name: 'success',
components: [
{
type: 'ui',
formComponentMappingKey: 'stepProgress',
componentParams: {
value: () => {
const isSelfEmployed =
getFormStore().getFieldValue('employmentStatus') ===
'self-employed'
return {
steps: [
{ label: 'Financial' },
{ label: isSelfEmployed ? 'Business' : '' },
{ label: 'Loan terms' },
{ label: 'Review' },
].filter((l) => Boolean(l.label)),
}
},
dependencies: () => [
getFormStore().getFieldValue('employmentStatus'),
],
},
},
{
type: 'ui',
formComponentMappingKey: 'title',
componentParams: {
children: 'Success',
},
},
{
type: 'ui',
formComponentMappingKey: 'paragraph',
componentParams: {
children:
'Your loan application has been submitted successfully. We will contact you shortly.',
},
},
],
},
],
})
)
return <Form formStore={loanApplicationFormStore} />
}
Auto save
Edit profile form
The following edit profile form showcases the auto-save feature:
- The form will be saved after second passes after the last change made and there are no validation errors
- A loading indicator will be shown while saving
- If the First name or Last name field contains the word "test", an error message will be shown
- In the Form rendered in modal tab we can see how the form can be rendered in a modal
- How the useCreateFormStore hook's
resetStoreOnMount
option can be used to keep the form state even if the component is unmounted and mounted again - In the Save instead of auto save tab we can see how the triggerSave event resets the isDirty state and we can use it to disable the button for optimal user experience
Edit profile
import { Form, useCreateFormStore } from 'react-flexyform'
import { CodeExampleWrapper } from '../ui/code-example-wrapper'
export const EditProfileFormExample = () => {
const editProfileFormStore = useCreateFormStore(
'editProfileForm',
(getStoreState) => ({
initialData: {
firstName: 'John',
lastName: 'Smith',
email: 'john.smith@gmail.com',
},
autoSaveOptions: {
enabled: true,
autoSaveOn: ['fieldValueChange'],
autoSaveDebounceDurationInMs: 1000,
},
validationOptions: {
validateFieldsOn: ['fieldValueChange'],
},
events: {
onSave: async (abortController) => {
// Pass in abort controller to cancel the request if necessary in case of race conditions
await saveProfile(getFormStore.getAllFieldValues(), abortController)
},
},
components: [
{
type: 'ui',
formComponentMappingKey: 'title',
componentParams: {
children: 'Edit profile',
size: 'xl',
},
},
{
name: 'firstName',
type: 'field',
formComponentMappingKey: 'text',
componentParams: {
label: 'First name',
wrapperParams: {
span: {
xs: 12,
sm: 6,
},
},
},
validationRules: {
required: {
message: 'This field is required',
},
},
},
{
name: 'lastName',
type: 'field',
formComponentMappingKey: 'text',
componentParams: {
label: 'Last name',
wrapperParams: {
span: {
xs: 12,
sm: 6,
},
},
},
validationRules: {
required: {
message: 'This field is required',
},
},
},
{
name: 'email',
type: 'field',
formComponentMappingKey: 'text',
componentParams: {
label: 'Email',
disabled: true,
},
},
{
name: 'github',
type: 'field',
formComponentMappingKey: 'text',
componentParams: {
label: 'Github profile',
},
validationRules: {
url: {
message: 'Must be a valid URL',
},
},
},
{
name: 'bio',
type: 'field',
formComponentMappingKey: 'textarea',
componentParams: {
label: 'Bio',
},
validationRules: {
maxLength: {
value: 100,
message: () =>
`Max 100 characters allowed (current: ${getStoreState().getFieldValue('bio')?.length || 0})`,
},
},
},
{
type: 'ui',
formComponentMappingKey: 'errorMessage',
},
],
})
)
return <Form formStore={editProfileFormStore} />
}