Is there a way to add watch to a non scope variable. I want to add a watch to local variable. I have something like this
function EditAssetRegistryController(assetregistryService, manufacturerService, assettypeService, projectService, $localStorage, $routeParams) {
var vm = this;
vm.manufacturers = [];
vm.projects = [];
vm.asset_types = [];
vm.ch_group_uniq = 'none';
}
here is there a way to add watch to vm.ch_group_uniq?
I know how it will be done with scope variable but I have scenarios where I have to check many complex variables.
Well, you can easily add a watch for anything by passing a function as the first parameter:
$scope.$watch(function watchFunction(scope) {
return vm.ch_group_uniq
}, handler)
A few things to consider: watchFunction must return the same value if nothing has changed. This can lead to some gotchas, for example, returning the result of some array operations: [1,2,3].filter(...) will always return a new array, and lead to an endless $digest cycle. Also note the third parameter of $scope.$watch, which indicates whether to use an identity comparison, or angular.equals when comparing the values. (Check out the docs for further information - https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$watch)
However, your specific problem seems to be trying to use controllerAs and custom $watch-es. There's a very handy library that addresses this issue specifically: https://github.com/christopherthielen/angular-360-no-scope
$watch will not work as normal syntax with controllerAs. You need to bind it to $scope, and then you can watch that variable:
Code
$scope.$watch(angular.bind(this, function (ch_group_uniq) {
return this.ch_group_uniq;
}), function (newVal, oldVal) {
console.log('Name changed to ' + newVal);
});
Here is the reference Todd Motto Article
A cleaner syntax using ES6
$scope.$watch(() => {
return this.thingToWatch;
}, (newVal, oldVal) => {
// Your code here...
});
Related
The easiest way to explain this question is with some sample code so here is a very simple directive written in ES6 syntax:
export default class IsFoo {
constructor() {
// Set the directive properties
this.restrict = 'A';
this.require = 'ngModel';
}
link(scope, element, attributes, controller) {
let foo = scope.$eval(attributes.foo);
controller.$validators.isFooBar = (modelValue) => {
// make sure we have the most recent value foo
foo = attributes.foo;
return foo === 'bar';
};
scope.$watch(() => {return attributes.foo;}, () => controller.$validate());
}
static directiveFactory() {
IsFoo.instance = new IsFoo();
return IsFoo.instance;
}
}
IsFoo.directiveName = 'isFooBar';
That is a rough version of my directive with all the actual important validation removed.. it's pretty simple.
If I change the watch line to be:
scope.$watch(attributes.foo), ()=>controller.$validate());
It doesn't work. Why? Why does the function returning the attributes.foo work?
What is the difference that causes the end result to be different?
Also, disclaimer, I'm intentionally not using scope isolation because the directive is being used on an element that has another directive that uses scope isolation.. so they collide and you get an error Multiple directives asking for new/isolated scope on: xxx.
My rough guess is that it is related to how closures behave in javascript but I can't wrap my head around how the two behaving differently.
Thanks for any insight you can provide.
The interface for scope.$watch is according to the documentation the following:
$watch(watchExpression, listener, [objectEquality]);
With watchExpression being either a string or a function. If it is a string, it is interpreted as a path within your scope object. Assuming attributes.foo is "test.something", it will watch scope.test.something - if it exists.
If you want to watch for changes of the value of attributes.foo, you have to use the function, or attach attributes.foo to your scope and passing "attributes.foo" as watchExpression.
This is my current implmentation to fire callback on customVar get change using $watch...
module.directive('mudirective', function() {
return {
scope: {
callback: '&'
},
template: '<h1>Hello</h1><button ng-click="changeVaar()>Click</button>"',
controller: function($scope) {
$scope.customVar = false;
$scope.changeVaar = function() {
// some large logical execution
// which set customeVar
$scope.customVar = '';//some value assgined
};
},
link: function($scope) {
$scope.$watch('customVar', function() {
$scope.callback();
});
}
};
});
But i would like to replace this $watch with setter...
Can anybody has idea how could it be possible?
OR
Other option to avoid $watch function but fire callback on customVar changes.
But callback should be fire once it is confirmed that customVar
has changed in directive itself.
First, I will answer the comments under the question. I had this use case when I saw a controller putting a watcher on a scope value only to detect changes while the value was changed only by assignments inside the controller itself...
The watch was calling a function updating the UI depending on the assigned value (null or not, whatever).
Of course, we could call this function on each assignment. Or replace the watch with a function setting the value given as parameter, and calling this function. But somehow, using a setter was more "transparent", made a minimal set of changes, and you are sure not to miss an assignment.
On hindsight, it is similar to the way MobX works (go see this library if you have complex dependency watching to do).
Second, here is how to do it:
Object.defineProperty($scope, 'watchedValue',
{
set(newValue) { $scope._watchedValue = newValue; this.doSomethingWith(newValue); },
get() { return $scope._watchedValue; },
});
When passing a function into a directive which then is passed into a nested child directive the function is always considered defined when checked in the scope of the child directive regardless if it is passed in or not in the parent directive.
Is there a better way to either pass in function pointers or check if they are defined when dealing with nested directives.
plunker
<body ng-app="myApp">
<div ng-controller="myController">
<dir1"></dir1>
</div>
<script type="text/ng-template" id="dir1">
<div>
<dir2 fun="fun()"></dir2>
</div>
</script>
<script type="text/ng-template" id="dir2">
<div>{{fun()}}</div>
<div>{{funDefined()}}</div> <!-- always true-->
</script>
</body>
var app = angular.module('myApp', []);
app.controller('myController', function($scope) {
$scope.fun = function() {
alert("function");
};
});
app.directive('dir1', function() {
return {
scope: {
fun: '&'
},
templateUrl: 'dir1'
};
});
app.directive('dir2', function() {
return {
scope: {
fun: '&'
},
link: function(scope, elem, attrs) {
scope.funDefined = function() {
return angular.isDefined(attrs.fun);
};
},
templateUrl: 'dir2'
};
});
If you set debugger inside your scope.funDefined method of dir2 you'll see that attrs.fun equals string "fun()". That's because you take raw value from attributes. And since it's a not empty string it'll always give you true.
Here is updated plunker
There's no elegant way I know to get what you want. Like it was mentioned before this line:
angular.isDefined(attrs.fun)
performs check on string so it will return true every time fun attribute is defined. And in your dir1 directive template you have <dir2 fun="fun()"></dir2> so fun is obviously defined (and it's string). If you take a look at angular's sources:
case '&':
// Don't assign Object.prototype method to scope
parentGet = attrs.hasOwnProperty(attrName) ? $parse(attrs[attrName]) : noop;
// Don't assign noop to destination if expression is not valid
if (parentGet === noop && optional) break;
destination[scopeName] = function(locals) {
return parentGet(scope, locals);
};
break;
you'll see that presence of the attribute will always result in some function assigned to the scope ($parse returns function even for string that doesn't make much sense).
So the only solution I can think of is to perform check in the first level directive (it's possible there since attribute is really undefined) and have two <dir2> tags (with and without fun attribute) - one always excluded using ng-if. Something like this. Again, I know, its ugly solution.
One side note - Angular's source also shows that scope property will not be set if there's no attribute and binding is optional (using &?) - then you can check scope.fun value instead of attrs.fun - some may find it more elegant.
The best way I could find is based in what #xersiee commented in another answer. The idea is to make the scope parameter optional in the parent directive and then use angular.isUndefined(scope.$parent.$eval(attribute.myFun)) to check if the function was passed or not. This is not explained in the official documentation... I wonder why.
As other people has mentioned, this solution is far from ideal because using scope.$parent is an anti-pattern, but again, this is the best option I could find.
Plunker with this solution: http://plnkr.co/edit/SUUMae?p=preview
Found such an idea in article:
Notice how the value function takes the scope as parameter (without
the $ in the name). Via this parameter the value function can access
the $scope and its variables.
$scope.$watch( function( scope ) {
return scope.val;
...
instead of what i used to:
$scope.$watch( function() {
return $scope.val;
...
Is it really better? And what is the reasoning behind this way?
From AngularJs docs
function(scope): called with current scope as a parameter.
So it does not change the behavior of your code. However this version prevents a capture of the $scope variable inside the callback :
$scope.$watch(function(scope) {
return scope.val;
}, function(value){ });
I was looking at one of the custom implementations of ng-blur (I know it's already available in the standard AngularJS now). The last line is what I don't understand.
.controller('formController', function($scope){
$scope.formData = {};
$scope.myFunc = function(){
alert('mew');
console.log(arguments.length);
}
})
.directive('mew', function($parse){
return function(scope, element, attr){
var fn = $parse(attr['mew']);
element.bind('blur', function(event){
scope.$apply(function(){
fn(scope);
});
});
}
});
In the view there's a simple mew="myFunc()" applied to inputs.
My question is why are we passing the scope to the function in the very last line of the directive. I tried to make it work without that but it doesn't. What's actually happening?
Also this too works scope.$apply(attr.mew). Same reason or something different?
$parse only does just that, it parses the string passed in, you need to call the resulting function with the current scope because otherwise how else would it know which function to call?
scope.$apply works in the following manner:
The expression is executed using the $eval() method.
Any exceptions from the execution of the expression are forwarded to the $exceptionHandler service.
The watch listeners are fired immediately after the expression was executed using the $digest() method.
The reason scope.$apply(attr.mew) is due to the fact that it's doing all of the above. It is parsing, and then applying the result of the parse to the scope.
Another option is to use an isolate scope to bind your directive to the mew attr.
return {
scope: {
mew: '&'
},
link: function (scope, element, attr) {
var fn = scope.mew;
element.bind('blur', function (event) {
scope.$apply(function () {
fn();
});
});
}
}
Example
For this specific example it will work, but as you said, the blur is out of the digest loop. In most of the use cases the function will change data on one scope or another, and the digest loop should run and catch those changes.