---
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
