Forms
Web forms are one of the main points of interaction between a user and a web site or application. In this guide, we will be building a simple sign in form using two popular libraries react-hook-form and yup.
Note: The AgDS component library does not have any opinions for how form state should be handled. All of our form components have been designed to work with any form library.
1. Install AgDS
If you haven’t already installed AgDS in your application, please refer to the Getting started guide.
2. Compose the user interface
Using the TextInput
, FormStack
, Button
and ButtonGroup
components, we can quickly compose the user interface for the form.
import { Button, ButtonGroup } from '@ag.ds-next/react/button';import { FormStack } from '@ag.ds-next/react/form-stack';import { TextInput } from '@ag.ds-next/react/text-input';
function FormExampleSignIn() { return ( <form> <FormStack> <TextInput label="Email address" type="email" maxWidth="xl" required /> <TextInput label="Password" type="password" maxWidth="xl" required /> <ButtonGroup> <Button type="submit">Sign in</Button> </ButtonGroup> </FormStack> </form> );}
3. Handling form state
We will be using react-hook-form
for handling form state and yup
for handling client-side validation.
3.1 Install dependencies
We need to install these new dependencies by running the following command in your terminal:
yarn add react-hook-form @hookform/resolvers yup
3.1 Define the form schema
Yup is a schema builder for runtime value parsing and validation. Define a schema, transform a value to match, assert the shape of an existing value, or both. Yup schema are extremely expressive and allow modeling complex, interdependent validations, or value transformation.
In this step, we will be creating a form schema for the email
and password
form fields with some simple validation.
For more information about yup
, please refer to the official documentation
import * as yup from 'yup';
const formSchema = yup .object({ email: yup .string() .email('Invalid email address') .required('Enter your email address'), password: yup.string().required('Enter your password'), }) .required();
type FormSchema = yup.InferType<typeof formSchema>;
3.3 Handling form state
In this step, we will be managing our form state with react-hook-form
, a performant, flexible and extensible library for handling form state. For more information about react-hook-form
, please refer to the official documentation
Since we are already using yup
for client-side validation, we have added noValidate
to our form element which disabled native browser validation.
import { useForm, SubmitHandler } from 'react-hook-form';import { yupResolver } from '@hookform/resolvers/yup';import * as yup from 'yup';import { Button, ButtonGroup } from '@ag.ds-next/react/button';import { FormStack } from '@ag.ds-next/react/form-stack';import { TextInput } from '@ag.ds-next/react/text-input';
const formSchema = yup .object({ email: yup .string() .email('Invalid email address') .required('Enter your email address'), password: yup.string().required('Enter your password'), }) .required();
type FormSchema = yup.InferType<typeof formSchema>;
export function FormExampleSignIn() { const { register, handleSubmit, formState: { errors }, } = useForm<FormSchema>({ resolver: yupResolver(formSchema), });
const onSubmit: SubmitHandler<FormSchema> = (data) => { console.log(data); };
return ( <form onSubmit={handleSubmit(onSubmit)} noValidate> <FormStack> <TextInput label="Email address" type="email" {...register('email')} maxWidth="xl" required /> <TextInput label="Password" type="password" {...register('password')} maxWidth="xl" required /> <ButtonGroup> <Button type="submit">Sign in</Button> </ButtonGroup> </FormStack> </form> );}
4. Handling invalid states
By making use of the invalid
and message
props available to the TextInput
component, we can let the user know they have an invalid form.
We have also added a PageAlert
to the top of the form which lists out all form errors.
import { useEffect, useRef, useState } from 'react';import { useForm, SubmitHandler, SubmitErrorHandler } from 'react-hook-form';import { yupResolver } from '@hookform/resolvers/yup';import * as yup from 'yup';import { Button, ButtonGroup } from '@ag.ds-next/react/button';import { Stack } from '@ag.ds-next/react/stack';import { FormStack } from '@ag.ds-next/react/form-stack';import { TextInput } from '@ag.ds-next/react/text-input';import { useScrollToField } from '@ag.ds-next/react/field';import { PageAlert } from '@ag.ds-next/react/page-alert';import { Text } from '@ag.ds-next/react/text';import { TextLink } from '@ag.ds-next/react/text-link';import { UnorderedList, ListItem } from '@ag.ds-next/react/list';
const formSchema = yup .object({ email: yup .string() .email('Invalid email address') .required('Enter your email address'), password: yup.string().required('Enter your password'), }) .required();
type FormSchema = yup.InferType<typeof formSchema>;
export function FormExampleSignIn() { const errorPageAlertRef = useRef<HTMLDivElement>(null); const [hasFocusedErrorRef, setHasFocusedErrorRef] = useState(false); const scrollToField = useScrollToField();
const { register, handleSubmit, formState: { errors }, } = useForm<FormSchema>({ resolver: yupResolver(formSchema), });
const onSubmit: SubmitHandler<FormSchema> = (data) => { console.log(data); };
const onError: SubmitErrorHandler<FormSchema> = (errors, event) => { console.log(errors, event); setHasFocusedErrorRef(false); };
// Only show the page alert if there is more than 1 error const hasErrors = Object.keys(errors).length > 1;
useEffect(() => { if (!(hasErrors || hasFocusedErrorRef)) return; errorPageAlertRef.current?.focus(); setHasFocusedErrorRef(true); }, [hasFocusedErrorRef, hasErrors]);
return ( <Stack gap={3}> {hasErrors && ( <PageAlert ref={errorPageAlertRef} tabIndex={-1} tone="error" title="There is a problem" > <Text as="p">Please correct the following fields and try again</Text> <UnorderedList> {Object.entries(errors).map(([key, value]) => ( <ListItem key={key}> <TextLink href={`#${key}`} onClick={scrollToField}> {value.message} </TextLink> </ListItem> ))} </UnorderedList> </PageAlert> )} <form onSubmit={handleSubmit(onSubmit, onError)} noValidate> <FormStack> <TextInput label="Email address" type="email" {...register('email')} invalid={Boolean(errors.email?.message)} message={errors.email?.message} maxWidth="xl" required /> <TextInput label="Password" type="password" {...register('password')} invalid={Boolean(errors.password?.message)} message={errors.password?.message} maxWidth="xl" required /> <ButtonGroup> <Button type="submit">Sign in</Button> </ButtonGroup> </FormStack> </form> </Stack> );}
What’s next?
Take a look at our wide range of form components, and install the ones you need in your application.
We recommend using TypeScript to get the most out of these components.
If you get stuck, feel free to reach out to us in the Design System Teams Chat.