React Material v5 (MUI 5) Masked Phone Number Validation

Overview

Validating user inputs is an essential part of any web application. Ensuring that data is entered correctly helps to reduce errors and improve the overall user experience. This blog post will show you how to validate phone number format with React and Material UI. Material UI is a popular React UI library that provides a range of pre-built components and design elements. React, and Material UI makes it easy to create an input form with a phone number field and validate the format of the entered data. In this tutorial, we will guide you through the process of using React and Material UI to validate phone number format and provide tips and best practices to help you along the way.

The code includes MUI v4 and v5 at the Full Stack Soup GitHub Repo. The input uses the react-imask and react-number-format 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. If the number does not meet the ten-digit validation rule, the field will have red borders using the error properties.  The Class Component version of is not included but available from the GitHub repo.

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



Source Files For This Article

Included are the source files for Material version 4 and 5.

Get Source Code

Libraries Required

React Material UI Version 5

Install the Material Core with Node Package Manager.

npm install @mui/material @emotion/react @emotion/styled

React Input Mask

Install the React Number Format and React IMask libraries with Node Package Manager.

npm install react-input-mask react-imask

Phone Input Field Component

The Input Component

The phone field uses the react imask library to render a masked pattern onto the field.

Build a masked phone number pattern with <IMaskInput/> from react imask.

const TextMaskCustom = React.forwardRef(function TextMaskCustom(props, ref) {
  const { onChange, ...other } = props;
  return (
    <IMaskInput
      {...other}
      mask="(#00) 000-0000"
      definitions={{
        '#': /[1-9]/,
      }}
      inputRef={ref}
      onAccept={(value) => onChange({ target: { name: props.name, value } })}
      overwrite
    />
  );
});

TextMaskCustom.propTypes = {
  name: PropTypes.string.isRequired,
  onChange: PropTypes.func.isRequired,
};

Passing the Mask Pattern to the Input

Using MUI 5’s <OutlinedInput/> property inputComponent, we can pass the pattern defined in TextMaskCustom created with <IMaskInput/>.

 <OutlinedInput                  
   ...
    inputComponent={TextMaskCustom}
 />

All together

 <FormControl variant="standard">
    <InputLabel htmlFor="text-mask-input"></InputLabel>
    <OutlinedInput                  
      helperText={props.helperText}          
      error={isDirtyField && !isValid}
      onChange={handleChange}
      onBlur={() => setIsDirtyField(true)}
      name="phone"
      label=""
      size={'small'}          
      value={phoneNumber}
      
      InputLabelProps={{
        shrink: true,
      }}
      inputComponent={TextMaskCustom}
    />
      <FormHelperText id="my-helper-text">{props.helperText}</FormHelperText>
    </FormControl>

The entire functional component InputPhoneFIeld.js code below.

import React from 'react';
import PropTypes from 'prop-types';
import InputLabel from '@mui/material/InputLabel';
import FormControl from '@mui/material/FormControl';
import FormHelperText from '@mui/material/FormHelperText';
import OutlinedInput from '@mui/material/OutlinedInput';
import { IMaskInput } from 'react-imask';

const TextMaskCustom = React.forwardRef(function TextMaskCustom(props, ref) {
  const { onChange, ...other } = props;
  return (
    <IMaskInput
      {...other}
      mask="(#00) 000-0000"
      definitions={{
        '#': /[1-9]/,
      }}
      inputRef={ref}
      onAccept={(value) => onChange({ target: { name: props.name, value } })}
      overwrite
    />
  );
});

TextMaskCustom.propTypes = {
  name: PropTypes.string.isRequired,
  onChange: PropTypes.func.isRequired,
};

export default function InputPhoneField(props) {
  
  const [phoneNumber, setPhoneNumber] = React.useState('')
  const [isDirtyField, setIsDirtyField] = React.useState(false)
  const [isValid, setIsValid] = React.useState(false)
  
  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);
    setIsValid(isValid);
    props.handleChange(val, isValid);
  }

  return (
    <>
     <FormControl variant="standard">
        <InputLabel htmlFor="text-mask-input"></InputLabel>
        <OutlinedInput                  
          helperText={props.helperText}          
          error={isDirtyField && !isValid}
          onChange={handleChange}
          onBlur={() => setIsDirtyField(true)}
          name="phone"
          label=""
          size={'small'}          
          value={phoneNumber}
          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 shows a red border. Create another variable to track if the field is pristine or dirty. This way, the field does not show an error before being touched. In the example below, we want to know if 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}
  />

Parent Functional ComponentNo Formik

Source code for the parent component

import React, { useState } from 'react';
import Button from '@mui/material/Button';
import InputPhoneField from './InputPhoneField';
import { Box } from '@mui/system';

export default function ParentFormFuncComp()
 {
    const [phone, setPhone] = useState('')
    const [isValidPhone, setIsValidPhone] = useState(false)
    
    const handlePhoneNumberChange = (value, isValid) => {
        setPhone(value)
        setIsValidPhone(isValid)        
    }

    const handleFormSubmit = () => {        
        alert(phone);
    }
              
    return (
        <Box sx={{ marginTop: '40px' }}>

            
            <InputPhoneField placeholder=""                    
                helperText="(Required)"
                label="Phone"
                fieldName="Phone"
                handleChange={handlePhoneNumberChange}
                value={phone} />
                {isValidPhone === true ?
                <Button variant="contained" color="primary" onClick={handleFormSubmit}>
                    Submit
                </Button>
                :
                <Button variant="contained" color="primary" disabled>
                    Submit
                </Button>
            }

        </Box>
    );
}

Validation With Formik

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

Libraries Required

Formik and Yup are used for form validation.

npm install formik yup

Formik Input Phone Field Component

Source code for the entire FormikInputPhoneField.js component.

import React from 'react';
import PropTypes from 'prop-types';
import { alpha, styled } from '@mui/material/styles';
import InputLabel from '@mui/material/InputLabel';
import FormControl from '@mui/material/FormControl';
import FormHelperText from '@mui/material/FormHelperText';
import OutlinedInput from '@mui/material/OutlinedInput';
import { IMaskInput } from 'react-imask';

const TextMaskCustom = React.forwardRef(function TextMaskCustom(props, ref) {
  const { onChange, ...other } = props;
  return (
    <IMaskInput
      {...other}
      mask="(#00) 000-0000"
      definitions={{
        '#': /[1-9]/,
      }}
      inputRef={ref}
      onAccept={(value) => onChange({ target: { name: props.name, value } })}
      overwrite
    />
  );
});

TextMaskCustom.propTypes = {
  name: PropTypes.string.isRequired,
  onChange: PropTypes.func.isRequired,
};


const ValidationOutlinedInputField = styled(OutlinedInput)({
  '& input:valid + fieldset': {
    borderColor: 'green',
    borderWidth: 2,
  },
  '& input:invalid + fieldset': {
    borderColor: 'red',
    borderWidth: 2,
  },
  '& input:valid:focus + fieldset': {
    borderLeftWidth: 6,
    padding: '4px !important', // override inline-style
  },
  '&:hover fieldset': {
    borderColor: 'yellow',
  },
});


export default function InputPhoneField(props) {
  
  const [phoneNumber, setPhoneNumber] = React.useState('')
  const [isDirtyField, setIsDirtyField] = React.useState(false)
  const [isValid, setIsValid] = React.useState(false)
  
  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);
    setIsValid(isValid);
    props.handleChange(val, isValid);
  }

  return (
    <>
     <FormControl variant="standard">
        <InputLabel htmlFor="text-mask-input"></InputLabel>
        <ValidationOutlinedInputField                  
          value={props.formik.values.phone}
          error={props.formik.touched.phone && Boolean(props.formik.errors.phone)}
          
          onChange={props.formik.handleChange}          
          name="phone"
          label=""
          size={'small'}          
          
          style={{marginTop: -1}}
          InputLabelProps={{
            shrink: true,
          }}
          inputComponent={TextMaskCustom}
        />
         <FormHelperText id="my-helper-text">{props.formik.touched.phone && props.formik.errors.phone}</FormHelperText>
        </FormControl>
    </>
  );
}

Break Down of the FormikInputPhoneField.js Component

The name, value, error, helper properties or in this case <FormHelperText> must be set as below. The onChange event used formik’s predefined handleChange method. The error property changes the border color to red if both the touched and error conditions are true.

name="phone"
value={props.formik.values.phone}
error={props.formik.touched.phone && Boolean(props.formik.errors.phone)}          
onChange={props.formik.handleChange}          

Display the Error & Warning Messages

For the helper text that displays errors

And warnings

Using MUI’s FormHelperText component, set the help and error messages.

<FormHelperText id="my-helper-text">{props.formik.touched.phone && props.formik.errors.phone}</FormHelperText>

For MUI’s <TextField> use the helper property to display the error and warning messages.

helper={props.formik.touched.phone && props.formik.errors.phone}

The Parent Component

Complete Source Code for the Parent Component FormikParentFormFuncComp.js

import React, {useState, useEffect} from 'react';
import Button from '@mui/material/Button';
import { Box } from '@mui/material';
import { useFormik } from 'formik';
import * as yup from 'yup';
import InputPhoneField from './FormikInputPhoneField';

export default function FormikParentFormFuncComp() {
    const [phone, setPhone] = useState('')
    const [isValidPhone, setIsValidPhone] = useState(false)
    
    const handlePhoneNumberChange = (value, isValid) => {
        console.log(value);
        console.log(isValid);
        setPhone(value)
        setIsValidPhone(isValid)        
    }

    const handleFormSubmit = () => {
        var data = {
          Phone: formik.values.phone,                     
        }
        alert(JSON.stringify(data, null, 2));
    }
      
    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 must have 10 digiits')
        .required('Phone is required'),  
    });

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

    return (
        <>
        <Box sx={{ marginTop: '40px' }}>
            <InputPhoneField placeholder=""
                formik={formik}
                helperText="(Required)"
                label="Phone"
                fieldName="Phone"
                handleChange={handlePhoneNumberChange}
                value={phone} />

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

        </Box>
        </>
    );
}

Break Down of the FormikParentFormFuncComp.js Component

Imports

Import the following libraries including the FormikInputPhoneField.js from above.

import React, {useState, useEffect} from 'react';
import Button from '@mui/material/Button';
import { Box } from '@mui/material';
import { useFormik } from 'formik';
import * as yup from 'yup';
import InputPhoneField from './FormikInputPhoneField';

Yup Validation Schema

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 must have 10 digiits')
   .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
    },
});

Render the Phone Input Component

Add the property formik={formik}

  <Box sx={{ marginTop: '40px' }}>
    <InputPhoneField placeholder=""
        formik={formik}
        helperText="(Required)"
        label="Phone"
        fieldName="Phone"
        handleChange={handlePhoneNumberChange}
       value={phone} />

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

</Box>

Conclusion

In conclusion, validating phone number format is crucial in building a web application that provides a positive user experience. Using React and Material UI, you can create an input form with a phone number field and validate the format of the entered data efficiently and effectively. Throughout this blog post, we walked through the process of using React and Material UI to validate phone number format. Whether you are a seasoned developer or just starting, this tutorial provides a great introduction to using React and Material UI for input validation. It will help you create more robust and user-friendly web applications for your next project.