Handling Forms With React and HTML5 Form Validation API
In this tutorial, we will take a look at how to handle form submission and validation by using the modern combination of React and HTML5.
Join the DZone community and get the full member experience.
Join For Freewhen we talk about user input within a web app we often think first of html forms. web forms have been available since the very first editions of html. apparently, the feature was introduced already in 1991 and standardized in 1995 as rfc 1866 . we use them everywhere, with almost every library and framework. but what about react? facebook gives a limited input on how to deal with forms . mainly it's about subscribing forms and controls for interaction events and passing state with the "value" property. so form validation and submission logic are up to you. decent ui implies you cover such logic as "on submit"/"on input" field validation, inline error messaging, toggling elements depending on validity, "pristine," "submitting," states, and more. can we not abstract this logic and simply plug it in our forms? we definitely can. the only question is what approach and solution to pick up.
forms in a devkit
if you go with a devkit like reactbootstrap or antdesign you are likely already happy with the forms. both provide components to build a form that meets diverse requirements. for example, in antdesign we define a form with the form element and a form field with formitem , which is a wrapper for any input control out of the set. you can set validation rules on formitem like:
<formitem>
{getfielddecorator('select', {
rules: [
{ required: true, message: 'please select your country!' },
],
})(
<select placeholder="please select a country">
<option value="china">china</option>
<option value="use">u.s.a</option>
</select>
)}
</formitem>
then, for instance, in form submission handler, you can run
this.props.form.validatefields()
to apply validation. it looks like everything is taken care of. yet the solution is specific to the framework. if you do not work with a devkit, you cannot benefit from its features.
building form based on schema
alternatively, we can go with a standalone component that builds forms for us based on provided json specs. for example, we can import the winterfell component and build a form as simple as that:
<winterfell schema={loginschema} ></winterfell>
however, the schema can be quite complex . besides, we bind ourselves to a custom syntax. another solution, react-jsonschema-form , looks similar but relies on json schema . json schema is a project-agnostic vocabulary designed to annotate and validate json documents. yet it binds us to the only features implemented in the builder and defined in the schema.
formsy
i would prefer a wrapper for my arbitrary html form that would take care of validation logic. here, one of the most popular solutions is
formsy
. what does it look like? we create our own component for a form field and wrap it with hoc,
withformsy
:
import { withformsy } from "formsy-react";
import react from "react";
class myinput extends react.component {
changevalue = ( event ) => {
this.props.setvalue( event.currenttarget.value );
}
render() {
return (
<div>
<input
onchange={ this.changevalue }
type="text"
value={ this.props.getvalue() || "" }
/>
<span>{ this.props.geterrormessage() }</span>
</div>
);
}
}
export default withformsy( myinput );
as you can see, the component receives the
geterrormessage()
function in props, which we can use for inline error messaging.
so we made a field component. let's place it in a form:
import formsy from "formsy-react";
import react from "react";
import myinput from "./myinput";
export default class app extends react.component {
onvalid = () => {
this.setstate({ valid: true });
}
oninvalid = () => {
this.setstate({ valid: false });
}
submit( model ) {
//...
}
render() {
return (
<formsy onvalidsubmit={this.submit} onvalid={this.onvalid} oninvalid={this.oninvalid}>
<myinput
name="email"
validations="isemail"
validationerror="this is not a valid email"
required
></myinput>
<button type="submit" disabled={ !this.state.valid }>submit</button>
</formsy>
);
}
}
we specify all the required field validators with the
validations
property (see the
list of available validators
). with
validationerror
,
we set the desired validation message and receive from the form validity state in
onvalid
and
oninvalid
handlers.
that looks simple, clean, and flexible. but i wonder why we don't rely on html5 built-in form validation , rather than going with countless custom implementations.
html5 form validation
the technology emerged quite a while ago. the first implementation came together with opera 9.5 in 2008. nowadays, it is available in all the modern browsers. form (data) validation introduces extra html attributes and input types, that can be used to set form validation rules. the validation can be also controlled and customized from javascript by using a dedicated api .
let's examine the following code:
<form>
<label for="answer">what do you know, jon snow?</label>
<input id="answer" name="answer" required>
<button>ask</button>
</form>
it's a simple form, except for one thing - the input element has a
required
attribute. so if we press the submit button immediately, the form won't be sent to the server. instead, we will see a tooltip next to the input saying that the value doesn't comply the given constraint (i.e. the field should not be empty).
now we set the input an additional constraint:
<form>
<label for="answer">what do you know, jon snow?</label>
<input id="answer" name="answer" required pattern="nothing|nix">
<button>ask</button>
</form>
so the value is not just required, but must comply to the regular expression given with attribute
pattern
.
the error message isn't that informative though, is it? we can customize it (for example, to explain what exactly we expect from the user) or just translate:
<form>
<label for="answer">what do you know, jon snow?</label>
<input id="answer" name="answer" required pattern="nothing|nix">
<button>ask</button>
</form>
const answer = document.queryselector( "[name=answer]" );
answer.addeventlistener( "input", ( event ) => {
if ( answer.validity.patternmismatch ) {
answer.setcustomvalidity("oh, it's not a right answer!");
} else {
answer.setcustomvalidity( "" );
}
});
so basically, on an input event, it checks the state of the
patternmismatch
property of the input validity state. any time the actual value doesn't match the pattern we define the error message. if we have any
other constraints
on the control, we can also cover them in the event handler.
not happy with the tooltips? yeah, they don't look the same in different browsers. let's add the following code:
<form novalidate>
<label for="answer">what do you know, jon snow?</label>
<input id="answer" name="answer" required pattern="nothing|nix">
<div data-bind="message"></div>
<button>ask</button>
</form>
const answer = document.queryselector( "[name=answer]" ),
answererror = document.queryselector( "[name=answer] + [data-bind=message]" );
answer.addeventlistener( "input", ( event ) => {
answererror.innerhtml = answer.validationmessage;
});
even with just this super brief introduction, you can see the power and flexibility of the technology. native form validation is pretty great. so why are we relying on countless custom libraries? why not going with built-in validation?
react meets form validation api
react-html5-form
connects react (and, optionally, redux) to the html5 form validation api. it exposes the components
form
and
inputgroup
(similar to formsy's custom input or
formitem
in antdesign). so
form
defines the form and its scope and
inputgroup
defines the scope of the field, which can have one or more inputs. we simply wrap an arbitrary form content (just plain html or react components) with these components. on user events, we can request form validation and get the updated states of the
form
and
inputgroup
components, accordingly, to underlying input validity.
well, let's see it in practice. first, we define the form scope:
import react from "react";
import { render } from "react-dom";
import { form, inputgroup } from "form";
const myform = props => (
<form>
{({ error, valid, pristine, submitting, form }) => (
<>
form content
<button disabled={ ( pristine || submitting ) } type="submit">submit</button>
</>
)}
</form>
);
render( <myform ></myform>, document.getelementbyid( "app" ) );
the scope receives state object with properties:
-
error - form error message (usually server validation message). we can set it with
form.seterror()
. - valid - boolean indicating if all the underlying inputs comply with the specified constraints.
- pristine - boolean indicating if the user has not interacted with the form yet.
-
submitting - boolean indicating the form is being processed (switches to true when the submit button pressed and back to false as soon as user-defined asynchronous
onsubmit
handler resolves). - form - instance of form component to access the api.
here we use just
pristine
and
submitting
properties to switch the submit button to a disabled state.
in order to register inputs for validation while contributing form content, we wrap them with
inputgroup
<inputgroup validate={[ "email" ]} }}>
{({ error, valid }) => (
<div>
<label htmlfor="emailinput">email address</label>
<input
type="email"
required
name="email"
id="emailinput" />
{ error && (<div classname="invalid-feedback">{error}</div>) }
</div>
)}
</inputgroup>
with the
validate
prop, we specify what inputs of the group shall be registered.
[ "email" ]
means we have the only input, with the name "email" applied to it.
in the scope, we receive the state object with the following properties:
- errors - an array of error messages for all the registered inputs.
- error - the last emitted error message.
- valid - boolean indicating if all the underlying inputs comply with the specified constraints.
- inputgroup - instance of the component to access the api.
after rendering, we get a form with an email field. if the value is empty or contains an invalid email address on submission, it shows the corresponding validation message next to the input.
remember we were struggling with customizing errors messages while using native form validation api? it's much easier with
inputgroup
:
<inputgroup
validate={[ "email" ]}
translate={{
email: {
valuemissing: "c'mon! we need some value",
typemismatch: "hey! we expect an email address here"
}
}}>
...
we can specify a map per input, where keys are validity properties and values are custom messages.
well, the message customization was easy. what about custom validation? we can do that through the
validate
prop:
<inputgroup validate={{
"email": ( input ) => {
if ( !email_whitelist.includes( input.current.value ) ) {
input.setcustomvalidity( "only whitelisted email allowed" );
return false;
}
return true;
}
}}>
...
in this case, instead of an array of input names, we provide a map, where keys are input names and values are validation handlers. the handler checks the input value (can be done asynchronously) and returns the validity's state as a boolean. with
input.setcustomvalidity
,
we assign a case-specific validation message.
validation upon submission isn't always what we want. let's implement an "on-the-fly" validation. first, we define an event handler for input event:
const oninput = ( e, inputgroup ) => {
inputgroup.checkvalidityandupdate();
};
actually, we just make the input group re-validate every time the user types in the input. we subscribe the control as follows:
<input
type="email"
required
name="email"
oninput={( e ) => oninput( e, inputgroup, form ) }
id="emailinput" />
from now on, as soon as we change the input value it gets validated and if it's invalid we immediately receive the error message.
you can find the source code of a demo with examples from above.
by the way, do you fancy connecting the component-derived form state-tree to a redux store? we can do that also.
the package exposes the reducer
html5form
containing state-trees of all the registered forms. we can connect it to the store like this:
import react from "react";
import { render } from "react-dom";
import { createstore, combinereducers } from "redux";
import { provider } from "react-redux";
import { app } from "./containers/app.jsx";
import { html5form } from "react-html5-form";
const appreducer = combinereducers({
html5form
});
// store creation
const store = createstore( appreducer );
render( <provider store={store}>
<app ></app>
</provider>, document.getelementbyid( "app" ) );
now, as we run the application, we can find all form related states in the store.
here is the source code of a dedicated demo.
recap
react has no built-in form validation logic. yet we can use third-party solutions. so it can be a devkit, it can be a form builder, it can be a hoc or wrapper component mixing form validation logic into arbitrary form content. my personal approach is a wrapper component that relies on html built-in form validation apis and exposes validity state in scopes of a form and a form field.
Published at DZone with permission of Dmitry Sheiko, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments