Ok, so let’s get started with my favorite form validator library thus far.
First:
import { Form, Field } from 'react-final-form'
Then get your big ol form:
<Form />
It’s not so big, but this is where everything will live.
Inside the Form is a Field tag. Inside each Field tag is an input. Behind the field tag is a simple form tag. To render the input, you must render it, I know, really simple. Let me show you what I mean.
import React from 'react'
import { Form, Field } from 'react-final-form'
import { XCircleIcon } from '@heroicons/react/solid'
import { useNavigate } from 'react-router-dom'
const required = (value) => (value ? undefined : 'Required')
const encode = (data) => {
return Object.keys(data)
.map((key) => encodeURIComponent(key) + '=' + encodeURIComponent(data[key]))
.join('&')
}
const ContactForm = () => {
const navigate = useNavigate()
const onSubmit = (values) => {
fetch('/', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: encode({ 'form-name': 'contact', ...values }),
})
.then(() => navigate('/thanks'))
.catch((error) => alert(error))
}
return (
<div className="bg-contact-pattern-2 relative bg-opacity-50 bg-cover bg-right-bottom p-10 ">
<div className="mx-auto max-w-7xl py-12 sm:py-24 sm:px-8 lg:px-8">
<div className="bg- relative shadow-xl">
<div className="grid grid-cols-1 rounded-md lg:grid-cols-3">
{/* Contact information */}
<div className="bg-contact-pattern relative overflow-hidden rounded-t-md bg-cover bg-center px-6 py-10 sm:px-10 lg:rounded-l-md xl:p-12">
<h3 className="text-lg font-medium text-gray-50">
Contact Information
</h3>
<p className="mt-6 max-w-3xl text-base text-gray-50">
Please feel free to contact me about employment opportunities
here.
</p>
<dl className="mt-8 space-y-6"></dl>
</div>
{/* Contact form */}
<div className="rounded-b-md bg-gray-100 px-6 py-10 sm:px-10 md:rounded-r-md lg:col-span-2 lg:rounded-r-md xl:p-12">
<h3 className="text-lg font-medium text-gray-900">
Send a message
</h3>
<Form
onSubmit={onSubmit}
validate={(values) => {
const errors = {}
if (values.email !== 'undefined') {
var pattern = new RegExp(
/^(("[\w-\s]+")|([\w-]+(?:\.[\w-]+)*)|("[\w-\s]+")([\w-]+(?:\.[\w-]+)*))(@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$)|(@\[?((25[0-5]\.|2[0-4][0-9]\.|1[0-9]{2}\.|[0-9]{1,2}\.))((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){2}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\]?$)/i
)
if (!pattern.test(values.email)) {
errors.email = 'Please enter valid email address.'
}
}
return errors
}}
render={({ handleSubmit, submitError }) => (
<form
onSubmit={handleSubmit}
name="contact"
method="POST"
data-netlify="true"
>
<input type="hidden" name="form-name" value="contact" />
<div className="mt-6 space-y-8 rounded-b-md sm:space-y-5">
<div>
<div className="">
<Field
name="name"
component="input"
placeholder=""
validate={required}
>
{({ input, meta, placeholder }) => (
<div className="col-span-6 sm:col-span-3">
<div>
<label
htmlFor="name"
className="block text-sm font-medium text-gray-700"
>
Name
</label>
<input
type="text"
name="name"
{...input}
placeholder={placeholder}
className="text-l mb-2 block w-full rounded-md px-4 py-2 pl-1 shadow focus:border-blue-500 focus:outline-none md:w-3/4 lg:w-3/4"
/>
</div>
{meta.error && meta.touched && (
<div className="mt-1 mb-2 w-full rounded-md bg-red-50 p-1 transition duration-500 ease-in-out sm:col-span-3 md:w-3/4 lg:w-3/4">
<div className="flex">
<div className="flex-shrink-0">
<XCircleIcon
className="h-5 w-5 text-red-400"
aria-hidden="true"
/>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">
{meta.error}
</h3>
</div>
</div>
</div>
)}
</div>
)}
</Field>
{submitError && (
<div className="mt-1 mb-2 rounded-md bg-red-50 p-1 transition duration-500 ease-in-out">
<div className="flex">
<div className="flex-shrink-0">
<XCircleIcon
className="h-5 w-5 text-red-400"
aria-hidden="true"
/>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">
{submitError}
</h3>
</div>
</div>
</div>
)}
</div>
<div>
<Field
name="email"
component="input"
placeholder=""
validate={required}
>
{({ input, meta, placeholder }) => (
<div className="col-span-6 sm:col-span-3">
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700"
>
E-mail
</label>
<input
type="text"
name="email"
{...input}
placeholder={placeholder}
className="text-l mb-2 block w-full rounded-md px-4 py-2 pl-1 shadow focus:border-blue-500 focus:outline-none md:w-3/4 lg:w-3/4"
/>
</div>
{meta.error && meta.touched && (
<div className="mt-1 mb-2 w-full rounded-md bg-red-50 p-1 transition duration-500 ease-in-out sm:col-span-3 md:w-3/4 lg:w-3/4">
<div className="flex">
<div className="flex-shrink-0">
<XCircleIcon
className="h-5 w-5 text-red-400"
aria-hidden="true"
/>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">
{meta.error}
</h3>
</div>
</div>
</div>
)}
</div>
)}
</Field>
{submitError && (
<div className="mt-1 mb-2 rounded-md bg-red-50 p-1 transition duration-500 ease-in-out">
<div className="flex">
<div className="flex-shrink-0">
<XCircleIcon
className="h-5 w-5 text-red-400"
aria-hidden="true"
/>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">
{submitError}
</h3>
</div>
</div>
</div>
)}
</div>
<Field
name="subject"
component="input"
placeholder=""
validate={required}
>
{({ input, meta, placeholder }) => (
<div className="col-span-6 sm:col-span-3">
<div>
<label
htmlFor="subject"
className="block text-sm font-medium text-gray-700"
>
Subject
</label>
<input
type="text"
name="subject"
{...input}
placeholder={placeholder}
className="text-l mb-2 block w-full rounded-md px-4 py-2 pl-1 shadow focus:border-blue-500 focus:outline-none"
/>
</div>
{meta.error && meta.touched && (
<div className="col-span-6 mt-1 mb-2 rounded-md bg-red-50 p-1 transition duration-500 ease-in-out sm:col-span-3">
<div className="flex">
<div className="flex-shrink-0">
<XCircleIcon
className="h-5 w-5 text-red-400"
aria-hidden="true"
/>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">
{meta.error}
</h3>
</div>
</div>
</div>
)}
</div>
)}
</Field>
<Field
name="message"
component="textarea"
validate={required}
className="text-l border-gray mb-2 block h-32 w-full rounded-md border-gray-300 p-4 px-4 py-2 pl-1 shadow focus:border-blue-500 focus:outline-none"
>
{({ input, meta, placeholder }) => (
<div className="col-span-6 sm:col-span-3">
<div>
<label
htmlFor="message"
className="block text-sm font-medium text-gray-700"
>
Message
</label>
<textarea
type="text"
name="message"
rows="8"
{...input}
placeholder={placeholder}
className="text-l mb-2 block w-full rounded-md px-4 py-2 pl-1 shadow focus:border-blue-500 focus:outline-none"
/>
</div>
{meta.error && meta.touched && (
<div className="col-span-6 mt-1 mb-2 rounded-md bg-red-50 p-1 transition duration-500 ease-in-out sm:col-span-3">
<div className="flex">
<div className="flex-shrink-0">
<XCircleIcon
className="h-5 w-5 text-red-400"
aria-hidden="true"
/>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">
{meta.error}
</h3>
</div>
</div>
</div>
)}
</div>
)}
</Field>
<div className="mt-5 sm:mt-6 sm:grid sm:grid-flow-row-dense sm:grid-cols-2 sm:gap-3">
<button
type="submit"
className="inline-flex w-full justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-indigo-500 sm:col-start-2 sm:text-sm"
>
Send
</button>
</div>
</div>
</div>
</form>
)}
/>
</div>
</div>
</div>
</div>
</div>
)
}
export default ContactForm
This is big ol form is the final result, but let’s break it down.
Inside the Form tag, you will need to clarify what happens onSubmit. You will also need to clarify submission errors as well.
Let’s start with defining what it means to have a required field:
const required = (value) => (value ? undefined : “Required”)
<Form
onSubmit={onSubmit}
validate={(values) => {
const errors = {};
if (values.email !== "undefined") {
var pattern = new RegExp(
/^(("[\w-\s]+")|([\w-]+(?:\.[\w-]+)*)|("[\w-\s]+")([\w-]+(?:\.[\w-]+)*))(@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$)|(@\[?((25[0-5]\.|2[0-4][0-9]\.|1[0-9]{2}\.|[0-9]{1,2}\.))((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){2}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\]?$)/i
);
if (!pattern.test(values.email)) {
errors.email = "Please enter valid email address.";
}
}
return errors;
}}
As you can see above, I add validation to qualify a legitimate email. That logic requires the user add an “@” symbol and “.com”
Next we render the form. Also, take note of data-netlify=“true” here
render={({ handleSubmit, submitError }) => (
<form
onSubmit={handleSubmit}
name='contact'
method='POST'
data-netlify='true'>
<input type='hidden' name='form-name' value='contact' />
<div className='mt-6 space-y-8 rounded-b-md sm:space-y-5'>
<div>
<div className=''>
<Field
name='name'
component='input'
placeholder=''
validate={required}>
{({ input, meta, placeholder }) => (
<div className='col-span-6 sm:col-span-3'>
<div>
<label
htmlFor='name'
className='block text-sm font-medium text-gray-700'>
Name
</label>
<input
type='text'
name='name'
{...input}
placeholder={placeholder}
className='block w-full px-4 py-2 pl-1 mb-2 rounded-md shadow md:w-3/4 lg:w-3/4 text-l focus:outline-none focus:border-blue-500'
/>
</div>
{meta.error && meta.touched && (
<div className='w-full p-1 mt-1 mb-2 transition duration-500 ease-in-out rounded-md md:w-3/4 lg:w-3/4 sm:col-span-3 bg-red-50'>
<div className='flex'>
<div className='flex-shrink-0'>
<XCircleIcon
className='w-5 h-5 text-red-400'
aria-hidden='true'
/>
</div>
<div className='ml-3'>
<h3 className='text-sm font-medium text-red-800'>
{meta.error}
</h3>
</div>
</div>
</div>
)}
</div>
)}
</Field>
{submitError && (
<div className='p-1 mt-1 mb-2 transition duration-500 ease-in-out rounded-md bg-red-50'>
<div className='flex'>
<div className='flex-shrink-0'>
<XCircleIcon
className='w-5 h-5 text-red-400'
aria-hidden='true'
/>
</div>
<div className='ml-3'>
<h3 className='text-sm font-medium text-red-800'>
{submitError}
</h3>
</div>
</div>
</div>
)}
</div>
<div className='mt-5 sm:mt-6 sm:grid sm:grid-cols-2 sm:gap-3 sm:grid-flow-row-dense'>
<button
type='submit'
className='inline-flex justify-center w-full px-4 py-2 text-base font-medium text-white bg-red-600 border border-transparent rounded-md shadow-sm hover:bg-red-700 focus:outline-none focus:ring-indigo-500 sm:col-start-2 sm:text-sm'>
Send
</button>
</div>
</div>
</div>
</form>
)}
```
Keep in mind, this is just for one input. Then you just rinse and repeat.
When you render you pass in handleSubmit and submitError.
Generally, place the error div underneath each input.
With React Final Form, you can easily customize error validation with your icons. I’ll let you handle the CSS and styling yourself. Or, you can just copy and paste my TailwindCSS styling. Once you have React Final Form set up, it is time to integrate Netlify.
Time to integrate Netlify.
Right after the render, in the form declare data-netlify=‘true’ and make the method POST. So like this:
<form
onSubmit={handleSubmit}
name='contact'
method='POST'
data-netlify='true'>
Underneath that, put a hidden input field. Don’t ask me why, just do it. You can put it anywhere, just make sure it’s hidden. Again, I’m using TailwindCSS to style my form.
<input type=’hidden’ name=’form-name’ value=’contact’ />
In your public folder of the React app, go into the index.html file. From there add another form. Place it in between the body. Whatever you named the form, remain consistent. As you can see above, the name is ‘contact’. So I’ll name it contact:
<form name=”contact” netlify netlify-honeypot=”bot-field” hidden action=”/”>
<input type=”text” name=”name” />
<input type=”text” name=”email” />
<input type=”text” name=”subject” />
<textarea name=”message”></textarea>
</form>
We add the honeypot for filtering spam. When finished with these two steps, Netlify should pick up the form, and it is successfully integrated. You can view it in the forms section on Netlify. But you are not a Jedi yet.
The final step is defining what happens when the user clicks submit.
So, I’m gonna save you the trouble of scouring for docs and just give it to you.
const encode = (data) => {
return Object.keys(data)
.map((key) => encodeURIComponent(key) + '=' + encodeURIComponent(data[key]))
.join('&')
}
const navigate = useNavigate()
const onSubmit = (values) => {
fetch('/', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: encode({ 'form-name': 'contact', ...values }),
})
.then(() => navigate('/thanks'))
.catch((error) => alert(error))
}
With ReactRouter v6, this is how we navigate through web pages. We define navigate by using useNavigate. If the submit is successful it will redirect to “/thanks” page. The values that is passed in refers to all of the RFF content. Double check the form-name again, and lastly use the spread operator on the values.
That should do it.