React FlexyForm

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 the events.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} />
}

On this page