Skip to content

Form Helpers

Naofumi Kagami edited this page Sep 17, 2022 · 3 revisions

Form handling

There are many ways to different ways to handle forms in React.

The React official documentation recommends the usage of Controller Components. On the other hand, the Next.js documentation on forms mostly discusses non-Javascript techniques. Even the Javascript form section uses non-controlled components in contrast to React's official recommendation. Furthermore, the most popular form validation library, React Hook Form also seems to recommend non-controlled components.

Our primary goal is to replicate basic Rails functionality. Hence we will focus on non-controlled components, which is what Rails uses. We have also provided a set of functions and components that will make working with forms even easier.

Note that the following functions and components are independent of rendering mode. You can use them in CSR, SSR or SSG pages.

submitForm()

export default function EditCategory ({category, breadcrumbs, actionButton}) {
  const router = useRouter()
  const [ errors, setErrors ] = useState([])

  const handleSubmit = async (event) => {
    event.preventDefault()

    submitForm({
      form: event.target,
      success: (response, jsonResult) => {
        router.push(`/categories/${jsonResult.id}`)
      },
      failure: (response, jsonResult) => {
        setErrors(jsonResult)
      }
    })
  }

  return (
  	  ...
      <form onSubmit={handleSubmit} action={`/api/categories/${category.id}`} method="PUT">
        <CsrfToken />
        <label htmlFor="name">
          Name
        </label>
	    <input
	      type="text"
	      name="category[name]"
	      id="name"
	      defaultValue={category.name}
	    />
        <label htmlFor="name">
          Description
        </label>
        <textarea
          name="category[description]"
          id="description"
          rows={5}
          defaultValue={category.description}
        />
        <button type="submit">
          Save
        </button>
      </form>
     ...
  )
}

The above is the code for sending a form. (The category, breadcrumbs, actionButton) are provided through a getServerSideProps() function that is not shown).

  1. submitForm() is a generic function that handles the display of a progress loader and errors. No extra code is necessary to handle these.
  2. We use the name attribute in the <input> tag to codify the data structure that we wish to send. This enables us to completely omit the code required to convert the values into a JSON structure. In this example, we are sending values that would be decoded as {category: {name: [name_value], description: [description_value]} } on the server. This format has been described in the Rails Guide and similar coding is available for PHP as well.
  3. We are sending POST data in the HTTP body that is formatted in application/x-www-form-urlencoded as opposed to application/json. The former is native to the browser and therefore no extra work is necessary to send forms in this format. Combined with the former item, this makes preparation of the form data for sending totally unnecessary.
  4. This form is an uncontrolled component. <input> tags do not have to update state via callback functions.
  5. Client side validation can be handled by HTML5 validation features, or by separate JavaScript callbacks that can be bound to the form via event listeners. This is not shown here.

One feature of submitForm() is that client-side code defines the response to successful and failed requests. In Rails ERB code (and also in Inertia.js), server-side code is responsible for determining where to redirect after a success, or how to show errors. In our submitForm(), the client decides, giving the front-end team maximum flexibility over the UI without having to touch the back-end code.

Configuring the loading indicator (the Loader component)

We have provided a Loader component so that you can displaying spinners to indicate that a form has been submitted. This component exports showLoader() and hideLoader() which are used inside submitForm() to control the display of the loader.

nextjs/components/layout/application.js

export default function Application({children, breadcrumbs, actionButton}) {
  return (
    <>
      <div className="relative">
        <Loader className="absolute top-3 right-3 z-10" width="200" height="200" />
      </div>

      ...
    </>
  )
}

FormTo()

This further abstracts away form management so that you send forms with no custom JavaScript. The following is sufficient to a) client-side validation to ensure a value is entered, b) send a form, c) show the loading indicator, and d) redirect to the created resource, client side. CSRF tokens are automatically sent.

<FormTo action="/answers" method="post">
  <input name="answer[value]" required />
</FormTo>

If you need to customise how the response or errors are handled, you can provide success and failure callbacks as extra props.

ButtonTo()

There are many cases where you need to send a non-GET request with a simple button interface (without any input forms). The ButtonTo() component is inspired by the button_to helper in Rails, and allows you to do just this.

The following code creates a <button> tag that is wrapped by a <form> tag that sends a non-GET request to the specified action. CSRF tokens are automatically sent.

<ButtonTo action={`/exams?exam[material_id]=${material.id}`}
          method="post"
          className="btn btn--primary"
          success={ (response, data) => router.push(`/answers/new?exam_id=${data.id}`) }
          data-name='js-text-btn'>確認テストを受ける</ButtonTo>