floatleft
floatleft

Reputation: 6541

Prevent entering any additional characters

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>

Upvotes: 24

Views: 1014

Answers (3)

starchild
starchild

Reputation: 496

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.

Upvotes: 5

Xartok
Xartok

Reputation: 346

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>

Upvotes: 2

Saltuk
Saltuk

Reputation: 1159

Can you check plunker please ... i hope this is what u want ..

http://plnkr.co/edit/0IBJBRb2JtvZnq6PtBiV?p=preview

supporting masking and regex .. (is beta) Usage : # for both , a for chars , 9 is for numbers ... ( mask filter is not working as accepted will check it ..

 <maskinput ng-mask-model="value" ng-mask="9 (999) 99-9999" ng-mask-regex="[^0-9]+"></maskinput>

Upvotes: 1

Related Questions