Forms are a necessary evil in most applications. I say evil as it is usually a struggle to deal with, to make them behave the way you want and for them to be as flexible as possible. We usually resort to using a library cause face it, forms are usually not that fun.

The below picture is meant to represent the calm state of mind I hope you will be in after having read this article, so I hope you enjoy it :)

How the Form reacts when the user interacts with it can differ and there is really no silver bullet here, but variants I’ve seen are:

instant validation , the content of an element is validated as soon as you type and if there are any errors this is being indicated right away

, the content of an element is validated as soon as you type and if there are any errors this is being indicated right away focus lost , an element is validated first when you move on to another element, this is slightly less in your face than the first version

, an element is validated first when you move on to another element, this is slightly less in your face than the first version dependant validation , the value of an element depends on the value of one or more other elements

, the value of an element depends on the value of one or more other elements server-side validation , the validness of an input element cannot be determined unless you do a call to an endpoint and get a response back

, the validness of an input element cannot be determined unless you do a call to an endpoint and get a response back disabled submit , in this scenario, if one or more elements are invalid you cannot press the submit button cause it is disabled

, in this scenario, if one or more elements are invalid you cannot press the submit button cause it is disabled hidden submit , in this scenario the submit button is simply not shown

, in this scenario the submit button is simply not shown working submit, in this scenario, you have to press the submit button to find out you have validation errors, a bit like slot machine :)

The above is not an exhaustive list but it shows that forms, as well as input elements, can behave in quite a different way.

Architectural Approach

You can have all the elements and their validation in one giant component, that consists of the form and all its input elements, but it is usually better to create dedicated components for the form as well as the input elements. Let’s talk about the input elements, in the form, for a second:

It should have the following properties:

render the element itself , this means this component will act as wrapper over the actual HTML input element, this will give us more control over how the element is rendered

, this means this component will act as wrapper over the actual HTML input element, this will give us more control over how the element is rendered should output the validation error , yes you can collect all the errors at the bottom of the form but it is usually a better idea to indicate on the element with a red border and an error message next to the element so the user understands what element they need to fix

, yes you can collect all the errors at the bottom of the form but it is usually a better idea to indicate on the element with a red border and an error message next to the element so the user understands what element they need to fix listen to changes , the element should be able to capture keystrokes or know when the element loses focus so it’s content can be validated

, the element should be able to capture keystrokes or know when the element loses focus so it’s content can be validated report to the form, the element should be able to report to the form and say which element is failing in the validation and in what way, so the form can take an appropriate measure like for example disabling or hiding the submit button

Working with input elements

Ok, so we said we need to create a component that wraps itself around an HTML element. To do so is quite simple:

import React from 'react'; class Input extends React.Component {

render() {

return (

<input { ...this.props} />

);

}

}

As you can see above we make the Input component render an input element tag. We also ensure that all properties set on the component make it to the underlying input by typing { ...this.props} . A more manual version of the last one would be to type:

<input title={this.props.title} value={this.props.value} ... >

Depending on how many attributes we want to send to the underlying input, it could be quite a lot of typing.

As we are now in control of how the input is rendered we can do all sorts of things like adding a div element, give it padding, margin, borders etc. The best part is we can reuse this component in a lot of places and all of our inputs will look nice and consistent.

Adding validation

Now we will see that it pays off to wrap our input element in a component. Adding validation to our element is as easy as:

add an error placeholder , this is just adding a place in our component where the errors should be shown

, this is just adding a place in our component where the errors should be shown add a function that validates the input value

validate on every value change, we need to add a callback to onChange

Render the error

Alter the render() method to the below:

render() {

const { error, data } = this.state;

const { desc } = this.props; return (

<InputContainer>

{error &&

<ErrorMessage>{error}</ErrorMessage>

}

<div>

{desc}

</div>

<InnerInput

value={data}

onChange={this.handleChange} {...this.props}

/>

</InputContainer>

);

}

Here we are conditionally displaying the error message, assuming it's on the state:

{error &&

<ErrorMessage>{error}</ErrorMessage>

}

We are also hooking up a handleChange() method to onChange .

Adding validation function

Next step is adding our validation function:

const validate = (val, errMessage, pattern) => {

const valid = new RegExp(pattern).test(val);

return valid ? '' : errMessage;

};

Our function above simply tests whether our input value matches a RegEx pattern and if so it's valid, if not, then we return the error message.

Managing the state

So who is calling this function? Well, the handleChange() method is, like so:

handleChange = (ev) => {

const { errMessage } = this.props;

const error = validate(ev.target.value, errMessage); this.setState({ data: ev.target.value, error, });

}

We do two things here, firstly we call validate() to see if there was an error and secondly we set the state, which is our value and the error. If the error is an empty string then it is counted as falsy . So we can always safely set the error property and any error message would only be visible when it should be visible.

The full code for our component so far looks like this:

// Input.js import React from 'react';

import styled from 'styled-components';

import PropTypes from 'prop-types'; const InnerInput = styled.input`

font-size: 20px;

`; const InputContainer = styled.div`

padding: 20px;

border: solid 1px grey;

`; const ErrorMessage = styled.div`

padding: 20px;

border: red;

background: pink;

color: white;

`; const validate = (val, errMessage, pattern) => {

const valid = new RegExp(pattern).test(val);

return valid ? '' : errMessage;

}; class Input extends React.Component {

static propTypes = {

name: PropTypes.string,

desc: PropTypes.string,

errMessage: PropTypes.string,

pattern: PropTypes.string,

}; state = {

error: '',

data: '',

} handleChange = (ev) => {

const {

errMessage,

name,

pattern

} = this.props; const error = validate(ev.target.value, errMessage, pattern);



this.setState({ data: ev.target.value, error, });

} render() {

const { error, data } = this.state;

const { desc } = this.props;

return (

<InputContainer>

{error &&

<ErrorMessage>{error}</ErrorMessage>

}

<div> {desc} </div>

<InnerInput

value={data}

onChange={this.handleChange} {...this.props}

/>

</InputContainer> );

}

} export default Input;

Communicating with the Form

Usually, when you put input elements in a form you want to be able to tell the form that one or more invalid inputs exist and you want the stop the form from being submitted. To do so we need send a message to our form every time a value changes and if there is a validation error, the form will know. To accomplish that we need to do the following:

add a notify input property, this will be a function we can call as soon as we validated the latest change

input property, this will be a function we can call as soon as we validated the latest change call the notify function

We, therefore, update our handleChange() method to now make a call to the notify() function that we pass in, like so:

handleChange = (ev) => {

const { errMessage, name, notify, pattern } = this.props;

const error = validate(ev.target.value, errMessage, pattern);



notify(name, error === '');

this.setState({ data: ev.target.value, error, });

}

notify() is called with two params, name and whether it is valid.

Setting up the form

Ok great, we have a way to communicate errors back to the form, what about the form itself, what does it need to do for this to work? It needs the following:

a method , that it can hook up the notify property

, that it can hook up the property determine what to do, if one or more elements are invalid, like for example disable the submit button

We decide on creating a dedicated component for our form as well:

// Form.js import React from 'react';

import styled from 'styled-components';

import Input from './Input'; const FormContainer = styled.form`

border: solid 2px;

padding: 20px;

`; class Form extends React.Component {

state = { isValid: true, } notify = (name, isValid) => { /* to be defined */ }



render() {

return (

<FormContainer>

<div>

<Input

errMessage="Must contain 2-3 digits"

pattern=" ^[0-9]{2,3}$ "

desc="2-3 characters"

name="first-name"

notify={this.notify}

title="I am a custom inbox" />

</div>

<button>

Submit

</button>

</FormContainer>

);

}

} export default Form;

At this point, we have hooked up our notify input property to a method on our component called notify() , like so:

<Input

errMessage="Must contain 2-3 digits"

pattern=" ^[0-9]{2,3}$ "

desc="2-3 characters"

name="first-name"

notify={this.notify}

title="I am a custom inbox"

/>

As you can see our notify() method doesn't do much yet, but it will:

notify = (name, isValid) => { /* to be defined */}

So what do we need to accomplish with a call to notify() ? The first thing we need to accomplish is telling the form that one of your inputs is invalid. The other is to set the whole form as invalid. Based on that we define our notify() code as the following:

notify = (name, isValid) => {

this.setState(

{ [name]: isValid, },

() => {

this.setState({

isValid: this.validForm(),

});

});

}

We see above that we after having updated our state for our input element we set the state for isValid and call the method validForm() to determine its value. The reason for setting the isValid state like this is that setState() doesn't happen straight away so it is only in the callback that we can guarantee that its state has been updated.

isValid is the property we will use in the markup to determine whether our form is valid. Let's define the method validForm() next:

validForm = () => {

const keys = Object.keys(this.state);

for (let i = 0; i < keys.length; i++) {

if (keys[i] === 'isValid') {

continue;

}

if (!this.state[keys[i]]) {

return false;

}

}

return true;

}

Above we are looping through our state and is looking for whether one of the input elements are invalid. We skip isValid as that is not an element state.

Determine form state

We have now set everything up to make it easy to indicate whether the form can be submitted or not. We can handle that in two ways:

disabling , the submit button

, the submit button submit and validate, let the user press the submit button but stop the submit from going through

If we do the first variant we only need to change the markup to the following:

render() {

return (

<FormContainer onSubmit={this.handleSubmit}>

<div>

<Input

errMessage="Must contain 2-3 digits"

pattern=" ^[0-9]{2,3}$ "

desc="2-3 characters"

name="first-name"

notify={this.notify}

title="I am a custom inbox"

/>

</div>

<button disabled={!this.state.isValid}>Submit</button

</FormContainer>

);

}

Let’s zoom in on the interesting bit:

<button disabled={!this.state.isValid}>Submit</button>

We read from our isValid property and we are to disable our button when we want.

The other version of stopping the submit from going through involves us adding some logic to the method handleSubmit() :

handleSubmit = (ev) => {

ev.preventDefault();

if (!this.state.isValid) {

console.log('form is NOT valid');

} else {

console.log('valid form')

}

}

Full code and results

If you managed to type in everything correctly you should get the following appearance:

The full code looks like this:

// Form.js import React from 'react'; import styled from 'styled-components'; import Input from './Input'; const FormContainer = styled.form` border: solid 2px; padding: 20px; `;

class Form extends React.Component { state = { isValid: true, } validForm = () => { const keys = Object.keys(this.state); for (let i = 0; i < keys.length; i++) { if (keys[i] === 'isValid') {

// filtering away `isValid`, this belongs to the form, as we only want to look at input elements continue; } if (!this.state[keys[i]]) { return false; } } return true; } notify = (name, isValid) => { this.setState(

{ [name]: isValid, }, () => { // this happens after the first `setState` this.setState({ isValid: this.validForm(), }); }); } render() { return (

<FormContainer onSubmit={this.handleSubmit}> <div> <Input

pattern="^[0-9]{2,3}$"

errMessage="Must contain 2-3 digits"

desc="2-3 characters"

name="first-name"

notify={this.notify}

title="I am a custom inbox"

/> <Input

pattern="^[0-9]{2,10}$"

errMessage="Must contain 2-10

digits" desc="2-10

characters"

name="last-name"

notify={this.notify}

title="I am a custom inbox"

/> </div> <button disabled={!this.state.isValid}>Submit</button> </FormContainer>

); } } export default Form;

and the Input component:

// Input.js import React from 'react'; import styled from 'styled-components'; import PropTypes from 'prop-types'; const InnerInput = styled.input` font-size: 20px; `; const InputContainer = styled.div` padding: 20px; border: solid 1px grey; `; const ErrorMessage = styled.div` padding: 20px; border: red; background: pink; color: white; `; const validate = (val, errMessage, pattern) => { const valid = new RegExp(pattern).test(val); return valid ? '' : errMessage; };

class Input extends React.Component { static propTypes = { name: PropTypes.string, desc: PropTypes.string, errMessage: PropTypes.string, }; state = { error: '', data: '', } handleChange = (ev) => { const { errMessage, name, notify, pattern } = this.props; const error = validate(ev.target.value, errMessage, pattern); notify(name, error === ''); this.setState({ data: ev.target.value, error, }); } render() { const { error, data } = this.state; const { desc } = this.props; return ( <InputContainer> {error && <ErrorMessage>{error}</ErrorMessage> } <div> {desc} </div> <InnerInput value={data} onChange={this.handleChange} {...this.props} /> </InputContainer>); } } export default Input;

At this point, you are probably thinking what about checkbox element, radio buttons, drop downs, async validation? Well, I just wanted to describe the idea with a smaller example. Just tell me in the comments and I will add that to the project but hopefully, I’m able to convey the idea anyway :)

Summary

We set out to describe an approach to how we could handle forms. This involved creating wrapper components around HTML input elements as well as the form itself. Thereafter we showed how we could report any validation errors back to the form so it could act in a suiting way. I’m sure there are more elegant solutions than this one but hopefully, you will have an understanding of how you can deal with forms.

Further reading

If you are doing forms and using Redux I recommend that you have a look Redux Forms library

I also recommend you have a look at the official docs on Forms, that is quite well written

Here is a sample project running the code above

My twitter