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 { action : string ; } export interface IValues { [ key : string ] : any ; } export interface IErrors { [ key : string ] : string ; } export interface IFormState { values : IValues ; errors : IErrors ; 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 } ; } private haveErrors ( errors : IErrors ) { let haveError : boolean = false ; Object . keys ( errors ) . map ( ( key : string ) => { if ( errors [ key ] . length > 0 ) { haveError = true ; } } ) ; return haveError ; } 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 { return true ; } /** * Submits the form to the http api * @returns {boolean} - Whether the form submission was successful or not */ private async submitForm(): Promise < boolean > { return true ; } public render() { const { submitSuccess , errors } = this . state ; return ( < form onSubmit = { this . handleSubmit } noValidate = { true } > < div className = " container " > { } < 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" ; type Editor = "textbox" | "multilinetextbox" | "dropdown" ; export interface IFieldProps { id : string ; label ? : string ; editor ? : Editor ; options ? : string [ ] ; 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 > ) } { } </ 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 { action : string ; 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 …