AngularJS dynamically change ngMask - javascript

I need to dynamically change a mask.
So I'm making this directive to handle it:
link: function($scope, $element, $attrs, ngModel) {
$scope.$watch($attrs.ngModel, function(value) {
if (value.length > 4) {
$element.attr('mask', '9999 9999');
}
else {
$element.attr('mask', '99999999');
}
});
}
The mask is being applied, I'm checking the DOM, but there's no effect whatsoever.
What I am missing here?

Can you do that logic in the dom instead of the link? Modyfing the attr probably won't do anything as it's already been parsed and it might not be watching it.
ng-model="maskModel" mask="{{ maskModel.length > 4 ? '9999 9999' : '99999999' }}"

I know this isn't what you are asking but it may help others coming here. A good alternative is to define an optional character. To do that just add an '?' after the character you want to be optional:
mask="9?9999-9999"
This is great for inputs like Brazilian phone numbers, which can have both 8 or 9 characters.

Use attrs.$observe(..) instead of $scope.$watch (..).

Related

Directive for restricting typing by Regex in AngularJS

I coded an angular directive for inhibiting typing from inputs by specifying a regex. In that directive I indicate a regex that will be used for allow the input data. Conceptually, it works fine, but there are two bugs in this solution:
In the first Plunker example the input must allow only numbers or numbers followed by a dot [.], or numbers followed by a dot followed by numbers with no more than four digits.
If I type a value '1.1111' and after that I go to the first digit and so type another digit (in order to get a value as '11.1111') , nothing happening. The bug is in the fact I use the expression elem.val() + event.key on my regex validator. I do not know how to get the whole
current value for a input on a keypress event;
The second one is the fact that some characters (grave, acute, tilde, circumflex) are being allowed on typing (press one of them more than once), althought the regex does not allow them.
What changes do I need to make in my code in order to get an effective type restriction by regex?
<html ng-app="app">
<head>
<script data-require="angularjs#1.6.4" data-semver="1.6.4" src="https://code.angularjs.org/1.6.4/angular.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body>
<h1>Restrict typing by RegExp</h1>
PATTERN 1 (^\d+$|^\d+[.]$|^\d+[.]\d{1,4}$) <input type="text" allow-typing="^\d+$|^\d+[.]$|^\d+[.]\d{1,4}$"/><br>
ONLY NUMBERS <input type="text" allow-typing="^[0-9]+$"/><br>
ONLY STRINGS <input type="text" allow-typing="^[a-zA-Z]+$"/>
</body>
</html>
Directive
angular.module('app', []).directive('allowTyping', function() {
return {
restrict: 'A',
link: function(scope, elem, attrs, ctrl) {
var regex = attrs.allowTyping;
elem.bind('keypress', function(event) {
var input = elem.val() + event.key;
var validator = new RegExp(regex);
if(!validator.test(input)) {
event.preventDefault();
return false;
}
});
}
};
});
If this were my code, I'd change tactics entirely: I would listen for input events instead of trying to micromanage the user interactions with the field.
The approach you are taking, in general, has problems. The biggest one is that keypress won't be emitted for all changes to the field. Notably,
It is not triggered by DELETE and BACKSPACE keys.
Input methods can bypass it. When you entered diacritics as diacritics, your code was not registering the change. In general, if the user is using an input method, there is no guarantee that each new character added to the field will result in a keypress event. It depends on the method the user has chosen.
keypress does not help when the user cuts from the field or pastes into the field.
You could add code to try to handle all the cases above, but it would get complex quick. You've already run into an issue with elem.val() + event.key because the keypress may not always be about a character inserted at the end of the field. The user may have moved the caret so you have to keep track of caret position. One comment suggested listening to keyup but that does not help with input methods or paste/cut events.
In contrast, the input event is generated when the value of the field changes, as the changes occur. All cases above are taken care of. This, for instance, would work:
elem.bind('input', function(event) {
var validator = new RegExp(regex);
elem.css("background-color", !validator.test(elem.val()) ? "red" : null);
});
This is a minimal illustration that you could plop into your fiddle to replace your current event handler. In a real application, I'd give the user a verbose error message rather than just change the color of the field and I'd create validator just once, outside the event handler, but this gives you the idea.
(There's also a change event but you do no want to use that. For text fields, it is generated when the focus leaves the field, which is much too late.)
See Plnkr Fixed as per your approach:
The explanation of why and the changes are explained below.
Side note: I would not implement it this way (use ngModel with $parsers and $formatters, e.g. https://stackoverflow.com/a/15090867/2103767) - implementing that is beyond the scope of your question. However I found a full implementation by regexValidate by Ben Lesh which will fit your problem domain:-
If I type a value '1.1111' and after that I go to the first digit and so type another digit (in order to get a value as '11.1111') , nothing happening.
because in your code below
var input = elem.val() + event.key;
you are assuming that the event.key is always appended at the end.
So how to get the position of the correct position and validate the the reconstructed string ? You can use an undocumented event.target.selectionStart property. Note even though you are not selecting anything you will have this populated (IE 11 and other browsers). See Plnkr Fixed
The second one is the fact that some characters (grave, acute, tilde, circumflex) are being allowed on typing (press one of them more than once), althought the regex does not allow them.
Fixed the regex - correct one below:
^[0-9]*(?:\.[0-9]{0,4})?$
So the whole thing looks as below
link: function(scope, elem, attrs, ctrl) {
var regex = attrs.allowTyping;
elem.bind('keypress', function(event) {
var pos = event.target.selectionStart;
var oldViewValue = elem.val();
var input = newViewValue(oldViewValue, pos, event.key);
console.log(input);
var validator = new RegExp(regex);
if (!validator.test(input)) {
event.preventDefault();
return false;
}
});
function newViewValue(oldViewValue, pos, key) {
if (!oldViewValue) return key;
return [oldViewValue.slice(0, pos), key, oldViewValue.slice(pos)].join('');
}
}
You specified 4 different patterns 3 different pattens in your regex separated by an alteration sign: ^\d+$|^\d+[.]$|^\d+[.]\d{1,4}$ - this will not fulfill the criteria of input must allow only numbers followed by a dot [.], followed by a number with no more than four digits. The bug "where nothing happens" occurs because the variable you are checking against is not what you think it is, check the screenshot on how you can inspect it, and what it is:
Can not reproduce.
You can change the event to keyup, so the test would run after every additional character is added.
It means you need to save the last valid input, so if the user tries to insert a character that'll turn the string invalid, the test will restore the last valid value.
Hence, the updated directive:
angular.module('app', [])
.directive('allowTyping', function() {
return {
restrict : 'A',
link : function(scope, elem, attrs, ctrl) {
var regex = attrs.allowTyping;
var lastInputValue = "";
elem.bind('keyup', function(event) {
var input = elem.val();
var validator = new RegExp(regex);
if (!validator.test(input))
// Restore last valid input
elem.val(lastInputValue).trigger('input');
else
// Update last valid input
lastInputValue = input;
});
}
};
});

How do I tidy up ng-class?

I've got a scenario whereby I have to detect if certain fields in an Angular form are valid and dirty.
I am currently doing this using ng-class which works perfectly. However, I am ending up with a massive expression which looks really messy and sloppy in the html. Below is an example:
data-ng-class="{'component-is-valid' :
form.firstName.$valid && form.firstName.$dirty && form.lastName.$valid && form.lastName.$dirty && form.emailAddress.$valid && form.emailAddress.$dirty && form.mobileNumber.$valid && form.mobileNumber.$dirty}"
As you can see, this is quite long.
Is there anyway I can extract this so that I retain the flexibility of ng-class but also free up my DOM?
Make a function on your scope that accepts your form object or input fields and returns the boolean you're describing above: ng-class="{'component-is-valid': checkValidity(form)}"
You can check the whole form validation in one hit with $ctrl.form.$valid instead of checking all, as Keegan G correctly states.
That said, there can be cases where your ngClass logic get's quite large and unreadable.
An alternative approach I often adopt is to move all logic to the controller. e.g.
Template:
<div ng-class="$ctrl.componentClasses()"></div>
Controller:
controller: function() {
var vm = this;
vm.isValid = function() {
// do all your checking here, ultimately it should return a bool
return [Boolean];
}
vm.componentClasses = function() {
return {
'my-class-name': vm.isValid()
}
}
}

How to combine Angular $parser with $validator?

I'm building an Angular directive which allows the user to enter a North American phone number in a variety of common formats (such as "1(301) 797-1483" or "301.797.1483"), but stores it internally as a normalized number in the form "3017971483".
I have the $parser working: it strips out all non-numeric characters as the user types, and strips off the first character if it's a "1". However, I'd like to add validation to this, such that:
If the $parser can't translate the current $viewValue to a valid normalized number (i.e., because it doesn't contain enough digits, or contains unacceptable junk characters), then:
$modelValue will be empty; and
the ModelController's $valid / $invalid flags will be set appropriately, and its $error.pattern property will be set to true. (I guess it doesn't have to be pattern, but that seems like the sensible one to use.)
This is basically how Angular handles default validation via attributes such as pattern and required. But I'm having a devil of a time figuring out how to make this work with my directive.
My code is below; you can also view this on CodePen. Here's my HTML:
<div ng-app="DemoApp">
<form ng-controller="MainController" name="phoneForm">
<input phone-input type="tel" name="phone" ng-model="phone">
<p ng-show="phoneForm.phone.$error.pattern">Invalid format!</p>
<p>$viewValue is: {{ phoneForm.phone.$viewValue }}</p>
<p>$modelValue is: {{ phone }}</p>
</form>
</div>
And here's my JavaScript:
angular
.module( 'DemoApp', [] )
.controller( 'MainController', [ '$scope', function( $scope ) {
$scope.phone = '';
} ] )
.directive( 'phoneInput', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function( scope, $el, attrs, ngModel ) {
// convert human-friendly input to normalized format for internal storage
ngModel.$parsers.push( function( value ) {
return normalizePhone( value );
} );
function normalizePhone( phone ) {
// remove all non-numeric characters
phone = phone.replace( /[^0-9]/g, '' );
// if the first character is a "1", remove it
if ( phone[0] === '1' ) {
phone = phone.substr( 1 );
}
return phone;
}
}
};
} );
If you play with the form, you'll notice that the "Invalid format!" message is never shown. I don't expect this code to show it - that's what I'm trying to figure out how to do cleanly.
I already have a solid regex for determining whether the $viewValue can be translated into a valid number - e.g., "1(301) 797-1483" passes the regex, but "1(301) 797-148" does not. What I'm trying to figure out is where/when/how to perform this check, and where/when/how to flag the model as invalid.
Simply adding pattern="^regex_goes_here$" to my <input> doesn't work - that checks the format of the $modelValue after normalization, which is not what I want.
I've tried a bunch of different things, but nothing quite behaves the way I want, and I'm out of ideas at this point.
What is the "right" way to combine a $parser with a $validator? Surely there's an established pattern for this.
A method passed to $validators receives as parameters both $modelValue and $viewValue, which means that you can actually validate by both of them (pen - enter some letters):
ngModel.$validators.pattern = function($modelValue, $viewValue) {
var pattern = /^\d*$/; // whatever your pattern is
return pattern.test($viewValue);
};

Using a watch inside a link is causing an infinite digest cycle.

I'm trying to write a directive that associates a score with a color.
I've made an attempt already, and the Plunker is here. The directive itself is here:
.directive('scorebox', function () {
function link ($scope, $elem, $attr) {
var one = 1;
$scope.$watch('[score,ptsPossible]', function (newValue) {
pctScore = newValue[0] / newValue[1]
if (pctScore <= 0.4) {
rating = 'low';
} else if (pctScore <= 0.6) {
rating = 'med';
} else if (pctScore <= 0.8) {
rating = 'high';
} else if (pctScore == 1) {
rating = 'perfect';
}
$elem.removeClass();
$elem.addClass('scorebox');
$elem.addClass(rating);
$elem.text(newValue[0] + "/" + newValue[1]);
});
};
return {
restrict: 'E',
scope: {
score: "=",
ptsPossible: "="
},
link:link
}
})
I've got a couple of problems.
First, it's pretty obvious to me that I'm not supposed to do a $watch inside a link function. I'm creating an infinite digest cycle, and that's not good. I'm still not sure why, though.
I'm not manipulating the DOM correctly. Even though I'm calling $elem.removeClass(), it's not working--the element retains any classes it had before.
What is the right way to do this?
As #miqid said, no need to $watch both score and ptsPossible, since you only want to react when score changes (at least in this situation you are presenting).
The problem here, is you are using jqLite's removeClass function instead of jQuery's. If jQuery is not included before Angular in the code, Angular will instead use jqLite functions, which is like a smaller, much simpler version of jQuery. It is also, slightly different. jQuery's removeClass(), will remove all classes is no parameter is passed. jqLite will not do the same, it will just remove those classes that you pass as parameter.
You never included jQuery at all, so that's what's happening. Here is the edited Plunker. You can check jQuery is now included in the top, and everything works as expected. And also the $watch is much simpler.
Just a suggestion to get things working:
There's no need to $watch both score and ptsPossible since the latter never changes after the value is loaded from its corresponding attribute value. You also have access to scope variables inside the $watch callback function.
That's unusual as I would've expected your removeClass() to work as well. You could instead try removeAttr('class') here in the meanwhile.
Here's a Plunker with the suggested changes.
You need to use $watchCollectionand not $watch. You are passing in an array to $watch which is how $watchCollection expects.

AngularJS filter causes IE8 to not render two-way binding

I have a bizarre issue with IE8 where if I try to render a $scope variable in a template via AngularJS's two-way data binding, it won't replace {{child.name}} with the proper value. This surely has something to do with the inefficiency of the following filter:
filter('truncate', function() {
return function(name) {
// It's just easier to use jQuery here
var windowWidth = $(window).width(),
nameElement = $('a:contains("' + name + '")'),
truncPnt = Math.floor(name.length * 0.9);
while (nameElement.width() > windowWidth * 0.75 && truncPnt > 6) {
truncPnt = Math.floor(truncPnt * 0.9);
name = name.substring(0, truncPnt);
nameElement.text(name + ' ...');
}
return name;
}
});
I then use this filter with an ng-repeat with:
<a class="entity-name" href="{{child.url}}" title="{{child.name}}" ng-cloak>{{child.name|truncate}}</a>
The overall goal is to have the variable passed into the filter truncated down depending on the width of the screen, replacing any truncated characters with " ...". I'm fairly confident this filter is the cause since I have a similar function that gets called on a .resize() of the $(window) handler, and if I were to take IE8, and resize the browser window, it causes the {{child.name}} to render as the proper value, but only if I resize the browser.
UPDATE:
So I've gotten rid of the above filter, and replaced it with a very similar directive. This is my first attempt at creating a custom directive, so I'm fairly certain it could be done better, minus the obvious flaw that I cannot seem to work around currently. The directive is as follows:
.directive('truncate', function() {
return {
restrict: 'A',
replace: true,
template: '<a class="entity-name" href="{{child.url}}" title="{{child.name}}">{{child.display}}</a>',
link: function(scope, element, attr) {
var widthThreshold = $(element[0]).parent().parent().width() * 0.85;
scope.$watch('child', function(val) {
var elementWidth = $(element[0]).width(),
characterCount = scope.child.name.length;
while ($(element[0]).width() > widthThreshold || characterCount > 5) {
characterCount--;
scope.child.display = scope.child.name.substring(0, characterCount) + ' ...';
}
});
}
}
});
And I replace the partial to simply:
<a truncate="child"></a>
The differences in this as opposed to the filter are as follows (minus the obvious filter vs. directive):
Replace windowWidth with widthThreshold, identifying the value by chaining jQuery's .parent() twice (essentially it's a more accurate value when getting the width of the parent (x2) element instead of the window).
Added an additional key to child called display. This will be a truncated version of child.name that is used for display, instead of using jQuery's .text() and just rendering using a truncated child.name.
truncPnt becomes characterCount (trying to remember not to abbreviate variables)
The problem now becomes that jQuery is freezing up the browser, until I kill the javascript (if prompted). Firefox may display it, Chrome has yet to not hang, and while I've yet to test in IE, I'd imagine worse than the former.
What can be done to properly get the value of two parents above the main element in question, and truncate child.display so that it will not wrap/extend past the parent div?
UPDATE 2:
I decided to ditch the thought of primarily DOM-based calculations in favor of mathematics, accounting for the width of the parent div, font size, and a ratio of God knows what. I seriously plugged away at a formula until I got something that consistently gave similar results no matter the font size. Media queries do impact the font-size CSS of the string in question, so I needed to account for that or else have some drastic differences in the length of the truncated string between different font-size's:
.directive('truncate', function() {
return {
restrict: 'A',
replace: true,
// {{child.display}} will be a truncated copy of child.name
template: '<a class="entity-name" href="{{child.url}}" title="{{child.name}}">{{child.display}}</a>',
link: function(scope, element, attr) {
var widthThreshold = $(element).parent().parent().width() * 0.85,
// get the font-size without the 'px' at the end, what with media queries effecting font
fontSize = $(element).css('font-size').substring(0, $(element).css('font-size').lastIndexOf('px')),
// ... Don't ask...
sizeRatio = 29760/20621,
characterCount = Math.floor((widthThreshold / fontSize) * sizeRatio);
scope.$watch('child', function(val) {
// Truncate it and trim any possible trailing white-space
var truncatedName = scope.child.name.substring(0, characterCount).replace(/^\s\s*/, '').replace(/\s\s*$/, '');
// Make sure characterCount isn't > the current length when accounting for the '...'
if (characterCount < scope.child.name.length + 3) {
scope.child.display = truncatedName + '...';
}
});
}
}
});
Interestingly enough, I believe I came full circle back to Brandon Tilley's comment regarding modifying the DOM versus modifying a property in the scope. Now that I've changed it to modifying a property, it would probably better serve in a filter? What is typically the deciding factor for whether or not this sort of manipulation should be handled in a filter versus a directive?
I refer to the documentation:
Directives
Directives are a way to teach HTML new tricks. During DOM compilation directives are matched against the HTML and executed. This allows directives to register behavior, or transform the DOM.
http://docs.angularjs.org/guide/directive
Filters
Angular filters format data for display to the user. In addition to formatting data, filters can also modify the DOM. This allows filters to handle tasks such as conditionally applying CSS styles to filtered output.
http://docs.angularjs.org/guide/dev_guide.templates.filters
I would use filters only for the purpose of changing the format of data and nothing else. To be honest I believe using a filter for your purpose is appropriate. And as the docs say a filter can modify the DOM I don't see a reason why u should use a directive at all, a filter seems to be what you are looking for. (Apart from the fact that a bug may force you to use a directive)

Categories

Resources