Forms and validation

Forms and validation
WIP

Forms are the gatekeepers to the most important part of your apps. They collect payment info, email addresses, and other things that contribute to your paycheque.

You want your forms to be so silky smooth that the users forget they’re even there.

The contact form that I’ve designed for you is decidedly not silky smooth. In fact, it’s bloody awful. For example, if you hit the “enter” key within the form… nothing happens. And if you submit empty data, well, you’ve submitted empty data.

So let’s spruce the form up a bit.

#Good forms use <form>

Let’s start with the Golden Rule Of Forms:

When writing HTML forms, you should always wrap your inputs in a <form> tag.

You may be surprised at this. The contact form from earlier didn’t use a form element, and it still worked. But it didn’t work very well.

By adding a <form> tag around your inputs, you’re giving the browser an extra piece of information. You’re telling it that these inputs can be submitted. And with this extra information, the browser adds extra functionality:

  • It allows the user to submit the form by hitting the “enter” key within child inputs.
  • It causes <button> tags to submit forms (unless they have a type="button" attribute).

So <form> elements allow inputs to be submitted. But what exactly is meant by the word “submit”?

#The onSubmit prop

If you’ve ever written a plain HTML form, you may be familiar with the <form> tag’s action attribute. This attribute tells the browser where to send the submitted information.

In a React form, you won’t often use the action prop. You want to process the data with JavaScript — not on the server. So in place of the action attribute, you’ll need to pass a callback to the form’s onSubmit prop.

A form element’s onSubmit callback will be called whenever the underlying HTML form is submitted. This means that it will be called when the user clicks a <button> inside the form, when the user hits the “enter” key while an input is focussed, or whenever the form element’s underlying DOM node has submit() called on it.

index.js
styles.css
import React from 'react'
import ReactDOM from 'react-dom'
import './styles.css'

class SearchForm extends React.Component {
  constructor() {
    super()
    this.state = {
      query: '',
    }
    this.handleChangeQuery = this.handleChangeQuery.bind(this)
    this.handleSubmit = this.handleSubmit.bind(this)
  }

  render() {
    return (
      <form
        className='SearchForm'
        onSubmit={this.handleSubmit}>
        <input
          placeholder="Search Raw React"
          value={this.state.query}
          onChange={this.handleChangeQuery}
        />
        <button type="submit">
          ▶︎
        </button>
      </form>
    )
  }
  
  handleChangeQuery(event) {
    this.setState({
      query: event.target.value,
    })
  }
  
  handleSubmit(event) {
    // Prevent the browser from reloading the page on form submission
    event.preventDefault()

    if (this.props.onSearch) {
      this.props.onSearch(this.state.query)
    }
  }
}

ReactDOM.render(
  <SearchForm
    onSearch={(query) => alert(`Searching for "${query}".`)}
  />,
  document.getElementById('root')
)
Build In Progress

One quirk of web forms is that by default, they’ll always reload the page when submitted, even if you don’t specify an action prop! This is particularly unhelpful in a React app, as you’ll lose the user’s input before you get a chance to process it.

To prevent the page from reloading, you’ll need to call the preventDefault() method of the callback’s event object, as demonstrated above. This prevents the browser’s default behavior — in this case preventing the page reload — and let’s you handle the submit event as you would handle any other user input.

#Validation

Submitting invalid data can be a rather unpleasant experience. Not only do you need to go back and enter the data from scratch, but now the app has probably also associated incorrect data with you, that you’ll need to fix. And that’s if you didn’t hit some edge case on the backend that caused the whole product to fall over with a spectacular kaboom.

Luckily, React makes form validation so simple that after you’ve built it, you’ll be like “wait, that’s it”? So I’m just going to go ahead and show you how easy it is.

In the following example, there are two changes from the earlier search form:

  • Within render(), an error is rendered if the current input state isn’t valid.
  • With handleSubmit(), the form component’s onSubmit callback is only called if the current input state does look valid. Otherwise, the error is stored.

Try hitting enter without entering a query to see how it behaves.

index.js
styles.css
import React from 'react'
import ReactDOM from 'react-dom'
import './styles.css'

class SearchForm extends React.Component {
  constructor() {
    super()
    this.state = {
      error: null,
      query: '',
      wasInvalid: false,
    }
    this.handleChangeQuery = this.handleChangeQuery.bind(this)
    this.handleSubmit = this.handleSubmit.bind(this)
  }

  render() {
    return (
      <form
        className={`
          SearchForm
          ${this.state.error ? 'SearchForm-invalid' : ''}
        `}
        onSubmit={this.handleSubmit}>
        <input
          placeholder="Search Raw React"
          value={this.state.query}
          onChange={this.handleChangeQuery}
        />
        <button type="submit">
          ▶︎
        </button>
        {this.state.error && (
          <div className='SearchForm-error'>
            {this.state.error}
          </div>
        )}
      </form>
    )
  }
  
  handleChangeQuery(event) {
    this.setState({
      error: null,
      query: event.target.value,
    })
  }
  
  handleSubmit(event) {
    event.preventDefault()
    
    if (!this.state.query) {
      // If the query was empty, set an error.
      this.setState({
        error: "What do you want to search for?",
      })
    }
    else {    
      if (this.props.onSearch) {
        this.props.onSearch(this.state.query)
      }
    }
  }
}

ReactDOM.render(
  <SearchForm
    onSearch={(query) => alert(`Searching for "${query}".`)}
  />,
  document.getElementById('root')
)
Build In Progress

#Now it’s your turn

Now that you’re familiar with how React forms work, let’s do a bit of work on your contact form’s UX.

I’ve supplied you with the solution to the previous exercise as a starting point — so if you haven’t yet completed the previous exercise, you’ll want to skip back and do that one first.

Your tasks are to:

  • Prevent required fields from being submitted when they’re empty.
  • Allow the user to submit the form by hitting “enter” in any input.
  • Get the submit button working without specifying an onClick prop.

As a hint, you’ll need two add two new items to state. One to hold errors on the name field, and one to hold errors on the email field.

ContactForm.js
index.js
Contacts.js
styles.css
import PropTypes from 'prop-types'
import React from 'react'

export class ContactForm extends React.Component {
  constructor() {
    super()
    this.state = {
      name: "",
      email: "",
     }
    this.handleChangeName = this.handleChangeName.bind(this)
    this.handleChangeEmail = this.handleChangeEmail.bind(this)
    this.handleClickAdd = this.handleClickAdd.bind(this)
  }

  render() {
    return (
      <div className='ContactForm'>
        <label>
          <span>Name</span>
          <input
            value={this.state.name}
            onChange={this.handleChangeName}
          />
        </label>
        <label>
          <span>E-mail</span>
          <input
            value={this.state.email}
            onChange={this.handleChangeEmail}
          />
        </label>
        <button onClick={this.handleClickAdd}>
          Add
        </button>
      </div>
    )
  }
  
  handleChangeName(event) {
    this.setState({
      name: event.target.value,
    })
  }
  
  handleChangeEmail(event) {
    this.setState({
      email: event.target.value,
    })
  }
  
  handleClickAdd(event) {
    this.props.onAddContact(this.state)
  }
}

ContactForm.propTypes = {
  onAddContact: PropTypes.func.isRequired,
}
Build In Progress

If you’re stuck, make sure that:

  • you’ve bound your callback for the onSubmit prop
  • you’re calling event.preventDefault() within the onSubmit callback

There are many correct ways of getting validation and form tags to work, so it’s okay if your result doesn’t match my solution. The important thing is that the form and button both submit via the form’s onSubmit prop, and that your validation works as expected. And once you’re happy with your result, click on through to the next lesson!

Progress to next section.