I am reviewing someone else's code and I see that they have $scope.$apply in their directive.
The scenario is that we got some event from the DOM and we want to change the scope.
From my experience, directives should call apply. It causes some weird side effects.
One of them is in the directive's test. All my tests have the same pattern
$compile( "<the html>" )(scope);
scope.$digest(); --> will error if directive calls apply
Should directives call apply?
What is the recommended solution for apply changes on scope when you get an event from DOM which is not wrapped in angular?
I would say that calling $scope.$apply or $scope.$digest is usually (although not always) a bad idea.
For your example, registering DOM event can go through angular using ng-click, ng-keydown, etc, which will conceal the need to call $apply or $digest.
The reason it is even needed, is obviously because there is some code executed "outside" angular, meaning, outside of the angular ecosystem and so basically angular doesn't "know" an event (or any other data related thing) has happened.
So to sum up, there should be a (very) good reason to call $apply or $digest.
How else?
Well, you could encapsulate these event capturing inside your own directive (although most if not all of them are covered on angular). These is exactly what angular itself does and will result $apply or $digest only when actually needed by the event itself.
/EDIT/
For instance, a simplified version of angular's ng-click can be translated into your own directive:
app.directive('myClick', ['$parse', function ($parse) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
var clickHandler = $parse(attr.myClick);
element.on('click', function(event) {
// Do some of your own logic if needed.
scope.$apply(function() {
// Calling the event handler.
clickHandler(scope, {$event: event});
});
});
}
}
}]);
By encapsulating this event handler, it can be reused (in this a form of a directive) and because being part of angular's world, any other logic using this directive, doesn't have to worry about $apply or $digest. It also means it can be used declaratively now (rather then operatively) which is what angular aspires anyway.
One thing to notice, this directive doesn't isolate its scope and doesn't introduce any other new variables on the scope (the event handler is being parsed on the linking function). This is important because it means there are is no overhead side effects on the parent scope (the scope that needs to "know" about this event - which is basically the main scope), since the directive's scope is inherited.
P.S You can also consider overriding directives or decorating other services on angular.
Well... if your directive wrapps some native events or anything outside of the Angular scope, you dont have much more options than calling "$apply()". From my experience this will only cause an error if this function is called from both, WITHIN the angular scope and from outside (e.g. ng-click as well as window-click event or something). If this is a case, you can still use the $timeout-Service. It's not the nicest solution, but from what I`ve heard its even the suggested one from the angular team.
Related
I've written an AngularJS (1.x) directive that wraps a pure javascript library object, called browser.
In order to update Angular scope variables and view in response to events, occurring within browser, I have to manually call $apply, invoking the digest loop:
scope.browser.on({
afterSetRange: function(){
if (!scope.$$phase) scope.$apply();
}
});
This works fine, when my directive is not creating its own scope - I say scope: false in Directive Definition Object (DDO). In that case scope refers to the page controller's $scope.
But when I'm using an isolated scope with scope: {myattr: '='}, this apparently doesn't save me from getting:
Error: [$rootScope:inprog] $digest is already in progress
I solved this problem by replacing if (!scope.$$phase) scope.$apply with $timout(angular.noop). (This means, that scope.$apply is poorly designed - it should just work, instead of forcing people to dig in its internals). What I really want is an asnwer to the following theoretical question, not practical help:
I don't understand the theory behind $digest loop. Documentation of rootScope.$new() implies that $digest event propagates from rootScope to its children. So, do we have a single digest loop for the whole angular app? Or a loop per each scope?
And how does Angular achieve synchronization between directive attributes and controller around it, when I'm using 2-way data binding in directive (e.g. scope: {myattr: '='})?
I am new to AngularJS. Please see the code below and tell me what it is doing.
$scope.$on('$viewContentLoaded', function(event) {});
How to use it in a controller to access the DOM?
$timeout(function() { });
I am looking for explanation and example of how to use $scope.$on() and $timeout() in real life and what it does.
$scope.$on registers a listener for the event passed as the first parameter and executes the function passed as the second on each instance of said event. $broadcast and $emit can be used to send out custom events of your own.
$timeout can be used in place of setTimeout but when called with no delay argument will simply wait for the next digest before executing its callback function.
As for DOM manipulation, this should not be carried out in a standard 'jQuery like fashion'. If manipulation of the DOM is required a custom directive can be defined to encapsulate this functionality and therefore allow the Angular framework to govern its syncopation.
I am creating reusable UI components with AngularJS directives. I would like to have a controller that contains my business logic with the nested components (directives). I want the directives to be able to manipulate a single property on the controller scope. The directives need to have an isolate scope because I might use the same directive more than once, and each instance needs to be bound to a particular controller scope property.
So far, the only way I can apply changes back to the controller's scope is to call scope.$apply() from the directive. But this breaks when I'm inside of an ng-click callback because of rootScope:inprog (scope operation in progress) errors.
So my question: What is the best way to make my controller aware when a child directive has updated a value on the controller's scope?
I've considered having a function on the controller that the directive could call to make an update, but that seems heavy to me.
Here is my code that breaks on an ng-click callback. Keep in mind that I don't just want to solve the ng-click issue. I want the best overall solution to apply reusable directives to modify a parent scope/model.
html
<div ng-controller="myCtrl">
<my-directive value="val1"></my-directive>
</div>
controller
...
.controller('myCtrl', ['$scope', function ($scope) {
$scope.val1 = 'something';
}});
directive
...
.directive('myDirective', [function () {
return {
link: function(scope) {
scope.buttonClick = function () {
var val = 'new value';
scope.value = val;
scope.$apply();
};
},
scope: {
value: '='
},
template: '<button ng-click="buttonClick()"></button>'
};
}]);
The purpose of two-way data binding in directives is exactly what you're asking about -- to "[allow] directives to modify a parent scope/model."
First, double-check that you have set up two-way data binding correctly on the directive attribute which exposes the variable you want to share between scopes. In the controller, you can use $watch to detect updates if you need to do something when the value changes. In addition, you have the option of adding an event-handler attribute to the directive. This allows the directive to call a function when something happens. Here's an example:
<div ng-controller="myCtrl">
<my-directive value="val1" on-val-change="myFunc"> <!-- Added on-change binding -->
<button ng-click="buttonClick()"></button>
</my-directive>
</div>
I think your question about $scope.apply is a red herring. I'm not sure what problem it was solving for you as you evolved this demo and question, but that's not what it's for, and FWIW your example works for me without it.
You're not supposed to have to worry about this issue ("make controller aware ... that [something] modified a value on a scope"); Angular's data binding takes care of that automatically.
It is a little complicated here because with the directive, there are multiple scopes to worry about. The outer scope belongs to the <div ng-controller=myCtrl>, and that scope has a .val property, and there's an inner scope created by the <my-directive> which also has a .val property, and the buttonClick handler inside myDirective modifies the inner one. But you declared myDirective's scope with value: '=' which sets up bidirectional syncing of that property value between the inner and outer scope.
So it should work automatically, and in the plunker I created from your question code, it does work automatically.
So where does scope.$apply come in? It's explicitly for triggering a digest cycle when Angular doesn't know it needs to. (And if you use it when Angular did know it needed a digest cycle already, you get a nested digest cycle and the "inprog" error you noticed.) Here's the doc link, from which I quote "$apply() is used to execute an expression in angular from outside of the angular framework". You need to use it, for example, when responding to an event handler set up with non-Angular methods -- direct DOM event bindings, jQuery, socket.io, etc. If you're using these mechanisms in an Angular app it's often best to wrap them in a directive or service that handles the Angular-to-non-Angular interface so the rest of your app doesn't have to worry about it.
(scope.$apply is actually a wrapper around scope.$digest that also manages exception handling. This isn't very clear from the docs. I find it easier to understand the name/behavior of $digest, and then consider $apply to be "the friendlier version of $digest that I'm actually supposed to use".)
One final note on $apply; it takes a function callback argument and you're supposed to do the work inside this callback. If you do some work and then call $apply with no arguments afterwards, it works, but at that point it's the same as $digest. So if you did need to use $apply here, it should look more like:
scope.buttonClick = function() {
scope.$apply(function() {
scope.value = newValue;
});
});
I'm trying to chain two nested directives that both use isolated scopes.
<div ng-controller="myController">
<my-dir on-done="done()">
<my-dir2 on-done="done()">
</my-dir2>
</my-dir>
</div>
I would like the second directive (my-dir2) to call the done() function of the first directive (my-dir) which in turn would call the controller one.
Unfortunately I don't know how to make the second directive access the callback of the first directive (so far the second directive is looking inside the high level controller, bypassing the first directive).
I think one could possibly make use of "require" but I can't since the two directives are not related (I want to use my-dir2 inside other directives not only my-dir).
To make it clear : I don't want to use require because it means that there would be a dependency of myDir on myDir2. My point is : I want to be able to reuse myDir2 inside others directives. So I don't want myDir2 to be based on myDir but I do want to inform the upper directive (myDir) when something is done (like in a callback in js).
I have made a plunker : as you can see in the javascript console, my-dir2 is calling directly the done function from the high level controller.
Does anyone has a clean way to deal with that kind of situation ?
Thanks
Update:
to be able write directives that are independent of each other you need to use events:
use $emit('myEvent', 'myData') to fire an event that will be handled by scopes that are upward in the hierarchy.
use $broadcast('myEvent', 'myData') to fire an event that will be handled by scopes that are downward in the hierarchy.
to handle the event that was fired by $emit or $broadcast use $on('myEvent', function(event, data){\\your code})
P.S.: in your case the $emit won't work because both directives scopes are on the same level in the hierarchy so you will need to use $rootScope.$broadcast('myEvent' \*, myData*\); I've updated my plunker to reflect the needed changes http://plnkr.co/edit/eTkO6sk6hpuYPnCjlSKn?p=info
The following will make inner directive dependent on the outer directive:
basically to be able to call a function in the first directive you need to do some changes:
add require = '^myDir' to myDir2
remove the onDone from myDir2 and keep the isolated scope
scope:{}
add controller parameter to link function in myDir2 link:
function(scope,element,attrs,controller)
in myDir1 controller change the definition of the done function
from $scope.done to this.done
call controller.done() in myDir2
here is a plunker with the needed changes http://plnkr.co/edit/eTkO6sk6hpuYPnCjlSKn
I think you can do something like these:
angular.element('my-dir').controller('myDir').done();
give a try!
I have a controller that has an object on its scope: $scope.objToTrack.
I have a directive that is inside a nested view that $watches for changes to that object.
It has isolate scope, but objToTrack is set as = so that it can be watched.
When I click the directive, it calls an expression that is a method on the controller which changes objToTrack.
Here's a plunker to illustrate my setup.
The problem is that objToTrack $watch callback isn't fired, although the object is changed.
If you switch between Test1 and Test2 states, changes made to objToTrack are visible. It's just that I don't understand why it doesn't work right away on click.
Thanks.
To answer question...if you bind your own event handlers to an element, and change angular scope within that event handler you need to call $apply so angular is made aware of the change and can run a digest
Example You have:
element.on('click',function(){
scope.onClick({number:RNG.int(200,300)});
});
Would need to be changed to:
element.on('click',function(){
scope.$apply(function(){
scope.onClick({number:RNG.int(200,300)});
});
});
It is a lot simpler if you use event directives already provided by angular. In this case you are writing considerable amount of extra code vs using ng-click. It also makes testing a lot easier when you stay within angular as much as possible
Also, if you want to pass an object into your directive you should not use curly braces.
In html, use obj-to-track="objToTrack", instead of obj-to-track="{{objToTrack}}".
Like this:
<div simple-directive obj-to-track="objToTrack" class="directive"></div>
And in directive.js: use '=' for bi-directional binding of the objToTrack.
Like this:
scope:{
objToTrack:'='
}
In your "test*.html" files, replace "on-click" by "ng-click".
"on-click" doesn't look in your current controller, "ng-click" does.