I personally use to find forms difficult. A lot of forms I have worked with would have fields that'd collect different types of data, perform different types of validation, and I never really had a structure or strategy to work with forms in such a way that I liked. A lot of the components in the forms, such as a text field, date picker, or specialized text fields that'd collect currency data or had requirements around the type of data entered (i.e. integers only, such as a zip code), would sometimes be hard for me to understand due to the varying considerations. I'd also feel my form components bloat significantly.
React to me (pre-hooks) really encouraged me to think more in an object-oriented way, which helped me personally to design forms and components that I liked more than previous libraries or patterns I used. It eventually felt a lot easier to visualize ways of managing forms, in terms of keeping the code DRY as possible, and also empowered me with ways to think about how I could have a relatively consistent interface for all of my input components no matter the data.
With that, this post will go into creating a form that will have 8 fields with 6 different types of validation, and an optional field. The form has a date picker, some text fields, an integer-only fixed-length field, and a currency field.
If you would like to access the code in this repo prior to reading the post, check the code out here. πππ
The following fields below will be in the form along with the validations that will be performed.
- First Name: Cannot be empty.
- Last Name: Cannot be empty.
- E-mail: Cannot be empty.
- Start Date: Cannot be empty (I'll use react-datetime, but this can be changed out to your own component or a library of your choosing).
- Personal URL (Optional): An optional field, that, if filled, must be a valid URL.
- Salary: Cannot be empty. Must be a valid currency (using react-currency-format) to collect currency information.
- Occupation: Cannot be empty.
- Zipcode: Cannot be empty. Is only going to allow for integer inputs.
I will start with a react component, with each field being represented in state with an object:
import React, { Component } from 'react';
class SignUpForm extends Component {
constructor(props) {
super(props);
this.state = {
firstName: {
value: "",
error: false
},
lastName: {
value: "",
error: false
},
email: {
value: "",
error: false
},
startdate: {
value: "",
error: false
},
url: {
value: "",
error: false
},
salary: {
value: "",
quantity: "",
error: false,
errorMessage: "Salary cannot be empty."
},
occupation: {
value: "",
error: false
},
zipcode: {
value: "",
error: false
}
}
}
render() {
return (null)
}
}
export default SignUpForm;
Each object in state for each field has at least two values, value
and error
, set to false. The reason for this is because the form will eventually control the error state of the children field components. The salary
key in state also has two additional properties, quantity
, and errorMessage
. The CurrencyFormat
component we use comes from react-currency-format
, and will return two values when the onChange
event is triggered, a value that represents the formatted currency amount (i.e. '$60,000'), and an unformatted currency amount (i.e. '60000').
The name of each key in state is going to be the name of each 'input' inside our form. So that way, whenever our various onChange
events are called, we can update the appropriate key in state.
Next, we'll add the fields to our render method that only collect text, 'First Name', 'Last Name', and 'Occupation', along with the corresponding onChange
method.
import React, { Component } from 'react';
import TextField from 'components/TextField.component';
import './signupform.css';
class SignUpForm extends Component {
constructor(props) {
super(props);
this.state = {
firstName: {
value: "",
error: false
},
lastName: {
value: "",
error: false
},
email: {
value: "",
error: false
},
startdate: {
value: "",
error: false
},
url: {
value: "",
error: false
},
salary: {
value: "",
quantity: "",
error: false,
errorMessage: "Salary cannot be empty."
},
occupation: {
value: "",
error: false
},
zipcode: {
value: "",
error: false
}
}
}
onChange(event) {
this.setState({ [event.target.name]: {
...this.state[event.target.name],
value: event.target.value,
error: this.state[event.target.name].error
}
})
}
render() {
let {
firstName,
lastName,
occupation
} = this.state;
return(
<div className="container">
<form className="signup-form">
<TextField
error={firstName.error}
value={firstName.value}
name="firstName"
errorMessage="First name cannot be empty."
label="First Name"
placeholder="First Name"
onChange={this.onChange.bind(this)} />
<TextField
error={lastName.error}
value={lastName.value}
name="lastName"
errorMessage="Last name cannot be empty."
label="Last Name"
placeholder="Last Name"
onChange={this.onChange.bind(this)} />
<TextField
error={occupation.error}
value={occupation.value}
name="occupation"
errorMessage="Occupation cannot be empty."
label="Occupation"
placeholder="Occupation"
onChange={this.onChange.bind(this)} />
</form>
</div>
)
}
}
export default SignUpForm;
If we look at the onChange event:
onChange(event) {
let { name, value } = event.target;
this.setState({ [name]: {
...this.state[name],
value: event.target.value
}
})
}
The event
object from the TextField
gets passed to the onChange
function in our SignUpForm
, where the 'name' and 'value' properties get extracted from event.target.
These values are used to update state for the key indicated by name. If the TextField's name is not represented as a key in state, you may get an undefined error when the onChange
event is triggered on that field.
Looking at our TextField component:
import React, { Component } from 'react';
import './TextField.css';
class TextField extends Component {
static defaultProps = {
size: "large"
}
setSize() {
let { size } = this.props;
if (size === 'large') return 'textfield-large';
}
render() {
let {
name,
value,
placeholder,
label,
errorMessage,
error,
onChange
} = this.props;
return(
<div className={`d-flex flex-column ${this.setSize()}`}>
<label className="form-item-label">{label}</label>
<input
className={`textfield regular-text ${this.setSize()}`}
value={value}
name={name}
placeholder={placeholder}
onChange={onChange.bind(this)} />
{(error ? <div className="error-text-container"><div className="error-text form-item-error">{errorMessage}</div></div> : null)}
</div>
)
}
}
export default TextField;
The TextField
component accepts several properties. The prop label
sets the label of the TextField, onChange
is a property that is passed from the parent component, and value
is passed down from the parent component as well. Whenever the input's onChange
event is triggered, the parent manages the update and passes the new value down as a prop. The property errorMessage
is shown when the property error
is true.
If a validation is triggered and failed in the parent, the TextField's error
property will be set to true.
At this point, instead of continuing to add additional fields to the form, I like to trigger the form submission and see if my currently added fields are working as expected, so let's do that below:
import React, { Component } from 'react';
import TextField from 'components/TextField.component';
import Button from 'components/Button.component';
import {
isFilled
} from 'common/helpers/validators';
import './signupform.css';
class SignUpForm extends Component {
constructor(props) {
super(props);
this.state = {
firstName: {
value: "",
error: false
},
lastName: {
value: "",
error: false
},
email: {
value: "",
error: false
},
startdate: {
value: "",
error: false
},
url: {
value: "",
error: false
},
salary: {
value: "",
quantity: "",
error: false,
errorMessage: "Salary cannot be empty."
},
occupation: {
value: "",
error: false
},
zipcode: {
value: "",
error: false
},
result: ""
}
}
onChange(event) {
let { name, value } = event.target;
this.setState({ [name]: {
...this.state[name],
value: event.target.value
}
})
}
isFieldValid(validator, key) {
let isValid = validator(this.state[key].value);
this.setState({
[key]: {
value: this.state[key].value,
error: !isValid,
errorMessage: this.state[key].errorMessage
}
});
return isValid;
}
submit(event) {
event.preventDefault();
if (!this.validate()) {
return;
}
let { firstName, lastName, occupation } = this.state;
this.setState({result: `Success! First Name: ${firstName.value}, Last Name: ${lastName.value}, Occupation: ${occupation.value}`});
}
validate() {
let fields = new Set();
fields.add(this.isFieldValid(isFilled, "firstName"));
fields.add(this.isFieldValid(isFilled, "lastName"));
fields.add(this.isFieldValid(isFilled, "occupation"));
return !fields.has(false);
}
render() {
let {
firstName,
lastName,
occupation
} = this.state;
return(
<div className="container">
<form onSubmit={this.submit.bind(this)} className="signup-form">
<TextField
error={firstName.error}
value={firstName.value}
name="firstName"
errorMessage="First name cannot be empty."
label="First Name"
placeholder="First Name"
onChange={this.onChange.bind(this)} />
<TextField
error={lastName.error}
value={lastName.value}
name="lastName"
errorMessage="Last name cannot be empty."
label="Last Name"
placeholder="Last Name"
onChange={this.onChange.bind(this)} />
<TextField
error={occupation.error}
value={occupation.value}
name="occupation"
errorMessage="Occupation cannot be empty."
label="Occupation"
placeholder="Occupation"
onChange={this.onChange.bind(this)} />
<Button
style={{marginTop: '25px'}}
type="submit"
label="Submit"/>
</form>
<div style={{marginTop: '25px'}}>{this.state.result}</div>
</div>
)
}
}
export default SignUpForm;
There were three functions added in addition to the Submit
button: submit
, validate
, and isFieldValid
. Let's first look at submit
:
submit(event) {
event.preventDefault();
if (!this.validate()) {
return;
}
let { firstName, lastName, occupation } = this.state;
this.setState({result: `Success! First Name: ${firstName.value}, Last Name: ${lastName.value}, Occupation: ${occupation.value}`});
}
We call event.preventDefault()
to stop the page from refreshing when the submit event is triggered on the form. After that, we have an if
statement, that exits the function if our validate
function returns false.
If our validate
function returns true
, then the values of the fields in our form is printed just below the form, just to temporarily test our expected behavior.
Our validate
function runs validations on all of our fields in our form:
validate() {
let fields = new Set();
fields.add(this.isFieldValid(isFilled, "firstName"));
fields.add(this.isFieldValid(isFilled, "lastName"));
fields.add(this.isFieldValid(isFilled, "occupation"));
return !fields.has(false);
}
If a field does not contain valid data, then a false
is added to the set, and thus validate
returns false. Our function isFieldValid
takes two arguments, a function to validate the data of the field, and the second argument is the key in state that the field corresponds to. That key is used to retrieve the field's value and pass it to it's validator:
isFieldValid(validator, key) {
let isValid = validator(this.state[key].value);
this.setState({
[key]: {
value: this.state[key].value,
error: !isValid,
errorMessage: this.state[key].errorMessage
}
});
We import a function isFilled
, which checks to see if the item passed is empty. To do that, I have used a function from validator. We didn't have to use validator, I just elected to use it for ease and convenience, but if you don't want to include another package, we can also replace the code within the isFilled
function with your own logic.
export const isFilled = (value) => {
return !validator.isEmpty(value + "") && value !== null;
}
At this point, if we click submit on an empty form, we will see:
If we add data and click the submit button, we will see the values we added to the fields in our form:
Next, we'll add our url
and email
fields, and make corresponding updates to the validate
function to check the validity of these two fields. url
is an optional field, but if it is not empty, it needs to be a valid URL.
import React, { Component } from 'react';
import TextField from 'components/TextField.component';
import Button from 'components/Button.component';
import {
isFilled,
isEmailValid,
isURLValid,
} from 'common/helpers/validators';
import './signupform.css';
class SignUpForm extends Component {
constructor(props) {
super(props);
this.state = {
firstName: {
value: "",
error: false
},
lastName: {
value: "",
error: false
},
email: {
value: "",
error: false
},
startdate: {
value: "",
error: false
},
url: {
value: "",
error: false
},
salary: {
value: "",
quantity: "",
error: false,
errorMessage: "Salary cannot be empty."
},
occupation: {
value: "",
error: false
},
zipcode: {
value: "",
error: false
},
result: ""
}
}
onChange(event) {
let { name, value } = event.target;
this.setState({ [name]: {
...this.state[name],
value: event.target.value
}
})
}
isFieldValid(validator, key) {
let isValid = validator(this.state[key].value);
this.setState({
[key]: {
value: this.state[key].value,
error: !isValid,
errorMessage: this.state[key].errorMessage
}
});
return isValid;
}
submit(event) {
event.preventDefault();
if (!this.validate()) {
return;
}
let { firstName, lastName, occupation } = this.state;
this.setState({result: `Success! First Name: ${firstName.value}, Last Name: ${lastName.value}, Occupation: ${occupation.value}`});
}
validate() {
let fields = new Set();
fields.add(this.isFieldValid(isFilled, "firstName"));
fields.add(this.isFieldValid(isFilled, "lastName"));
fields.add(this.isFieldValid(isFilled, "occupation"));
fields.add(this.isFieldValid(isEmailValid, "email"))
fields.add(this.isPersonalURLValid());
return !fields.has(false);
}
isPersonalURLValid() {
let { value } = this.state.url;
let isValid = isURLValid(value) || value.length === 0;
this.setState({
url: {
...this.state.url,
error: !isValid
}
});
return isValid;
}
render() {
let {
firstName,
lastName,
occupation,
url,
email
} = this.state;
return(
<div className="container">
<form onSubmit={this.submit.bind(this)} className="signup-form">
<TextField
error={firstName.error}
value={firstName.value}
name="firstName"
errorMessage="First name cannot be empty."
label="First Name"
placeholder="First Name"
onChange={this.onChange.bind(this)} />
<TextField
error={lastName.error}
value={lastName.value}
name="lastName"
errorMessage="Last name cannot be empty."
label="Last Name"
placeholder="Last Name"
onChange={this.onChange.bind(this)} />
<TextField
error={occupation.error}
value={occupation.value}
name="occupation"
errorMessage="Occupation cannot be empty."
label="Occupation"
placeholder="Occupation"
onChange={this.onChange.bind(this)} />
<TextField
error={url.error}
value={url.value}
name="url"
errorMessage="Please enter a vaild url."
label="Personal Website (Optional)"
placeholder="Personal Website"
onChange={this.onChange.bind(this)} />
<TextField
error={email.error}
value={email.value}
name="email"
errorMessage="Please enter a vaild e-mail."
label="E-mail"
placeholder="E-mail"
onChange={this.onChange.bind(this)} />
<Button
style={{marginTop: '25px'}}
type="submit"
label="Submit"/>
</form>
<div style={{marginTop: '25px'}}>{this.state.result}</div>
</div>
)
}
}
export default SignUpForm;
We've updated the validate
function:
validate() {
let fields = new Set();
fields.add(this.isFieldValid(isFilled, "firstName"));
fields.add(this.isFieldValid(isFilled, "lastName"));
fields.add(this.isFieldValid(isFilled, "occupation"));
fields.add(this.isFieldValid(isEmailValid, "email"))
fields.add(this.isPersonalURLValid());
return !fields.has(false);
}
To check for e-mail validity, I have again used a function from the validator library like so:
export const isEmailValid = (email) => {
return validator.isEmail(email)
}
There is another new function also called when validate
is called, and that is isPersonalURLValid
. Because 'Personal Website' is an optional field, it's okay for it to be empty, it just has to be a valid URL if not. Our function looks like:
isPersonalURLValid() {
let { value } = this.state.url;
let isValid = isURLValid(value) || value.length === 0;
this.setState({
url: {
...this.state.url,
error: !isValid
}
});
return isValid;
}
This function checks to see if the url
value is either an empty string or a valid URL using our isURLValid
function, again leaning on the validator
library to provide a function to check validity:
export const isURLValid = (url) => {
return validator.isURL(url);
}
With these fields added, our form now looks like this whenever submit is triggered and no data is entered:
If data is entered into the 'Personal Website' text field, our form looks like:
We have three fields left, our 'Desired Start Date', 'Zipcode' and 'Desired Salary' fields, so let's add them:
import React, { Component } from 'react';
import TextField from 'components/TextField.component';
import Button from 'components/Button.component';
import DatePicker from 'components/DatePicker.component';
import CurrencyFormat from 'react-currency-format';
import {
isFilled,
isEmailValid,
isURLValid,
isLengthValid
} from 'common/helpers/validators';
import './signupform.css';
class SignUpForm extends Component {
constructor(props) {
super(props);
this.state = {
firstName: {
value: "",
error: false
},
lastName: {
value: "",
error: false
},
email: {
value: "",
error: false
},
startdate: {
value: "",
error: false
},
url: {
value: "",
error: false
},
salary: {
value: "",
quantity: "",
error: false,
errorMessage: "Salary cannot be empty."
},
occupation: {
value: "",
error: false
},
zipcode: {
value: "",
error: false
},
result: ""
}
}
onChange(event) {
let { name, value } = event.target;
this.setState({ [name]: {
...this.state[name],
value: event.target.value
}
})
}
isFieldValid(validator, key) {
let isValid = validator(this.state[key].value);
this.setState({
[key]: {
value: this.state[key].value,
error: !isValid,
errorMessage: this.state[key].errorMessage
}
});
return isValid;
}
submit(event) {
event.preventDefault();
if (!this.validate()) {
return;
}
let { firstName, lastName, occupation, email, url, salary, startdate, zipcode } = this.state;
this.setState({result: `Success! First Name: ${firstName.value}, Last Name: ${lastName.value}, Occupation: ${occupation.value}, Email: ${email.value}, URL: ${url.value}, Zipcode: ${zipcode.value}, Desired Start Date: ${startdate.value}, Desired Salary: ${salary.value}`});
}
validate() {
let fields = new Set();
fields.add(this.isFieldValid(isFilled, "firstName"));
fields.add(this.isFieldValid(isFilled, "lastName"));
fields.add(this.isFieldValid(isFilled, "occupation"));
fields.add(this.isFieldValid(isEmailValid, "email"))
fields.add(this.isPersonalURLValid());
fields.add(this.isFieldValid(isFilled, "startdate"));
fields.add(this.isSalaryMin(min, 60000, "salary", "Minimum salary is $60,000."));
fields.add(this.isZipcodeValid());
return !fields.has(false);
}
isPersonalURLValid() {
let { value } = this.state.url;
let isValid = isURLValid(value) || value.length === 0;
this.setState({
url: {
...this.state.url,
error: !isValid
}
});
return isValid;
}
isSalaryMin(validator, value, key, errorMessage) {
this.setState({
[key]: {
quantity: this.state[key].quantity,
value: this.state[key].value,
error: !validator(this.state[key].quantity, value),
errorMessage: errorMessage
}
});
return validator(this.state[key].quantity, value);
}
onValueChange(values) {
const {formattedValue, value} = values;
this.setState({ salary: {
...this.state.salary,
value: formattedValue,
quantity: value,
}
});
}
onChangeDate(key, value) {
this.setState({
[key]: {
value,
error: false
},
});
}
onChangeZipcode(event) {
let { value } = event.target
if (value.length > 5) return;
let quantity = 0;
let OK = /[0-9+$]/.test(value)
if (!OK && value.length > 0) return;
if (value.length > 0) {
value = parseInt(value);
} else {
value = "";
}
this.setState({ zipcode: {
value
}
})
}
isZipcodeValid() {
let value = this.state.zipcode.value.toString();
let isValid = isLengthValid(value, 5);
let errorMessage = "Zipcode cannot be empty.";
if (!isValid && value.length > 0) {
errorMessage = "Zipcode must be five digits.";
}
this.setState({
zipcode: {
...this.state.zipcode,
error: !isValid,
errorMessage
}
});
return isValid;
}
render() {
let {
firstName,
lastName,
occupation,
url,
email,
startdate,
salary,
zipcode
} = this.state;
return(
<div className="container">
<form onSubmit={this.submit.bind(this)} className="signup-form">
<TextField
error={firstName.error}
value={firstName.value}
name="firstName"
errorMessage="First name cannot be empty."
label="First Name"
placeholder="First Name"
onChange={this.onChange.bind(this)} />
<TextField
error={lastName.error}
value={lastName.value}
name="lastName"
errorMessage="Last name cannot be empty."
label="Last Name"
placeholder="Last Name"
onChange={this.onChange.bind(this)} />
<TextField
error={occupation.error}
value={occupation.value}
name="occupation"
errorMessage="Occupation cannot be empty."
label="Occupation"
placeholder="Occupation"
onChange={this.onChange.bind(this)} />
<TextField
error={url.error}
value={url.value}
name="url"
errorMessage="Please enter a vaild url."
label="Personal Website (Optional)"
placeholder="Personal Website"
onChange={this.onChange.bind(this)} />
<TextField
error={email.error}
value={email.value}
name="email"
errorMessage="Please enter a vaild e-mail."
label="E-mail"
placeholder="E-mail"
onChange={this.onChange.bind(this)} />
<DatePicker
timeFormat={false}
isValidDate={(current) => current > new Date()}
value={(startdate.value ? new Date(startdate.value) : null)}
placeholder="Desired Start Date"
errorMessage="Desired start date cannot be empty."
error={startdate.error}
onChange={this.onChangeDate.bind(this, "startdate")}
label="Desired Start Date"
size="large"/>
<CurrencyFormat
thousandSeparator={true}
prefix='$'
customInput={TextField}
name="salary"
value={salary.quantity}
error={salary.error}
errorMessage={salary.errorMessage}
label="Desired Salary - Min. $60,000"
placeholder='Desired Salary'
onValueChange={this.onValueChange.bind(this)} />
<TextField
name="zipcode"
label="Zipcode"
error={zipcode.error}
value={zipcode.value}
errorMessage={zipcode.errorMessage}
placeholder="Zipcode"
onChange={this.onChangeZipcode.bind(this)} />
<Button
style={{marginTop: '25px'}}
type="submit"
label="Submit"/>
</form>
<div style={{marginTop: '25px'}}>{this.state.result}</div>
</div>
)
}
}
export default SignUpForm;
We've added another five functions for the three added fields. Starting with zipcode
, we've added another validate function, isZipcodeValid
, and onChangeZipcode
. The 'Zipcode' field is 5 digits, and can only contain integers, thus, our onChangeZipcode
function will disallow non-integer characters, and limit the length of the value to 5:
onChangeZipcode(event) {
let { value } = event.target
if (value.length > 5) return;
let OK = /[0-9+$]/.test(value)
if (!OK && value.length > 0) return;
if (value.length > 0) {
value = parseInt(value);
} else {
value = "";
}
this.setState({ zipcode: {
value
}
})
}
For this field, I use a regular expression to check for validity, but you can use any library or method you prefer.
For the function to check the inputs validity, we check for the fields length:
isZipcodeValid() {
let value = this.state.zipcode.value.toString();
let isValid = isLengthValid(value, 5);
let errorMessage = "Zipcode cannot be empty.";
if (!isValid && value.length > 0) {
errorMessage = "Zipcode must be five digits.";
}
this.setState({
zipcode: {
...this.state.zipcode,
error: !isValid,
errorMessage
}
});
return isValid;
}
If the value for zipcode
is greater than 0 in length, but less than 5, then the errorMessage
for zipcode becomes "Zipcode must be five digits.". If the field is empty, then the errorMessage
is: "Zipcode cannot be empty."
This example is to illustrate an instance in which a field could have multiple possible reasons of failure that you may want to communicate to the user.
Our next field that we'll look at is 'Desired Start Date'. We've added an onChange
function specific to this field:
onChangeDate(key, value) {
this.setState({
[key]: {
value,
error: false
},
});
}
Our DatePicker
component itself looks like:
import Datetime from 'react-datetime'
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import './DatePicker.css';
const dateFormat = 'MM/DD/YYYY';
class DatePicker extends Component {
static propTypes = {
label: PropTypes.string,
size: 'large'
}
constructor(props) {
super(props);
this.state = {
value: props.value,
active: false,
date: null,
focused: false
};
}
setSize() {
let { size } = this.props;
if (size === 'large') return 'textfield-large';
}
getBase() {
let { timeFormat, value, onChange, placeholder, isValidDate } = this.props;
let className = `textfield regular-text ${this.setSize()}`;
return (<Datetime
isValidDate={isValidDate}
timeFormat={timeFormat}
dateFormat="YYYY-MM-DD"
placeholder={placeholder}
value={(value ? moment(value, dateFormat) : null)}
onChange={onChange.bind(this)}
inputProps={{ className, readOnly: true, placeholder: `${placeholder}`}} />);
}
render() {
let { size, error, errorMessage } = this.props;
return (
<div className={`d-flex flex-column ${this.setSize()}`}>
<label className="form-item-label">{this.props.label}</label>
{this.getBase()}
{(error ? <div className="error-text-container"><div className="error-text form-item-error">{errorMessage}</div></div> : null)}
</div>
);
}
}
export default DatePicker;
And, in our form, the instance of our DatePicker
component in our form looks like:
<DatePicker
timeFormat={false}
isValidDate={(current) => current > new Date()}
value={(startdate.value ? new Date(startdate.value) : null)}
placeholder="Desired Start Date"
errorMessage="Desired start date cannot be empty."
error={startdate.error}
onChange={this.onChangeDate.bind(this, "startdate")}
label="Desired Start Date"
size="large"/>
I personally find react-datetime
to be very friendly, as you can set a date range, pass a css class to the DatePicker
which will style it to your liking, and specify a time option as well, if interested. But, you can use any DatePicker
of your choice.
Our last field is our salary
field:
<CurrencyFormat
thousandSeparator={true}
prefix='$'
customInput={TextField}
name="salary"
value={salary.quantity}
error={salary.error}
errorMessage={salary.errorMessage}
label="Desired Salary - Min. $60,000"
placeholder='Desired Salary'
onValueChange={this.onValueChange.bind(this)} />
We've added two functions for this field, 'isSalaryMin' and 'onValueChange'.
The CurrencyFormat
component from react-currency-format I also find to be easy to use. I didn't want to spend time to make my own currency component, so I used this library and integrated it to work with my form.
CurrencyFormat
is great because you can pass a base component to the customInput
prop, which will effectively wrap the CurrencyFormat
magic around the passed component. You can still access the props of your base component as well.
onChange
of the currency input, two values are returned, the formatted currency amount, and the unformatted currency amount. We have a custom function onValueChange
to grab those two values and set them in state for the salary key:
onValueChange(values) {
const {formattedValue, value} = values;
this.setState({ salary: {
...this.state.salary,
value: formattedValue,
quantity: value,
}
});
}
The function we use to validate the value of the salary amount, isSalaryMin
, is invoked in the validate
function. It's had a couple updates, so let's take a closer look:
validate() {
let fields = new Set();
fields.add(this.isFieldValid(isFilled, "firstName"));
fields.add(this.isFieldValid(isFilled, "lastName"));
fields.add(this.isFieldValid(isFilled, "occupation"));
fields.add(this.isFieldValid(isEmailValid, "email"))
fields.add(this.isPersonalURLValid());
fields.add(this.isFieldValid(isFilled, "startdate"));
fields.add(this.isSalaryMin(min, 60000, "salary", "Minimum salary is $60,000."));
fields.add(this.isZipcodeValid());
return !fields.has(false);
}
The isSalaryMin
function is passed a validator, the minimum salary of $60,000 in integer form, and an error message that is set on error.
I added these for arguments to the isSalaryMin
function because I thought about also adding an upper bound to salary and naming the function isSalaryValid
instead, so that way I could reuse the function for both the lower and upper bound validation. It'd allow me to pass a different validator function and other arguments, but for the purposes of this example, there is no upper bound.
We see that our other new fields added are validated. The isZipcodeValid
function is called, and we also check to see if a date has been selected.
Finally, looking at our isSalaryMin
function, we have:
isSalaryMin(validator, value, key, errorMessage) {
this.setState({
[key]: {
quantity: this.state[key].quantity,
value: this.state[key].value,
error: !validator(this.state[key].quantity, value),
errorMessage: errorMessage
}
});
return validator(this.state[key].quantity, value);
}
With our form complete, our form looks like:
With this approach, I am able to share my validators across my entire app, I can easily change out the guts of my components at any time, and the parent form component is in charge of validation.
My form component is under 300 lines of code, and I am definitely certain there is probably opportunity to make the form smaller and more lean or easier to understand.
Please check out a repo with the final code here. πππ
Top comments (0)