Everyone's Favorite Thing to Build: Forms

Wait, don't close your browser! You had to know this was coming eventually, didn't you? And you've probably realized by now we wouldn't even have this section in the tutorial unless Redwood had figured out a way to make forms less soul-sucking than usual. In fact Redwood might even make you love building forms. Well, love is a strong word. Like building forms. Tolerate building them?

We already have a form or two in our app; remember our posts scaffold? And those work pretty well! How hard can it be? (Hopefully you haven't sneaked a peek at that code—what's coming next will be much more impressive if you haven't.)

Let's build the simplest form that still makes sense for our blog, a "contact us" form.

The Page

yarn rw g page contact

We can put a link to Contact in our layout's header:

// web/src/layouts/BlogLayout/BlogLayout.js

import { Link, routes } from '@redwoodjs/router'

const BlogLayout = ({ children }) => {
  return (
    <>
      <header>
        <h1>
          <Link to={routes.home()}>Redwood Blog</Link>
        </h1>
        <nav>
          <ul>
            <li>
              <Link to={routes.about()}>About</Link>
            </li>
            <li>
              <Link to={routes.contact()}>Contact</Link>
            </li>
          </ul>
        </nav>
      </header>
      <main>{children}</main>
    </>
  )
}

export default BlogLayout

And then use the BlogLayout in the ContactPage:

// web/src/pages/ContactPage/ContactPage.js

import BlogLayout from 'src/layouts/BlogLayout'

const ContactPage = (props) => {
  return <BlogLayout></BlogLayout>
}

export default ContactPage

Double check that everything looks good and then let's get to the good stuff.

Introducing Form Helpers

Forms in React are infamously annoying to work with. There are Controlled Components and Uncontrolled Components and third party libraries and many more workarounds to try and make forms in React as simple as they were originally intended to be: an <input> field with a name attribute that gets submitted somewhere when you click a button.

We think Redwood is a step or two in the right direction by not only freeing you from writing controlled component plumbing, but also dealing with validation and errors automatically. Let's see how it works.

For now we won't be talking to the database in our Contact form so we won't create a cell. Let's create the form right on the page. Redwood forms start with the...wait for it...<Form> tag:

// web/src/pages/ContactPage/ContactPage.js

import { Form } from '@redwoodjs/web'
import BlogLayout from 'src/layouts/BlogLayout'

const ContactPage = (props) => {
  return (
    <BlogLayout>
      <Form></Form>
    </BlogLayout>
  )
}

export default ContactPage

Well that was anticlimactic. You can't even see it in the browser. Let's add a form field so we can at least see something. Redwood ships with several inputs and a plain text input box is <TextField>. We'll also give the field a name attribute so that once there are multiple inputs on this page we'll know which contains which data:

// web/src/pages/ContactPage/ContactPage.js

import { Form, TextField } from '@redwoodjs/web'
import BlogLayout from 'src/layouts/BlogLayout'

const ContactPage = (props) => {
  return (
    <BlogLayout>
      <Form>
        <TextField name="input" />
      </Form>
    </BlogLayout>
  )
}

export default ContactPage

Something is showing! Still, pretty boring. How about adding a submit button?

// web/src/pages/ContactPage/ContactPage.js

import { Form, TextField, Submit } from '@redwoodjs/web'
import BlogLayout from 'src/layouts/BlogLayout'

const ContactPage = (props) => {
  return (
    <BlogLayout>
      <Form>
        <TextField name="input" />
        <Submit>Save</Submit>
      </Form>
    </BlogLayout>
  )
}

export default ContactPage

We have what might actually be considered a real, bonafide form here. Try typing something in and clicking "Save". Nothing blew up on the page but we have no indication that the form submitted or what happened to the data (although you may have noticed an error in the Web Inspector). Next we'll get the data in our fields.

onSubmit

Similar to a plain HTML form we'll give <Form> an onSubmit handler. That handler will be called with a single argument—an object containing all of the submitted form fields:

// web/src/pages/ContactPage/ContactPage.js

const ContactPage = (props) => {
  const onSubmit = (data) => {
    console.log(data)
  }

  return (
    <BlogLayout>
      <Form onSubmit={onSubmit}>
        <TextField name="input" />
        <Submit>Save</Submit>
      </Form>
    </BlogLayout>
  )
}

Now try filling in some data and submitting:

Great! Let's turn this into a more useful form by adding a couple fields. We'll rename the existing one to "name" and add "email" and "message":

// web/src/pages/ContactPage/ContactPage.js

import { Form, TextField, TextAreaField, Submit } from '@redwoodjs/web'
import BlogLayout from 'src/layouts/BlogLayout'

const ContactPage = (props) => {
  const onSubmit = (data) => {
    console.log(data)
  }

  return (
    <BlogLayout>
      <Form onSubmit={onSubmit}>
        <TextField name="name" />
        <TextField name="email" />
        <TextAreaField name="message" />
        <Submit>Save</Submit>
      </Form>
    </BlogLayout>
  )
}

export default ContactPage

See the new <TextAreaField> component here which generates an HTML <textarea> but that contains Redwood's form goodness. If we reload now our fields are there but there's no indication of which is which and everything is kind of jumbled together:

Let's add some labels and just a tiny bit of styling to at least separate the fields onto their own lines.

// web/src/pages/ContactPage/ContactPage.js

return (
  <BlogLayout>
    <Form onSubmit={onSubmit}>
      <label htmlFor="name" style={{ display: 'block' }}>
        Name
      </label>
      <TextField name="name" style={{ display: 'block' }} />

      <label htmlFor="email" style={{ display: 'block' }}>
        Email
      </label>
      <TextField name="email" style={{ display: 'block' }} />

      <label htmlFor="message" style={{ display: 'block' }}>
        Message
      </label>
      <TextAreaField name="message" style={{ display: 'block' }} />

      <Submit style={{ display: 'block' }}>Save</Submit>
    </Form>
  </BlogLayout>
)

That's a little better. Try filling out the form and submitting and you should get a console message with all three fields now.

Validation

"Okay Redwood tutorial author," you're saying, "what's the big deal? You built up Redwood's form helpers as The Next Big Thing but there are plenty of libraries that will let me skip creating controlled inputs manually. So what?" And you're right! Anyone can fill out a form correctly (although there are plenty of QA folks who would challenge that assertion), but what happens when someone leaves something out, or makes a mistake, or tries to haxorz our form? Now who's going to be there to help? Redwood, that's who!

All three of these fields should be required in order for someone to send a message to us. Let's enforce that with the standard HTML required attribute:

// web/src/pages/ContactPage/ContactPage.js

return (
  <BlogLayout>
    <Form onSubmit={onSubmit}>
      <label htmlFor="name" style={{ display: 'block' }}>
        Name
      </label>
      <TextField name="name" style={{ display: 'block' }} required />

      <label htmlFor="email" style={{ display: 'block' }}>
        Email
      </label>
      <TextField name="email" style={{ display: 'block' }} required />

      <label htmlFor="message" style={{ display: 'block' }}>
        Message
      </label>
      <TextAreaField name="message" style={{ display: 'block' }} required />

      <Submit style={{ display: 'block' }}>Save</Submit>
    </Form>
  </BlogLayout>
)

Now when trying to submit there'll be message from the browser noting that a field must be filled in. This is better than nothing, but these messages can't be styled. Can we do better?

Yes! Let's update that required call to instead be an object we pass to a custom attribute on Redwood form helpers called validation:

// web/src/pages/ContactPage/ContactPage.js

return (
  <BlogLayout>
    <Form onSubmit={onSubmit}>
      <label htmlFor="name" style={{ display: 'block' }}>
        Name
      </label>
      <TextField
        name="name"
        style={{ display: 'block' }}
        validation={{ required: true }}
      />

      <label htmlFor="email" style={{ display: 'block' }}>
        Email
      </label>
      <TextField
        name="email"
        style={{ display: 'block' }}
        validation={{ required: true }}
      />

      <label htmlFor="message" style={{ display: 'block' }}>
        Message
      </label>
      <TextAreaField
        name="message"
        style={{ display: 'block' }}
        validation={{ required: true }}
      />

      <Submit style={{ display: 'block' }}>Save</Submit>
    </Form>
  </BlogLayout>
)

And now when we submit the form with blank fields...the Name field gets focus. Boring. But this is just a stepping stone to our amazing reveal! We have one more form helper component to add—the one that displays errors on a field. Oh it just so happens that it's plain HTML so we can style it however we want!

<FieldError>

Introducing <FieldError> (don't forget to include it in the import statement at the top):

// web/src/pages/ContactPage/ContactPage.js

import {
  Form,
  TextField,
  TextAreaField,
  Submit,
  FieldError,
} from '@redwoodjs/web'
import BlogLayout from 'src/layouts/BlogLayout'

const ContactPage = (props) => {
  const onSubmit = (data) => {
    console.log(data)
  }

  return (
    <BlogLayout>
      <Form onSubmit={onSubmit}>
        <label htmlFor="name" style={{ display: 'block' }}>
          Name
        </label>
        <TextField
          name="name"
          style={{ display: 'block' }}
          validation={{ required: true }}
        />
        <FieldError name="name" />

        <label htmlFor="email" style={{ display: 'block' }}>
          Email
        </label>
        <TextField
          name="email"
          style={{ display: 'block' }}
          validation={{ required: true }}
        />
        <FieldError name="email" />

        <label htmlFor="message" style={{ display: 'block' }}>
          Message
        </label>
        <TextAreaField
          name="message"
          style={{ display: 'block' }}
          validation={{ required: true }}
        />
        <FieldError name="message" />

        <Submit style={{ display: 'block' }}>Save</Submit>
      </Form>
    </BlogLayout>
  )
}

export default ContactPage

Note that the name attribute matches the name of the input field above it. That's so it knows which field to display errors for. Try submitting that form now.

But this is just the beginning. Let's make sure folks realize this is an error message (see the new style attributes):

// web/src/pages/ContactPage/ContactPage.js

return (
  <BlogLayout>
    <Form onSubmit={onSubmit}>
      <label htmlFor="name" style={{ display: 'block' }}>
        Name
      </label>
      <TextField
        name="name"
        style={{ display: 'block' }}
        validation={{ required: true }}
      />
      <FieldError name="name" style={{ color: 'red' }} />

      <label htmlFor="email" style={{ display: 'block' }}>
        Email
      </label>
      <TextField
        name="email"
        style={{ display: 'block' }}
        validation={{ required: true }}
      />
      <FieldError name="email" style={{ color: 'red' }} />

      <label htmlFor="message" style={{ display: 'block' }}>
        Message
      </label>
      <TextAreaField
        name="message"
        style={{ display: 'block' }}
        validation={{ required: true }}
      />
      <FieldError name="message" style={{ color: 'red' }} />

      <Submit style={{ display: 'block' }}>Save</Submit>
    </Form>
  </BlogLayout>
)

You know what would be nice, if the input itself somehow displayed the fact that there was an error. Check out the errorStyle attributes:

// web/src/pages/ContactPage/ContactPage.js

return (
  <BlogLayout>
    <Form onSubmit={onSubmit}>
      <label htmlFor="name" style={{ display: 'block' }}>
        Name
      </label>
      <TextField
        name="name"
        style={{ display: 'block' }}
        errorStyle={{ display: 'block', borderColor: 'red' }}
        validation={{ required: true }}
      />
      <FieldError name="name" style={{ color: 'red' }} />

      <label htmlFor="email" style={{ display: 'block' }}>
        Email
      </label>
      <TextField
        name="email"
        style={{ display: 'block' }}
        errorStyle={{ display: 'block', borderColor: 'red' }}
        validation={{ required: true }}
      />
      <FieldError name="email" style={{ color: 'red' }} />

      <label htmlFor="message" style={{ display: 'block' }}>
        Message
      </label>
      <TextAreaField
        name="message"
        style={{ display: 'block' }}
        errorStyle={{ display: 'block', borderColor: 'red' }}
        validation={{ required: true }}
      />
      <FieldError name="message" style={{ color: 'red' }} />

      <Submit style={{ display: 'block' }}>Save</Submit>
    </Form>
  </BlogLayout>
)

Oooo, what if the label could change as well? It can, but we'll need Redwood's custom <Label> component for that (note that for becomes name just like the other components). Don't forget the import:

// web/src/pages/ContactPage/ContactPage.js

import {
  Form,
  TextField,
  TextAreaField,
  Submit,
  FieldError,
  Label,
} from '@redwoodjs/web'
import BlogLayout from 'src/layouts/BlogLayout'

const ContactPage = (props) => {
  const onSubmit = (data) => {
    console.log(data)
  }

  return (
    <BlogLayout>
      <Form onSubmit={onSubmit}>
        <Label
          name="name"
          style={{ display: 'block' }}
          errorStyle={{ display: 'block', color: 'red' }}
        >
          Name
        </Label>
        <TextField
          name="name"
          style={{ display: 'block' }}
          errorStyle={{ display: 'block', borderColor: 'red' }}
          validation={{ required: true }}
        />
        <FieldError name="name" style={{ color: 'red' }} />

        <Label
          name="email"
          style={{ display: 'block' }}
          errorStyle={{ display: 'block', color: 'red' }}
        >
          Email
        </Label>
        <TextField
          name="email"
          style={{ display: 'block' }}
          errorStyle={{ display: 'block', borderColor: 'red' }}
          validation={{ required: true }}
        />
        <FieldError name="email" style={{ color: 'red' }} />

        <Label
          name="message"
          style={{ display: 'block' }}
          errorStyle={{ display: 'block', color: 'red' }}
        >
          Message
        </Label>
        <TextAreaField
          name="message"
          style={{ display: 'block' }}
          errorStyle={{ display: 'block', borderColor: 'red' }}
          validation={{ required: true }}
        />
        <FieldError name="message" style={{ color: 'red' }} />

        <Submit style={{ display: 'block' }}>Save</Submit>
      </Form>
    </BlogLayout>
  )
}

export default ContactPage

In addition to style and errorStyle you can also use className and errorClassName

Validating Input Format

We should make sure the email field actually contains an email:

// web/src/pages/ContactPage/ContactPage.js

<TextField
  name="email"
  style={{ display: 'block' }}
  errorStyle={{ display: 'block', borderColor: 'red' }}
  validation={{
    required: true,
    pattern: {
      value: /[^@]+@[^.]+\..+/,
    },
  }}
/>

That is definitely not the end-all-be-all for email address validation, but pretend it's bulletproof. Let's also change the message on the email validation to be a little more friendly:

// web/src/pages/ContactPage/ContactPage.js

<TextField
  name="email"
  style={{ display: 'block' }}
  errorStyle={{ display: 'block', borderColor: 'red' }}
  validation={{
    required: true,
    pattern: {
      value: /[^@]+@[^.]+\..+/,
      message: 'Please enter a valid email address',
    },
  }}
/>

You may have noticed that trying to submit a form with validation errors outputs nothing to the console—it's not actually submitting. That's a good thing! Fix the errors and all is well.

When a validation error appears it will disappear as soon as you fix the content of the field. You don't have to click "Submit" again to remove the error messages.

Finally, you know what would really be nice: if the fields were validated as soon as the user leaves each one so they don't fill out the whole thing and submit just to see multiple errors appear. Let's do that:

// web/src/pages/ContactPage/ContactPage.js

<Form onSubmit={onSubmit} validation={{ mode: 'onBlur' }}>

Well, what do you think? Was it worth the hype? A couple of new components and you've got forms that handle validation and wrap up submitted values in a nice data object, all for free.

Redwood's forms are built on top of React Hook Form so there is even more functionality available than we've documented here.

Redwood has one more trick up its sleeve when it comes to forms but we'll save that for when we're actually submitting one to the server.

Having a contact form is great, but only if you actually get the contact somehow. Let's create a database table to hold the submitted data and create our first GraphQL mutation.