I'm using this Angular Directive to format phone numbers in an input to (999) 999-9999
. This works great until a user makes a mistake and modifies the entered phone number.
You can replicate this issue by running the code below and doing the following:
• Enter the phone number (555) 123-4567
• Place your cursor after the 4
character and delete it.
• Type in 0
twice.
You can see that the 0
is added twice and the 7
character is dropped.
Another issue is if a user attempts to delete and change the 1
character. Their cursor is pushed to the very end of the input.
I'm sure this is due to an issue with the phonenumber filter, but I'm not sure how to approach this.
function MyCntl($scope) { $scope.myModel = {}; $scope.myPrompt = "Input your phonenumber here!"; } var phonenumberModule = angular.module('phonenumberModule', []) .directive('phonenumberDirective', ['$filter', function($filter) { /* Intended use: <phonenumber-directive placeholder='prompt' model='someModel.phonenumber'></phonenumber-directive> Where: someModel.phonenumber: {String} value which to bind only the numeric characters [0-9] entered ie, if user enters 617-2223333, value of 6172223333 will be bound to model prompt: {String} text to keep in placeholder when no numeric input entered */ function link(scope, element, attributes) { // scope.inputValue is the value of input element used in template scope.inputValue = scope.phonenumberModel; scope.$watch('inputValue', function(value, oldValue) { value = String(value); var number = value.replace(/[^0-9]+/g, ''); scope.phonenumberModel = number; scope.inputValue = $filter('phonenumber')(number); }); } return { link: link, restrict: 'E', scope: { phonenumberPlaceholder: '=placeholder', phonenumberModel: '=model', }, //templateUrl: '/static/phonenumberModule/template.html', template: '<input name="phonenumber" ng-model="inputValue" type="tel" class="phonenumber" placeholder="{{phonenumberPlaceholder}}" title="Phonenumber (Format: (999) 9999-9999)">', }; }]) .filter('phonenumber', function() { /* Format phonenumber as: c (xxx) xxx-xxxx or as close as possible if phonenumber length is not 10 if c is not '1' (country code not USA), does not use country code */ return function (number) { /* @param {Number | String} number - Number that will be formatted as telephone number Returns formatted number: (###) ###-#### if number.length < 4: ### else if number.length < 7: (###) ### Does not handle country codes that are not '1' (USA) */ if (!number) { return ''; } number = String(number); // Will return formattedNumber. // If phonenumber isn't longer than an area code, just show number var formattedNumber = number; // if the first character is '1', strip it out and add it back var c = (number[0] == '1') ? '1 ' : ''; number = number[0] == '1' ? number.slice(1) : number; // # (###) ###-#### as c (area) front-end var area = number.substring(0,3); var front = number.substring(3, 6); var end = number.substring(6, 10); if (front) { formattedNumber = (c + "(" + area + ") " + front); } if (end) { formattedNumber += ("-" + end); } return formattedNumber; }; });
.phonenumber { min-width: 200px; }
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script> <div ng-app="phonenumberModule" ng-controller="MyCntl"> <p>phonenumber value: {{ myModel.phonenumber }}</p> <p>formatted phonenumber: {{ myModel.phonenumber | phonenumber }}</p> <form name="phoneForm"> <phonenumber-directive placeholder="myPrompt" model='myModel.phonenumber'></phonenumber-directive> <div ng-show="phoneForm.phonenumber.$error.minlength"> <p>Enter a valid phone number</p> </div> </form> </div>
The typical way to prevent input of certain characters is to listen to the keydown event, check the character that is being input, then use event. preventDefault() to stop the input from occurring.
Modifying a user's input as they type can be distracting and always leads to problems like those you describe.
In general, it is easier to implement and less frustrating to the user to modify the UI instead. A common solution for the case of phone numbers is to put three input fields side-by-side, with the formatting characters displayed in-between.
Using Angular, a single directive can be created to wrap this multi-part input mechanism and expose a single concatenated result to the rest of the app.
I think you should use an editable field (like input of type text) to enter the number and a read-only field (like a label) to show it formatted, because the formatted value is just a display concern, so it should not be editable.
So I modified your snippet to do that.
function MyCntl($scope) { $scope.myModel = {}; $scope.myPrompt = "Input your phonenumber here!"; } var phonenumberModule = angular.module('phonenumberModule', []) .directive('phonenumberDirective', ['$filter', function($filter) { /* Intended use: <phonenumber-directive placeholder='prompt' model='someModel.phonenumber'></phonenumber-directive> Where: someModel.phonenumber: {String} value which to bind only the numeric characters [0-9] entered ie, if user enters 617-2223333, value of 6172223333 will be bound to model prompt: {String} text to keep in placeholder when no numeric input entered */ function link(scope, element, attributes) { // scope.inputValue is the value of input element used in template scope.inputValue = scope.phonenumberModel; scope.$watch('inputValue', function(value, oldValue) { value = String(value); oldValue = String(oldValue); // get rid of input non digits chars var number = value.replace(/[^0-9]+/g, ''); var oldNumber = oldValue.replace(/[^0-9]+/g, ''); var filteredNumber = $filter('phonenumber')(number); // get rid of filter non digits chars scope.phonenumberModel = filteredNumber.replace(/[^0-9]+/g, ''); inputValue = scope.phonenumberModel; var filteredOldNumber = $filter('phonenumber')(oldNumber); if(filteredNumber.length === filteredOldNumber.length) { scope.maxlength = filteredNumber.length; } else { scope.maxlength = Math.max(number.length, 11); } }); } return { link: link, restrict: 'E', scope: { phonenumberPlaceholder: '=placeholder', phonenumberModel: '=model', }, //templateUrl: '/static/phonenumberModule/template.html', template: '<input name="phonenumber" ng-model="inputValue" type="tel" maxlength="{{maxlength || 11}}" class="phonenumber" placeholder="{{phonenumberPlaceholder}}" title="Phonenumber (Format: (999) 9999-9999)"> <label>Formatted:{{inputValue | phonenumber}}</label>', }; }]) .filter('phonenumber', function() { /* Format phonenumber as: c (xxx) xxx-xxxx or as close as possible if phonenumber length is not 10 if c is not '1' (country code not USA), does not use country code */ return function (number) { /* @param {Number | String} number - Number that will be formatted as telephone number Returns formatted number: (###) ###-#### if number.length < 4: ### else if number.length < 7: (###) ### Does not handle country codes that are not '1' (USA) */ if (!number) { return ''; } number = String(number); // Will return formattedNumber. // If phonenumber isn't longer than an area code, just show number var formattedNumber = number; // if the first character is '1', strip it out and add it back var c = (number[0] == '1') ? '1 ' : ''; number = number[0] == '1' ? number.slice(1) : number; // # (###) ###-#### as c (area) front-end var area = number.substring(0,3); var front = number.substring(3, 6); var end = number.substring(6, 10); if (front) { formattedNumber = (c + "(" + area + ") " + front); } if (end) { formattedNumber += ("-" + end); } return formattedNumber; }; });
.phonenumber { min-width: 200px; }
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script> <div ng-app="phonenumberModule" ng-controller="MyCntl"> <p>phonenumber value: {{ myModel.phonenumber }}</p> <p>formatted phonenumber: {{ myModel.phonenumber | phonenumber }}</p> <form name="phoneForm"> <phonenumber-directive placeholder="myPrompt" model='myModel.phonenumber'></phonenumber-directive> <div ng-show="phoneForm.phonenumber.$error.minlength"> <p>Enter a valid phone number</p> </div> </form> </div>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With