---
# System prepended metadata

title: Form Validation
tags: [Projects]

---

---
title: Form Validation
tags: Projects
---

## Introduction
It's a pain to handle validation all by ourselves especially each column has its own rule, meanwhile we have to deal with layout, such a nightmare even just imagine doing these all. 

No worries, now, we have ready to go tools.

## Tools
* [Yup](https://www.npmjs.com/package/yup) - Validaion rules
* [Formik](https://formik.org/docs/overview) - Form handling
* [Chakra UI](https://chakra-ui.com/docs/getting-started) - Layout UI

## Result in Next.js/TypeScript
Let's see codes first.

### Componenets

* InputField.tsx  
  * ==Beacause field will repeat many times, we package into a component, and we will need it to compatible with 'text', 'password', 'radio', 'textarea', 'checkbox', 'select', etc.==
  * password: We have to handle show/hide situation
  * radio/checkbox/select: We have to handle when the option is ==(string | number)[ ]== or ==object[ ]==

```typescript=
import {
  Button,
  Checkbox,
  CheckboxGroup,
  FormControl,
  FormErrorMessage,
  FormLabel,
  Input,
  InputGroup,
  InputRightElement,
  Radio,
  RadioGroup,
  Select,
  Stack,
  Textarea
} from '@chakra-ui/react';
import { useField } from 'formik';
import React, { InputHTMLAttributes, useState } from 'react';
import { BiHide, BiShow } from 'react-icons/bi';
import { IFieldType } from './SubmitForm';

type InputFieldProps = InputHTMLAttributes<HTMLInputElement> &
  InputHTMLAttributes<HTMLTextAreaElement> &
  InputHTMLAttributes<HTMLSelectElement> & {
    label: string;
    name: string;
    type?: IFieldType;
    options?: IOption[];
  };

export type IOption = string | IOptionObject;

interface IOptionObject {
  id: string;
  value: string;
}

const InputField: React.FC<InputFieldProps> = ({
  label,
  // eslint-disable-next-line no-unused-vars
  size: _,
  type = 'text',
  options,
  ...props
}: InputFieldProps) => {
  const [field, { error, touched }] = useField(props);

  const [show, setShow] = useState(false);
  const handleClick = () => setShow(!show);

  const isString = (value) => {
    return typeof value === 'string';
  };

  return (
    <FormControl
      isInvalid={!!error && !!touched}
      width={{ base: '100%', md: type === 'textarea' ? '100%' : '50%' }}
      p={2}
    >
      <FormLabel htmlFor={field.name}>{label}</FormLabel>
      {type === 'text' && <Input {...field} {...props} id={field.name} />}
      {type === 'password' && (
        <InputGroup size="md">
          <Input
            pr="4.5rem"
            {...field}
            {...props}
            id={field.name}
            type={show ? 'text' : 'password'}
          />
          <InputRightElement width="4.5rem">
            <Button h="1.75rem" size="md" onClick={handleClick}>
              {show ? <BiShow /> : <BiHide />}
            </Button>
          </InputRightElement>
        </InputGroup>
      )}
      {type === 'textarea' && (
        <Textarea resize="none" {...field} {...props} id={field.name} />
      )}
      {type === 'radio' && options && (
        <RadioGroup
          colorScheme="green"
          {...field}
          id={field.name}
          {...(props as unknown)}
        >
          <Stack spacing="24px">
            {options.map((x) => (
              <Radio
                {...field}
                key={isString(x) ? x : x['id']}
                value={isString(x) ? x : x['id']}
              >
                {isString(x) ? x : x['value']}
              </Radio>
            ))}
          </Stack>
        </RadioGroup>
      )}
      {type === 'checkbox' && options && (
        <CheckboxGroup
          colorScheme="green"
          {...field}
          {...(props as unknown)}
          onChange={(values) => console.log(values)}
        >
          <Stack spacing="24px">
            {options.map((x) => (
              <Checkbox
                {...field}
                key={isString(x) ? x : x['id']}
                value={isString(x) ? x : x['id']}
              >
                {isString(x) ? x : x['value']}
              </Checkbox>
            ))}
          </Stack>
        </CheckboxGroup>
      )}

      {type === 'select' && options && options.length && (
        <Select
          {...field}
          {...props}
          placeholder={
            isString(options[0]) ? options[0] : options[0]['value'] || ''
          }
          id={field.name}
        >
          {options.map((x, index) => {
            if (index !== 0) {
              return (
                <option
                  key={isString(x) ? x : x['id']}
                  value={isString(x) ? x : x['id']}
                >
                  {isString(x) ? x : x['value']}
                </option>
              );
            }
          })}
        </Select>
      )}
      <FormErrorMessage>{error}</FormErrorMessage>
    </FormControl>
  );
};

export default InputField;
```

* SubmitForm.tsx
  * Here we arrange initial values for the form, and define rules schema with yup, then use integrate these with formik.
  * Because I use this form with only 1 API, so I just code the API path in this file, and don't want to make it as a dynamic prop.
  * Users have to tick agreement before submitting.
  * Will show message after submitting.
```typescript=
import { Box, Button, Checkbox, Flex } from '@chakra-ui/react';
import axios from 'axios';
import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React, { useState } from 'react';
import * as Yup from 'yup';
import { RequiredStringSchema } from 'yup/lib/string';
import { Locales } from '../../i18n/locales';
import InputField, { IOption } from './InputField';

export type IFieldType = 'text' | 'textarea' | 'radio' | 'checkbox' | 'select';

export interface IField {
  type?: IFieldType;
  options?: IOption[];
  label: string;
  name: string;
  initialValue: string | (string | number)[];
  rule: RequiredStringSchema<string, Record<string, unknown>> | unknown;
}

interface SubmitFormProps {
  data: IField[];
  submit: string;
  afterSubmit: string;
  api: string;
  agreement?: string;
}

const SubmitForm: React.FC<SubmitFormProps> = ({
  data,
  submit,
  afterSubmit,
  api,
  agreement
}) => {
  const router = useRouter();
  const currentLang = router.locale as Locales;

  const [hasAgreed, setHasAgreed] = useState(false);

  const [hasSubmitted, setHasSubmitted] = useState(false);

  //  We arrange all initialValues to an object we need.
  const initialValues = data.reduce((pre, cur) => {
    pre[cur['name']] = cur['initialValue'];
    return pre;
  }, {});
  
  //  We arrange all rules to an object we need.
  const generateShape = data.reduce((pre, cur) => {
    pre[cur['name']] = cur['rule'];
    return pre;
  }, {});

  const submitValidation = Yup.object().shape(generateShape);

  return (
    <>
      {hasSubmitted ? (
        <Box
          textAlign="center"
          fontSize={{ base: '12px', md: 'inherit' }}
          bg="gray.100"
          py={2}
        >
          {afterSubmit}
        </Box>
      ) : (
        <Formik
          initialValues={initialValues}
          validationSchema={submitValidation}
          onSubmit={async (values, actions) => {
            if ((agreement && hasAgreed) || !agreement) {
              try {
                const {
                  data: { status }
                } = await axios.post(
                  `${process.env.NEXT_PUBLIC_API_URL}${api}?lang=${currentLang}`,
                  values
                );

                if (status === 200) {
                  actions.setSubmitting(false);
                  setHasSubmitted(true);
                  actions.resetForm();
                }
              } catch (e) {
                console.log(e);
              }
            }
          }}
        >
          {({ isSubmitting, values, errors }) => (
            <Form>
              <Flex wrap="wrap">
                {data &&
                  data.map((x) => (
                    <InputField
                      key={x.label}
                      label={x.label}
                      name={x.name}
                      type={x.type}
                      options={x.options}
                    />
                  ))}
              </Flex>

              {agreement && (
                <Checkbox
                  isInvalid={!hasAgreed}
                  py={{ base: 5, md: 10 }}
                  colorScheme="red"
                  isChecked={hasAgreed}
                  onChange={(e) => setHasAgreed(e.target.checked)}
                >
                  {agreement}
                </Checkbox>
              )}

              <Button
                mt={5}
                w={{ base: '50vw', md: '300px' }}
                display="block"
                mx="auto"
                color="white"
                bg="red.600"
                _hover={{ bgColor: 'red.600' }}
                isLoading={isSubmitting}
                type="submit"
              >
                {submit}
              </Button>

              <Box as="pre" marginY={10}>
                {JSON.stringify(values, null, 2)}
                <br />
                <br />
                {JSON.stringify(errors, null, 2)}
              </Box>
            </Form>
          )}
        </Formik>
      )}
    </>
  );
};

export default SubmitForm;
```

### Usage
* So here, we just have to define label, name, inital value, rule for each column, then import the component, and you can get your form.

```typescript=
import { Box, Flex, Image, Text } from '@chakra-ui/react';
import { GetStaticProps } from 'next';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/router';
import React from 'react';
import * as Yup from 'yup';
import InfoTitle from '../../components/Common/InfoTitle';
import InfoTitleSub from '../../components/Common/InfoTitleSub';
import { IField } from '../../components/Form/SubmitForm';
import Wrapper from '../../components/Wrapper';
import { Locales } from '../../i18n/locales';

const SubmitForm = dynamic(() => import('../../components/Form/SubmitForm'), {
  ssr: false
});

const contactUs: React.FC<{}> = () => {
  const { t } = useTranslation(['contactUs']);
  const router = useRouter();
  const currentLang = router.locale as Locales;
  const isArabic = currentLang === 'ar';

  const data: IField[] = [
    {
      label: t('firstName'),
      name: 'name',
      initialValue: '',
      rule: Yup.string()
        .min(2, t('tooShort'))
        .max(50, t('tooLong'))
        .required(t('required'))
    },
    {
      label: t('lastName'),
      name: 'surname',
      initialValue: '',
      rule: Yup.string()
        .min(1, t('tooShort'))
        .max(50, t('tooLong'))
        .required(t('required'))
    },
    {
      label: t('contactNumber'),
      name: 'mobile',
      initialValue: '',
      rule: Yup.string()
        .min(8, t('tooShort'))
        .max(15, t('tooLong'))
        .required(t('required'))
    },
    {
      label: t('emailAddress'),
      name: 'email',
      initialValue: '',
      rule: Yup.string().email(t('invalidFormat')).required(t('required'))
    },
    {
      label: t('area'),
      name: 'area',
      initialValue: '',
      rule: Yup.string()
        .min(2, t('tooShort'))
        .max(50, t('tooLong'))
        .required(t('required'))
    },
    {
      label: t('category'),
      name: 'type',
      type: 'select',
      options: [
        t('pleaseSelectAnItem'),
        t('generalInquiry'),
        t('disputesComplaints'),
        t('partner')
      ],
      initialValue: '',
      rule: Yup.string().required(t('required'))
    },
    {
      label: 'category2',
      name: 'category2',
      type: 'radio',
      options: [
        { id: '1', value: t('generalInquiry') },
        { id: '2', value: t('disputesComplaints') },
        { id: '3', value: t('partner') }
      ],
      initialValue: '',
      rule: Yup.string().required(t('required'))
    },
    {
      label: 'category3',
      name: 'category3',
      type: 'checkbox',
      options: [t('generalInquiry'), t('disputesComplaints'), t('partner')],
      initialValue: [],
      rule: Yup.array().min(2, t('required'))
    },
    {
      label: t('yourAccount'),
      name: 'login',
      initialValue: '',
      rule: Yup.string()
        .min(3, t('tooShort'))
        .max(20, t('tooLong'))
        .notRequired()
    },
    {
      label: 'password',
      name: 'password',
      type: 'password',
      initialValue: '',
      rule: Yup.string().required(t('required'))
    },
    {
      label: t('areYouOurExistingCustomer'),
      name: 'iScustomer',
      type: 'select',
      options: [t('pleaseSelectAnItem'), t('No'), t('Yes')],
      initialValue: '',
      rule: Yup.string().required(t('required'))
    },
    {
      label: t('leaveAMessage'),
      name: 'content',
      type: 'textarea',
      initialValue: '',
      rule: Yup.string().notRequired()
    }
  ];

  return (
    <Wrapper>
     ...

          <SubmitForm
            data={data}
            agreement={t('iAgreeTo')}
            submit={t('submit')}
            afterSubmit={t('weWillContactYou')}
            api="index/contact"
          />
          
     ...
    </Wrapper>
  );
};

export default contactUs;
```

### Layout
![](https://i.imgur.com/y0hfG7g.jpg)