Carl Rippon

Building SPAs

Carl Rippon
BlogBooks / CoursesAbout
This site uses cookies. Click here to find out more

Building a React Form Component with TypeScript: The Basics

June 05, 2018
reacttypescript

This is the second post in a series of blog posts where we are building our own super simple form component in React and TypeScript. In the last post we created our project. In this post we are going to implement very basic Form and Field components. We’ll use the render props pattern so that any content can be injected into the form. We’ll also create the 1st version of our “contact us” form.

Basic Form

Okay, let’s start by creating a file called Form.tsx for our Form component in the src folder and add the code below which gives us a starting point for our form.

The form simply renders a form element containing a submit button. If the submit button is pressed, a “The form was successfully submitted!” message appears.

import * as React from "react";

interface IFormProps {
  /* The http path that the form will be posted to */
  action: string;
}

export interface IValues {
  /* Key value pairs for all the field values with key being the field name */
  [key: string]: any;
}

export interface IErrors {
  /* The validation error messages for each field (key is the field name */
  [key: string]: string;
}

export interface IFormState {
  /* The field values */
  values: IValues;

  /* The field validation error messages */
  errors: IErrors;

  /* Whether the form has been successfully submitted */
  submitSuccess?: boolean;
}

export class Form extends React.Component<IFormProps, IFormState> {
  constructor(props: IFormProps) {
    super(props);

    const errors: IErrors = {};
    const values: IValues = {};
    this.state = {
      errors,
      values
    };
  }

  /**
   * Returns whether there are any errors in the errors object that is passed in
   * @param {IErrors} errors - The field errors
   */
  private haveErrors(errors: IErrors) {
    let haveError: boolean = false;
    Object.keys(errors).map((key: string) => {
      if (errors[key].length > 0) {
        haveError = true;
      }
    });
    return haveError;
  }

  /**
   * Handles form submission
   * @param {React.FormEvent<HTMLFormElement>} e - The form event
   */
  private handleSubmit = async (
    e: React.FormEvent<HTMLFormElement>
  ): Promise<void> => {
    e.preventDefault();

    if (this.validateForm()) {
      const submitSuccess: boolean = await this.submitForm();
      this.setState({ submitSuccess });
    }
  };

  /**
   * Executes the validation rules for all the fields on the form and sets the error state
   * @returns {boolean} - Whether the form is valid or not
   */
  private validateForm(): boolean {
    // TODO - validate form
    return true;
  }

  /**
   * Submits the form to the http api
   * @returns {boolean} - Whether the form submission was successful or not
   */
  private async submitForm(): Promise<boolean> {
    // TODO - submit the form
    return true;
  }

  public render() {
    const { submitSuccess, errors } = this.state;
    return (
      <form onSubmit={this.handleSubmit} noValidate={true}>
        <div className="container">
          {/* TODO - render fields */}
          <div className="form-group">
            <button
              type="submit"
              className="btn btn-primary"
              disabled={this.haveErrors(errors)}
            >
              Submit
            </button>
          </div>
          {submitSuccess && (
            <div className="alert alert-info" role="alert">
              The form was successfully submitted!
            </div>
          )}
          {submitSuccess === false &&
            !this.haveErrors(errors) && (
              <div className="alert alert-danger" role="alert">
                Sorry, an unexpected error has occurred
              </div>
            )}
          {submitSuccess === false &&
            this.haveErrors(errors) && (
              <div className="alert alert-danger" role="alert">
                Sorry, the form is invalid. Please review, adjust and try again
              </div>
            )}
        </div>
      </form>
    );
  }
}

The form is structured to perform validation and give information to users about any problems but this isn’t fully implemented yet. Likewise, the form submission process needs fully implementing. We’ll comeback to this stuff later in the post along with implementing an instance of the Form component so that we can see this in action.

Basic Field

Let’s make a start on a Field component now. Let’s create a file called Field.tsx in the src folder and paste in the code below.

Our stateless Field component takes props for the field name, the label text as well as details of the editor. We have used defaultProps to make a field with a text input appear by default, if no props are supplied.

We render the label with the appropriate editor (a input, textarea or select).

import * as React from "react";
import { IErrors } from "./Form";

/* The available editors for the field */
type Editor = "textbox" | "multilinetextbox" | "dropdown";

export interface IFieldProps {
  /* The unique field name */
  id: string;

  /* The label text for the field */
  label?: string;

  /* The editor for the field */
  editor?: Editor;

  /* The drop down items for the field */
  options?: string[];

  /* The field value */
  value?: any;
}

export const Field: React.SFC<IFieldProps> = ({
  id,
  label,
  editor,
  options,
  value
}) => {
  return (
    <div className="form-group">
      {label && <label htmlFor={id}>{label}</label>}

      {editor!.toLowerCase() === "textbox" && (
        <input
          id={id}
          type="text"
          value={value}
          onChange={
            (e: React.FormEvent<HTMLInputElement>) =>
              console.log(e) /* TODO: push change to form values */
          }
          onBlur={
            (e: React.FormEvent<HTMLInputElement>) =>
              console.log(e) /* TODO: validate field value */
          }
          className="form-control"
        />
      )}

      {editor!.toLowerCase() === "multilinetextbox" && (
        <textarea
          id={id}
          value={value}
          onChange={
            (e: React.FormEvent<HTMLTextAreaElement>) =>
              console.log(e) /* TODO: push change to form values */
          }
          onBlur={
            (e: React.FormEvent<HTMLTextAreaElement>) =>
              console.log(e) /* TODO: validate field value */
          }
          className="form-control"
        />
      )}

      {editor!.toLowerCase() === "dropdown" && (
        <select
          id={id}
          name={id}
          value={value}
          onChange={
            (e: React.FormEvent<HTMLSelectElement>) =>
              console.log(e) /* TODO: push change to form values */
          }
          onBlur={
            (e: React.FormEvent<HTMLSelectElement>) =>
              console.log(e) /* TODO: validate field value */
          }
          className="form-control"
        >
          {options &&
            options.map(option => (
              <option key={option} value={option}>
                {option}
              </option>
            ))}
        </select>
      )}

      {/* TODO - display validation error */}
    </div>
  );
};
Field.defaultProps = {
  editor: "textbox"
};

We have lots of TODOs where we need to reference state and functions from the Form component which we’ll get to later.

Rendering fields using render props

Okay, now let’s start to make Form and Field work together. We’ll start by rendering fields in the appropriate place in the Form component using the render props pattern

So, first we’ll create the render prop:

interface IFormProps {
  /* The http path that the form will be posted to */
  action: string;

  /* A prop which allows content to be injected */
  render: () => React.ReactNode;
}

We’ll then make use of this in render():

public render() {
  const { submitSuccess, errors } = this.state;
  return (
    <form onSubmit={this.handleSubmit} noValidate={true}>
      <div className="container">

        {this.props.render()}

        <div className="form-group">
          <button
            type="submit"
            className="btn btn-primary"
            disabled={this.haveErrors(errors)}
        >
          Submit
        </button>
      </div>
      ...
      </div>
    </form>
  );
}

this.props.render() will simply render the injected content.

Creating ContactUsForm

Let’s build the first version of the “contact us” form by creating ContactUsForm.tsx and pasting in the following code:

import * as React from "react";
import { Form } from "./Form";
import { Field } from "./Field";

export const ContactUsForm: React.SFC = () => {
  return (
    <Form
      action="http://localhost:4351/api/contactus"
      render={() => (
        <React.Fragment>
          <div className="alert alert-info" role="alert">
            Enter the information below and we'll get back to you as soon as we
            can.
          </div>
          <Field id="name" label="Name" />
          <Field id="email" label="Email" />
          <Field
            id="reason"
            label="Reason"
            editor="dropdown"
            options={["", "Marketing", "Support", "Feedback", "Jobs"]}
          />
          <Field id="notes" label="Notes" editor="multilinetextbox" />
        </React.Fragment>
      )}
    />
  );
};

If we npm start the app, it should look like the following:

Contact us form

Wrapping up

This is a great start and it’s fantastic we’ve got to the point of rendering our “contact us” form. However, there is lots of work still to do …

In the next post we’ll use the context api to share state and functions between Form and Field. This will enable us to start to manage the field values properly.

Did you find this post useful?

Let me know by sharing it on Twitter.
Click here to share this post on Twitter

If you to learn more about using TypeScript with React, you may find my course useful:

Using TypeScript with React

Find out more