FormValidation v0.8.1 is released, supports Bootstrap 4 alpha 3

Validating international phone numbers

Examples

FormValidation provides the built-in phone validator to validate the phone number in various countries. Behind the scenes, it uses Javascript regular expressions to check if a given number matches the pattern of phone numbers in particular country. Therefore, despite the fact that it doesn't cover all possible formats of phone number in the world, you can use the regexp validator to test a phone number in your country.

This example helps you validate an international phone number by using a different approach. We will use the intl-tel-input plugin for that purpose.

intl-tel-input is a popular jQuery plugin for entering and validating international telephone numbers. Below is some of its advanced features:

  • Provides a very friendly user interface to enter a phone number. All countries are shown as a drop list with the flags and suggestion phone number
  • Provides up-to-date patterns of phone numbers in the world. The data are taken from Google libphonenumber library so they are completely stable
  • Has a few of APIs to validate and integrate with other tools

Using the intl-tel-input plugin

It's quite easy to use the intl-tel-input plugin for an input field:

<!-- The paths might be changed to suit with your folder structure -->
<link rel="stylesheet" href="/vendor/intl-tel-input/build/css/intlTelInput.css" />
<script src="/vendor/intl-tel-input/build/js/intlTelInput.min.js"></script>
  • Attach the plugin to field
$(document).ready(function() {
    $('#contactForm')
        .find('[name="phoneNumber"]')
            .intlTelInput({
                utilsScript: '/vendor/intl-tel-input/lib/libphonenumber/build/utils.js',
                autoPlaceholder: true,
                preferredCountries: ['fr', 'us', 'gb']
            });
});

In the next sections, you will see how to integrate the intl-tel-input plugin with FormValidation.

You should look at the basic principles when integrating FormValidation with other plugins

Using the callback validator

We can make the intl-tel-input plugin work with FormValidation easily by combining its isValidNumber() method and the callback validator:

$('#contactForm').formValidation({
    ...
    fields: {
        phoneNumber: {
            validators: {
                callback: {
                    message: 'The phone number is not valid',
                    callback: function(value, validator, $field) {
                        return value === '' || $field.intlTelInput('isValidNumber');
                    }
                }
            }
        }
    }
});

There is an important note that the input must be revalidated when the user choose another country from the drop list. Unfortunately, intl-tel-input doesn't provide an event or callback that is executed after choosing a country. But it can be done by using a simple click event handler on the countries list element which has .country-list class:

$('#contactForm')
    .formValidation(...)
    // Revalidate the number when changing the country
    .on('click', '.country-list', function() {
        $('#contactForm').formValidation('revalidateField', 'phoneNumber');
    });

The field is revalidated by the revalidateField() method.

You can try this approach yourself in the live form below:

<link rel="stylesheet" href="/vendor/intl-tel-input/build/css/intlTelInput.css" />

<form id="contactForm" class="form-horizontal">
    <div class="form-group">
        <label class="col-xs-3 control-label">Phone number</label>
        <div class="col-xs-5">
            <input type="tel" class="form-control" name="phoneNumber" />
        </div>
    </div>
</form>

<script src="/vendor/intl-tel-input/build/js/intlTelInput.min.js"></script>
<script>
$(document).ready(function() {
    $('#contactForm')
        .find('[name="phoneNumber"]')
            .intlTelInput({
                utilsScript: '/vendor/intl-tel-input/lib/libphonenumber/build/utils.js',
                autoPlaceholder: true,
                preferredCountries: ['fr', 'us', 'gb']
            });

    $('#contactForm')
        .formValidation({
            framework: 'bootstrap',
            icon: {
                valid: 'glyphicon glyphicon-ok',
                invalid: 'glyphicon glyphicon-remove',
                validating: 'glyphicon glyphicon-refresh'
            },
            fields: {
                phoneNumber: {
                    validators: {
                        callback: {
                            message: 'The phone number is not valid',
                            callback: function(value, validator, $field) {
                                return value === '' || $field.intlTelInput('isValidNumber');
                            }
                        }
                    }
                }
            }
        })
        // Revalidate the number when changing the country
        .on('click', '.country-list', function() {
            $('#contactForm').formValidation('revalidateField', 'phoneNumber');
        });
});
</script>

Developing custom validator

If you often use intl-tel-input plugin in multiple forms, it's the right time to write a custom validator and reuse it whenever you want.

It's easy to develop new validator, assume that it's named as intPhoneNumber:

$(document).ready(function() {
    // Define new validator
    FormValidation.Validator.intPhoneNumber = {
        init: function(validator, $field, options) {
            // Attach the intlTelInput on field
            $field.intlTelInput({
                utilsScript: '/vendor/intl-tel-input/lib/libphonenumber/build/utils.js',
                autoPlaceholder: true,
                preferredCountries: 'fr,us,gb',
            });

            // Revalidate the field when changing the country
            var $form     = validator.getForm(),
                fieldName = $field.attr('data-fv-field');
            $form.on('click.country.intphonenumber', '.country-list', function() {
                $form.formValidation('revalidateField', fieldName);
            });
        },

        destroy: function(validator, $field, options) {
            $field.intlTelInput('destroy');

            // Turn off the event
            validator.getForm().off('click.country.intphonenumber');
        },

        validate: function(validator, $field, options) {
            return $field.val() === '' || $field.intlTelInput('isValidNumber');
        }
    };
});

The following table recaps the purpose of init, destroy and validate methods used above:

Method Functionality
init

This method does anything you need to prepare the validation.

It's called once right after attaching the validator to field

destroy This method is called when you destroy the FormValidation instance by using the destroy() method
validate This is the validation method. It returns true or false that indicates the field is valid or invalid

These methods take three parameters:

  • validator is the FormValidation instance. So you can call public methods on it
  • $field is the field element
  • options is an object containing the validator options

Applying the new validator to field is easy:

$('#contactForm').formValidation({
    ...
    fields: {
        phoneNumber: {
            validators: {
                intPhoneNumber: {
                    message: 'The phone number is not valid'
                }
            }
        }
    }
});

There's still a small thing we should improve. As seen in the code above, there're some hard coded options when attaching intl-tel-input to field:

FormValidation.Validator.intPhoneNumber = {
    init: function(validator, $field, options) {
        // The utilsScript, autoPlaceholder and preferredCountries options are fixed
        $field.intlTelInput({
            utilsScript: '/vendor/intl-tel-input/lib/libphonenumber/build/utils.js',
            autoPlaceholder: true,
            preferredCountries: 'fr,us,gb',
        });

        ...
    }

    ...
};

You don't need to worry about this. It's possible to define and pass options to the options parameter as following:

FormValidation.Validator.intPhoneNumber = {
    html5Attributes: {
        message: 'message',
        autoplaceholder: 'autoPlaceholder',
        preferredcountries: 'preferredCountries',
        utilsscript: 'utilsScript'
    },

    init: function(validator, $field, options) {
        // Determine the preferred countries
        var autoPlaceholder    = options.autoPlaceholder === true || options.autoPlaceholder === 'true',
            preferredCountries = options.preferredCountries || 'us';
        if ('string' === typeof preferredCountries) {
            preferredCountries = preferredCountries.split(',');
        }

        // Attach the intlTelInput on field
        $field.intlTelInput({
            utilsScript: options.utilsScript || '',
            autoPlaceholder: autoPlaceholder,
            preferredCountries: preferredCountries
        });

        ...
    }

    ...
};

The html5Attributes property defines the HTML attributes which can be mapped with the validator options. These attributes can be used in the declarative mode, for example:

<input name="phoneNumber"
    data-fv-intphonenumber="true"
    data-fv-intphonenumber-autoplaceholder="true"
    data-fv-intphonenumber-preferredcountries="fr,us,gb"
    data-fv-intphonenumber-utilsscript="/vendor/intl-tel-input/lib/libphonenumber/build/utils.js"
    data-fv-intphonenumber-message="The phone number is not valid" />

The snippet code below is a programmatic usage of passing the options:

$('#contactForm').formValidation({
    ...
    fields: {
        phoneNumber: {
            validators: {
                intPhoneNumber: {
                    utilsScript: '/vendor/intl-tel-input/lib/libphonenumber/build/utils.js',
                    autoPlaceholder: true,
                    preferredCountries: 'fr,us,gb',
                    message: 'The phone number is not valid'
                }
            }
        }
    }
});

You can take a look at the code tab to see how all of these things work together:

<link rel="stylesheet" href="/vendor/intl-tel-input/build/css/intlTelInput.css" />

<form id="contactForm" class="form-horizontal">
    <div class="form-group">
        <label class="col-xs-3 control-label">Phone number</label>
        <div class="col-xs-5">
            <input type="tel" class="form-control" name="phoneNumber" />
        </div>
    </div>
</form>

<script src="/vendor/intl-tel-input/build/js/intlTelInput.min.js"></script>
<script>
$(document).ready(function() {
    // Define new validator
    FormValidation.Validator.intPhoneNumber = {
        html5Attributes: {
            message: 'message',
            autoplaceholder: 'autoPlaceholder',
            preferredcountries: 'preferredCountries',
            utilsscript: 'utilsScript'
        },

        init: function(validator, $field, options) {
            // Determine the preferred countries
            var autoPlaceholder    = options.autoPlaceholder === true || options.autoPlaceholder === 'true',
                preferredCountries = options.preferredCountries || 'us';
            if ('string' === typeof preferredCountries) {
                preferredCountries = preferredCountries.split(',');
            }

            // Attach the intlTelInput on field
            $field.intlTelInput({
                utilsScript: options.utilsScript || '',
                autoPlaceholder: autoPlaceholder,
                preferredCountries: preferredCountries
            });

            // Revalidate the field when changing the country
            var $form     = validator.getForm(),
                fieldName = $field.attr('data-fv-field');
            $form.on('click.country.intphonenumber', '.country-list', function() {
                $form.formValidation('revalidateField', fieldName);
            });
        },

        destroy: function(validator, $field, options) {
            $field.intlTelInput('destroy');

            validator.getForm().off('click.country.intphonenumber');
        },

        validate: function(validator, $field, options) {
            return $field.val() === '' || $field.intlTelInput('isValidNumber');
        }
    };

    $('#contactForm').formValidation({
        framework: 'bootstrap',
        icon: {
            valid: 'glyphicon glyphicon-ok',
            invalid: 'glyphicon glyphicon-remove',
            validating: 'glyphicon glyphicon-refresh'
        },
        fields: {
            phoneNumber: {
                validators: {
                    intPhoneNumber: {
                        utilsScript: '/vendor/intl-tel-input/lib/libphonenumber/build/utils.js',
                        autoPlaceholder: true,
                        preferredCountries: 'fr,us,gb',
                        message: 'The phone number is not valid'
                    }
                }
            }
        }
    });
});
</script>

Bonus: Showing useful validation message

What if you want to provide more descriptive validation message. For example, how to inform the user that the number is too short, too long or even not a number.

We can archive this requirement by adding a dynamic message supported by the callback validator.

The message is determined based on the validation error that is retrieved by the getValidationError() method:

$('#contactForm').formValidation({
    ...
    fields: {
        phoneNumber: {
            validators: {
                callback: {
                    callback: function(value, validator, $field) {
                        var isValid = value === '' || $field.intlTelInput('isValidNumber'),
                            err     = $field.intlTelInput('getValidationError'),
                            message = null;
                        switch (err) {
                            case intlTelInputUtils.validationError.INVALID_COUNTRY_CODE:
                                message = 'The country code is not valid';
                                break;

                            case intlTelInputUtils.validationError.TOO_SHORT:
                                message = 'The phone number is too short';
                                break;

                            case intlTelInputUtils.validationError.TOO_LONG:
                                message = 'The phone number is too long';
                                break;

                            case intlTelInputUtils.validationError.NOT_A_NUMBER:
                                message = 'The value is not a number';
                                break;

                            default:
                                message = 'The phone number is not valid';
                                break;
                        }

                        return {
                            valid: isValid,
                            message: message
                        };
                    }
                }
            }
        }
    }
});

Here is the full working demonstration:

<link rel="stylesheet" href="/vendor/intl-tel-input/build/css/intlTelInput.css" />

<form id="contactForm" class="form-horizontal">
    <div class="form-group">
        <label class="col-xs-3 control-label">Phone number</label>
        <div class="col-xs-5">
            <input type="tel" class="form-control" name="phoneNumber" />
        </div>
    </div>
</form>

<script src="/vendor/intl-tel-input/build/js/intlTelInput.min.js"></script>
<script>
$(document).ready(function() {
    $('#contactForm')
        .find('[name="phoneNumber"]')
            .intlTelInput({
                utilsScript: '/vendor/intl-tel-input/lib/libphonenumber/build/utils.js',
                autoPlaceholder: true,
                preferredCountries: ['fr', 'us', 'gb']
            });

    $('#contactForm')
        .formValidation({
            framework: 'bootstrap',
            icon: {
                valid: 'glyphicon glyphicon-ok',
                invalid: 'glyphicon glyphicon-remove',
                validating: 'glyphicon glyphicon-refresh'
            },
            fields: {
                phoneNumber: {
                    validators: {
                        callback: {
                            callback: function(value, validator, $field) {
                                var isValid = value === '' || $field.intlTelInput('isValidNumber'),
                                    err     = $field.intlTelInput('getValidationError'),
                                    message = null;
                                switch (err) {
                                    case intlTelInputUtils.validationError.INVALID_COUNTRY_CODE:
                                        message = 'The country code is not valid';
                                        break;

                                    case intlTelInputUtils.validationError.TOO_SHORT:
                                        message = 'The phone number is too short';
                                        break;

                                    case intlTelInputUtils.validationError.TOO_LONG:
                                        message = 'The phone number is too long';
                                        break;

                                    case intlTelInputUtils.validationError.NOT_A_NUMBER:
                                        message = 'The value is not a number';
                                        break;

                                    default:
                                        message = 'The phone number is not valid';
                                        break;
                                }

                                return {
                                    valid: isValid,
                                    message: message
                                };
                            }
                        }
                    }
                }
            }
        })
        // Revalidate the number when changing the country
        .on('click', '.country-list', function() {
            $('#contactForm').formValidation('revalidateField', 'phoneNumber');
        });
});
</script>

Bonus: Asking number to match given type

The patterns provided by Google libphonenumber covers most of possible types of a phone number such as mobile, fixed line, free phone lines, voice over IP, etc.

You can see the full list of these types from source code of phonenumberutil.js

The type can be retrieved by the getNumberType() method. By adding the type to the callback return value, and reuse it later, we can treat a phone number as invalid one if it doesn't match our type.

The following code illustrates how to accept the mobile phone numbers only:

$('#contactForm')
    .formValidation({
        ...
        fields: {
            phoneNumber: {
                validators: {
                    callback: {
                        message: 'The phone number is not valid',
                        callback: function(value, validator, $field) {
                            return {
                                valid: value === '' || $field.intlTelInput('isValidNumber'),
                                type: $field.intlTelInput('getNumberType')
                            };
                        }
                    }
                }
            }
        }
    })
    .on('success.validator.fv', function(e, data) {
        if (data.field === 'phoneNumber' && data.validator === 'callback' && data.element.val() !== '') {
            if (data.result.type !== intlTelInputUtils.numberType.MOBILE) {
                data.fv
                    // Mark the field as invalid
                    .updateStatus('phoneNumber', 'INVALID', 'callback')
                    // Update the message
                    .updateMessage('phoneNumber', 'callback', 'We accept the mobile numbers only');
            } else {
                // Reset the message
                data.fv.updateMessage('phoneNumber', 'callback', 'The phone number is not valid');
            }
        }
    });
For United States, getNumberType() returns FIXED_LINE_OR_MOBILE because there's no way to differentiate between fixed-line and mobile numbers

In short, it handles the success.validator.fv event which is triggered when the field passes a validator. Then depend on the type returned by the validation result, it can use the updateStatus() method to mark field as invalid one.

The updateMessage() method is also used to set or reset the validation message.

Take a look at the Using data returned by validator and Asking credit card number to match with selected type examples to get inspired of the idea
<link rel="stylesheet" href="/vendor/intl-tel-input/build/css/intlTelInput.css" />

<form id="contactForm" class="form-horizontal">
    <div class="form-group">
        <label class="col-xs-3 control-label">Phone number</label>
        <div class="col-xs-5">
            <input type="tel" class="form-control" name="phoneNumber" />
        </div>
    </div>
</form>

<script src="/vendor/intl-tel-input/build/js/intlTelInput.min.js"></script>
<script>
$(document).ready(function() {
    $('#contactForm')
        .find('[name="phoneNumber"]')
            .intlTelInput({
                utilsScript: '/vendor/intl-tel-input/lib/libphonenumber/build/utils.js',
                autoPlaceholder: true,
                preferredCountries: ['fr', 'us', 'gb']
            });

    $('#contactForm')
        .formValidation({
            framework: 'bootstrap',
            icon: {
                valid: 'glyphicon glyphicon-ok',
                invalid: 'glyphicon glyphicon-remove',
                validating: 'glyphicon glyphicon-refresh'
            },
            fields: {
                phoneNumber: {
                    validators: {
                        callback: {
                            message: 'The phone number is not valid',
                            callback: function(value, validator, $field) {
                                return {
                                    valid: value === '' || $field.intlTelInput('isValidNumber'),
                                    type: $field.intlTelInput('getNumberType')
                                };
                            }
                        }
                    }
                }
            }
        })
        .on('success.validator.fv', function(e, data) {
            if (data.field === 'phoneNumber' && data.validator === 'callback' && data.element.val() !== '') {
                // You can see type of phone number by printing out data.result.type
                // console.log(data.result.type);
                if (data.result.type !== intlTelInputUtils.numberType.MOBILE) {
                    data.fv
                        // Mark the field as invalid
                        .updateStatus('phoneNumber', 'INVALID', 'callback')
                        // Update the message
                        .updateMessage('phoneNumber', 'callback', 'We accept the mobile numbers only');
                } else {
                    // Reset the message
                    data.fv.updateMessage('phoneNumber', 'callback', 'The phone number is not valid');
                }
            }
        })
        // Revalidate the number when changing the country
        .on('click', '.country-list', function() {
            $('#contactForm').formValidation('revalidateField', 'phoneNumber');
        });
});
</script>