Skip to main content
Version: 3.x

Building a Form

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.home()}>Home</Link>
</li>
<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 for the ContactPage by making sure its wrapped by the same <Set> as the other pages in the routes file:

web/src/Routes.js
import { Router, Route, Set } from '@redwoodjs/router'
import ScaffoldLayout from 'src/layouts/ScaffoldLayout'
import BlogLayout from 'src/layouts/BlogLayout'

const Routes = () => {
return (
<Router>
<Set wrap={ScaffoldLayout} title="Posts" titleTo="posts" buttonLabel="New Post" buttonTo="newPost">
<Route path="/posts/new" page={PostNewPostPage} name="newPost" />
<Route path="/posts/{id:Int}/edit" page={PostEditPostPage} name="editPost" />
<Route path="/posts/{id:Int}" page={PostPostPage} name="post" />
<Route path="/posts" page={PostPostsPage} name="posts" />
</Set>
<Set wrap={BlogLayout}>
<Route path="/article/{id:Int}" page={ArticlePage} name="article" />
<Route path="/contact" page={ContactPage} name="contact" />
<Route path="/about" page={AboutPage} name="about" />
<Route path="/" page={HomePage} name="home" />
</Set>
<Route notfound page={NotFoundPage} />
</Router>
)
}

export default Routes

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 in the HTML spec: 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.

We won't be pulling any data from the database on our Contact page so we won't create a cell. Let's create the form right in the page. Redwood forms start with the...wait for it...<Form> tag:

web/src/pages/ContactPage/ContactPage.js
import { MetaTags } from '@redwoodjs/web'
import { Form } from '@redwoodjs/forms'

const ContactPage = () => {
return (
<>
<MetaTags title="Contact" description="Contact page" />

<Form></Form>
</>
)
}

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 the <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 { MetaTags } from '@redwoodjs/web'
import { Form, TextField } from '@redwoodjs/forms'

const ContactPage = () => {
return (
<>
<MetaTags title="Contact" description="Contact page" />

<Form>
<TextField name="input" />
</Form>
</>
)
}

export default ContactPage

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

web/src/pages/ContactPage/ContactPage.js
import { MetaTags } from '@redwoodjs/web'
import { Form, TextField, Submit } from '@redwoodjs/forms'

const ContactPage = () => {
return (
<>
<MetaTags title="Contact" description="Contact page" />

<Form>
<TextField name="input" />
<Submit>Save</Submit>
</Form>
</>
)
}

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. Next we'll get the data from 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
import { MetaTags } from '@redwoodjs/web'
import { Form, TextField, Submit } from '@redwoodjs/forms'

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

return (
<>
<MetaTags title="Contact" description="Contact page" />

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

export default ContactPage

Now try filling in some data and submitting, then checking out the console in Web Inspector:

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 { MetaTags } from '@redwoodjs/web'
import { Form, TextField, TextAreaField, Submit } from '@redwoodjs/forms'

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

return (
<>
<MetaTags title="Contact" description="Contact page" />

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

export default ContactPage

See the new <TextAreaField> component here which generates an HTML <textarea> but that contains Redwood's form goodness:

Let's add some labels:

web/src/pages/ContactPage/ContactPage.js
import { MetaTags } from '@redwoodjs/web'
import { Form, TextField, TextAreaField, Submit } from '@redwoodjs/forms'

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

return (
<>
<MetaTags title="Contact" description="Contact page" />

<Form onSubmit={onSubmit}>
<label htmlFor="name">Name</label>
<TextField name="name" />

<label htmlFor="email">Email</label>
<TextField name="email" />

<label htmlFor="message">Message</label>
<TextAreaField name="message" />

<Submit>Save</Submit>
</Form>
</>
)
}

export default ContactPage

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 statement), 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 (
<Form onSubmit={onSubmit}>
<label htmlFor="name">Name</label>
<TextField name="name" required />

<label htmlFor="email">Email</label>
<TextField name="email" required />

<label htmlFor="message">Message</label>
<TextAreaField name="message" required />

<Submit>Save</Submit>
</Form>
)

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 (
<Form onSubmit={onSubmit}>
<label htmlFor="name">Name</label>
<TextField name="name" validation={{ required: true }} />

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

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

<Submit>Save</Submit>
</Form>
)

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 { MetaTags } from '@redwoodjs/web'
import {
FieldError,
Form,
TextField,
TextAreaField,
Submit,
} from '@redwoodjs/forms'

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

return (
<>
<MetaTags title="Contact" description="Contact page" />

<Form onSubmit={onSubmit}>
<label htmlFor="name">Name</label>
<TextField name="name" validation={{ required: true }} />
<FieldError name="name" />

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

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

<Submit>Save</Submit>
</Form>
</>
)
}

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. Remember the basic styles we added to index.css back at the start? There's an .error class in there that we can use. Set the className attribute on <FieldError>:

web/src/pages/ContactPage/ContactPage.js
import { MetaTags } from '@redwoodjs/web'
import {
FieldError,
Form,
TextField,
TextAreaField,
Submit,
} from '@redwoodjs/forms'

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

return (
<>
<MetaTags title="Contact" description="Contact page" />

<Form onSubmit={onSubmit}>
<label htmlFor="name">Name</label>
<TextField name="name" validation={{ required: true }} />
<FieldError name="name" className="error" />

<label htmlFor="email">Email</label>
<TextField name="email" validation={{ required: true }} />
<FieldError name="email" className="error" />

<label htmlFor="message">Message</label>
<TextAreaField name="message" validation={{ required: true }} />
<FieldError name="message" className="error" />

<Submit>Save</Submit>
</Form>
</>
)
}

export default ContactPage

You know what would be nice? If the input itself somehow displayed the fact that there was an error. Check out the errorClassName attributes on the inputs:

web/src/pages/ContactPage/ContactPage.js
import { MetaTags } from '@redwoodjs/web'
import {
FieldError,
Form,
TextField,
TextAreaField,
Submit,
} from '@redwoodjs/forms'

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

return (
<>
<MetaTags title="Contact" description="Contact page" />

<Form onSubmit={onSubmit}>
<label htmlFor="name">Name</label>
<TextField
name="name"
validation={{ required: true }}
errorClassName="error"
/>
<FieldError name="name" className="error" />

<label htmlFor="email">Email</label>
<TextField
name="email"
validation={{ required: true }}
errorClassName="error"
/>
<FieldError name="email" className="error" />

<label htmlFor="message">Message</label>
<TextAreaField
name="message"
validation={{ required: true }}
errorClassName="error"
/>
<FieldError name="message" className="error" />

<Submit>Save</Submit>
</Form>
</>
)
}

export default ContactPage

Oooo, what if the label could change as well? It can, but we'll need Redwood's custom <Label> component for that. Note that the htmlFor attribute of <label> becomes the name prop on <Label>, just like with the other Redwood form components. And don't forget the import:

web/src/pages/ContactPage/ContactPage.js
import { MetaTags } from '@redwoodjs/web'
import {
FieldError,
Form,
Label,
TextField,
TextAreaField,
Submit,
} from '@redwoodjs/forms'

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

return (
<>
<MetaTags title="Contact" description="Contact page" />

<Form onSubmit={onSubmit}>
<Label name="name" errorClassName="error">
Name
</Label>
<TextField
name="name"
validation={{ required: true }}
errorClassName="error"
/>
<FieldError name="name" className="error" />

<Label name="email" errorClassName="error">
Email
</Label>
<TextField
name="email"
validation={{ required: true }}
errorClassName="error"
/>
<FieldError name="email" className="error" />

<Label name="message" errorClassName="error">
Message
</Label>
<TextAreaField
name="message"
validation={{ required: true }}
errorClassName="error"
/>
<FieldError name="message" className="error" />

<Submit>Save</Submit>
</Form>
</>
)
}

export default ContactPage
Error styling

In addition to className and errorClassName you can also use style and errorStyle. Check out the Form docs for more details on error styling.

And notice that if you fill in something in a field that's marked as an error, the error instantly goes away! This is great feedback for our users that they're doing what we want, and they don't have to wait to click the "Save" button again just to see if what they changed is now correct.

Validating Input Format

We should make sure the email field actually contains an email, by providing a pattern. This is definitely not the end-all-be-all for email address validation, but for now let us 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"
validation={{
required: true,
pattern: {
value: /^[^@]+@[^.]+\..+$/,
message: 'Please enter a valid email address',
},
}}
errorClassName="error"
/>
info

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. This is great feedback for users (and eagle-eyed QA testers) since they receive instant feedback what they changed is now correct.

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} config={{ 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.

info

Redwood's forms are built on top of React Hook Form so there is even more functionality available than we've documented here. Visit the Form docs to learn more about all form functionalities.

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.