a first look at redwoodJS part 5 - contact, react hook form

ajcwebdev - Jul 4 '20 - - Dev Community

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
Enter fullscreen mode Exit fullscreen mode
✔ 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...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Our ContactPage component contains the same boilerplate we saw when we created our home page and our about page.

03-ContactPage-rendered

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

We can now navigate to any of our three pages.

04-ContactPage-BlogLayout-rendered

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

05-ContactPage-Form-rendered

We'll add a little CSS in a moment, but first see what happens if we try to input data.

06-ContactPage-input

If we click the save button we'll get an error.

07-error-message

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
Enter fullscreen mode Exit fullscreen mode

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:

08-console.log(data)

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)
  }
Enter fullscreen mode Exit fullscreen mode

We can pull out the value of name by console logging data.name:

09-console.log(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'
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

10-ContactPage-email-message-rendered

We'll test out all the fields:

11-ContactPage-email-message-input

We now are getting back an object with three key/value pairs.

12-ContactPage-email-message-console.log

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)
  }
Enter fullscreen mode Exit fullscreen mode

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.

13-data.email-data.message-console.log

5.5 validation

What happens if we only fill out some of the form and try to submit?

14-just-name-input

The form doesn't care, it simply takes the empty inputs and returns an empty string.

15-empty-email-message

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

16-textfield-errors

Now when we try to submit an empty field we see the color change to red.

17-textfield-errors-with-name

Once we give input the red error color goes away.

18-invalid-email

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"
/>
Enter fullscreen mode Exit fullscreen mode

Here's a regular expression provided in the Redwood tutorial.

19-email-error

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'
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Now if we try to submit without giving input we are told that the field is required.

20-name-email-message-required

If we enter an invalid email we are told that the email is not formatted correctly.

21-email-formatted-incorrectly

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>
Enter fullscreen mode Exit fullscreen mode

Now we have the label and the input field turn red if there's an error.

22-label-errors

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>
Enter fullscreen mode Exit fullscreen mode

Since the FieldError will only show on errors we can just inline styles with style={{ color: 'red' }}.

23-all-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' }
  }
>
Enter fullscreen mode Exit fullscreen mode

Now when we enter a field and leave without filling it out we are immediately given feedback.

24-form-validation-mode-onBlur-rendered

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>
)
Enter fullscreen mode Exit fullscreen mode

In the next part we'll connect our contact form to our database so we can persistent the data entered into the form.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .