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;
});
Related
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.
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'm trying to write a script to access the rootscope and change a variables value from the dev tools. I can get the scope and the rootscope, but my changes don't show up in my bindings. However if I use a bound input everything is fine.
Example:
<input ng-model="mode"></input>
<span ng-bind-html="mode"></span>
update each other fine but
angular.element("body").scope().$root.mode = 'test'
Updates the controller but has no effect on the bindings. I have compared my original controller and my console controller using === and they are equal.
So from what I read I need to broadcast or emit that I've changed the scope, however I can't seem to figure out what to broadcast , or if I am broadcasting correctly.
Broadcast code attempts:
scope.$apply(function() {root.$emit('change', {mode: 1})})
scope.$apply(function() {root.$emit('stateChangeSuccess', {mode: 1})})
root.$apply(function() {root.$emit('change', {mode: 1})})
root.$apply(function() {root.$emit('stateChangeSuccess', {mode: 1})})
Additionally it should be noted that changing the input will change the span but not my copy of the root scope.
root.mode = 'console'
type 'input text' in the input
root.mode returns 'console'
I don't think this is possible with events (perhaps I'm wrong). There is another way to do make changes from the console though.
The reason angular.element("body").scope().$root.mode = 'test' doesn't update your bindings is that Angular doesn't "know" that a change to the scope has been made. If you are making updates to your data model from "outside" the Angular world, you will need to wrap you changes in an $apply() function:
var $rootScope = angular.element("body").scope().$root;
$rootScope.$apply(function() {
$rootScope.mode = 'hi';
});
FYI, when sending an event from the $rooteScope, you'll want to use $broadcast instead of $emit. $broadcast broadcasts an event "downward" to all descendant scopes, while $emit sends the event "upwards" to ancestor scopes. If you $emit from the $rootScope, there are no ancestor scopes to pick up the event (though perhaps the $rootScope will?).
If I understand you correctly, scope() === $root, otherwise you're making some false assumptions here.
WHat I believe is happening is that you are changing value of root scope but your bindings are actually using another scope due to prototype inheritance(eg the string property is actually created in scope() when you bind against it).
Try either of these two tricks:
1) Either try scope.$apply(function(){scope.mode ='test'});
2) $root.mode={name:'test'}; and bind against mode.name in html
Notice that you should be able to fire a $digest cycle using $apply on root scope and it should propagate down to child scopes too. $root.$apply();
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.