React - How to format phone number as user types
You can normalize
the input
like so
- the
value
is up-to-date in relation toevent.target.value
previousValue
is what has already been validated and set tostate
This is structured in a way to prevent invalid characters from updating the input and also limits the input to 10 numbers.
Click the 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}}>(</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}}>)</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>
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)
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.