Skip to content

Commit 90959fa

Browse files
committed
add validation error message on the top of the input
* derive errorMessage from isValid handler #204 * add errorMesage styles for each style * add instruction how to validate number, close #207 * fix: value not formatted when changed programmatically
1 parent 801836c commit 90959fa

File tree

8 files changed

+124
-39
lines changed

8 files changed

+124
-39
lines changed

README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,10 @@ If `enableAreaCodeStretch` is added, the part of the mask with the area code wil
385385
<td> showDropdown </td>
386386
<td> false </td>
387387
</tr>
388+
<tr>
389+
<td> defaultInvalidMessage </td>
390+
<td> string </td>
391+
</tr>
388392
</table>
389393

390394
### Custom localization
@@ -417,12 +421,28 @@ handleOnChange(value, data, event) {
417421
```
418422

419423
### Check validity of the phone number
424+
`isValid(value, country, countries, hiddenAreaCodes)`
425+
426+
```jsx
427+
<PhoneInput
428+
isValid={(value, country) => {
429+
if (value.match(/12345/)) {
430+
return 'Invalid value: '+value+', '+country.name;
431+
} else if (value.match(/1234/)) {
432+
return false;
433+
} else {
434+
return true;
435+
}
436+
}}
437+
/>
438+
```
439+
420440
```jsx
421441
import startsWith from 'lodash.startswith';
422442

423443
<PhoneInput
424-
isValid={(inputNumber, onlyCountries) => {
425-
return onlyCountries.some((country) => {
444+
isValid={(inputNumber, country, countries) => {
445+
return countries.some((country) => {
426446
return startsWith(inputNumber, country.dialCode) || startsWith(country.dialCode, inputNumber);
427447
});
428448
}}

src/index.js

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,11 @@ class PhoneInput extends React.Component {
8181
onBlur: PropTypes.func,
8282
onClick: PropTypes.func,
8383
onKeyDown: PropTypes.func,
84-
isValid: PropTypes.func,
84+
isValid: PropTypes.oneOfType([
85+
PropTypes.bool,
86+
PropTypes.func,
87+
]),
88+
defaultErrorMessage: PropTypes.string,
8589
}
8690

8791
static defaultProps = {
@@ -113,7 +117,6 @@ class PhoneInput extends React.Component {
113117
autoFormat: true,
114118
enableAreaCodes: false,
115119
enableTerritories: false,
116-
isValid: (inputNumber, onlyCountries) => true,
117120
disableCountryCode: false,
118121
disableDropdown: false,
119122
enableLongNumbers: false,
@@ -143,6 +146,9 @@ class PhoneInput extends React.Component {
143146
enableClickOutside: true,
144147
showDropdown: false,
145148

149+
isValid: true, // (value, selectedCountry, onlyCountries, hiddenAreaCodes) => true | false | 'Message'
150+
defaultErrorMessage: '',
151+
146152
onEnterKeyPress: null, // null or function
147153

148154
keys: {
@@ -279,16 +285,17 @@ class PhoneInput extends React.Component {
279285

280286
// Hooks for updated props
281287
updateCountry = (country) => {
288+
const { onlyCountries } = this.state
282289
let newSelectedCountry;
283-
if (country.indexOf(0) >= '0' && country.indexOf(0) <= '9') {
284-
newSelectedCountry = this.state.onlyCountries.find(o => o.dialCode == +country);
290+
if (country.indexOf(0) >= '0' && country.indexOf(0) <= '9') { // digit
291+
newSelectedCountry = onlyCountries.find(o => o.dialCode == +country);
285292
} else {
286-
newSelectedCountry = this.state.onlyCountries.find(o => o.iso2 == country);
293+
newSelectedCountry = onlyCountries.find(o => o.iso2 == country);
287294
}
288295
if (newSelectedCountry && newSelectedCountry.dialCode) {
289296
this.setState({
290297
selectedCountry: newSelectedCountry,
291-
formattedNumber: this.props.disableCountryCode ? '' : this.props.prefix+newSelectedCountry.dialCode
298+
formattedNumber: this.props.disableCountryCode ? '' : this.formatNumber(newSelectedCountry.dialCode, newSelectedCountry),
292299
});
293300
}
294301
}
@@ -301,9 +308,8 @@ class PhoneInput extends React.Component {
301308

302309
if (value === '') return this.setState({ selectedCountry, formattedNumber: '' });
303310

304-
let newSelectedCountry;
305311
let inputNumber = value.replace(/\D/g, '');
306-
let formattedNumber = value;
312+
let newSelectedCountry, formattedNumber;
307313

308314
// if new value start with selectedCountry.dialCode, format number, otherwise find newSelectedCountry
309315
if (selectedCountry && startsWith(value, selectedCountry.dialCode)) {
@@ -844,8 +850,22 @@ class PhoneInput extends React.Component {
844850
}
845851

846852
render() {
847-
const { onlyCountries, selectedCountry, showDropdown, formattedNumber } = this.state;
848-
const { disableDropdown, renderStringAsFlag } = this.props;
853+
const { onlyCountries, selectedCountry, showDropdown, formattedNumber, hiddenAreaCodes } = this.state;
854+
const { disableDropdown, renderStringAsFlag, isValid, defaultErrorMessage } = this.props;
855+
856+
let isValidValue, errorMessage;
857+
if (typeof isValid === 'boolean') {
858+
isValidValue = isValid;
859+
} else {
860+
const isValidProcessed = isValid(formattedNumber.replace(/\D/g, ''), selectedCountry, onlyCountries, hiddenAreaCodes)
861+
if (typeof isValidProcessed === 'boolean') {
862+
isValidValue = isValidProcessed;
863+
if (isValidValue === false) errorMessage = defaultErrorMessage
864+
} else { // typeof === 'string'
865+
isValidValue = false;
866+
errorMessage = isValidProcessed;
867+
}
868+
}
849869

850870
const containerClasses = classNames({
851871
[this.props.containerClass]: true,
@@ -855,7 +875,7 @@ class PhoneInput extends React.Component {
855875
const inputClasses = classNames({
856876
[this.props.inputClass]: true,
857877
'form-control': true,
858-
'invalid-number': !this.props.isValid(formattedNumber.replace(/\D/g, ''), onlyCountries),
878+
'invalid-number': !isValidValue,
859879
'open': showDropdown,
860880
});
861881
const selectedFlagClasses = classNames({
@@ -865,6 +885,7 @@ class PhoneInput extends React.Component {
865885
const flagViewClasses = classNames({
866886
[this.props.buttonClass]: true,
867887
'flag-dropdown': true,
888+
'invalid-number': !isValidValue,
868889
'open': showDropdown,
869890
});
870891
const inputFlagClasses = `flag ${selectedCountry && selectedCountry.iso2}`;
@@ -874,6 +895,7 @@ class PhoneInput extends React.Component {
874895
className={containerClasses}
875896
style={this.props.style || this.props.containerStyle}
876897
onKeyDown={this.handleKeydown}>
898+
{errorMessage && <div className='invalid-number-message'>{errorMessage}</div>}
877899
<input
878900
className={inputClasses}
879901
style={this.props.inputStyle}

src/style/bootstrap.less

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,12 @@
1717
border-color: #80bdff;
1818
outline: 0;
1919
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
20+
&.invalid-number {
21+
box-shadow: 0 0 0 0.2rem rgba(222,0,0,.25);
22+
}
2023
}
2124
&.invalid-number {
2225
border: 1px solid #f44336;
23-
border-left-color: #cacaca;
24-
&:focus {
25-
box-shadow: 0 0 0 1px #f44336;
26-
}
27-
}
28-
&.open {
29-
z-index: 2;
3026
}
3127
}
3228
.flag-dropdown {
@@ -148,4 +144,14 @@
148144
opacity: .7;
149145
}
150146
}
147+
.invalid-number-message {
148+
position: absolute;
149+
z-index: 1;
150+
font-size: 13px;
151+
left: 25px;
152+
top: -7px;
153+
background: #fff;
154+
padding: 0 5px;
155+
color: #de0000;
156+
}
151157
}

src/style/material.less

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,21 @@
2323
}
2424
&.invalid-number {
2525
border: 1px solid #f44336;
26-
border-left-color: #cacaca;
2726
&:focus {
2827
box-shadow: 0 0 0 1px #f44336;
2928
}
3029
&+div:before {
3130
content: 'Error';
31+
display: none;
3232
color: #f44336;
3333
width: 27px;
3434
}
3535
}
36-
&.open {
37-
z-index: 2;
38-
}
3936
&+div:before {
4037
content: 'Phone input';
4138
position: absolute;
4239
top: -7px;
43-
left: 19px;
40+
left: 25px;
4441
display: block;
4542
background: white;
4643
padding: 0 5px;
@@ -170,4 +167,14 @@
170167
opacity: .7;
171168
}
172169
}
170+
.invalid-number-message {
171+
position: absolute;
172+
z-index: 1;
173+
font-size: 13px;
174+
left: 25px;
175+
top: -7px;
176+
background: #fff;
177+
padding: 0 5px;
178+
color: #de0000;
179+
}
173180
}

src/style/plain.less

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
position: relative;
66
font-size: 14px;
77
letter-spacing: .01rem;
8-
z-index: 0;
98
margin-top: 0 !important;
109
margin-bottom: 0 !important;
1110
padding-left: 48px;
@@ -26,9 +25,6 @@
2625
background-color: #FAF0F0;
2726
}
2827
}
29-
&.open {
30-
z-index: 2;
31-
}
3228
}
3329
.flag-dropdown {
3430
outline: none;
@@ -37,14 +33,11 @@
3733
bottom: 0;
3834
padding: 0;
3935
border: 1px solid #cacaca;
40-
border-radius: 3px 0 0 3px;
4136
&.open {
4237
z-index: 2;
4338
background: #fff;
44-
border-radius: 3px 0 0 0;
4539
.selected-flag {
4640
background: #fff;
47-
border-radius: 3px 0 0 0;
4841
}
4942
}
5043
&:hover, &:focus, &.open {
@@ -154,4 +147,14 @@
154147
opacity: .7;
155148
}
156149
}
150+
.invalid-number-message {
151+
position: absolute;
152+
z-index: 1;
153+
font-size: 13px;
154+
left: 46px;
155+
top: -8px;
156+
background: #fff;
157+
padding: 0 2px;
158+
color: #de0000;
159+
}
157160
}

src/style/semantic-ui.less

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
position: relative;
66
font-size: 14px;
77
letter-spacing: .01rem;
8-
z-index: 0;
98
margin-top: 0 !important;
109
margin-bottom: 0 !important;
1110
padding-left: 48px;
@@ -33,7 +32,6 @@
3332
&.open {
3433
box-shadow: rgba(34, 36, 38, 0.15) 0px 2px 3px 0px;
3534
border-color: rgb(150, 200, 218);
36-
z-index: 2;
3735
border-radius: 5px 5px 0 0;
3836
border-bottom: none;
3937
box-shadow: none;
@@ -170,4 +168,14 @@
170168
&::-webkit-scrollbar-track { background-color: #e6e6e6; }
171169
&::-webkit-scrollbar-thumb { background-color: #c5c5c4; border-radius: 5px; }
172170
}
171+
.invalid-number-message {
172+
position: absolute;
173+
z-index: 1;
174+
font-size: 13px;
175+
left: 46px;
176+
top: -8px;
177+
background: #fff;
178+
padding: 0 2px;
179+
color: #de0000;
180+
}
173181
}

src/style/style.less

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
position: relative;
66
font-size: 14px;
77
letter-spacing: .01rem;
8-
z-index: 0;
98
margin-top: 0 !important;
109
margin-bottom: 0 !important;
1110
padding-left: 48px;
@@ -27,9 +26,6 @@
2726
background-color: #FAF0F0;
2827
}
2928
}
30-
&.open {
31-
z-index: 2;
32-
}
3329
}
3430
.flag-dropdown {
3531
outline: none;
@@ -40,6 +36,9 @@
4036
background-color: #f5f5f5;
4137
border: 1px solid #cacaca;
4238
border-radius: 3px 0 0 3px;
39+
&.invalid-number {
40+
border-color: #d79f9f;
41+
}
4342
&.open {
4443
z-index: 2;
4544
background: #fff;
@@ -155,4 +154,14 @@
155154
opacity: .7;
156155
}
157156
}
157+
.invalid-number-message {
158+
position: absolute;
159+
z-index: 1;
160+
font-size: 13px;
161+
left: 46px;
162+
top: -8px;
163+
background: #fff;
164+
padding: 0 2px;
165+
color: #de0000;
166+
}
158167
}

test/dev:js/demo.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ class Demo extends React.Component {
113113
/>
114114
<p>Custom regions selected: {`{['north-america', 'carribean']}`}</p>
115115
<PhoneInput
116-
country='us'
116+
country='ca'
117117
regions={['north-america', 'carribean']}
118118
/>
119119
<p>Disabled dropdown and country code</p>
@@ -186,6 +186,16 @@ class Demo extends React.Component {
186186
value={this.state.value}
187187
onChange={(value, country, e) => {console.log(value, country, e); this.setState({ value })}}
188188
enableAreaCodes
189+
defaultErrorMessage='Invalid value'
190+
isValid={(value, country) => {
191+
if (value.match(/12345/)) {
192+
return 'Invalid value: '+value+', '+country.name
193+
} else if (value.match(/1234/)) {
194+
return false
195+
} else {
196+
return true
197+
}
198+
}}
189199
/>
190200
<PhoneInput
191201
containerStyle={{marginBottom: '15px'}}

0 commit comments

Comments
 (0)