I have a directive that's watching a controller property which is modified within an event handler.
The code looks something like this:
vm = this;
vm.someProperty = false;
// Event listeners
$scope.$on('controller.loaded', function (event, data) {
// data.someProperty === true.
angular.extend(vm, data);
});
I then have a directive that uses this property:
<body mydirective="someController.someProperty">
and an $observe on the property value inside the directive:
attrs.$observe(attrs.mydirective, function (value) {
When I make changes to vm.someproperty from within the event listener, the $observe handler is never triggered. My guess is that the change is outside of an angular scope that would allow it to register the change.
Do I need to trigger something in this case to make sure all observed properties and their dependencies are re calculated?
Related
I had to add a $destroy event listener to the element object in a directive from what I found with this answer Why isn't $destroy triggered when I call element.remove?
Resulting in a link function made with a scope/element....
controller: "MyCtrl",
link: function(scope, element) {
element.on("$destroy", function() {
scope.func();
});
}
Where func is a function defined in MyCtrl.
This works for what I want...but I'm having trouble testing the element.on("$destroy" event.
After injecting/mocking in my directive test, I create the element such like...
this.$compile = $injector.get("$compile");
this.$rootScope = $injector.get("$rootScope");
this.$scope = this.$rootScope.$new();
this.template = "<my-dir></my-dir>";
this.initElement = function() {
this.element = this.$compile(this.template)(this.$scope);
return this.element;
};
Trying to write a unit test, with destroying the scope. The element destroy event isn't triggered...and my this.element does not have a $destroy function it to call. So I'm not sure exactly how I trigger the element's $destroy event.
it("when element destroyed, call scope.func", function() {
this.$httpBackend.whenGET("app/my-dir.tpl.html").respond(200);
this.$scope.unsubscribeToMapMoveEvents = jasmine.createSpy("func");
this.initElement();
this.$scope.$destroy();
expect(this.$scope.func).toHaveBeenCalled();
});
I think the problem I'm facing in this unit test is the same reason why I moved this logic from the ctrl to the directives link function
Any help on how I can test this element on destroy workflow?
My solution was the directive that is defined using this controller is using a $rootScope passed in, and that was breaking it to where the destroy wouldn't kick off.
I instead changed the directive to be initialized with it's own empty scope like....
controller: "MyCtrl",
scope: {}
Why does angular use an ng-click="f()" attribute for handling click events, instead of $(el).on('click', f)?
I'm asking this because it's generally considered a bad practice to use the html onclick attribute, so why does Angular use this approach?
Your confusion comes from misunderstanding what's behind onclick and ng-click. Although they are both attributes, they are processed by different entities. The former is the way to add event listener to trigger callback when a click event occurs on DOM element. This attribute is processed by a browser and your callback, specified in this attribute, is executed by a browser. The latter is called angular directive in a form of an attribute, and a browser knows nothing about it. It's processed by the framework and some logic, like triggering event handler, is set up by the framework. If you don't use the framework, than ng-click is going to live there unattended, and you won't have your callback executed on click event.
Here is the relevant part of ngClick directive - the code executed by the framework when it processes ng-click attribute:
ngEventDirectives['ngClick'] = ['$parse', '$rootScope', function($parse, $rootScope) {
return {
restrict: 'A',
compile: function($element, attr) {
// fn "points" at the function you specified in the `ng-click`
// and will be executed below when a click event occurs
var fn = $parse(attr['ngClick'], null, true);
return function ngEventHandler(scope, element) {
element.on(eventName, function(event) {
var callback = function() {
// here `fn` is being executed
fn(scope, {$event:event});
};
if (forceAsyncEvents[eventName] && $rootScope.$$phase) {
scope.$evalAsync(callback);
} else {
scope.$apply(callback);
}
});
};
}
};
}];
You can see, that when angular processes ng-click attribute, it executes the function ngEventHandler, which binds the custom callback to the click event of the DOM:
element.on(eventName, function(event) {
When you set an "onclick" property of a DOM element, the value remains exactly what you assign. The browser does not perform the same "convert the string to a function" operation that it does when you create an "onclick" attribute in the HTML source.
The "ng-" properties are interpreted by Angular code, and it can do whatever it wants to set up the event handler, including treating the string as the body of a function.
If you want to set up an event handler via the DOM "onclick" property, you have to assign a function, not a string:
foo.onclick = myFunction;
or
foo.onclick = function() { myFunction(someArgument); };
The magic behind two way binding is digest cycle.
What is $apply?
$apply takes you from javascript context to angular context. Internally uses $digest(triggers digest cycle).
So, Javascript - $apply - digest cycle - view.
Difference between $(element).on('click', callback) and ng-click.
1.$(element).on('click', callback)
$(element).on('click', function(){
//do your stuff
//It is in Javascript context
//Not known to angular
})
2.ng-click implentation
element.on(eventName, function(event) { //Here, event name is 'click'
var callback = function() {
fn(scope, { $event: event });
};
scope.$apply(callback); //Here, ng-click wraps your code in $apply to take you from javascript context to angular context (Triggers digest cycle).
});
$("#my-button").on('click', myFunction)
is a code in JavaScript, using jQuery.
ng-click = "myFunction()"
is using AngularJS's ability to read through the attributes of the tags.
onclick = "myFunction();"
is the "old", HTML way to call a function.
I've got the following code where I'm trying to show/hide a text box based on if a key was pressed in the global window scope. However, every time a key is pressed, it does not seem to fire the watch service. Why is this?
Plnkr here http://plnkr.co/edit/qL9ShNKegqJfnyMvichk
app.controller('MainCtrl', function($scope, $window) {
$scope.name = '';
angular.element($window).on('keypress', function(e) {
//this changes the name variable
$scope.name = String.fromCharCode(e.which);
console.log($scope.name)
})
$scope.$watch('name', function() {
console.log('hey, name has changed!');
});
});
It is because you are handling the keypress event outside of the digest cycle. I would strongly encourage you to let angular do its thing with databinding or using ngKeypress
Otherwise, in your handler, call $scope.$digest().
angular.element($window).on('keypress', function(e) {
//this changes the name variable
$scope.name = String.fromCharCode(e.which);
console.log($scope.name);
$scope.$digest();
})
On a high level view, watching a value on a scope needs two parts:
First: the watcher - like you created one. Every watcher has two parts, the watch function (or like here the value) and the listener function. The watch function returns the watched object, the listener function is called when the object has changed.
Second: the $digest cycle. The $digest loops over all watchers on a scope, calls the watch function, compares the returned newValue with the oldValue and calls the corresponding listener function if these two do not match. This is called dirty-checking.
But someone has to kick the $digest. Angular does it inside its directives for you, so you don't care. Also all build-in services start the digest. But if you change the object outside of angular's control you have to call $digest yourself, or the preferred way, use $apply.
$scope.$apply(function(newName) {
$scope.name = newName;
});
$apply first evaluates the function and then starts the $digest.
In your special case, I would suggest to use ngKeypress to do it the angular way.
Currently working on a project where we found huge memory leaks when not clearing broadcast subscriptions off destroyed scopes. The following code has fixed this:
var onFooEventBroadcast = $rootScope.$on('fooEvent', doSomething);
scope.$on('$destroy', function() {
//remove the broadcast subscription when scope is destroyed
onFooEventBroadcast();
});
Should this practice also be used for watches? Code example below:
var onFooChanged = scope.$watch('foo', doSomething);
scope.$on('$destroy', function() {
//stop watching when scope is destroyed
onFooChanged();
});
No, you don't need to remove $$watchers, since they will effectively get removed once the scope is destroyed.
From Angular's source code (v1.2.21), Scope's $destroy method:
$destroy: function() {
...
if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;
...
this.$$watchers = this.$$asyncQueue = this.$$postDigestQueue = [];
...
So, the $$watchers array is emptied (and the scope is removed from the scope hierarchy).
Removing the watcher from the array is all the unregister function does anyway:
$watch: function(watchExp, listener, objectEquality) {
...
return function deregisterWatch() {
arrayRemove(array, watcher);
lastDirtyWatch = null;
};
}
So, there is no point in unregistering the $$watchers "manually".
You should still unregister event listeners though (as you correctly mention in your post) !
NOTE:
You only need to unregister listeners registered on other scopes. There is no need to unregister listeners registered on the scope that is being destroyed.
E.g.:
// You MUST unregister these
$rootScope.$on(...);
$scope.$parent.$on(...);
// You DON'T HAVE to unregister this
$scope.$on(...)
(Thx to #John for pointing it out)
Also, make sure you unregister any event listeners from elements that outlive the scope being destroyed. E.g. if you have a directive register a listener on the parent node or on <body>, then you must unregister them too.
Again, you don't have to remove a listener registered on the element being destroyed.
Kind of unrelated to the original question, but now there is also a $destroyed event dispatched on the element being destroyed, so you can hook into that as well (if it's appropriate for your usecase):
link: function postLink(scope, elem) {
doStuff();
elem.on('$destroy', cleanUp);
}
I would like to add too #gkalpak's answer as it lead me in the right direction..
The application I was working on created a memory leak by replacing directives whom had watches. The directives were replaced using jQuery and then complied.
To fix i added the following link function
link: function (scope, elem, attrs) {
elem.on('$destroy', function () {
scope.$destroy();
});
}
it uses the element destroy event to in turn destroy the scope.
Iām trying to understand interactions between the Angular world and the non-Angular world.
Given a directive that one declares like this:
<dir1 id="d1" attr1="100"/>
If code outside angular changes the directive this way:
$("#d1").attr("attr1", 1000);
How can the directive know that one of its attribute has changed?
It would be best to make this change inside the directive instead. If, for whatever reason, that's not possible, then there are a couple of options.
Outside the app, get a reference to any DOM element within the app. Using that reference, you can then get a reference to its scope. You could use your element with id d1. For example:
var domElement = document.getElementById('d1');
var scope = angular.element(domElement).scope();
Here are a couple of options:
Option 1
Modify the model instead of making a direct change to the view. In the link function, store the initial attribute value in a scope variable like:
scope.myvalue = attrs.attr1;
Then you can change the value outside the app (using the above reference to scope) like:
scope.$apply(function(){
scope.myvalue = 1000;
console.log('attribute changed');
});
Here is a fiddle
Option 2
If the view is manipulated directly with jQuery, I don't know of any use of $observe, $watch, or an isolate scope binding to the attribute that will work, because they all bind to the attribute expression itself, just once, when the link function is first run. Changing the value will cause those bindings to fail. So you'd have to $watch the attribute on the DOM element itself (rather than through attrs):
scope.$watch(function(){
return $(el).attr('attr1'); // Set a watch on the actual DOM value
}, function(newVal){
scope.message = newVal;
});
Then you can change the value outside the app (using the above reference to scope) like:
scope.$apply(function(){
$("#d1").attr("attr1",1000);
});
Here is a fiddle
Use a Web Components library like x-tags by Mozilla or Polymer by Google. This option works without maunally calling $scope.$apply every time the attribute changes.
I use x-tags because of their wider browser support. While defining a new custom tag (directive) you can set the option lifecycle.attributeChanged to a callback function, which will fire every time an argument is changed.
The official docs aren't very helpful. But by trial and error and diving into the code I managed to find out how it works.
The callback function's context (the this object) is the element itself. The one whose attribute has changed. The callback can take three arguments:
name ā the name of the attribute,
oldValue and
newValue ā these speak for themselves.
So now, down to business:
The code
This will watch the attribute for changes:
xtag.register('dir1', {
lifecycle: {
attributeChanged: function (attribute, changedFrom, changedTo) {
// Find the element's scope
var scope = angular.element(this).scope();
// Update the scope if our attribute has changed
scope.$apply(function () {
if (attribute == 'attr1') scope.style = changedTo;
});
}
}
});
The attributeChanged callback only fires when the arguments' values actually change. To get their initial values you need to scan the lot manually. The easiest way seems to be while defining the directive:
myApp.directive('dir1', function () {
return {
... ,
link: function (scope, element, attributes) {
scope.attr1 = element[0].getAttribute('attr1');
}
};
});