This is the fourth 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 leveraged the context api to encapsulate the managing of form values. In this post we’ll tackle validation - a must for any form.

We haven’t validated any of the field values yet but we obviously need to do so. We are going to validate when a field loses focus as well as during form submission.

Refactoring some code

We are going to put a validation function in Form . This means that Form needs to know about all the fields within it. So, we are going to create a fields prop and refactor some code …

Here’s the new fields prop which is an object literal containing IFieldProps :

export interface IFields { [ key : string ] : IFieldProps ; } interface IFormProps { action : string ; fields : IFields ; render : ( ) => React . ReactNode ; }

This means we need to refactor ContactUsForm :

import * as React from "react" ; import { Form , IFields } from "./Form" ; import { Field } from "./Field" ; export const ContactUsForm : React . SFC = ( ) => { const fields : IFields = { name : { id : "name" , label : "Name" } , email : { id : "email" , label : "Email" } , reason : { id : "reason" , label : "Reason" , editor : "dropdown" , options : [ "" , "Marketing" , "Support" , "Feedback" , "Jobs" ] } , notes : { id : "notes" , label : "Notes" , editor : "multilinetextbox" } } ; return ( < Form action = " http://localhost:4351/api/contactus " fields = { fields } 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 { ... fields . name } /> < Field { ... fields . email } /> < Field { ... fields . reason } /> < Field { ... fields . notes } /> </ React.Fragment > ) } /> ) ; } ;

Validation state

We already have state in Form for validation errors:

export interface IErrors { [ key : string ] : string ; } export interface IFormState { values : IValues ; errors : IErrors ; submitSuccess ? : boolean ; }

Validator functions

Now that we have the right structure for validation, let’s get on with creating some functions that are going to do the validation …

Let’s create some validator functions in Form :

export const required = ( values : IValues , fieldName : string ) : string => values [ fieldName ] === undefined || values [ fieldName ] === null || values [ fieldName ] === "" ? "This must be populated" : "" ; export const isEmail = ( values : IValues , fieldName : string ) : string => values [ fieldName ] && values [ fieldName ] . search ( /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/ ) ? "This must be in a valid email format" : "" ; export const maxLength = ( values : IValues , fieldName : string , length : number ) : string => values [ fieldName ] && values [ fieldName ] . length > length ? ` This can not exceed ${ length } characters ` : "" ;

Specifying validation on a field

With our validator functions in place let’s introduce a prop to Field to allow consumers to add validation:

export interface IValidation { rule : ( values : IValues , fieldName : string , args : any ) => string ; args ? : any ; } export interface IFieldProps { ... validation ? : IValidation ; }

The above allows consumers to add any validator function that satisfies the ( IValues, string, any) => string signature.

Driving validation from Form

Moving back to Form , let’s create that validate() function we talked about earlier. This function invokes the validator function if there is one and adds the validation error to the errors state.

private validate = ( fieldName : string ) : string => { let newError : string = "" ; if ( this . props . fields [ fieldName ] && this . props . fields [ fieldName ] . validation ) { newError = this . props . fields [ fieldName ] . validation ! . rule ( this . state . values , fieldName , this . props . fields [ fieldName ] . validation ! . args ) ; } this . state . errors [ fieldName ] = newError ; this . setState ( { errors : { ... this . state . errors , [ fieldName ] : newError } } ) ; return newError ; } ;

We can now expose validate() in IFormContext :

export interface IFormContext extends IFormState { setValues : ( values : IValues ) => void ; validate : ( fieldName : string ) => void ; }

So, let’s add this to the instance of IFormContext in Form.render() :

const context : IFormContext = { ... this . state , setValues : this . setValues , validate : this . validate } ;

Calling Form.validate from Field

Moving back to Field , let’s call validate() from IFormContext when the editor loses focus:

{ editor ! . toLowerCase ( ) === "textbox" && ( < input ... onBlur = { ( ) => context . validate ( id ) } ... /> ) } { editor ! . toLowerCase ( ) === "multilinetextbox" && ( < textarea ... onBlur = { ( ) => context . validate ( id ) } ... /> ) } { editor ! . toLowerCase ( ) === "dropdown" && ( < select ... onBlur = { ( ) => context . validate ( id ) } ... </ select > ) }

Showing the validation errors

So, when the editor loses focus, validation should now occur and errors in Form state should be set. The validation errors aren’t rendering though, so, let’s implement that in Field …

Let’s display the validation error under the label and editor:

* Gets the validation error for the field * @param {IErrors} errors - All the errors from the form * @returns {string[]} - The validation error */ const getError = ( errors : IErrors ) : string => ( errors ? errors [ id ] : "" ) ; ... return ( < FormContext.Consumer > { ( context : IFormContext ) => ( < div className = " form-group " > ... { getError ( context . errors ) && ( < div style = { { color : "red" , fontSize : "80%" } } > < p > { getError ( context . errors ) } </ p > </ div > ) } </ div > ) } </ FormContext.Consumer > ) ;

Let’s also highlight the editor if the field is invalid:

* Gets the inline styles for editor * @param {IErrors} errors - All the errors from the form * @returns {any} - The style object */ const getEditorStyle = ( errors : IErrors ) : any => getError ( errors ) ? { borderColor : "red" } : { } ; ... return ( < FormContext.Consumer > { ( context : IFormContext ) => ( < div className = " form-group " > { editor ! . toLowerCase ( ) === "textbox" && ( < input ... style = { getEditorStyle ( context . errors ) } ... /> ) } { editor ! . toLowerCase ( ) === "multilinetextbox" && ( < textarea ... style = { getEditorStyle ( context . errors ) } ... /> ) } { editor ! . toLowerCase ( ) === "dropdown" && ( < select ... style = { getEditorStyle ( context . errors ) } ... /> ) } ... </ div > ) } </ FormContext.Consumer > ) ;

Adding validation to ContactUsForm

Okay, now that we’ve implemented all these validation bits in Form and Field , let’s make use of this in ContactUsForm …

We want the person’s name and the reason for contact to be required fields. The person’s email should be a valid email. The notes should also be limited to 1000 characters.

The code changes are below:

import * as React from "react" ; import { Form , IFields , required , isEmail , maxLength } from "./Form" ; import { Field } from "./Field" ; export const ContactUsForm : React . SFC = ( ) => { const fields : IFields = { name : { id : "name" , label : "Name" , validation : { rule : required } } , email : { id : "email" , label : "Email" , validation : { rule : isEmail } } , reason : { id : "reason" , label : "Reason" , editor : "dropdown" , options : [ "" , "Marketing" , "Support" , "Feedback" , "Jobs" ] , validation : { rule : required } } , notes : { id : "notes" , label : "Notes" , editor : "multilinetextbox" , validation : { rule : maxLength , args : 1000 } } } ; return ( ... ) ; } ;

Here’s a screen shot of the form in an invalid state:

Performing validation on form submission

There’s 1 remaining validation bit to implement. This is validating all the fields during the submission process. To do this we need to implement validateForm() that was created in the first post …

private validateForm ( ) : boolean { const errors : IErrors = { } ; Object . keys ( this . props . fields ) . map ( ( fieldName : string ) => { errors [ fieldName ] = this . validate ( fieldName ) ; } ) ; this . setState ( { errors } ) ; return ! this . haveErrors ( errors ) ; }

So, if we hit the submit button without filling in the form, all the validation rules will be invoked, the errors will be displayed and the form won’t be submitted.

Wrapping up

Our components are looking fairly sophisticated with the inclusion of basic validation capabilities. Validation in practice can be a lot more complex though … for example, having multiple rules for a field (e.g. having the email required as well as a valid format). Validation rules also sometimes need to be asynchronous (e.g. the rule invokes a web api) … Our components can be expanded to deal with these complexities but this is beyond the scope of what I plan to cover in these blog posts.

The final bit we need to implement in our components is submitting our form to our web api. We’ll cover that in our next post.