Custom validator directive combined with other directive fires multiple times - angularjs

I have a custom validation directive iban which checks if the value is a valid (Dutch) IBAN. That validator needs the value to be uppercased. I also have an uppercase directive which changes a value to uppercase. I want to combine both on one input element:
<input type="text" ng-model="iban" name="iban" capitalize="" iban="" />
I've created a jsfiddle demonstrating the situation.
I am struggling with the correct order of execution. If the user types in a value, I want capitalize to fire first, so the iban validator receives an uppercased value. If the model value is set from code, I also want to uppercase it first.
When the user types in a lowercase character, the uppercase directive calls ctrl.$setViewValue to set the view value. This triggers another run through the parsers. So both the uppercase directive and the iban directive are executed twice. The console log shows:
parsers.capitalize: nL12HHBA0429672071
uppercasing: nL12HHBA0429672071 => NL12HHBA0429672071, setting view value
parsers.capitalize: NL12HHBA0429672071
uppercasing: NL12HHBA0429672071 already uppercased
parsers.iban: NL12HHBA0429672071
setting validity to: true
returning NL12HHBA0429672071
parsers.iban: NL12HHBA0429672071
setting validity to: true
returning NL12HHBA0429672071
I would think it is not the intention to loop through your parsers multiple times.
Another problem is when I set the value from code to an invalid IBAN that's already uppercased (last link in my fiddle). In that case, the uppercase directive won't have to do anything. The iban directives formatter will set validity to false, and just return the value. If it's an lowercase invalid IBAN, the uppercase directive will call setViewValue, causing the IBAN directives parser code to execute which will return undefined. so that situation will change the model's value to undefined.
Am I making things too complicated? Should I try to create just an iban directive which will make sure that an uppercased value gets stored in the model when the user enters a valid lowercased iban value? Should I just keep the lowercased value in the model if it set from code? And maybe just use style="text-transform: uppercase" on the element to always show the value as if it is uppercased? A disadvantage would be that if the model is set to a valid but lowercased value, the form will show the uppercased value, which would be valid, while the model value is actually invalid.

There is definitely some complexity here. And in playing around with, there is some Angular weirdness as well (at least in my eyes - I'll get to that).
One complexity introduced here is that your capitalize $formatter actually changes the model value. I think this goes against the intent of the formatter function (transforming the value in model -> view direction). The View (and the formatter via its directive lives in the View) should only change the model when the change originates from the View. This keeps the model as the source of truth, and if it is set to an invalid value, then so be it - the validity should be reflected in the View, but it should not try to "fix" the model.
With this in mind, let's also use $validators for validation (rather than $parsers/$formatters pipeline):
.directive("iban", function(){
return {
require: "?ngModel",
link: function(scope, element, attrs, ngModel){
if (!ngModel) return;
ngModel.$validators.iban = function(modelValue, viewValue){
// after parser ran, validate the resulting modelValue
return validate(modelValue);
};
function validate(val){
return modelValue === "VALID IBAN"; // for the sake of example
}
}
};
});
$parsers (changing the model value) and $formatters (changing the view value) are called before $validators run.
Another complexity though (and what seems like an Angular's weirdness) is that your capitalize formatter can make the $viewValue to become valid for an invalid $modelValue. This on its own behaves correctly - it formats the $viewValue and keeps the validity as false (since the model is false). However, if you now change the model to the currently-set (and valid) $viewValue, then Angular decides to skip (src) the validators (since it finds no difference between new and old $viewValues), and so the validity never becomes valid despite both the model and view values being valid. Same, for valid-to-invalid case (the invalid low case value never invalidates the model).
This is, however, a rare case, and if coded properly should be avoided altogether. Why? Because the model should rarely (if ever) assume invalid values and should operate within the valid range.
ng-model ensures this (by default, unless you allowInvalid) by setting the model to undefined for invalid values.
So, for your question, decide whether low case IBAN is considered invalid in the ViewModel that you defined:
If low case is invalid, then never assign a low case value to your ViewModel iban property - plunker
If low case is valid, then do case-insensitive iban validator - plunker

Related

AngularJS: how to avoid the nonassign error when providing default values to missing attributes?

This is a question & answer case, because I struggled a bit with the issue, so I wish to keep track of it and its solution.
Hoping it can help somebody, and perhaps other people can provide alternative solutions.
My case: I had to work on a large AngularJS 1.4.6 project (I will name it prj), written in TypeScript, defining a number of directives (prj-ddd) with various attributes (prj-aaa), some of them being boolean.
They implemented these boolean attributes with string binding: prjEnabled: "#" or prjRequired: "#".
So they tested the values like so: if ($scope.prjEnabled === "false"), or in templates: <div ng-show="prjRequired === 'true'"> (yeah, no controllerAs usage as well...).
These attributes are optional: obviously, here, prjEnabled defaults to "true", and prjRequired defaults to "false", among others, because the tests are false when these values are undefined.
This is verbose, inelegant (IMO), prone to errors (lot of raw strings, implicit default value), and not very efficient (although probably not in a perceptible way).
So I started to replace these bindings with expression / two-way binding: prjEnabled: "=" and prjRequired: "=".
Thus, when AngularJS sees <prj-component prj-enabled="false">, it provides directly a boolean in the directive's scope.
Nice touch: with such literal values, it creates no binding, so no performance hit.
I have to cope with absent attribute, so I added in the link function of the directives something like:
if (scope.prjEnabled === undefined) {
scope.prjEnabled = true;
}
And the usages become: if (!$scope.prjEnabled), or in templates: <div ng-show="prjRequired">.
So far, so good. But we also have explicit bindings: <prj-component prj-enabled="{{someScopeValue}}"> or even <prj-component prj-enabled="{{foo.canEdit && bar.valid}}">.
Since we have two-way bindings, I just replaced them with: <prj-component prj-enabled="someScopeValue"> or <prj-component prj-enabled="foo.canEdit && bar.valid">.
Well, if someScopeValue is undefined, the directive will detect that and provide the default value, which will go up to the parent scope. Annoying, but in most cases, probably harmless. The real value is generally provided later (eg. after a network request).
But sometime, I have the following error in the console:
https://code.angularjs.org/1.4.6/docs/error/$compile/nonassign?p0=foo.canEdit%20%26%26%20bar.valid&p1=prjComponent
I followed the advice given in the corresponding page, replacing the bindings with "=?", but I still have the error.
Ideally, I would replace these bindings with "<?", avoiding the first issue (overwriting the value in the parent's scope) and the second one (no assignment to an expression).
But this needs AngularJS 1.5+, and I cannot upgrade the version in the current project.
So how can I solve this problem?
It was tricky and I had to try various solutions, until I saw the light and remembered that with "=?" binding, there is a way to distinguish an absent attribute from one with an undefined value.
I rewrote the default value assignment as such:
if (!("prjEnabled" in scope)) { scope.prjEnabled = true; }
If you are not familiar with the in operator, it is roughly equivalent to scope.hasOwnProperty("prjEnabled"), but it also checks the prototype hierarchy. I use it mostly because it is nicer / terser than the function call...
I made a Plunkr application (out of an official AngularJS one) to reproduce the error, to see when it happens, and to explore various cases: http://plnkr.co/edit/Z2AIag?p=preview
It shows that with "=" binding and assigning default value if undefined, the error appears when the attribute is empty (an unlikely case) or when AngularJS silently captures an exception (here wrong.scopeValue && wrong.unknown where $scope.wrong doesn't exist, causing the ReferenceError exception) and provides undefined as value.
With "=?", we still have the second issue (the empty attribute value case doesn't throw).
If we test with the "x" is scope method, we get rid of this annoying case, quite common when wrong is a variable filled later (network update, etc.).
The only case where the error can still happen is if we assign a value to the bound attribute, and only in the wrong case.
How to manually create one-way binding with default value:
app.directive("myDirective", function() {
return {
scope: {
//oneWay: "<?"
},
link: postLink
};
function postLink (scope, elem, attrs) {
if (!attrs.oneWay) {
scope.oneWay = defaultValue;
} else {
scope.$watch(attrs.oneWay, function(value) {
scope.oneWay = value;
});
};
}
})
For versions of AngularJS (<1.5) that do not have one-way binding, it can be implemented with a watcher.

Why does binding trigger a $watch although no data was changed?

I have an entity in the scope that is being edited by the user. Each time it gets modified, I want to trigger some custom validation. So I have:
// validate the position if anything has changed
$scope.$watch("entity", function() {
if ($scope.entity.Id) {
$scope.validate();
}
}, true /* watch "by value" (see: https://docs.angularjs.org/guide/scope#scope-life-cycle) */);
So far so good. Now, since this is a big entity with quite some fields, not all of the fields participate initially in data-binding. Only portions of the fields are visible to the user by using a tab control. When the user switches tabs, another portion of the entity is shown.
However, when the additional controls get bound to the corresponding fields of the entity, the $watch gets triggered even though the binding doesn't change any value on the entity.
Why is this the case and how could I prevent it?
Note:
I thought that the data-binding is possibly adding some internal $... fields to entity but these are hopefully disregarded (at least this is the case in angular.equals so they should probably be disregarded in the $watch too, I assume).
$watch with object equality flag true compares the properties of the old object copy with the current object. Therefore the listener fires when a property name is changed, added, or removed. You can try something like the following:
$scope.$watch("entity", function(newVal, oldVal) {
if(Object.keys(newVal).length !== Object.keys(oldVal).length)
return; //Detect added properties
if ($scope.entity.Id) {
$scope.validate();
}
}, true

triggering $onChanges for updated one way binding

I'm really happy with the "new" $onChanges method you can implement in a component's controller. However it only seems to be triggered when the bound variable is overwritten from outside my component, not (for instance) when an item is added to an existing array
It this intended behaviour or a bug? Is there another way of listening to updates to my input bindings, besides doing a $scope.$watch on it?
I'm using Angular 1.5.3
First TL;DR
For an array that is bounded via one-way binding, a watch expression is added that does not check for object equality but uses reference checking. This means that adding an element to the array will never fire the '$onChanges' method, since the watcher will never be 'dirty'.
I've created a plnkr that demonstrates this:
http://plnkr.co/edit/25pdLE?p=preview
Click the 'add vegetable in outer' and 'change array reference in outer' and look at the 'Number of $onChanges invocation'. It will only change with the latter button.
Complete explanation
To fully grasp what is going on, we should check the angular code base. When a '<' binding is found, the following code is used to set up a watch expression.
case '<':
if (!hasOwnProperty.call(attrs, attrName)) {
if (optional) break;
attrs[attrName] = void 0;
}
if (optional && !attrs[attrName]) break;
parentGet = $parse(attrs[attrName]);
destination[scopeName] = parentGet(scope);
// IMPORTANT PART //
removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newParentValue) {
var oldValue = destination[scopeName];
recordChanges(scopeName, newParentValue, oldValue);
destination[scopeName] = newParentValue;
}, parentGet.literal);
// ------------- //
removeWatchCollection.push(removeWatch);
break;
The important part here is how the 'scope.$watch' expression is set up. The only parameters passed are the parsed expression and the listener function. The listener function is fired once the '$watch' is found dirty in the digest cycle. If it is fired, the listener will execute the 'recordChanges' method. This records an '$onChanges' callback task that will be executed in the '$postDigest' phase and notify all components that are listening for the '$onChanges' lifecycle hook to tell them if the value has changed.
What's important to keep in mind here, if the '$watcher' is never dirty, the '$onChanges' callback is not triggered. But even more importantly, by the way the '$watch' expression is created, it will NEVER be dirty, UNLESS the reference changes. If you wanted to check for equality between objects instead of reference, you should pass an extra third parameter that asks for this:
$watch: function(watchExp, listener, objectEquality, prettyPrintExpression)
As this is not the case here with the way the one way binding is set up, it will ALWAYS check for reference.
This means, if you add an element to an array, the reference is not changed. Meaning the '$watcher' will never be dirty, meaning the '$onChanges' method will not be called for changes to the array.
To demonstrate this, I've created a plnkr:
http://plnkr.co/edit/25pdLE?p=preview
It contains two components, outer and inner.
Outer has primitive string value that can be changed through an input box and an array that can be extended by adding an element or have its reference changed.
Inner has two one-way bounded variables, the value and the array. It listens for all changes.
this.$onChanges = setType;
function setType() {
console.log("called");
vm.callCounter++;
}
If you type into the input field, the '$onChanges' callback is fired every time. This is logical and expected, since a string is primitive so it cannot be compared by reference, meaning the '$watcher' will be dirty, and the '$onChanges' lifecycle hook fired.
If you click the 'Add vegetable in outer', it will execute the following code:
this.changeValueArray = function() {
vm.valueArray.push("tomato");
};
Here we just add a value to the existing bounded array. We're working by reference here, so the '$watcher' is not fired and there is no callback. You will not see the counter increment or the 'called' statement in your console.
Note: If you click the 'Add something to the array' inside the inner component, the array in outer component also changes. This is logical, since we are updating the exact same array by reference. So even though it is a one-way binding, the array can be updated from inside the inner component.
If you change the reference in the outer component by clicking 'Change array reference in outer', the '$onChanges' callback is fired as expected.
As to answer your question: Is this intended behaviour or a bug? I guess this is intended behaviour. Otherwise they would have given you the option to define your '<' binding in a way that it would check for object equality. You can always create an issue on github and just ask the question if you'd like.

Get pristine value for form element in Angular

Does Angular have anything built in that returns the pristine value of an input element?
I see that there's a $setPristine(), but there's no function to get the pristine value? Right now, I just create a copy of the pristine value in my controller when the controller initializes. I really can't believe that's correct—that there isn't anything that will give me the original form field's value.
The form field is dirty but the value is the same as it was when the form field was pristine. The user has dirtied the field, but when the user leaves the field, the value is the same as it was before the field was dirtied. What does Angular provide that will tell me that?
When your input element is dirty angular applies the ng-dirty class to it. You can check to see if this class is present on the element. It also applies the ng-pristine class on the element before it has been changed.
One thing to note: if you change the models value and set it back to it's original value, it will still be dirty. That tripped me up a bit.
not sure I understood what you're asking. but if you want to check if the element is clean or dirty just $scope.formName.inputName.$dirty (or $pristine) this will return true or false accordingly.
and if you want the value...well thats also simple :)

Using a variable for ng-required doesn't re-evaluate fields

I have a form where my intent is for required fields to not always be enforced. For example if the user is saving the document as a draft they can enter as little information as they like, if they try and publish the document then they have to enter all the required fields. I'm using a boolean on the controller which changes according to which button has been pressed e.g.
<input type="text" ng-model="field2" ng-required="enforceRequired" />
The problem is that the fields are not re-evaluated when the boolean changes so the form is submitted and then it becomes invalid. Please see this JSFiddle to see what I mean. If you fill in field1 and then click publish it will succeed on the first click and THEN become invalid.
How can I force the validation to run before the form is submitted?
Yarons is right, you are changing the value too late, by late I mean after the form validations has been run. What you can do as a workaround is, after changing the required value, let angular do another cycle and then show your alert. This can be done via $timeout service, although I must mention that it is usually not a good practise to change the flow of your digest actions. It gets pretty messy pretty soon.
Change your publish function like this (and don't forget to inject $timeout)
$scope.publish = function () {
$scope.enforceRequired = true;
$timeout(function () {
if ($scope.form.$valid) {
alert("Published!");
}
});
};
Working fiddle: http://jsfiddle.net/bh9q00Le/14/
The problem is that you are changing the value of enforceRequired in the middle of the digest loop, so the watchers are not re-rendered before you check the input fields' validity (read about digest here).
If you want to get around it, I suggest one of the following methods:
change the value of enforceRequired before you call saveDraft or publish. see example.
call $scope.$apply() after you change the value of enforceRequired. see another example.

Resources