Building a React Form Component with TypeScript: The Basics
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:
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.
If you to learn more about using TypeScript with React, you may find my course useful: