I have an AngularJs component having bindings to heroes, which is an array. How to watch this input for array changes? I tried $scope.watch("heroes", ...) and $onChanges, but didn't work so far.
bindings: {
heroes: '<',
}
Here is my plunker: https://plnkr.co/edit/J8xeqEQftGq3ULazk8mS?p=preview
The ControllerAs structure needs a special watch expression, since the attributes are not on the $scope.
//This one works and is the best one (> AngularJs 1.5)
$scope.$watch("$ctrl.heroes.length", function () {
console.log("ControllerAs syntax"); // Triggers once on init
});
//This one works as well
var ctrl = this;
$scope.$watch(() => {
return ctrl.heroes.length;
}, (value) => {
console.log("complex watch"); // Triggers once on init
});
See example here: https://plnkr.co/edit/J8xeqEQftGq3ULazk8mS?p=preview
The issue occurs because $scope.$watch by default doesn't deeply watch objects. Which means since you never destroy/recreate your array, the reference doesnt really change therefore $scope.$watch doesnt see any change. If you watched heroes.length, that primitive would change and your $scope.$watch would fire the corresponding listening function. By using $scope.$watch with the true option you are telling the angular engine to deeply watch all properties.
This is pretty intensive to do for large objects because $scope.$watch using angular.copy to track changes
If you were to use $scope.$watchCollection angular would create a shallow copy and would be less memory intensive. So I feel your 3 main options are
Watch heroes.length , add true or use $watchCollection
I feel that using heroes.length would be your best bet, so the code would look like
$scope.$watch('heroes.length',function(){});
The other two options are described below
$scope.$watch('heroes',function(){
//do somthing
},true)
or
$scope.$watchCollection
The benefit of using watchCollection is, that it requires less memory to deeply watch an object.
Shallow watches the properties of an object and fires whenever any of
the properties change (for arrays, this implies watching the array
items; for object maps, this implies watching the properties). If a
change is detected, the listener callback is fired.
The obj collection is observed via standard $watch operation and is
examined on every call to $digest() to see if any items have been
added, removed, or moved. The listener is called whenever anything
within the obj has changed. Examples include adding, removing, and
moving items belonging to an object or array.
Related
I have a directive that abstracts a menu. The menu item selection needs to be notified to its parent controller so it can take required action. There are multiple ways I could get this achieved.
Pass a scope variable from controller to directive and observe the change on this variable. Within the directive change this variable to indicate the selection option.
Pass a callback method from controller to directive. Invoke the callback from directive upon change.
Observe the changes in controller using $scope.$on and notify from the directive using scope.$emit
I could not clearly arrive at which one option is better. I am leaning towards option 3 as it seems to be cleaner but I am not sure if this has a unwanted coupling. I would like to hear an opinion from others, which solution would favour clear dependency and good for testability.
UPDATE:
After reading the suggestions and thought, I picked up Option 2 for below reasons:
It is very obvious by looking into the HTML about the dependency
<menu save="onSave()" filterByDate="filterByDate(date)"></menu>
Unit testing is very explicit and tell about the API (interface) the directive
I personally don't like using observe\emit\on unless i have too. It isn't always obvious looking at someone elses code where it is being set. This can lead to spaghetti code. If you have a call back, i find the direct link is more obvious to the eye and easier to find. This is in conjunction with well named properties etc. At the end of the day it's a matter of personal\team taste.
I recommend using a modified version of 1. But instead of passing a variable, inject a service wherever you need.
The service can look something like this:
yourApp.factory('SelectedMenu',
function () {
//set a default or just initialize it
var menuItem = {
id: 1,
code:"x"
};
return {
getId: function () { return menuItem.id; },
getCode: function() { return menuItem.code;},
setId: function(newId){menuItem.id = newId},
setCode: function(newCode){menuItem.code=newCode;},
};
}
);
In the directive(s) which controlls the behavior create a watch:
$scope.$watch(
function() {
//should be idempotent! can execute multiple times per $digest cycle when a change is detected
// (good compromise for not polluting $rootScope)
return SelectedMenu.getId();
},
function( newValue) {
//do your thing...
}
);
When you need to use it/update the value you can set the code or any other property you want and finally set the id to trigger the update.
SelectedMenu.setId(someones.id);
It is the cleanest solution i came across. If you want, you can abstract the usage even more with another service if the data object is more complex.
I've written a pretty simple directive that adds/removes a css class on an element when the element is clicked.
app.directive('dropdown', function() {
var open = false, element,
callback = function(){
open = !open;
if (open) {
element.addClass('open');
} else {
element.removeClass('open');
}
};
return {
scope: {},
link: function(scope, elem){
element = elem;
elem.bind('click', callback);
scope.$on('$destroy', function(){
elem.unbind('click', callback);
elem.remove();
});
}
};
});
I think that the $destroy method is probably unnecessary. Since I've used the built in jqlite the listener will be destroyed along with the element right? Also is there any benefit to calling elem.remove(). I've seen it in some examples but not sure if I see the need.
Any thoughts appreciated
C
You don't have to remove the element manually for sure. You also don't need to unbind anything from scope because it will be handled by angularjs itsef.
For jquery dom listeners:
In case you are referencing JQuery then angular will use it instead of his internal jqLite implementation. It means that the native jquery remove method will be used for the element removal. And the jquery documentation for remove says:
Similar to .empty(), the .remove() method takes elements out of the
DOM. Use .remove() when you want to remove the element itself, as well
as everything inside it. In addition to the elements themselves, all
bound events and jQuery data associated with the elements are removed.
So i think that you don't need to unbind your listeners.
But I'm not 100% sure about this:)
In your case, you should be fine since the event is bound to the element that gets removed and thus the handler gets destroyed along with the element itself. Now, if your directive binds an event to a parent outside of its own DOM element, then that would need to be removed manually on the $destroy.
However, closure can cause any object to stay alive so that's something you do need to worry about. You could introduce a new function still referencing variable objects in the functions whose scope you are trying to destroy and that prevents GC from doing what you likely want it to. Again, that won't affect your current example, but it's something to always consider.
I created a custom directive with an isolate scope that uses two-way data binding back to the parent scope. The scope/bindings all appear to be working correctly, but the template/view is not automatically updating the bound properties in the dom when they change. I can force the dom to update by reading the directive's model.
http://plnkr.co/edit/vPGm1oO0sSaHVvcrp2Ev
Note: In this plunkr example I use am using the isActive property on the wiglet 1's scope as the bound property. Note that I print the value to the console when the scope is created and also when it is updated 2 seconds later via a window.timeout... so you can see at this point that while the data has changed, the dom has not. To see the dom change click the 'print' button on either of the wiglets, which simply prints the value of isActive to console again. This causes the dom to update.
Is this a bug, or am I doing something wrong?
AngularJS only updates bound variables during a scope digest cycle; this normally happens automatically for Angular managed events (ng-click, etc.), but in asynchronous code, such as a setTimeout, you must manually call Scope#$apply() or Scope#$digest():
$window.setTimeout(function(){
$scope.$apply(function() {
$scope.wiglets[0].isActive = true;
console.log("Wiglet 1 isActive:", $scope.wiglets[0].isActive);
});
}, 2000);
This is so common with setTimeout that AngularJS has a built in service, called $timeout, that does this for you:
$timeout(function(){
$scope.wiglets[0].isActive = true;
console.log("Wiglet 1 isActive:", $scope.wiglets[0].isActive);
}, 2000);
Example: http://plnkr.co/edit/XDK06uhYMNcI6fs6SuHP?p=preview
I'm having an issue getting a watch to work within a directive. I've put together a simple example here. http://plnkr.co/edit/A7zbrsh8gJhdpM30ZH2P
I have a service and two directives. One directive changes a property in the service, and another directive has a watch on that property. I expected the watch to fire when the property is changed but it doesn't.
I've seen a few other questions like this on the site, but the accepted solutions on them have not worked here. I've considered using $broadcast or trying to implement an observer, but it seems like this should work and I don't want to over complicate things if possible.
Mark Rajcok' answer is incomplete. Even with angular.copy(), $watch listener will be called once and never again.
You need to $watch a function:
$scope.$watch(
// This is the important part
function() {
return demoService.currentObject;
},
function(newValue, oldValue) {
console.log('demoService.currentObject has been changed');
// Do whatever you want with demoService.currenctObject
},
true
);
Here the plunker that works: http://plnkr.co/edit/0mav32?p=preview
Open your browser console to see that both the directive and the demoService2 are notified about demoService.currentObject changes.
And btw angular.copy() is not even needed in this example.
Instead of
this.currentObject = newObject;
use
angular.copy(newObject, this.currentObject);
With the original code, the viewer directive is watching the original object, {}. When currentObject is set to newObject, the $watch is still looking for a change to the original object, not newObject.
angular.copy() modifies the original object, so the $watch sees that change.
I would like to subscribe to an ngChange event, but from code rather than the markup. That is, given a $scope and an expression that is bindable via ngModel, I want to subscribe to any changes made to that expression by any ngModel directive that binds to that expression. Is this possible?
something like:
$scope.field = "hello";
$scope.onButtonClick = function() {
$scope.field = "button clicked!";
}
// this callback is only when the user types in an input bound to field
// not when they click the button with ng-click="onButtonClick()"
$scope.$watchNgChange("field", function() {
console.log("user caused field to change via a binding");
});
// this callback is called for both ngModel binding changes and onButtonClick.
$scope.$watch("field", function() {
console.log("field was changed");
});
I can't just use $watch, because that will capture all changes, including those from loading the data from the database, from ng-click callbacks, and changes initiated from $watch callbacks for other expressions (in this case, if there are any circular references, then it's too easy to have $watch callbacks to get into an infinite loop and error out after 10 digest cycles), and who knows what else.
First, anytime I have tried to do something like this is turned out to be a bad idea - I was just working around design problems in my code or logic problems in my business logic. In general, the code should not care HOW the data was changed, only that it HAS changed.
Second, $watch can give you both the old and new value - this has been enough for me. if the old value not equal to the new value, I want to update the related data model(s). If that old and new value are equal, I want to ignore the update.
Finally, You may consider using resolve with your routes eliminate "database loading" as the fully located data can be passed into your controller (assuming you return a promise).
.
Jeremy you just describe what in Angular is known as a Directive. It is best practice to always use directives each time you need to touch the DOM. This logic should never live in the controller or even the Service.
directives are a big tricky but there is tons of documentation for it.
Visit docs.angularjs.org/directives
Don't do it, it sounds like you are trying to introduce the DOM logic (e.g. if the user has interacted with a DOM element) into the controller.
If you read the source code of a ngChange directive, you will found it requires a ngModel which is used as the bridge between the view and the controller.
I recommend creating a copy of the model and used the copy for data binding using ngModel+ngChange in your view, and then you can $watch that copy and do whatever you want.
$scope.field = "hello"; //the field you care
$scope.fieldCopy = $scope.field; //use 'fieldCopy' for databinding
In the html code you can have multiple way of changing the model fieldCopy
<input ngModel="fieldCopy" name='foo' />
<input ngModel="fieldCopy" name='foo2' />
You then watch the fieldCopy for changes related to user interaction and copy the change to 'field':
$scope.$watch("fieldCopy", function() {
$scope.field = $scope.fieldCopy;
console.log("user caused field to change via a binding");
});
If you want to keep fieldCopy in sync with field, add another watch:
$scope.$watch("field", function() {
$scope.fieldCopy = $scope.field;
});