score:25

Accepted answer

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 if no 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)}`; 
  }
};

const normalizeInput = (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)}`;
  }
};

const validateInput = value => {
  let error = ""
  
  if (!value) error = "Required!"
  else if (value.length !== 14) error = "Invalid phone format. ex: (555) 555-5555";
  
  return error;
};
    
class Form extends React.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(
      <form className="form" onSubmit={this.handleSubmit}>
        <div className="input-container">
          <p className="label">Phone:</p>
          <input
            className="input"
            type="text"
            name="phone"
            placeholder="(xxx) xxx-xxxx"
            value={this.state.phone}
            onChange={this.handleChange}
          />
          {this.state.error && <p className="error">{this.state.error}</p>}
        </div>
        <div className="btn-container">
          <button 
             className="btn danger"
             type="button"
             onClick={this.handleReset}
           >
            Reset
          </button>
          <button className="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: 0 10px;
  cursor: pointer;
  text-align: center;
  box-sizing: border-box;
  padding: 0 30px;
  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: 0 10px;
  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;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>


<div id='root'>
</div>


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

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

const initialState = {
 areaCode: "",
 prefix: "",
 suffix: "",
 error: ""
};
    
class Form extends React.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;
    } else if(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(
      <form className="form" onSubmit={this.handleSubmit}>
        <div className="input-container">
          <div className="label">
            Phone:
          </div>
          <div className="parenthesis" style={{ marginLeft: 10, marginRight: 2}}>&#40;</div>
          <input
            className="input area-code"
            type="text"
            name="areaCode"
            placeholder="xxx"
            value={this.state.areaCode}
            onChange={this.handleChange}
            maxLength="3"
          />
           <div className="parenthesis" style={{ marginRight: 2}}>&#41;</div>
          <input
            ref={node => this.setInputRef("prefix", node)}
            className="input prefix"
            type="text"
            name="prefix"
            placeholder="xxx"
            value={this.state.prefix}
            onChange={this.handleChange}
            maxLength="3"
          />
          <div className="dash">-</div>
          <input
            ref={node => this.setInputRef("suffix", node)}
            className="input suffix"
            type="text"
            name="suffix"
            placeholder="xxxx"
            value={this.state.suffix}
            onChange={this.handleChange}
            maxLength="4"
          />
        </div>
         <p className="error">{this.state.error}</p>
        <div className="btn-container">
          <button 
             className="btn danger"
             type="button"
             onClick={this.handleReset}
           >
            Reset
          </button>
          <button className="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: 0 10px;
  cursor: pointer;
  text-align: center;
  box-sizing: border-box;
  padding: 0 30px;
  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: 0 3px;
  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;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>


<div id='root'>
</div>

score:0

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

score:0

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);
                }
              }}

score:1

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.

score:3

It's all about formatting. Any key that prints a character
should cause a rewrite of the input field.
This way the user only sees valid formatted field, no matter what he does.

The regex is simple ^\D*(\d{0,3})\D*(\d{0,3})\D*(\d{0,4})

function getFormattedPhoneNum( input ) {
  let output = "(";
  input.replace( /^\D*(\d{0,3})\D*(\d{0,3})\D*(\d{0,4})/, function( match, g1, g2, g3 )
      {
        if ( g1.length ) {
          output += g1;
          if ( g1.length == 3 ) {
              output += ")";
              if ( g2.length ) {
                  output += " " + g2; 
                  if ( g2.length == 3 ) {
                      output += " - ";
                      if ( g3.length ) {
                          output += g3;
                      }
                  }
              }
           }
        }
      }       
    );        
  return output;
 }       

console.log( getFormattedPhoneNum("") );
console.log( getFormattedPhoneNum("2") );
console.log( getFormattedPhoneNum("asdf20as3d") );
console.log( getFormattedPhoneNum("203") );
console.log( getFormattedPhoneNum("203-44") );
console.log( getFormattedPhoneNum("444sg52asdf22fd44gs") );
console.log( getFormattedPhoneNum("444sg526sdf22fd44gs") );
console.log( getFormattedPhoneNum("444sg526sdf2244gs") );
console.log( getFormattedPhoneNum(" ra098 848 73653k-atui ") );

You could even get fancier and show underscores where a character
should be at any given time.
Like
(___) ___ - ____
(20_) ___ - ____
(123) 456 - ____

etc... (let me know if you want this)


Related Query

More Query from same tag