React - How to format phone number as user types
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)
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 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}}>(</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>