Agriculture Design System
Beta
Design System for the Export Service

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.