For this tutorial we are going to use react-hook-form
and Typescript. We will go through the hooks and make it reproducible and fully funcional.
In my next.js
project I have created a contact page, where I want my form to animate on first load. My imports are the followings
import { useMemo, useState, BaseSyntheticEvent, useEffect } from 'react'
import { useForm } from 'react-hook-form'
import FormErrorMessage from '@/components/error-message'
import { ContactData, ContactResponse } from '@/interfaces/contact'
import { motion } from 'framer-motion'
import { fadeInUp, stagger } from '@/utils/motion'
import { event as logEvent } from '@/lib/gtag'
import useResetScroll from 'hooks/useResetScroll'
I will show you what the components look like and the interfaces
This is FormErrorMessage (very easy, it just return the props you give it, else null):
export default function FormErrorMessage({ message }: { message?: string }) {
if (!message) {
return null
}
return (
<span
role="error"
className="text-red-500 text-sm ml-0 sm:ml-5 my-5 sm:my-0 inline-block"
>
{message}
</span>
)
}
This is useResetScroll hook (again very easy, it scrolls to the top of the window whenever triggered):
import { useEffect } from 'react'
export default function useResetScroll() {
useEffect(() => {
window.scrollTo(0, 0)
}, [])
}
The logEvent function will help us submit`to our Google analytics, detailed information about our users:
export const GA_TRACKING_ID = your google analytics track id
export const pageview = (url: string) => {
;(window as any).gtag('config', GA_TRACKING_ID, {
page_path: url,
})
}
export const event = ({ //our function
action,
category,
label,
value,
}: {
action: string
category: string
label: string
value: number | string
}) => {
;(window as any).gtag('event', action, {
event_category: category,
event_label: label,
value,
})
}
Another interesting component to take a look at is the fadeInUp, stagger
by framer-motion
. It's just a better way to hold the information you need. You will see it in use...
export const easing = [0.6, -0.05, 0.01, 0.99]
export const fadeInUp = {
initial: {
y: 20,
opacity: 0,
transition: { duration: 0.6, ease: easing },
},
animate: {
y: 0,
opacity: 1,
transition: {
duration: 0.6,
ease: easing,
},
},
}
export const stagger = {
animate: {
transition: {
staggerChildren: 0.1,
delayChildren: 0.3,
},
},
}
The final form is the following
export default function Contact() {
const { register, handleSubmit, errors } = useForm()
const [status, setStatus] = useState('idle')
useResetScroll()
useEffect(() => {
if (status === 'success' || status === 'error') {
setTimeout(() => setStatus('idle'), 3000)
}
}, [status]) //on every status change
const submitMessage = async (
data: ContactData,
event: BaseSyntheticEvent
) => {
const { name, email, message } = data
if (!name || !email || !message.trim()) {
logEvent({ //google information
action: 'User submitted form without filling details',
category: 'engagement',
label: 'user_error',
value: 0,
})
return
}
try {
setStatus('sending')
const contactResponse = await fetch(`/api/contact`, {
method: 'POST',
body: JSON.stringify({
name: name.trim(),
email,
message: message.trim(),
}),
})
const contactStatus: ContactResponse = await contactResponse.json()
if (contactStatus.success) {
logEvent({ //google information
action: 'User sent a message',
category: 'engagement',
label: 'user_success',
value: 100,
})
setStatus('success')
event.target.reset()
} else {
setStatus('error')
}
} catch (error) {
setStatus('error')
}
}
const buttonText = useMemo(() => {
if (status === 'success') return 'Message sent!'
else if (status === 'error') return 'Mission Failed š¶'
else if (status === 'sending') return 'Sending message'
return 'Send message'
}, [status])
return (
<motion.div
className="p-0 w-full"
initial="initial"
animate="animate"
exit={{ opacity: 0 }}
>
<Head title="Contact - Adithya NR" />
<Container>
<div className="md:w-2/3 w-full mx-auto">
<motion.h1
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="md:text-5xl text-3xl font-bold mb-3"
>
Get in touch
</motion.h1>
<motion.p initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
Send me a message here and I'll get back to you ASAP
</motion.p>
<motion.form
variants={stagger}
onSubmit={handleSubmit(submitMessage)}
>
<motion.div variants={fadeInUp} className="form-group">
<label className="label" htmlFor="name">
Your name
</label>
<input
className="input"
type="text"
ref={register({ required: 'Name cannot be empty š' })}
name="name"
id="name"
data-testid="name"
placeholder="Mike Wazowski"
/>
{errors.name ? (
<FormErrorMessage message={errors.name.message} />
) : null}
</motion.div>
<motion.div variants={fadeInUp} className="form-group">
<label className="label" htmlFor="email">
Your email address
</label>
<input
className="input"
type="email"
data-testid="email"
ref={register({
required: 'Please provide your email address š',
pattern: {
value: /[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]+/u,
message: 'Invalid email address...',
},
})}
name="email"
id="email"
placeholder="mike@monstersinc.com"
/>
{errors.email ? (
<FormErrorMessage message={errors.email.message} />
) : null}
</motion.div>
<motion.div variants={fadeInUp} className="form-group">
<label className="label" htmlFor="message">
Your message
</label>
<textarea
ref={register({
required: 'Please leave a message š¢',
minLength: { value: 10, message: 'Message too short š' },
})}
className="input"
name="message"
data-testid="message"
id="message"
rows={5}
/>
{errors.message ? (
<FormErrorMessage message={errors.message.message} />
) : null}
</motion.div>
<motion.input
variants={fadeInUp}
whileHover={{ scale: 1.05, x: 0 }}
whileTap={{ scale: 0.5, x: 0 }}
type="submit"
role="submit"
value={buttonText}
className={`submit-button hover:text-white w-full sm:w-auto ${
status === 'success' ? 'bg-green-600 text-black' : ''
}`}
/>
</motion.form>
</div>
</Container>
<br />
</motion.div>
)
}
The interisting part about this form is the google integration. This article explains how to use Tag Manager to set up Universal Analytics event tags that are triggered in response to clicks on links, clicks on other types of elements, at timed intervals, and when a forms are submitted.. Apperently though there is no CSRF protection it would be good to take note of it.
Hope you find this read helpful. Peace
Final result: Amazing portfolio developer