React – Material UI – Validate Masked Phone Number Input Field

Overview

This is a step by step on how to create a component that validates a masked phone field using Material UI and React Number Format library. You can get the code for this post at the Full Stack Soup GitHub Repo here. The input uses the react-text-mask library and Material UI to build a phone input field that only accepts 10 digit phone numbers. The field will have the standard blue borders unless it has been touched. Using the error properties the field will have red borders if the number does not meet the 10 digit validation rule.

Please check out the following article with source files on GitHub.



Source Files For This Article

Get Source Code

Prerequisite

Create a new React-App.

npx create-react-app my-app
cd my-app

React Material UI Version 4

Install the Material Core with Node Package Manager.

npm install @material-ui/core @material-ui/icons

React Input Mask

Install the React Text Mask with Node Package Manager.

npm install react-input-mask

Parent Form Class Component

Create a new class component labeled FormParent.js. Add the following imports and state variables. Grid is used to align the field and submit button.

import React, {Component} from 'react';
import Grid from '@material-ui/core/Grid';
import Button from '@material-ui/core/Button';
import InputPhoneField from './InputPhoneField';

export default class OfferSupportForm extends React.Component {

constructor(props) {
     super(props);
     this.state = {
         Phone: '',
         IsValidPhone: false,
     };           
     this.handlePhoneNumberChange.bind(this);
}

Handle Input and Submit Events

Handle both phone number and submit events.

handlePhoneNumberChange = (value, isValid) => {        
    this.setState({Phone: value, isValidPhone: isValid});                        }

handleSubmitForm = (event) => {        
  console.log('handleSubmitForm  ', event);
}

Render Phone Field

Render the Input Field and the Submit Button. Notice the submit button is disabled until IsValidPhone is equal to true.

 render() {
    return (                        
        <>            
            <Grid container spacing={1} style={{marginTop: '20px'}}>        
            
                <Grid sm={4} align="left"></Grid>
                
                <Grid sm={1} >                    
                    <InputPhoneField    helperText="(Required)" 
                                        label="Phone" 
                                        fieldName="Phone" 
                                        handleChange={this.handlePhoneNumberChange}  />
                </Grid>

                <Grid sm={1} align="right" className="fieldLayoutT25B25R5">                    
                    {this.state.IsValidPhone === true ?
                        <Button variant="contained"  color="primary" onClick={this.handleSubmitForm}>                             
                            Submit
                        </Button>
                        :
                        <Button variant="contained"  color="primary" disabled>                             
                            Submit
                        </Button>
                    }                                                             
                </Grid>
                        
            </Grid>                                            
        </>
    );
}

Masked Input Component

Create a new JavaScript file labeled InputPhoneFIeld.js and import the following libraries.

import React from 'react';
import PropTypes from 'prop-types';
import MaskedInput from 'react-text-mask';
import InputLabel from '@material-ui/core/InputLabel';
import FormControl from '@material-ui/core/FormControl';
import FormHelperText from '@material-ui/core/FormHelperText';
import OutlinedInput from '@material-ui/core/OutlinedInput';

Masking the Input

Create a function using <MaskedInput /> to define the Mask and valid characters, in this case numbers.

The mask{[‘(‘, /[1-9]/, /\d/, /\d/, ‘)’, ‘ ‘, /\d/, /\d/, /\d/, ‘-‘, /\d/, /\d/, /\d/, /\d/]} will allow three digit area code in braces following by another seven numbers with a hyphen after the third character.

function TextMaskCustom(props) {
  const { inputRef, ...other } = props;

  return (
    <MaskedInput      
      {...other}
      ref={(ref) => {
        inputRef(ref ? ref.inputElement : null);
      }}
      mask={['(', /[1-9]/, /\d/, /\d/, ')', ' ', /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/]}
      placeholderChar={'\u2000'}
      showMask
    />
  );
}

In the function portion of the component set the phone number state and the change event handler. The handleChange() will count the number of digits 0-9 and if there are 10 digits, set a boolean variable to true. Both the phone n umber and validation boolean variable are sent to the parent by props.handlePhoneNumberChange( val, isValid ).

export default function InputMaskedPhoneField(props) {

  const [phoneNumber, setPhoneNumber] = React.useState(0)
  
  const handleChange = event => {
    const val = event.target.value;
    
    let count = 0;
    
    for (let i = 0; i < val.length; i++) 
      if (val.charAt(i) in [0,1,2,3,4,5,6,7,8,9]) 
          count++
    
    var isValid = (count === 10) ? true : false;
    
    setPhoneNumber(val);
    props.handlePhoneNumberChange(val, isValid);
  }

Render the Phone Number Input Field

Render the TextMaskCustom() function in OutlinedInput

Set the inputComponent property to TextMaskCustom().

The input field will render as an outlined field with the label embedded in the upper left using the InputLabelProps setting shrink to true.

  return (
    <>
      <FormControl  variant="outlined" size={'small'} >
        <InputLabel htmlFor="phone" >Phone</InputLabel>        
        <OutlinedInput                     
          onChange={(e) => handleChange(e)}
          name="phone"
          label="Phone"
          size={'small'}
          value={phoneNumber}
         // Shrink Label
          InputLabelProps={{
            shrink: true,
          }}
          inputComponent={TextMaskCustom}
        />
        <FormHelperText id="my-helper-text">{props.helperText}</FormHelperText>
      </FormControl>
    </>
  );

Material Field Error Property

In addition to disabling a Submit Button we can use the error property in Material <TextField /> which will show a red border input. Before showing an error when the form hasn’t been touched we can set another variable to track if the field is pristine or dirty. In the example below we just want to know is the field is dirty (has been modified by the user). Also we can contain the states inside the input component.

const [isValid, setIsValid] = useState(false)  
const [dirty, setDirty] = useState(false);

Show Error

Using the error property to show a red border if the field does not have a valid phone number.

error={dirty && !isValid}              

Pristine or Dirty Input

Using the onBlur() event, set the dirty to true. This means the user touched the field and went elsewhere on the form.

  <OutlinedInput                                     
    error={dirty && isValid === false}                                        
    onBlur={() => setDirty(true)}
    onChange={(e) => handleChange(e)}
    name="phone"
    label="Phone"
    size={'small'}          
    value={phoneNumber}
    InputLabelProps={{
      shrink: true,
    }}
    inputComponent={TextMaskCustom}
  />

Validation With Formik

Formik with Yup are the most commonly used form validation library combo for react. The method above is easier to understand at first but can result in more code to maintain.

Please check out the following articles each with source files for a working example on GitHub.
React Form Validation with Formik & Yup Click Here.
React Stepper Form Validation with Formik & Yup Click Here.

Install Formik and Yup

Formik and Yup are used for form validation.

npm i formik
npm i yup

Yup Validation Schema

Import the following libraries

import { useFormik } from 'formik';
import * as yup from 'yup';

Create a validation schema with Yup, This can validate against the masked phone number Regex.

const phoneRegExp = /^((\+[1-9]{1,4}[ -]?)|(\([0-9]{2,3}\)[ -]?)|([0-9]{2,4})[ -]?)*?[0-9]{3,4}[ -]?[0-9]{3,4}$/;

const validationSchema = yup.object({
  phone: yup
    .string('Enter your phone number')
    .matches(phoneRegExp, 'Phone number is not valid')
    .required('Phone is required'),  

});

Formik

Create a formik function with the useFormik() hook. The validationSchema defined with Yup will check if the input is valid and if it passes then submit the form.

const formik = useFormik({
    initialValues: {
      phone: '',
    },
    validationSchema: validationSchema ,    
    onSubmit: values => {     
        // Handle Submit
    },
});

Masked Input

The input events and properties, onChange, helperText, error, and value are handled by formik.

<OutlinedInput                                     
    name="phone"
    label="Phone"
    size={'small'}          
    value={props.formik.values.phone}
    error={props.formik.touched.phone &&      Boolean(props.formik.errors.phone)}
    helperText={props.formik.touched.phone && props.formik.errors.phone}    
    onChange={props.formik.handleChange}
    InputLabelProps={{
      shrink: true,
    }}
    inputComponent={TextMaskCustom}
  />

Submit Form

The onClick calls the formik.handleSubmit event

<Button variant="contained" color="primary" onClick={formik.handleSubmit}>
    Submit
</Button>