onebeyond/react-form-builder

View on GitHub
src/builder.js

Summary

Maintainability
F
4 days
Test Coverage
F
50%
/** @jsxRuntime classic */
/** @jsx jsx */
import React, { useEffect, useRef, useState } from 'react'
import { jsx } from 'theme-ui'
import { useForm, FormProvider } from 'react-hook-form'
import { DevTool } from '@hookform/devtools'

import Button from './Fields/Button'
import Label from './Fields/Label'
import QuestionCheckbox from './Questions/Checkbox'
import QuestionRadio from './Questions/Radio'
import QuestionSelect from './Questions/Select'
import QuestionCountry from './Questions/Country'
import QuestionCountryV2 from './Questions/CountryV2/index.js'
import QuestionCountrySubdivision from './Questions/CountrySubdivision/index.jsx'
import QuestionInput from './Questions/Input'
import QuestionTextarea from './Questions/Textarea'
import QuestionDate from './Questions/Date'
import QuestionPhone from './Questions/Phone'
import QuestionStatic from './Questions/Static'
import QuestionMultipleCheckboxes from './Questions/MultipleCheckboxes'
import QuestionMarkdown from './Questions/Markdown'
import QuestionSelectImage from './Questions/SelectImage'
import QuestionCounty from './Questions/County'
import QuestionGender from './Questions/Genre'
import QuestionAge from './Questions/Age'
import QuestionAutocomplete from './Questions/Autocomplete'
import QuestionImageInput from './Questions/ImageInput'
import QuestionRecaptcha from './Questions/Recaptcha'

import styles from './styles.js'

const FormBuilder = ({
  onSubmit: onSubmitForm,
  currentPath,
  isLoading,
  form,
  idForm = '',
  isMobile,
  isoCode,
  onLinkOpen,
  countryAndRegionsData,
  language,
  formErrors = [],
  ...props
}) => {
  const useFormObj = useForm()
  const [formDataValues, setFormDataValues] = useState({})
  const hasRecaptcha = form.questions.some(question => question.type === 'recaptcha')
  const recaptchaRef = useRef(null)

  useEffect(() => {
    if (formErrors && formErrors.length > 0) {
      formErrors.forEach((error) => {
        useFormObj.setError(error.field, { type: error.type })
      })
    }
  }, [formErrors, useFormObj])

  const {
    formState: { errors }
  } = useFormObj

  const QuestionsMap = (question) => {
    return {
      box: (
        <div
          sx={{ variant: 'forms.boxContainer' }}
          data-testid='question-builder'
        >
          {question.label && <Label>{question.label}</Label>}
          {question &&
            Array.isArray(question.children) &&
            question.children.map((question, i) => {
              return (
                <React.Fragment key={i}>
                  {QuestionsMap(question)[question.type] ||
                    QuestionsMap(question).default}
                </React.Fragment>
              )
            })}
        </div>
      ),
      input: <QuestionInput useForm={useFormObj} question={question} />,
      image_input: (
        <QuestionImageInput useForm={useFormObj} question={question} />
      ),
      password: <QuestionInput useForm={useFormObj} question={question} />,
      textarea: <QuestionTextarea question={question} />,
      select: (
        <>
          <QuestionSelect useForm={useFormObj} question={question} />
          {question.dependentQuestions &&
            question.dependentQuestions.map(
              ConditionalQuestion(question.dependentQuestions, question.name)
            )}
        </>
      ),
      select_images: (
        <>
          <QuestionSelectImage
            useForm={useFormObj}
            question={question}
            form={form}
          />
          {question.dependentQuestions &&
            question.dependentQuestions.map(
              ConditionalQuestion(question.dependentQuestions, question.name)
            )}
        </>
      ),
      country: (
        <>
          <QuestionCountry
            useForm={useFormObj}
            question={question}
            countryAndRegionsData={countryAndRegionsData}
            language={language}
          />
          {question.dependentQuestions &&
            question.dependentQuestions.map(
              ConditionalQuestion(question.dependentQuestions, question.name)
            )}
        </>
      ),
      country_v2: (
        <>
          <QuestionCountryV2
            question={question}
            language={language}
          />
          {question.dependentQuestions &&
            question.dependentQuestions.map(
              ConditionalQuestion(question.dependentQuestions, question.name)
            )}
        </>
      ),
      country_subdivision: (
        <>
          <QuestionCountrySubdivision
            question={question}
          />
        </>
      ),
      county: (
        <QuestionCounty useForm={useFormObj} question={question} />
      ),
      gender: (
        <>
          <QuestionGender
            useForm={useFormObj}
            question={question}
            language={language}
          />
          {question.dependentQuestions &&
            question.dependentQuestions.map(
              ConditionalQuestion(question.dependentQuestions, question.name)
            )}
        </>
      ),
      age: (
        <>
          <QuestionAge useForm={useFormObj} question={question} />
          {question.dependentQuestions &&
            question.dependentQuestions.map(
              ConditionalQuestion(question.dependentQuestions, question.name)
            )}
        </>
      ),
      autocomplete: (
        <>
          <QuestionAutocomplete useForm={useFormObj} question={question} />
          {question.dependentQuestions &&
            question.dependentQuestions.map(
              ConditionalQuestion(question.dependentQuestions, question.name)
            )}
        </>
      ),
      checkbox: (
        <QuestionCheckbox
          useForm={useFormObj}
          question={question}
          form={form}
          onLinkOpen={onLinkOpen}
        />
      ),
      static: (
        <QuestionStatic useForm={useFormObj} question={question} form={form} />
      ),
      radio: (
        <>
          <QuestionRadio
            useForm={useFormObj}
            question={question}
            onLinkOpen={onLinkOpen}
          />
          {question.dependentQuestions &&
            question.dependentQuestions.map(
              ConditionalQuestion(question.dependentQuestions, question.name)
            )}
        </>
      ),
      date: (
        <QuestionDate
          useForm={useFormObj}
          question={question}
          language={language}
          isMobile={isMobile}
        />
      ),
      phone: (
        <QuestionPhone
          useForm={useFormObj}
          question={question}
          isMobile={isMobile}
          isoCode={isoCode}
        />
      ),
      multiple_checkboxes: (
        <>
          <QuestionMultipleCheckboxes
            useForm={useFormObj}
            question={question}
          />
          {question.dependentQuestions &&
            question.dependentQuestions.map(
              ConditionalQuestion(
                question.dependentQuestions,
                question.name,
                question.type
              )
            )}
        </>
      ),
      markdown: (
        <QuestionMarkdown
          useForm={useFormObj}
          question={question}
          form={form}
          currentPath={currentPath}
          onLinkOpen={onLinkOpen}
        />
      ),
      recaptcha: (
        <QuestionRecaptcha
          formDataValues={formDataValues}
          onSubmitForm={onSubmitForm}
          question={question}
          ref={recaptchaRef}
        />
      )
    }
  }

  function ConditionalQuestion(question, name, preQuestionType) {
    return (dependentQuestion, i) => {
      const nestedQuestion = dependentQuestion && dependentQuestion.question

      const getConditions = () =>
        dependentQuestion.conditions || dependentQuestion.condition

      const conditionValue = useFormObj.watch(name)
      const getFormattedValue = () =>
        conditionValue && conditionValue.value
          ? conditionValue.value
          : conditionValue

      const renderComponent = () => (
        <React.Fragment key={i}>
          {
            QuestionsMap(dependentQuestion.question)[
              dependentQuestion.question.type
            ]
          }

          {nestedQuestion.dependentQuestions
            ? nestedQuestion.dependentQuestions.map(
                ConditionalQuestion(
                  nestedQuestion.question,
                  dependentQuestion.name
                )
              )
            : null}
        </React.Fragment>
      )

      if (preQuestionType === 'multiple_checkboxes') {
        const getMultiFormattedValue = () =>
          conditionValue && conditionValue.value
            ? conditionValue.value
            : conditionValue || []

        return getMultiFormattedValue() &&
          getMultiFormattedValue().some((e) => getConditions().includes(e))
          ? renderComponent()
          : null
      }

      return getConditions().includes(getFormattedValue())
        ? renderComponent()
        : null
    }
  }

  const formatData = async (data) => {
    await Promise.all(
      Object.keys(data).map(async (key) => {
        if (data[key] instanceof Date) {
          data[key] = data[key].toISOString()
        }
        if (data[key] instanceof FileList) {
          const reader = new FileReader()
          try {
            const encodedFile = await new Promise((resolve, reject) => {
              reader.onload = (event) => {
                return resolve(event.target.result)
              }
              reader.onerror = (event) => {
                return reject(event)
              }

              reader.readAsDataURL(data[key][0])
            })
            data[key] = encodedFile
          } catch {
            useFormObj.setError(key, { type: 'encodingError' })
          }
        }
      })
    )
    return data
  }

  const onSubmit = async (data) => {
    if (isLoading) return
    if (hasRecaptcha) {
      recaptchaRef.current?.execute()
      setFormDataValues(await formatData(data))

    } else {
      onSubmitForm(await formatData(data))
    }
  }

  return (
    <FormProvider {...useFormObj}>
      <form
        id={idForm}
        sx={{
          variant:
            form && form.layout
              ? 'forms.container.' + (form && form.layout)
              : 'forms.container',
          pointerEvents: isLoading ? 'none' : 'auto'
        }}
        onSubmit={useFormObj.handleSubmit(onSubmit)}
        {...props}
      >
        {form &&
          Array.isArray(form.questions) &&
          form.questions.map((question, i) => {
            return (
              <React.Fragment key={i}>
                {QuestionsMap(question)[question.type] ||
                  QuestionsMap(question).default}
              </React.Fragment>
            )
          })}
        {form &&
          form.callForAction &&
          form.callForAction.map((cfa, key) => {
            return (
              <div sx={{ variant: 'forms.submitContainer' }} key={key}>
                {form.accessibilityError && (
                  <div
                    className='visuallyhidden'
                    sx={{
                      variant: 'text.accessibilityError',
                      display:
                        Object.keys(errors).length !== 0 ? 'flex' : 'none'
                    }}
                    aria-live='assertive'
                  >
                    {form.accessibilityError}
                  </div>
                )}
                <Button
                  sx={styles.fitContent}
                  key={cfa.caption}
                  disabled={isLoading}
                  isLoading={isLoading}
                  id={cfa.id}
                  caption={cfa.caption}
                  type={cfa.type}
                  {...cfa}
                />
              </div>
            )
          })}
      </form>
      <DevTool control={useFormObj.control} />
    </FormProvider>
  )
}

export default FormBuilder