Skip to content Skip to sidebar Skip to footer

React - How To Format Phone Number As User Types

I need to format a 10 digit string to this format: '(123) 456-7890'. However, I need this to happen as the user types. So if the user has inputted only 3 digits, the input should d

Solution 1:

You can normalize the input like so

  • the value is up-to-date in relation to event.target.value
  • previousValue is what has already been validated and set to state

This is structured in a way to prevent invalid characters from updating the input and also limits the input to 10 numbers.

Click the Run code snippet button below for a working example.


const normalizeInput = (value, previousValue) => {
  //return nothing ifno value
  if (!value) return value; 

  // only allows 0-9 inputs
  const currentValue = value.replace(/[^\d]/g, '');
  const cvLength = currentValue.length; 

  if (!previousValue || value.length > previousValue.length) {

    // returns: "x", "xx", "xxx"if (cvLength < 4) return currentValue; 

    // returns: "(xxx)", "(xxx) x", "(xxx) xx", "(xxx) xxx",
    if (cvLength < 7) return`(${currentValue.slice(0, 3)}) ${currentValue.slice(3)}`; 

    // returns: "(xxx) xxx-", (xxx) xxx-x", "(xxx) xxx-xx", "(xxx) xxx-xxx", "(xxx) xxx-xxxx"
    return `(${currentValue.slice(0, 3)}) ${currentValue.slice(3, 6)}-${currentValue.slice(6, 10)}`; 
  }
};

constnormalizeInput = (value, previousValue) => {
  if (!value) return value;
  const currentValue = value.replace(/[^\d]/g, '');
  const cvLength = currentValue.length;
  
  if (!previousValue || value.length > previousValue.length) {
    if (cvLength < 4) return currentValue;
    if (cvLength < 7) return`(${currentValue.slice(0, 3)}) ${currentValue.slice(3)}`;
    return`(${currentValue.slice(0, 3)}) ${currentValue.slice(3, 6)}-${currentValue.slice(6, 10)}`;
  }
};

constvalidateInput = value => {
  let error = ""if (!value) error = "Required!"elseif (value.length !== 14) error = "Invalid phone format. ex: (555) 555-5555";
  
  return error;
};
    
classFormextendsReact.Component {
  constructor() {
    super();
    
    this.state = { phone: "", error: "" };
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.handleReset = this.handleReset.bind(this);
  }
  
  handleChange({ target: { value } }) {   
    this.setState(prevState=> ({ phone: normalizeInput(value, prevState.phone) }));
  };
  
  handleSubmit(e) {
    e.preventDefault();
    const error = validateInput(this.state.phone);
    
    this.setState({ error }, () => {
       if(!error) {
         setTimeout(() => {
           alert(JSON.stringify(this.state, null, 4));
         }, 300)
       }
    });
  }
  
  handleReset() {
     this.setState({ phone: "", error: "" });
  };
  
  render() {
    return(
      <formclassName="form"onSubmit={this.handleSubmit}><divclassName="input-container"><pclassName="label">Phone:</p><inputclassName="input"type="text"name="phone"placeholder="(xxx) xxx-xxxx"value={this.state.phone}onChange={this.handleChange}
          />
          {this.state.error && <pclassName="error">{this.state.error}</p>}
        </div><divclassName="btn-container"><buttonclassName="btn danger"type="button"onClick={this.handleReset}
           >
            Reset
          </button><buttonclassName="btn primary"type="submit">Submit</button></div></form>
    );
  }
}

ReactDOM.render(
  <Form />,
  document.getElementById('root')
);
html {
  font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
  font-size: 16px;
  font-weight: 400;
  line-height: 1.5;
  -webkit-text-size-adjust: 100%;
  background: #fff;
  color: #666;
}

.btn {
  color: #fff;
  border: 1px solid transparent;
  margin: 010px;
  cursor: pointer;
  text-align: center;
  box-sizing: border-box;
  padding: 030px;
  vertical-align: middle;
  font-size: .875rem;
  line-height: 38px;
  text-align: center;
  text-decoration: none;
  text-transform: uppercase;
  transition: .1s ease-in-out;
  transition-property: color,background-color,border-color;
}

.btn:focus {
  outline: 0;
}

.btn-container {
  text-align: center;
  margin-top: 10px;
}

.form {
  width: 550px;
  margin: 0 auto;
}

.danger {
  background-color: #f0506e;
  color: #fff;
  border: 1px solid transparent;
}
 
.danger:hover {
  background-color: #ee395b;
  color: #fff;
}

.error {
  margin: 0;
  margin-top: -20px;
  padding-left: 26%;
  color: red;
  text-align: left;
}

.input {
  display: inline-block;
  height: 40px;
  font-size: 16px;
  width: 70%;
  padding: 010px;
  background: #fff;
  color: #666;
  border: 1px solid #e5e5e5;
  transition: .2s ease-in-out;
  transition-property: color,background-color,border;
 }

.input-container {
  width: 100%;
  height: 60px;
  margin-bottom: 20px;
  display: inline-block;
}

.label {
  width: 25%;
  padding-top: 8px;
  display: inline-block;
  text-align: center;
  text-transform: uppercase;
  font-weight: bold;
  height: 34px;
  border-top-left-radius: 4px;
  border-bottom-left-radius: 4px;
  background: rgb(238, 238, 238);
}

.primary {
  background-color: #1e87f0;
}

.primary:hover {
  background-color: #0f7ae5;
  color: #fff;
}
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script><divid='root'></div>

Or... have 3 separate inputs and combine them when done.

constvalidateInput = value => {
  let error = ""if (!value) error = "Required!"elseif (value.length !== 14) error = "Invalid phone format. ex: (555) 555-5555";
  
  return error;
};

const initialState = {
 areaCode: "",
 prefix: "",
 suffix: "",
 error: ""
};
    
classFormextendsReact.Component {
  constructor() {
    super();
    
    this.state = initialState;
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.handleReset = this.handleReset.bind(this);
    this.setInputRef = this.setInputRef.bind(this);
  }
  
  handleChange({ target: { name, value } }) {
    let valueChanged = false;
    this.setState(prevState => {
      const nextValue = value.replace(/[^\d]/g, '');
      const changedValue = prevState[name];
      if (changedValue.length !== nextValue.length) valueChanged = true;
  
      return { [name]: nextValue }
    }, () => {
      if(valueChanged) this.handleFocus(name)
    });
  };
  
  setInputRef(name, element) {
   this[name] = element;
  }
  
  handleFocus(name){
    const { areaCode, prefix, suffix } = this.state;
    const areaCodeFilled = areaCode.length === 3;
    const prefixFilled = prefix.length === 3;

    if(areaCodeFilled && name === "areaCode") {
      this.prefix.focus();
      this.prefix.selectionEnd = 0;
    } elseif(prefixFilled && name === "prefix") {
      this.suffix.focus();
      this.suffix.selectionEnd = 0;
    }
  }
  
  handleSubmit(e) {
    e.preventDefault();
    const { areaCode, prefix, suffix } = this.state;
    const phoneNumber = `(${areaCode}) ${prefix}-${suffix}`const error = validateInput(phoneNumber);
    
    this.setState({ error }, () => {
       if(!error) {
         setTimeout(() => {
           alert(phoneNumber);
         }, 300)
       }
    });
  }
  
  handleReset() {
     this.setState(initialState);
  };
  
  render() {
    return(
      <formclassName="form"onSubmit={this.handleSubmit}><divclassName="input-container"><divclassName="label">
            Phone:
          </div><divclassName="parenthesis"style={{marginLeft:10, marginRight:2}}>&#40;</div><inputclassName="input area-code"type="text"name="areaCode"placeholder="xxx"value={this.state.areaCode}onChange={this.handleChange}maxLength="3"
          /><divclassName="parenthesis"style={{marginRight:2}}>&#41;</div><inputref={node => this.setInputRef("prefix", node)}
            className="input prefix"
            type="text"
            name="prefix"
            placeholder="xxx"
            value={this.state.prefix}
            onChange={this.handleChange}
            maxLength="3"
          />
          <divclassName="dash">-</div><inputref={node => this.setInputRef("suffix", node)}
            className="input suffix"
            type="text"
            name="suffix"
            placeholder="xxxx"
            value={this.state.suffix}
            onChange={this.handleChange}
            maxLength="4"
          />
        </div><pclassName="error">{this.state.error}</p><divclassName="btn-container"><buttonclassName="btn danger"type="button"onClick={this.handleReset}
           >
            Reset
          </button><buttonclassName="btn primary"type="submit">Submit</button></div></form>
    );
  }
}

ReactDOM.render(
  <Form />,
  document.getElementById('root')
);
html {
  font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
  font-size: 16px;
  font-weight: 400;
  line-height: 1.5;
  -webkit-text-size-adjust: 100%;
  background: #fff;
  color: #666;
}

.btn {
  color: #fff;
  border: 1px solid transparent;
  margin: 010px;
  cursor: pointer;
  text-align: center;
  box-sizing: border-box;
  padding: 030px;
  vertical-align: middle;
  font-size: .875rem;
  line-height: 38px;
  text-align: center;
  text-decoration: none;
  text-transform: uppercase;
  transition: .1s ease-in-out;
  transition-property: color,background-color,border-color;
}

.btn:focus {
  outline: 0;
}

.btn-container {
  text-align: center;
  margin-top: 10px;
}

.form {
  width: 550px;
  margin: 0 auto;
}

.danger {
  background-color: #f0506e;
  color: #fff;
  border: 1px solid transparent;
}
 
.danger:hover {
  background-color: #ee395b;
  color: #fff;
}

.error {
  margin: 0;
  height: 24px;
  margin-top: -20px;
  padding-left: 26%;
  color: red;
  text-align: right;
}

.input {
  display: flex;
  height: 40px;
  font-size: 16px;
  width: 33%;
  padding: 03px;
  background: #fff;
  color: #666;
  outline: none;
  border: 0;
}
 
.area-code,.prefix {
 width: 27px;
}

.suffix {
 width: 38px;
}

.dash,.parenthesis {
 display: flex;
}

.input-container {
  width: 100%;
  margin-bottom: 20px;
  display: flex;
  flex-direction: row;
  align-items: center;
  border-top-left-radius: 4px;
  border-bottom-left-radius: 4px;

    border: 1px solid #e5e5e5;
  transition: .2s ease-in-out;
  transition-property: color,background-color,borde
}

.label {
  height: 100%;
  background: rgb(238, 238, 238);
  width: 25%;
  padding-top: 8px;
  display: flex;
  text-transform: uppercase;
  justify-content: space-around;
  font-weight: bold;
  height: 34px;
}

.primary {
  background-color: #1e87f0;
}

.primary:hover {
  background-color: #0f7ae5;
  color: #fff;
}
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script><divid='root'></div>

Solution 2:

Your current code's regex only matches when ten digits are entered (3, 3, then 4). You could update the regex to accept a range of digits, such as:

^\(?([0-9]{0,3})\)?[-. ]?([0-9]{0,3})[-. ]?([0-9]{0,4})$

Or you could have the regex simply make sure that 0-10 digits are entered ([0-9]{0,10}) and then split the string yourself into substrings of length 3, 3, and 4. Doing it the latter way seems better since you only want to show certain characters depending on how many digits the user has entered:

1 -> (1

123 -> (123)

1234567 -> (123) 456-7

1234567890 -> (123) 456-7890

You would have to handle each of these cases which a simple replace won't do.

Solution 3:

I have used this library: https://www.npmjs.com/package/libphonenumber-js it has a AsYouType function, that you can use on your inputs

Solution 4:

perhaps, something as the following will work for someone:

   onChange={(e) => {
                if (e.target.value.length < 13) {
                  var cleaned = ("" + e.target.value).replace(/\D/g, "");

                  let normValue = `${cleaned.substring(0, 3)}${
                    cleaned.length > 3 ? "-" : ""
                  }${cleaned.substring(3, 6)}${
                    cleaned.length > 6 ? "-" : ""
                  }${cleaned.substring(6, 11)}`;

                  handleChange(normValue);
                }
              }}

Post a Comment for "React - How To Format Phone Number As User Types"