We see it as a Rails replacement. Anything you would normally do with Rails we hope that you’ll be able to do with Redwood.
Tom Preston-Werner - Full Stack Radio
Part 5 - Contact, React Hook Form
If you've made it this far into my series of blog posts I commend you and hope you've found them useful. Here's what we've done so far:
- In part 1 we created our RedwoodJS app.
- In part 2 we created links between different pages and a reusable layout.
- In part 3 we got the database up and running and learned CRUD operations for our blog posts.
- In part 4 we set up the frontend to query data from the backend to render a list of blog posts to the front page.
In this part we'll be combining everything we've learned up to this point to generate a contact page and take input from a user. We'll be using the same form tags that we learned about in part 4, which are wrappers around react-hook-form.
This is the simplest way to create a form, but Redwood can be used with other popular React form libraries like Formik or you can use react-hook-form directly.
5.1 ContactPage
The first step is to enter the yarn redwood generate page
command to create our contact page.
yarn rw g page contact
✔ Generating page files...
✔ Successfully wrote file `./web/src/pages/ContactPage/ContactPage.stories.js`
✔ Successfully wrote file `./web/src/pages/ContactPage/ContactPage.test.js`
✔ Successfully wrote file `./web/src/pages/ContactPage/ContactPage.js`
✔ Updating routes file...
This should look familiar if you've followed along with the whole series.
// web/src/pages/ContactPage/ContactPage.js
import { Link, routes } from '@redwoodjs/router'
const ContactPage = () => {
return (
<>
<h1>ContactPage</h1>
<p>
Find me in <code>./web/src/pages/ContactPage/ContactPage.js</code>
</p>
<p>
My default route is named <code>contact</code>, link to me with `
<Link to={routes.contact()}>Contact</Link>`
</p>
</>
)
}
export default ContactPage
Our ContactPage
component contains the same boilerplate we saw when we created our home page and our about page.
Go to BlogLayout and add a link to the contact page.
// web/src/layouts/BlogLayout/BlogLayout.js
import { Link, routes } from '@redwoodjs/router'
const BlogLayout = ({ children }) => {
return (
<>
<header>
<h1>
<Link to={routes.home()}>ajcwebdev</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
Now we'll import BlogLayout
into ContactPage.js
and wrap our contact page content in the BlogLayout
component.
// web/src/pages/ContactPage/ContactPage.js
import BlogLayout from 'src/layouts/BlogLayout'
const ContactPage = () => {
return (
<BlogLayout>
<h1>Contact</h1>
<p>Tell me stuff about my things!</p>
</BlogLayout>
)
}
export default ContactPage
We can now navigate to any of our three pages.
5.3 Form
We're going to import the Form
tags. Refer to the Redwoodjs docs to learn more about these tags.
// web/src/pages/ContactPage/ContactPage.js
import BlogLayout from 'src/layouts/BlogLayout'
import {
Form,
Label,
TextField,
Submit
} from '@redwoodjs/forms'
Once the tags are imported, create a Form
with a Label
, TextField
, and Submit
button.
// web/src/pages/ContactPage/ContactPage.js
// imports
const ContactPage = () => {
return (
<BlogLayout>
<h1>Contact</h1>
<p>Tell me stuff about my things!</p>
<Form>
<Label name="name" />
<TextField name="input" />
<Submit>Save</Submit>
</Form>
</BlogLayout>
)
}
export default ContactPage
We'll add a little CSS in a moment, but first see what happens if we try to input data.
If we click the save button we'll get an error.
This makes sense, we haven't told our form what to do yet with the data. Let's create a function called onSubmit
that will take in a data
object and console log the data
object.
// web/src/pages/ContactPage/ContactPage.js
const ContactPage = () => {
const onSubmit = (data) => {
console.log(data)
}
return (
<BlogLayout>
<h1>Contact</h1>
<p>Tell me stuff about my things!</p>
<Form onSubmit={onSubmit}>
<Label name="name" />
<TextField name="input" />
<Submit>Save</Submit>
</Form>
</BlogLayout>
)
}
export default ContactPage
The onSubmit
prop accepts a function name or anonymous function to be called if validation is successful. This function will be called with a single object containing key/value pairs of all Redwood form helper fields in your form.
Now if enter data into the form and click save we'll see the following in our console:
5.4 data
Our input is contained in the data
object. Right now it only has a key/value pair for name but we'll be adding more in a moment.
Before doing that, what can we do with this data
object?
// web/src/pages/ContactPage/ContactPage.js
const ContactPage = () => {
const onSubmit = (data) => {
console.log(data)
console.log(data.name)
}
We can pull out the value of name by console logging data.name
:
We want to be able to accept a longer message from our users, so we're going to import the TextAreaField
tag.
// web/src/pages/ContactPage/ContactPage.js
import {
Form,
Label,
TextField,
TextAreaField,
Submit
} from '@redwoodjs/forms'
We now how a TextField
for name and email, and a TextAreaField
for a message.
// web/src/pages/ContactPage/ContactPage.js
<Form onSubmit={onSubmit}>
<Label name="name" />
<TextField name="name" />
<Label name="email" />
<TextField name="email" />
<Label name="message" />
<TextAreaField name="message" />
<Submit>Save</Submit>
</Form>
To make this look a little nicer we're going to include just a little CSS.
/* web/src/index.css */
button, input, label, textarea {
display: block;
outline: none;
}
label {
margin-top: 1rem;
}
Our buttons, inputs, and labels are now display: block
which adds a line break after any appearance of these tags, and the label also has a little bit of margin on the top.
We'll test out all the fields:
We now are getting back an object with three key/value pairs.
We can console log any part of the object that we want.
// web/src/pages/ContactPage/ContactPage.js
const ContactPage = () => {
const onSubmit = (data) => {
console.log(data)
console.log(data.name)
console.log(data.email)
console.log(data.message)
}
Now if we look at our console we'll see each output and it even tells us which file and line corresponds to each piece of data.
5.5 validation
What happens if we only fill out some of the form and try to submit?
The form doesn't care, it simply takes the empty inputs and returns an empty string.
We want to add some validation so the user can't submit the form unless they've given input for all three fields.
5.6 errorClassName
We gave each TextField
an errorClassName
with the attribute error
. The validation
prop accepts an object containing options for react-hook-form.
// web/src/pages/ContactPage/ContactPage.js
<Form onSubmit={onSubmit}>
<Label name="name" />
<TextField
name="name"
errorClassName="error"
validation={{ required: true }}
/>
<Label name="email" />
<TextField
name="email"
errorClassName="error"
validation={{ required: true }}
/>
<Label name="message" />
<TextAreaField
name="message"
errorClassName="error"
validation={{ required: true }}
/>
<Submit>Save</Submit>
</Form>
Right now we're just adding the required
attribute, but later we'll use the validation
prop for a regular expression.
In our CSS we'll add the following properties for errors.
/* web/src/index.css */
.error {
color: red;
}
input.error, textarea.error {
border: 1px solid red;
}
Now when we try to submit an empty field we see the color change to red.
Once we give input the red error color goes away.
There's still an issue, which is we can submit an email that is invalid.
// web/src/pages/ContactPage/ContactPage.js
<TextField
name="email"
validation={{
required: true,
pattern: {
value: /[^@]+@[^.]+\..+/,
},
}}
errorClassName="error"
/>
Here's a regular expression provided in the Redwood tutorial.
5.7 FieldError
Now we get an error if we don't provide a valid email address. It would really be nice if we could tell our user why they are getting an error.
// web/src/pages/ContactPage/ContactPage.js
import {
Form,
Label,
TextField,
TextAreaField,
FieldError,
Submit
} from '@redwoodjs/forms'
We're going to import FieldError
to show error messages to our users.
// web/src/pages/ContactPage/ContactPage.js
<Form onSubmit={onSubmit}>
<Label name="name" />
<TextField
name="name"
errorClassName="error"
validation={{ required: true }}
/>
<FieldError name="name" />
<Label name="email" />
<TextField
name="email"
errorClassName="error"
validation={{
required: true,
pattern: { value: /[^@]+@[^.]+\..+/, },
}}
/>
<FieldError name="email" />
<Label name="message" />
<TextAreaField
name="message"
errorClassName="error"
validation={{ required: true }}
/>
<FieldError name="message" />
<Submit>Save</Submit>
</Form>
Now if we try to submit without giving input we are told that the field is required.
If we enter an invalid email we are told that the email is not formatted correctly.
If we add the errorClassName
to the Label
tags we'll also turn the labels red if there's an error.
// web/src/pages/ContactPage/ContactPage.js
<Form onSubmit={onSubmit}>
<Label
name="name"
errorClassName="error"
/>
<TextField
name="name"
errorClassName="error"
validation={{ required: true }}
/>
<FieldError name="name" />
<Label
name="email"
errorClassName="error"
/>
<TextField
name="email"
errorClassName="error"
validation={{
required: true,
pattern: { value: /[^@]+@[^.]+\..+/, },
}}
/>
<FieldError name="email" />
<Label
name="message"
errorClassName="error"
/>
<TextAreaField
name="message"
errorClassName="error"
validation={{ required: true }}
/>
<FieldError name="message" />
<Submit>Save</Submit>
</Form>
Now we have the label and the input field turn red if there's an error.
Might as well make everything red while we're at it.
// web/src/pages/ContactPage/ContactPage.js
<Form onSubmit={onSubmit}>
<Label
name="name"
errorClassName="error"
/>
<TextField
name="name"
errorClassName="error"
validation={{ required: true }}
/>
<FieldError
name="name"
style={{ color: 'red' }}
/>
<Label
name="email"
errorClassName="error"
/>
<TextField
name="email"
errorClassName="error"
validation={{
required: true,
pattern: { value: /[^@]+@[^.]+\..+/, },
}}
/>
<FieldError
name="email"
style={{ color: 'red' }}
/>
<Label
name="message"
errorClassName="error"
/>
<TextAreaField
name="message"
errorClassName="error"
validation={{ required: true }}
/>
<FieldError
name="message"
style={{ color: 'red' }}
/>
<Submit>Save</Submit>
</Form>
Since the FieldError
will only show on errors we can just inline styles with style={{ color: 'red' }}
.
Glorious red as far as the eye can see. It would also be nice if we could tell the user a field is required before they hit the submit button. We'll do that by adding mode: 'onBlur'
and pass that into validation
.
// web/src/pages/ContactPage/ContactPage.js
<Form
onSubmit={onSubmit}
validation={
{ mode: 'onBlur' }
}
>
Now when we enter a field and leave without filling it out we are immediately given feedback.
And for now that's our entire form. Here's a look at all the form code.
// web/src/pages/ContactPage/ContactPage.js
return (
<BlogLayout>
<h1>Contact</h1>
<p>Tell me stuff about my things!</p>
<Form onSubmit={onSubmit} validation={{ mode: 'onBlur' }}>
<Label
name="name"
errorClassName="error"
/>
<TextField
name="name"
errorClassName="error"
validation={{ required: true }}
/>
<FieldError
name="name"
style={{ color: 'red' }}
/>
<Label
name="email"
errorClassName="error"
/>
<TextField
name="email"
errorClassName="error"
validation={{
required: true, pattern: { value: /[^@]+@[^.]+\..+/, },
}}
/>
<FieldError
name="email"
style={{ color: 'red' }}
/>
<Label
name="message"
errorClassName="error"
/>
<TextAreaField
name="message"
errorClassName="error"
validation={{ required: true }}
/>
<FieldError
name="message"
style={{ color: 'red' }}
/>
<Submit>Save</Submit>
</Form>
</BlogLayout>
)
In the next part we'll connect our contact form to our database so we can persistent the data entered into the form.