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