Various reusable React components with Formik and MUI

by admin
man typing on laptop

Recently we have built a large application in React, using Formik for the Forms and MUI for the interface. For that, we created various components (Checkbox, Text input, Date picker, Time picker, Select, Code editor component).

Below you’ll find the complete components! Hope it helps someone.

All components include showing a title above the field, and an error display component.

Formik Checkbox input component

import React from 'react'

import { FormikContextType, useFormikContext } from 'formik'
import { Checkbox, FormControlLabel } from '@mui/material'

import * as _ from 'lodash'

interface AppCheckboxInputProps {
  name: string
  label?: string
  required?: boolean
  [x: string]: any
}

const AppCheckboxInput: React.FC<AppCheckboxInputProps> = ({ name, label, required = false, ...otherProps }) => {
  const { setFieldTouched, setFieldValue, values }: FormikContextType<any> = useFormikContext()

  return (
    <FormControlLabel
      control={
        <Checkbox
          id={name}
          required={required}
          checked={_.get(values, name) || false}
          onBlur={() => setFieldTouched(name)}
          onChange={event => setFieldValue(name, event.target.checked)}
          {...otherProps}
        />
      }
      label={label}
    />
  )
}

export default AppCheckboxInput

Formik Datepicker input

This component uses Luxon for date handling.

import React, { ReactNode } from 'react'
import { FormikContextType, useFormikContext } from 'formik'

import DatePickerWrapper from 'src/@core/styles/libs/react-datepicker'

import nl from 'date-fns/locale/nl'

import DatePicker from 'react-datepicker'
import { DateTime } from 'luxon'

// ** Custom Component Imports
import CustomTextField from 'src/@core/components/mui/text-field'

interface AppDateInputProps {
  name: string
  label: string
  required?: boolean
  [x: string]: any
}

const AppDateInput: React.FC<AppDateInputProps> = ({ name, label, required = false, ...otherProps }) => {
  const { setFieldTouched, setFieldValue, values, errors, touched }: FormikContextType<any> = useFormikContext()

  const onChange = (event: any) => {
    setFieldValue(name, event)
  }

  const setInputValue = (value: any) => {
    if (value && value instanceof Date) {
      return value
    } else if (value) {
      return DateTime.fromISO(value).toJSDate()
    }
  }

  return (
    <div>
      <DatePicker
        selected={setInputValue(values[name]) as any}
        popperPlacement='bottom'
        locale={nl}
        dateFormat={'dd-MM-yyyy'}
        onChange={event => onChange(event)}
        placeholderText='Click to select a date'
        {...otherProps}
        customInput={
          <CustomTextField
            fullWidth={true}
            label={label || ''}
            onBlur={() => setFieldTouched(name)}
            error={(touched[name] && errors[name]) as boolean}
            helperText={touched[name] && (errors[name] as ReactNode)}
            required={required}
          />
        }
      />
    </div>
  )
}

export default AppDateInput

Formik Dropzone input

// ** Third Party Components
import { useDropzone } from 'react-dropzone'

// ** Styles
import { FormikContextType, useFormikContext } from 'formik'
import { useState } from 'react'
import { Box, Typography, Stack } from '@mui/material'
import Icon from 'src/@core/components/icon'

interface AppDropzoneInputProps {
  name: string
  callback?: (e: string) => void
}

const AppDropzoneInput: React.FC<AppDropzoneInputProps> = ({ name, callback }) => {
  const { getRootProps, getInputProps } = useDropzone({
    onDrop: files => {
      setFieldValue(name, files[0])
      setFiles(files)
      setFieldTouched(name)

      if (callback) callback(files[0].name)
    }
  })

  const [files, setFiles] = useState<File[]>([])

  const { setFieldValue, setFieldTouched }: FormikContextType<any> = useFormikContext()

  return (
    <Box {...getRootProps({ className: 'dropzone' })}>
      <input {...getInputProps()} />
      <Box
        sx={{
          border: '2px dashed rgba(47, 43, 61, 0.16)',
          display: 'flex',
          textAlign: 'center',
          alignItems: 'center',
          flexDirection: 'column'
        }}
      >
        {!files.length && (
          <>
            <Box
              sx={{
                mb: 8.75,
                width: 48,
                height: 48,
                display: 'flex',
                borderRadius: 1,
                alignItems: 'center',
                justifyContent: 'center'
              }}
            >
              <Icon icon='tabler:upload' fontSize='1.75rem' />
            </Box>
            <Typography variant='h4' sx={{ mb: 2.5 }}>
              Zet de bestanden hier neer of klik om te bladeren
            </Typography>
          </>
        )}

        {files.length > 0 && (
          <Box sx={{ marginTop: 10, marginBottom: 10 }}>
            <Typography variant='h6' sx={{ mb: 2.5 }}>
              Geselecteerd bestand (klik om een andere te selecteren):
            </Typography>
            <Stack direction='row' spacing={2}>
              <Icon icon='tabler:file-check' fontSize='1.25rem' />
              <Typography variant='h6' sx={{ color: 'text.secondary' }}>
                {files[0]?.name}
              </Typography>
            </Stack>
          </Box>
        )}
      </Box>
    </Box>
  )
}

export default AppDropzoneInput

Formik Text input

import { FormikContextType, useFormikContext } from 'formik'
import { ReactNode } from 'react'
import CustomTextField from 'src/@core/components/mui/text-field'

import * as _ from 'lodash'

interface AppTextInputProps {
  name: string
  label?: string
  required?: boolean
  disabled?: boolean
  multiline?: boolean
  maxRows?: number
  callback?: (value: string) => void
  [x: string]: any
}

const AppTextInput: React.FC<AppTextInputProps> = ({
  name,
  label,
  required = false,
  disabled = false,
  multiline,
  maxRows,
  callback,
  ...otherProps
}) => {
  const { setFieldTouched, setFieldValue, values, errors, touched }: FormikContextType<any> = useFormikContext()

  const onChange = (event: any) => {
    const inputType = event.target.type

    if (inputType === 'number') {
      const targetValue = event.target.value.replace(',', '.')
      setFieldValue(name, targetValue)
    } else {
      setFieldValue(name, event.target.value)
    }

    if (callback) {
      callback(event.target.value)
    }
  }

  return (
    <div>
      <CustomTextField
        fullWidth
        disabled={disabled}
        label={label}
        maxRows={maxRows}
        multiline={multiline}
        error={(_.get(touched, name) && _.get(errors, name)) as boolean}
        required={required}
        value={_.get(values, name)}
        onBlur={() => setFieldTouched(name, true)}
        onChange={event => onChange(event)}
        helperText={_.get(touched, name) && (_.get(errors, name) as ReactNode)}
        {...otherProps}
      />
    </div>
  )
}

export default AppTextInput

Formik Selectable tree component

This component uses the react-checkbox-tree component (Github), and Fontawesome for the icons.

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { FormikContextType, useFormikContext } from 'formik'
import React, { useState } from 'react'
import { Typography } from '@mui/material'
import CheckboxTree, { Node } from 'react-checkbox-tree'

import * as _ from 'lodash'

interface AppTreeFormInputProps {
  name: string
  treeData: Node[]
  checkStrictly?: boolean
  disabled?: boolean
  title: string
}

const AppTreeFormInput: React.FC<AppTreeFormInputProps> = ({
  name,
  treeData,
  checkStrictly,
  disabled,
  title,
  ...props
}) => {
  const { setFieldValue, values }: FormikContextType<any> = useFormikContext()
  const [expanded, setExpanded] = useState<string[]>([])

  const onCheck = (checkedKeys: any) => {
    if (checkStrictly) {
      setFieldValue(name, checkedKeys.checked as string[])
    } else {
      setFieldValue(name, checkedKeys as string[])
    }
  }

  return (
    <>
      <Typography variant='h4' sx={{ mb: 1 }}>
        {title}
      </Typography>
      <CheckboxTree
        nodes={treeData}
        checked={_.get(values, name)}
        onCheck={checked => onCheck(checked)}
        onExpand={expanded => setExpanded(expanded)}
        expanded={expanded}
        icons={{
          check: <FontAwesomeIcon className='rct-icon rct-icon-check' icon={['far', 'square-check']} />,
          uncheck: <FontAwesomeIcon className='rct-icon rct-icon-uncheck' icon={['far', 'square']} />,
          halfCheck: <FontAwesomeIcon className='rct-icon rct-icon-half-check' icon='check-square' />,
          expandOpen: <FontAwesomeIcon className='rct-icon rct-icon-expand-open' icon='chevron-down' fontSize={15} />,
          expandAll: <FontAwesomeIcon className='rct-icon rct-icon-expand-all' icon='plus-square' fontSize={15} />,
          collapseAll: <FontAwesomeIcon className='rct-icon rct-icon-collapse-all' icon='minus-square' />,
          parentClose: <FontAwesomeIcon className='rct-icon rct-icon-parent-close' icon='folder' />,
          parentOpen: <FontAwesomeIcon className='rct-icon rct-icon-parent-open' icon='folder-open' />,
          leaf: <FontAwesomeIcon className='rct-icon rct-icon-leaf-close' icon='file' />,
          expandClose: <FontAwesomeIcon className='rct-icon rct-icon-expand-close' icon='chevron-right' />
        }}
      />
    </>
  )
}

export default AppTreeFormInput

How to use the components?

All components can be used inside the Formik content, so for example:

<Formik
            enableReinitialize={true}
            initialValues={
              {
                name: data?.data ? data?.data?.name : '',
              } as AddOrUpdateUserInput
            }
            onSubmit={handleSubmit}
            validationSchema={validationSchema}
          >
            {({ handleSubmit }: FormikProps<AddOrUpdateUserInput>) => (
              <Form onSubmit={handleSubmit}>
                <Grid container spacing={4}>
                  <Grid item xs={6}>
                    <AppTextInput name='name' label='Firstname' required />
                  </Grid>
                </Grid>
              </Form>
            )}
          </Formik>

Related Posts

Leave a Comment