Is 'IsolateScope' in Angularjs using $watch by default? - angularjs

I don't understand why or why not angularjs isolated scope use or not $watch?
For example:
app.directive('fooDirective', function () {
return {
scope: {
readonly: '=' or '#' or '&'
},
link: function (scope, element, attrs) {
// should I use $watch here or not ?
scope.$watch('readonly', function () {
// do I require to do so???
});
}
};
});

If you have HTML like this
<div foo-directive readonly="question.readonly">
The the following happens: scope.readonly within your directive gets the value of question.readonly (from outside the isolated scope). Whenever the value of question.readonly changes, the value of scope.readonly changes accordingly. You have nothing to do for that to happen.
But if you want to do something additionally whenever scope.readonly changes, like changing the color of an element when it's no longer read-only, the you need your own watcher (scope.$watch).

Isolated scopes and $watch is not the same. Using an isolated scope like
scope: {
myAttr: '='
}
tells $compile to bind to the my-attr="" attribute. That means if you change the value in your directive it gets updated in the parent scope(s) as well.
On the other hand, using $watch triggers a function if that value changes.

Related

Angularjs directive two-way bound variable changes are not triggering $digest on the parent scope

Apologies if some of the JS is syntactically off. I wrote it while looking at my CoffeeScript
I have a text editor that I've extracted into a directive and I want to share some state between it and its containing template:
Main containing template
<div class="content">
<editor class="editor" ng-model="foo.bar.content" text-model="foo.bar"></editor>
</div>
Template Controller
angular.module('foo').controller('fooController', ['$scope', ... , function ($scope, ...) {
$scope.foo = {}
$scope.foo.bar = {}
$scope.foo.bar.content = 'starting content'
$scope.$watch('foo.bar', function () {
console.log('content changed')
}, true)
}
The template two-way binds on its scope object $scope.foo.bar with the editor directive. When the text is changed, the editor's 'text-change' handler is fired and a property on the bound object is changed.
Editor Directive
angular.module('foo').directive('editor'), function (
restrict: 'E',
templateUrl: 'path/to/editor.html',
require: 'ng-model',
scope: {
textModel: '='
},
controller: [
...
$scope.editor = 'something that manages the text'
...
],
link: function (scope, ...) {
scope.editor.on('text-change', function () {
$scope.textModel.content = scope.editor.getText()
// forces parent to update. It only triggers the $watch once without this
// scope.$parent.$apply()
}
}
However, changing this property in the directive seems not to be hitting the deep $watch I've set on foo.bar. After some digging, I was able to use the directive's parent reference to force a $digest cycle scope.$parent.$apply(). I really shouldn't need to though, since the property is shared and should trigger automatically. Why does it not trigger automatically?
Here are some good readings that I've encountered that are pertinent:
$watch an object
https://www.sitepoint.com/understanding-angulars-apply-digest/
The on function is a jqLite/jQuery function. It will not trigger digest cycle. It is basically outside the angular's conscious. You need to manually trigger digest cycle using $apply.

How to watch for a property change in a directive

I have an angular directive (with isolate scope) set up like this
<div ng="$last" somedirective datajson=mydata myprop="{{ mydata.myspecialprop }}"></div>
(which actually gets rendered multiple times because it's inside an ng-repeat.)
Following the instructions of this SO answer, I tried to observe myprop for changes in the directive, however, the code inside the $scope.watch never runs even when the property changes (on a click event). I also tried scope.$watch(attrs.myprop, function(a,b){...)and it never ran either. How do I watch for the change in myprop
myapp.directive('somedirective', function () {
return {
restrict: 'AE',
scope: {
datajson: '=',
myprop: '#'
},
link: function (scope, elem, attrs) {
scope.$watch(scope.myprop, function (a, b) {
if (a != b) {
console.log("doesn't get called when a not equal b");
} else {
}
});
}
};
}
Update: the click event that changes the property is handle in the controller and I'm guessing this isn't reflected back in the isolate scope directive so that $watch is never getting triggered. Is there a way to handle that?
When you use an interpolation binding (#) you cannot use scope.$watch, which is reserved for two-way bindings (=) or internal scope properties.
Instead, you need to use attrs.$observe which does a similar job:
link: function(scope, elem, attrs){
attrs.$observe('myprop', function(newVal, oldVal) {
// For debug purposes
console.log('new', newVal, 'old', oldVal);
if (newVal != oldVal){
console.log("doesn't get called when newVal not equal oldVal");
} else {
// ...
}
});
}
Also, everytime myprop change, the newVal will be different from its oldVal, so it is a bit weird that you skip that case which is the only one which will happen.
NOTE: you also forgot doublequotes for the datajson two-way binding: datajson="mydata"
Intead of passing a string, try it as variable
scope: {
datajson: '=',
myprop: '=' //change to equal so you can watch it (it has more sense i think)
}
then change html, removing braces from mydata.myspecialprop
<div ng="$last" somedirective datajson="mydata" myprop="mydata.myspecialprop"></div>

How to implement an ng-change for a custom directive

I have a directive with a template like
<div>
<div ng-repeat="item in items" ng-click="updateModel(item)">
<div>
My directive is declared as:
return {
templateUrl: '...',
restrict: 'E',
require: '^ngModel',
scope: {
items: '=',
ngModel: '=',
ngChange: '&'
},
link: function postLink(scope, element, attrs)
{
scope.updateModel = function(item)
{
scope.ngModel = item;
scope.ngChange();
}
}
}
I would like to have ng-change called when an item is clicked and the value of foo has been changed already.
That is, if my directive is implemented as:
<my-directive items=items ng-model="foo" ng-change="bar(foo)"></my-directive>
I would expect to call bar when the value of foo has been updated.
With code given above, ngChange is successfully called, but it is called with the old value of foo instead of the new updated value.
One way to solve the problem is to call ngChange inside a timeout to execute it at some point in the future, when the value of foo has been already changed. But this solution make me loose control over the order in which things are supposed to be executed and I assume that there should be a more elegant solution.
I could also use a watcher over foo in the parent scope, but this solution doesn't really give an ngChange method to be implmented and I have been told that watchers are great memory consumers.
Is there a way to make ngChange be executed synchronously without a timeout or a watcher?
Example: http://plnkr.co/edit/8H6QDO8OYiOyOx8efhyJ?p=preview
If you require ngModel you can just call $setViewValue on the ngModelController, which implicitly evaluates ng-change. The fourth parameter to the linking function should be the ngModelCtrl. The following code will make ng-change work for your directive.
link : function(scope, element, attrs, ngModelCtrl){
scope.updateModel = function(item) {
ngModelCtrl.$setViewValue(item);
}
}
In order for your solution to work, please remove ngChange and ngModel from isolate scope of myDirective.
Here's a plunk: http://plnkr.co/edit/UefUzOo88MwOMkpgeX07?p=preview
tl;dr
In my experience you just need to inherit from the ngModelCtrl. the ng-change expression will be automatically evaluated when you use the method ngModelCtrl.$setViewValue
angular.module("myApp").directive("myDirective", function(){
return {
require:"^ngModel", // this is important,
scope:{
... // put the variables you need here but DO NOT have a variable named ngModel or ngChange
},
link: function(scope, elt, attrs, ctrl){ // ctrl here is the ngModelCtrl
scope.setValue = function(value){
ctrl.$setViewValue(value); // this line will automatically eval your ng-change
};
}
};
});
More precisely
ng-change is evaluated during the ngModelCtrl.$commitViewValue() IF the object reference of your ngModel has changed. the method $commitViewValue() is called automatically by $setViewValue(value, trigger) if you do not use the trigger argument or have not precised any ngModelOptions.
I specified that the ng-change would be automatically triggered if the reference of the $viewValue changed. When your ngModel is a string or an int, you don't have to worry about it. If your ngModel is an object and your just changing some of its properties, then $setViewValue will not eval ngChange.
If we take the code example from the start of the post
scope.setValue = function(value){
ctrl.$setViewValue(value); // this line will automatically evalyour ng-change
};
scope.updateValue = function(prop1Value){
var vv = ctrl.$viewValue;
vv.prop1 = prop1Value;
ctrl.$setViewValue(vv); // this line won't eval the ng-change expression
};
After some research, it seems that the best approach is to use $timeout(callback, 0).
It automatically launches a $digest cycle just after the callback is executed.
So, in my case, the solution was to use
$timeout(scope.ngChange, 0);
This way, it doesn't matter what is the signature of your callback, it will be executed just as you defined it in the parent scope.
Here is the plunkr with such changes: http://plnkr.co/edit/9MGptJpSQslk8g8tD2bZ?p=preview
Samuli Ulmanen and lucienBertin's answers nail it, although a bit of further reading in the AngularJS documentation provides further advise on how to handle this (see https://docs.angularjs.org/api/ng/type/ngModel.NgModelController).
Specifically in the cases where you are passing objects to $setViewValue(myObj). AngularJS Documentatation states:
When used with standard inputs, the view value will always be a string (which is in some cases parsed into another type, such as a Date object for input[date].) However, custom controls might also pass objects to this method. In this case, we should make a copy of the object before passing it to $setViewValue. This is because ngModel does not perform a deep watch of objects, it only looks for a change of identity. If you only change the property of the object then ngModel will not realize that the object has changed and will not invoke the $parsers and $validators pipelines. For this reason, you should not change properties of the copy once it has been passed to $setViewValue. Otherwise you may cause the model value on the scope to change incorrectly.
For my specific case, my model is a moment date object, so I must clone the object first before then calling setViewValue. I am lucky here as moment provides a simple clone method: var b = moment(a);
link : function(scope, elements, attrs, ctrl) {
scope.updateModel = function (value) {
if (ctrl.$viewValue == value) {
var copyOfObject = moment(value);
ctrl.$setViewValue(copyOfObject);
}
else
{
ctrl.$setViewValue(value);
}
};
}
The fundamental issue here is that the underlying model does not get updated until the digest cycle that happens after scope.updateModel has finished executing. If the ngChange function requires details of the update that is being made then those details can be made available explicitly to ngChange, rather than relying on the model updating having been previously applied.
This can be done by providing a map of local variable names to values when calling ngChange. In this scenario, you can mapping the new value of the model to a name which can be referenced in the ng-change expression.
For example:
scope.updateModel = function(item)
{
scope.ngModel = item;
scope.ngChange({newValue: item});
}
In the HTML:
<my-directive ng-model="foo" items=items ng-change="bar(newValue)"></my-directive>
See: http://plnkr.co/edit/4CQBEV1S2wFFwKWbWec3?p=preview

Directive scope is not refreshing when provided value will be assign to directive scope

I created my own directive in angularjs and I noticed that directive scope is not refreshing when I change scope in main controller.
I made simple example which after 3s changing scope value, but the value is not changed in the directive. The issue exists only if I assign directive provided value to the directive scope.
Entire example is available here: jsfiddle
My directive:
myApp.directive('textPresenter', function() {
return {
transclude : false,
restrict: 'E',
scope: {
adfName : '='
},
template: '{{xxx}}' ,
controller: function($scope){
$scope.xxx = $scope.adfName; //this is the issue
}
};
});
$scope.xxx should refresh, after scope change in main controller.
The scope value adfName in the template is bound to the one in your main controller by Angular, and will change when that one changes. If you change the template to:
template: '{{adfName}}'
as in http://jsfiddle.net/6bp7c/1/ , then you can see this.
However, from your comment, you say you can't do this. Then one possibility would be setup a watcher in your directive, to watch adfName, and set xxx to be equal to it when it changes:
$scope.$watch('adfName', function(newAdfName) {
$scope.xxx = newAdfName;
});
as can be seen at http://jsfiddle.net/7V9FV/ . This is necessary because
$scope.xxx = $scope.adfName
just copies the contents of adfName and put it into xxx. If adfName later changes, then the contents of xxx are unaffected.
I am curious as to why you have to copy the variable though, and not just change the template to use the one passed into the directive.

how to set an interpolated value in angular directive?

How do I set the interpolated value in a directive? I can read the correct value from the following code, but I have not been able to set it.
js:
app.directive('ngMyDirective', function () {
return function(scope, element, attrs) {
console.log(scope.$eval(attrs.ngMyDirective));
//set the interpolated attrs.ngMyDirective value somehow!!!
}
});
html:
<div ng-my-directive="myscopevalue"></div>
where myscopevalue is a value on my controller's scope.
Whenever a directive does not use an isolate scope and you specify a scope property using an attribute, and you want to change that property's value, I suggest using $parse. (I think the syntax is nicer than $eval's.)
app.directive('ngMyDirective', function ($parse) {
return function(scope, element, attrs) {
var model = $parse(attrs.ngMyDirective);
console.log(model(scope));
model.assign(scope,'Anton');
console.log(model(scope));
}
});
fiddle
$parse works whether or not the attribute contains a dot.
If you want to set a value on the scope but don't know the name of the property (ahead of time), you can use object[property] syntax:
scope[attrs.myNgDirective] = 'newValue';
If the string in the attribute contains a dot (e.g. myObject.myProperty), this won't work; you can use $eval to do an assignment:
// like calling "myscopevalue = 'newValue'"
scope.$eval(attrs.myNgDirective + " = 'newValue'");
[Update: You should really use $parse instead of $eval. See Mark's answer.]
If you're using an isolate scope, you can use the = annotation:
app.directive('ngMyDirective', function () {
return {
scope: {
theValue: '=ngMyDirective'
},
link: function(scope, element, attrs) {
// will automatically change parent scope value
// associated by the variable name given to `attrs.ngMyDirective`
scope.theValue = 'newValue';
}
}
});
You can see an example of this in this Angular/jQuery color picker JSFiddle example, where assigning to scope.color inside the directive automatically updates the variable passed into the directive on the controller's scope.

Resources