Registration form example

This is a simple login form:

  • If you enter an email address that doesn't end with @functn.com you will see an error message from the server.

Create an account

Please enter your full name without any middle names
Please enter a valid email address. Example: test@test.com
Please enter your phone number with the country code. Example: +43
Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, and one number.
Favorite Animal (Required)
Country (Required)
Preferred Contact Method (Required)We will contact you via the method you select
Read our full Terms and Conditions

registration-form.tsx

'use client';
 
import { RiHeart2Line, RiLockPasswordLine } from '@remixicon/react';
import Link from 'next/link';
import { Button } from '@/ui/components/button/button';
import { Checkbox } from '@/ui/components/forms/checkbox';
import {
  Combobox,
  ComboboxItem,
  ComboboxListBox,
  ComboboxSearchField,
} from '@/ui/components/forms/combobox';
import { Form, FormRoot } from '@/ui/components/forms/form';
import { useForm } from '@/ui/components/forms/hooks/use-form';
import { Radio, RadioGroup } from '@/ui/components/forms/radio';
import { Select, SelectItem } from '@/ui/components/forms/select';
import { TextField } from '@/ui/components/forms/text-field';
import {
  FieldErrorSummary,
  FormErrors,
  ServerError,
  validators,
} from '@/ui/components/forms/validation';
import { registerAction } from './action';
import { RegistrationFormValues } from './types';
 
export const RegistrationForm = () => {
  const form = useForm<RegistrationFormValues>({
    onSubmit: async ({ values, reset }) => {
      await registerAction(values);
      reset();
    },
  });
 
  return (
    <FormRoot
      context={form}
      className="max-w-lg rounded-lg border-2 border-gray-200 p-4 shadow-xl"
    >
      <h2 id="registration-form-heading" className="mb-4 text-3xl font-bold">
        Create an account
      </h2>
 
      <FormErrors>
        <ServerError
          errorMessage={form.serverError}
          title="Could not register"
        />
        <FieldErrorSummary
          title="There are errors in your submission"
          subtitle="Please correct your input in these fields:"
          fieldLabels={{
            name: 'Name',
            email: 'Email Address',
            phone: 'Phone Number',
            password: 'Password',
            favoriteAnimal: 'Favorite Animal',
            contactMethod: 'Preferred Contact Method',
            country: 'Country',
            'terms-and-conditions': 'Terms and Conditions',
          }}
        />
      </FormErrors>
 
      <Form
        className="flex flex-col gap-4"
        aria-labelledby="registration-form-heading"
      >
        <TextField
          id="name"
          name="name"
          label="Name"
          type="text"
          description="Please enter your full name without any middle names"
          autoComplete="name"
          isRequired
          validate={(value) => {
            if (!value) {
              return 'Please enter your name';
            }
          }}
        />
 
        <TextField
          id="email"
          name="email"
          label="Email Address"
          type="email"
          description="Please enter a valid email address. Example: test@test.com"
          autoComplete="email"
          validate={(value) => {
            if (!validators.email(value)) {
              return 'Invalid email address';
            }
          }}
          isRequired
        />
 
        <TextField
          id="phone"
          name="phone"
          label="Phone Number"
          type="tel"
          description="Please enter your phone number with the country code. Example: +43"
          autoComplete="tel"
        />
 
        <TextField
          id="password"
          name="password"
          label="Password"
          type="password"
          description="Password must be at least 8 characters long and contain at least one
          uppercase letter, one lowercase letter, and one number."
          autoComplete="new-password"
          icon={<RiLockPasswordLine size={18} />}
          isRequired
          validate={(value) => {
            if (value.length < 8) {
              return 'Password must be at least 8 characters long';
            }
            if (!/[A-Z]/.test(value)) {
              return 'Password must contain at least one uppercase letter';
            }
            if (!/[a-z]/.test(value)) {
              return 'Password must contain at least one lowercase letter';
            }
            if (!/[0-9]/.test(value)) {
              return 'Password must contain at least one number';
            }
          }}
        />
 
        <Select
          id="favoriteAnimal"
          name="favoriteAnimal"
          label="Favorite Animal"
          icon={<RiHeart2Line size={18} />}
          isRequired
          validate={(value) => {
            if (!value) {
              return 'Please select an animal';
            }
            if (value !== 'dog') {
              return 'Please select dogs as your favorite animal';
            }
          }}
        >
          <SelectItem id="cat">Cat</SelectItem>
          <SelectItem id="dog">Dog</SelectItem>
          <SelectItem id="kangaroo">Kangaroo</SelectItem>
          <SelectItem id="panda">Panda</SelectItem>
          <SelectItem id="snake">Snake</SelectItem>
          <SelectItem id="lion">Lion</SelectItem>
          <SelectItem id="tiger">Tiger</SelectItem>
          <SelectItem id="elephant">Elephant</SelectItem>
          <SelectItem id="giraffe">Giraffe</SelectItem>
          <SelectItem id="zebra">Zebra</SelectItem>
          <SelectItem id="hippo">Hippo</SelectItem>
          <SelectItem id="rhino">Rhino</SelectItem>
          <SelectItem id="leopard">Leopard</SelectItem>
          <SelectItem id="cheetah">Cheetah</SelectItem>
          <SelectItem id="monkey">Monkey</SelectItem>
          <SelectItem id="fox">Fox</SelectItem>
          <SelectItem id="wolf">Wolf</SelectItem>
        </Select>
 
        <Combobox
          id="country"
          name="country"
          label="Country"
          isRequired
          validate={(value) => {
            if (!value) {
              return 'Please select a country';
            }
          }}
        >
          <ComboboxSearchField placeholder="Search country..." />
          <ComboboxListBox>
            <ComboboxItem id="austria">Austria</ComboboxItem>
            <ComboboxItem id="ireland">Ireland</ComboboxItem>
            <ComboboxItem id="poland">Poland</ComboboxItem>
            <ComboboxItem id="uk">United Kingdom</ComboboxItem>
            <ComboboxItem id="hungary">Hungary</ComboboxItem>
            <ComboboxItem id="spain">Spain</ComboboxItem>
            <ComboboxItem id="portugal">Portugal</ComboboxItem>
            <ComboboxItem id="kangaroo">Australia</ComboboxItem>
            <ComboboxItem id="in">India</ComboboxItem>
            <ComboboxItem id="br">Brazil</ComboboxItem>
            <ComboboxItem id="mx">Mexico</ComboboxItem>
            <ComboboxItem id="it">Italy</ComboboxItem>
            <ComboboxItem id="es">Spain</ComboboxItem>
            <ComboboxItem id="se">Sweden</ComboboxItem>
            <ComboboxItem id="no">Norway</ComboboxItem>
            <ComboboxItem id="fi">Finland</ComboboxItem>
          </ComboboxListBox>
        </Combobox>
 
        <RadioGroup
          id="contactMethod"
          name="contactMethod"
          label="Preferred Contact Method"
          isRequired
          validate={(value) => {
            if (!value) {
              return 'Please select a contact method';
            }
            if (value === 'pigeon') {
              return 'Please select a modern contact method';
            }
          }}
          orientation="vertical"
        >
          <Radio value="email">Email</Radio>
          <Radio value="phone">Phone</Radio>
          <Radio value="pigeon">Carrier Pigeon</Radio>
        </RadioGroup>
 
        <Checkbox
          id="terms-and-conditions"
          name="terms-and-conditions"
          value="terms-and-conditions"
          label="I agree to the terms and conditions"
          description={
            <>
              Read our full{' '}
              <Link
                href="/terms-and-conditions"
                className="text-primary-500 hover:underline"
              >
                Terms and Conditions
              </Link>
            </>
          }
          isRequired
          validate={(value) => {
            if (!value) {
              return 'Please agree to the terms and conditions';
            }
          }}
          className="pt-2"
        />
 
        <div>
          <Button type="submit" loading={form.isPending}>
            Create your account
          </Button>
        </div>
      </Form>
    </FormRoot>
  );
};

action.ts

'use server';
 
import { RegistrationFormValues } from './types';
 
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
 
export const registerAction = async (
  values: Partial<RegistrationFormValues>,
) => {
  await wait(2000);
 
  // We can do some basic input validation, this should have all been caught
  // by the client-side validation so in the case this fails we either have
  // a bug with the client code or someone is calling this API maliciously.
  // Either way, just return a generic error message to the user and bail out.
  if (!values.email || !values.password) {
    throw new Error('Both email and password are required');
  }
 
  if (!values.email.endsWith('@functn.com')) {
    throw new Error('You must use a @functn.com email address to register');
  }
};