How can I reinitialize an Angular directive? - angularjs

I'm using the fcsaNumber directive to check for the validity of numbers entered into an input box. I got this working, but the value im checking for is dynamic, meaning it can change depending upon what options have been selected. It appears this directive only initializes once. what do I need to do to get it to reinitialize when the options have changed?
//this works fine
<input name="Employees" type="text" ng-model="Input.Employees" fcsa-number="{min: 1}" required="">
//this only inits once, so its not dynamic
<input name="Employees" type="text" ng-model="Input.Employees" fcsa-number="{{getMin()}}" required="">

Rebuilding the whole directive would work, but it's a brute-force solution -- and particularly undesirable for directives like this one that accept user input, which would get blown away were you to re-init the directive.
Instead you can use scope.$watch or attrs.$observe to allow the directive to more readily respond to changes in its attribute values:
module.directive('foo', function() {
return {
scope: {
'bar': '#'
},
link: function(scope, element, attrs) {
scope.$watch('bar', function() {
// runs when scoped value 'bar' changes
});
attrs.$observe('baz', function() {
// Similar to $watch, but without isolate scope
});
}
}
});
AngularJS : Difference between the $observe and $watch methods has a good explanation of the differences between $observe and $watch.

You could potentially use ng-if to initialize / tear down the directive in question. (ng-if docs)
The ngIf directive removes or recreates a portion of the DOM tree
based on an expression. If the expression assigned to ngIf evaluates
to a false value then the element is removed from the DOM, otherwise a
clone of the element is reinserted into the DOM - CodeHater
Here's a great stack overflow answer on the differences between ng-if and ng-show.

Related

Trying to figure out how a directive works, to design a test for it

I'm using Jasmine+Karma and need to find a way to test an angular directive used to alert the user if passwords don't match - it seems to accomplish this with a directive the renders true or false, and there is with ngShow on the HTML that displays when this, along with a couple other properties, are true.
Here's the directive. I'm having a little difficulty understanding how it works.
app.directive('passwordMatch', [function () {
return {
restrict: 'A',
scope:true,
require: 'ngModel',
link: function (scope, elem, attrs, control) {
var checker = function () {
var e1 = scope.$eval(attrs.ngModel);
var e2 = scope.$eval(attrs.passwordMatch);
if(e2!=null)
return e1 == e2;
};
scope.$watch(checker, function (n) {
control.$setValidity("passwordNoMatch", n);
});
}
};
}]);
<small class="errorMessage" data-ng-show="signupForm.password2.$dirty && signupForm.password2.$error.passwordNoMatch && !signupForm.password2.$error.required"> Password do not match.</small>
So as far as I'm able to tell, what's happening is scope.$watch is watching the checker function for a change, which then gets put into the listeners argument and updates the property on the DOM?
How does it do that, then when the purpose is to detect if passwords do not match - if they don't match, then e1 === e2 is false, and this value is passed into $scope.watch(checker, function(n)...? If that was how it worked, then wouldn't it set the of value passwordNoMatch to false, which would make ng-show hidden?
Or is that not how it works, it works another way?
And, before that, what's going on with the link: function part?
Where is the scope coming from (it just says scope:true in the directive?)
And the elem? And the attr (the attributes from html elements?)?
Is angular just looking through a list of each and every one of them, the elements and attributes, and the scope? Is there a passwordMatch property already in there somehow?
What is $eval doing?
You have a lot of questions here and spending some time with the Angular docs would help you answer a lot of them. I'll put links to the relevant docs so you can get a fuller explanation.
So as far as I'm able to tell, what's happening is scope.$watch is watching the checker function for a change, which then gets put into the listeners argument and updates the property on the DOM? How does it do that?
I think you are almost there. When you watch a function it gets called on every digest cycle and if the return value of the function has changed then it calls the function you passed as the second argument i.e.
function(n){
control.$setValidity("passwordNoMatch", n);
}
To understand this function you need to understand what the require option does. You have set require to 'ngModel' and basically what this means is that the control variable passed in as the 4th argument to your link function is a reference to NgModelController, which provides an API for the ngModel directive.
The $setValidity("passwordNoMatch", n) bit sets the value of the property 'passwordNoMatch' on the $error object to the value of n. So, n here is the return value of the function you are watching and the $error object is a property of the FormController that is available on all angular forms which have the name attribute defined in the HTML form tag. So, basically this function is what sets the value of the signupForm.password2.$error.passwordNoMatch that you see in your <small> tag.
Where is the scope coming from (it just says scope:true in the directive?)
The scope in link function is (from the Angular docs)..
The scope to be used by the directive for registering watches.
The scope: true bit is what tells Angular whether to create a new scope for the directive or to create an 'isolate scope' which does not 'prototypically inherit from the parent scope'. I would recommend that you spend some quality time reading about the directive definition object if you really want to grok directives.
What is $eval doing?
The first argument passed to scope.$eval is executed as an Angular Expression and the result is returned. So in your code I suspect they will both return strings from your password fields which you then check to see if they match.
Hope that helps.

Why can't we use interpolation expression with "ng-model" but can use with "ng-src"?

I am trying to use the interpolation expression with the ng-model directive but it doesn't work. On the contrary, when I use interpolation with ng-src, it works perfectly fine. What is this difference due to?
It all depends upon how the directive has been setup.
Some directives like ng-model, ng-show and ng-click do not use interpolation symbols whereas directives ng-src take interpolation.
Interpolation are supported on directives that work only with strings. If we look at ng-src implementation you will find
attr.$observe(normalized, function(value) {
if (!value)
return;
attr.$set(attrName, value);
if (msie) element.prop(attrName, attr[attrName]);
});
attr.$observe watches for change in attribute not model. Model changes cause attribute changes (due to interpolation), hence causing the trigger to fire.
For all other directives like ng-model, the attribute value is an expression which is evaluated in current scope and is not limited to string value.
If you are developing your own directives, isolated scope properties = and # help you achieve something similar.
It's to do with when the code in the directive looks at the attribute: before or after interpolation.
I suspect ngModel uses the attribute, and by that I mean passing it to $parse or $eval, as it is before interpolation.
However, the directive ngSrc uses the attribute, and by that I mean setting the src attribute or not to the attribute value, after interpolation.
The reason as to why ngModel uses the value before interpolation, but ngSrc uses it after, I suspect is to do with how they're coded up. Consider the directive
<my-directive my-attr="{{ 'name' }}"></my-directive>
coded up as:
app.directive('myDirective', function() {
return {
restrict: 'E',
link: function($scope, $element, $attrs) {
console.log('Link:', $attrs.myAttr); // Outputs "name"
},
controller: function($scope, $element, $attrs) {
console.log('Controller:', $attrs.myAttr); // Outputs "{{ 'name' }}"
}
};
});
The un-interpolated value is seen in the controller, but the post-interpolated value is seen in the linking function. So the order of the (relevant) events is
controller,
then interpolation,
then linking function.
You can see this example in a Plunker. I suspect ngModel uses the value from the controller, and therefore sees the pre-interpolated value, but ngSrc uses the linking function, and so sees the post-interpolated value.
One reason as to why ngModel uses the controller rather than the linking function, I suspect is so that it can expose the controller to other directives, which can use it via their require option.
A slightly confounding complication is that ngSrc doesn't expect the attribute to be a valid Angular expression. Rather than passing it through $parse or $eval, it just uses the post-interpolated value of the attribute directly. (i.e. it doesn't need the URL to be wrapped in quotes).
If you wanted to, you could write a directive that combines interpolation with Angular expressions. From the linking function you can pass the attribute to $parse or $eval. If the post-interpolated value is a valid Angular expression, this would work. You could do the same thing from the controller, but you would have to pass the value through $interpolate first.
As you have discovered, ngModel does't support this. However, I don't see why you couldn't write some basic version of ngModel that supports both, and so could be used as
my-model="myScope{{ 'VariableName' }}"
that accesses the scope variable myScopeVariableName.
To bring it back tot the question again, why ngModel itself doesn't support this, I suspect is because use cases are limited (the OP didn't mention one in the question), and so would add complexity to the directive without good reason.
It has to do with one way vs two way data binding provided by directives isolated scope. Check out "Isolated Scope Explained" section in the follwoing post:
http://www.undefinednull.com/2014/02/11/mastering-the-scope-of-a-directive-in-angularjs/

Use ngModel with plain ngController instead of directive?

In my application I would like to preserve the option of using plain controllers for certain sections of code - as opposed to creating directives for one-off things that will never be re-used.
In these cases I often want to publish some data from the controller to be used in the contained section. Now, I am aware that I could simply bind items in the controller's scope, however I'd like to specify the "model" location explicitly just to make the code more maintainable and easier to read. What I'd like to use is ng-model as it would be used on a custom directive, but just along side my plain controller:
<div ng-controller="AppController" ng-model='fooModel'>
{{fooModel}}
</div>
However I can see no way to get a reference to the generated ngModelController without using a directive and the 'require' injection.
I am aware that I could make my own attribute fairly easily by injecting the $attr into my controller and do something like:
<div ng-controller="AppController" my-model='fooModel'>
{{fooModel}}
</div>
In which case I just manually take or parse the myModel value and stick my model into the $scope under that name. However that feels wrong in this case - I really only need one "model" for a controller and I'd prefer not to have to add this boilerplate to every controller when ngModel exists. (It's the principle of the thing!)
My questions are:
1) Is there some way to use ngModel along with a plain controller to get the effect above?
2) I have been trying to figure out where ngModelControllers are stored so that I could look at the situation in the debugger but have not been able to find them. When using an ngModel directive should I see these in the scope or parent scope? (Where do they live?!?)
UPDATE: As suggested in answers below $element.controller() can be used to fetch the controller. This works (http://plnkr.co/edit/bZzdLpacmAyKy239tNAO?p=preview) However it's a bit unsatisfying as it requires using $evalAsync.
2) I have been trying to figure out where ngModelControllers are stored so that I could look at the situation in the debugger but have not been able to find them. When using an ngModel directive should I see these in the scope or parent scope? (Where do they live?!?)
The answer depends slightly on where you want to access the controller from.
From outside the element with ng-model
It requires "name" attributes on both the element with the ng-model attribute, and a parent form (or ngForm). So say you have the form with name myForm and the element with ng-model attribute with name myInput, then you can access the ngModelController for myFoo from the parent scope as myForm.myInput. For example, for debugging purposes:
<p>myFoo: {{myForm.myInput.$modelValue}}<p>
<form name="myForm">
<div ng-controller="InnerController" name="myInput" ng-model="model.foo"></div>
</form>
as can be seen at http://plnkr.co/edit/IVTtvIXlBWXGytOEHYbn?p=preview
From inside the element with ng-model
Similar to the answer from #pixelbits, using $evalAsync is needed due to the order of controller creation, but you can alternatively use angular.element.controller function to retrieve it:
app.controller('InnerController', function($scope, $element) {
$scope.$evalAsync(function() {
$scope.myModelController = $element.controller('ngModel');
});
});
Used, inside the controller to view it, for debugging purposes, as:
<div ng-controller="InnerController" ng-model="model.foo">
<p>myFoo: {{myModelController.$modelValue}}<p>
</div>
As can be seen at http://plnkr.co/edit/C7ykMHmd8Be1N1Gl1Auc?p=preview .
1) Is there some way to use ngModel along with a plain controller to get the effect above?
Once you have the ngModelController inside the directive, you can change its value just as you would were you using a custom directive accessing the ngModelController, using the $setViewValue function:
myModelController.$setViewValue('my-new-model-value');
You can do this, for example, in response to a user action that triggers an ngChange handler.
app.controller('InnerController', function($scope, $element) {
$scope.$evalAsync(function() {
$scope.myModelController = $element.controller('ngModel');
});
$scope.$watch('myModelController.$modelValue', function(externalModel) {
$scope.localModel = externalModel;
});
$scope.changed = function() {
$scope.myModelController.$setViewValue($scope.localModel);
};
});
Note the extra watcher on $modelValue to get the initial value of the model, as well as to react to any later changes.
It can be used with a template like:
{{model.foo}}
<div ng-controller="InnerController" ng-model="model.foo">
<p><input type="text" ng-model="localModel" ng-change="changed()"></p>
</div>
Note that this uses ngChange rather than a watcher on localModel. This is deliberate so that $setViewValue is only called when the user has interacted with the element, and not in response to changes to the model from the parent scope.
This can be seen at http://plnkr.co/edit/uknixs6RhXtrqK4ZWLuC?p=preview
Edit: If you would like to avoid $evalAsync, you can use a watcher instead.
$scope.$watch(function() {
return $element.controller('ngModel');
}, function(ngModelController) {
$scope.myModelController = ngModelController;
});
as seen at http://plnkr.co/edit/gJonpzLoVsgc8zB6tsZ1?p=preview
As a side-note, so far I seem to have avoided nesting plain controllers like this. I think if a certain part of the template's role is to control a variable by ngModel, it is a prime candidate for writing a small directive, often with an isolated scope to ensure there are no unexpected effects due to scope inheritance, that has a clear API, and uses require to access the ngModelController. Yes, it might not be reused, but it does help enforce a separation of responsibilities between parts of the code.
When you declare directives on an element:
<div ng-controller="AppController" ng-model='fooModel'>
{{fooModel}}
</div>
You can retrieve the controller instance for any directive by calling jQlite/jQuery $element.data(nameOfController), where nameOfController is the normalized name of the directive with a $ prefix, and a Controller suffix.
For example, to retrieve the controller instance for the ngModel directive you can do:
var ngModelController = $element.data('$ngModelController');
This works as long as the ngModel directive has already been registered.
Unfortunately, ngController executes with the same priority as ngModel, and for reasons that are implementation specific, ngModel is not registered by the time that the ngController function executes. For this reason, the following does not work:
app.controller('ctrl', function ($scope, $element) {
var ngModelController = $element.data('$ngModelController');
// this alerts undefined because ngModel has not been registered yet
alert(ngModelController);
});
To fix this, you can wrap the code within $scope.$evalAsync, which guarantees that the directives have been registered before the callback function is executed:
app.controller('ctrl', function ($scope, $element) {
$scope.$evalAsync(function() {
var ngModelController = $element.data('$ngModelController');
alert(ngModelController);
});
});
Demo JSFiddle

The way angular bootstraps

I have a question about the way Angular bootstraps. Consider this directive:
app.directive('finder', function () {
return {
restrict: 'E',
replace: true,
template:'<div><input type="text" data-ng-model="my-input" style="width:80%;" /></div>',
compile: function (element, attributes) {
if (attributes.hasOwnProperty('required')) {
element.removeAttr('required');
element.find(':input').attr('required', true);
element.removeClass("ng-invalid ng-invalid-required");
}
return {
post: function postLink(scope, iElement, attributes) {
// Do some other stuff
}
}
}
}
})
And I use this directive as follows:
<div>
<finder required></finder>
</div>
Some CSS:
.ng-invalid-required {
background: yellow;
}
As you can see in the directive, in the compile phase, I remove the required attribute from the element and add the attribute to the input element. The result is what you (sort of) should expect: the main element doesn't have the attribute anymore, but the input field does.
Now the 'strange' part: The output shows that the div also has a yellow background. As it turns out, the main element (div) has the ng-invalid-required class! This is strange because I removed the required attribute in the compile phase...
How is this possible?
Apparently, Angular scans the whole DOM for directives and collect the directives with the belonging elements. After the scan, Angular is going to compile all the directives. At one point it is going to compile my finder directive. This directive removes the required attribute and add it to my input field. Later on it compiles the required attribute. Because the 'original element' is in the collected list it is going to add the ng-invalid-required class to the linked element (the div).
Is this correct? Or is there another explanation for this behaviour?
EDIT (thanks to Nikos Paraskevopoulos suggestion in his comment).
Interesting: When I add this to the directive:
priority: 1000,
terminal: true
It works like I would expect. But when I leave one of those properties, it doesn't work anymore. Could someone explain to me why this works?
(Moved from comment to answer + explanation)
Try specifying both priority (to a value so that your directive runs before the requiredDirective) and terminal: true in your directive definition.
Explanation:
The reason for the original problem is that the requiredDirective executes on the element. Obviously, as you point out, Angular scans the whole DOM for directives and collects the directives and executes them. To clarify: first it collects them, then executes them.
This means that the requiredDirective will execute despite your compile() function having removed the required attribute, because Angular has already "collected" the directives.
The terminal: true property:
If set to true then the current priority will be the last set of directives which will execute
This means that if you set priority to a value so that your directive executes before the requiredDirective, the latter will not execute at all! Obviously, Angular stops the collection of directives when it encounters one with terminal: true.
"required" is compiled before your element, so I think that even if you remove required attribute it was already marked as required and angularjs adds ng-invalid class.
Try to change the priority of your directive to 1000, so it will be compiled before required directive and so you can remove required attribute:
app.directive('finder', function () {
return {
priority: 100,
restrict: 'E',
...
};
});

How to 'unwatch' an expression

Say I have an ng-repeat with a big array.
When ng-repeat runs, it adds every element of that array to an isolated scope, as well as having the array itself in a scope. That means that $digest checks the entire array for changes, and on top of that, it checks every individual element in that array for changes.
See this plunker as an example of what I'm talking about.
In my use case, I never change a single element of my array so I don't need to have them watched. I will only ever change the entire array, in which case ng-repeat would re-render the table in it's entirety. (If I'm wrong about this please let me know..)
In an array of (say) 1000 rows, that's 1000 more expressions that I don't need evaluated.
How can I deregister each element from the watcher while still watching the main array?
Perhaps instead of deregistering I could have more control of my $digest and somehow skip each individual row?
This specific case is actually an example of a more general issue. I know that $watch returns a 'deregisteration' function, but that doesn't help when a directive is registering the watches, which is most of the time.
To have a repeater with a large array that you don't watch to watch every item.
You'll need to create a custom directive that takes one argument, and expression to your array, then in the linking function you'd just watch that array, and you'd have the linking function programmatically refresh the HTML (rather than using an ng-repeat)
something like (psuedo-code):
app.directive('leanRepeat', function() {
return {
restrict: 'E',
scope: {
'data' : '='
},
link: function(scope, elem, attr) {
scope.$watch('data', function(value) {
elem.empty(); //assuming jquery here.
angular.forEach(scope.data, function(d) {
//write it however you're going to write it out here.
elem.append('<div>' + d + '</div>');
});
});
}
};
});
... which seems like a pain in the butt.
Alternate hackish method
You might be able to loop through $scope.$$watchers and examine $scope.$$watchers[0].exp.exp to see if it matches the expression you'd like to remove, then remove it with a simple splice() call. The PITA here, is that things like Blah {{whatever}} Blah between tags will be the expression, and will even include carriage returns.
On the upside, you might be able to just loop through the $scope of your ng-repeat and just remove everything, then explicitly add the watch you want... I don't know.
Either way, it seems like a hack.
To remove a watcher made by $scope.$watch
You can unregister a $watch with the function returned by the $watch call:
For example, to have a $watch only fire once:
var unregister = $scope.$watch('whatever', function(){
alert('once!');
unregister();
});
You can, of course call the unregister function any time you want... that was just an example.
Conclusion: There isn't really a great way to do exactly what you're asking
But one thing to consider: Is it even worth worrying about? Furthermore is it truly a good idea to have thousands of records loaded into dozens of DOMElements each? Food for thought.
EDIT 2 (removed bad idea)
$watch returns a function that unbinds the $watch when called. So this is all you need for "watchOnce":
var unwatchValue = scope.$watch('value', function(newValue, oldValue) {
// Do your thing
unwatchValue();
});
Edit: see the other answer I posted.
I've gone and implemented blesh's idea in a seperable way. My ngOnce directive just destroys the child scope that ngRepeat creates on each item. This means the scope doesn't get reached from its parents' scope.$digest and the watchers are never executed.
Source and example on JSFiddle
The directive itself:
angular.module('transclude', [])
.directive('ngOnce', ['$timeout', function($timeout){
return {
restrict: 'EA',
priority: 500,
transclude: true,
template: '<div ng-transclude></div>',
compile: function (tElement, tAttrs, transclude) {
return function postLink(scope, iElement, iAttrs, controller) {
$timeout(scope.$destroy.bind(scope), 0);
}
}
};
}]);
Using it:
<li ng-repeat="item in contents" ng-once>
{{item.title}}: {{item.text}}
</li>
Note ng-once doesn't create its own scope which means it can affect sibling elements. These all do the same thing:
<li ng-repeat="item in contents" ng-once>
{{item.title}}: {{item.text}}
</li>
<li ng-repeat="item in contents">
<ng-once>
{{item.title}}: {{item.text}}
</ng-once>
</li>
<li ng-repeat="item in contents">
{{item.title}}: {{item.text}} <ng-once></ng-once>
</li>
Note this may be a bad idea
You can add the bindonce directive to your ng-repeat. You'll need to download it from https://github.com/pasvaz/bindonce.
edit: a few caveats:
If you're using {{}} interpolation in your template, you need to replace it with <span bo-text>.
If you're using ng- directives, you need to replace them with the right bo- directives.
Also, if you're putting bindonce and ng-repeat on the same element, you should try either moving the bindonce to a parent element (see https://github.com/Pasvaz/bindonce/issues/25#issuecomment-25457970 ) or adding track by to your ng-repeat.
If you are using angularjs 1.3 or above, you can use the single bind syntax as
<li ng-repeat="item in ::contents">{{item}}</li>
This will bind the value and will remove the watchers once the first digest cycle is run and the value changes from undefined to defined for the first time.
A very helpful BLOG on this.

Resources