How to watch for a property change in a directive - angularjs

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>

Related

AngularJs directive, scope with value and function

I have a directive myEditable that toggle a <div> with an <input type=text> to allow inline edition :
<my-editable value="vm.contact.name"></my-editable>
I was happy with it until I read some articles that say that $scope.$apply should not be used. I'm using it when the user save his changes to update the model (vm.contact.name in my case) :
function save() {
scope.$apply(function(){
scope.value = editor.find('input').val();
});
toggle();
}
But since it is a bad thing, I would like to pass a callback method to my directive. This callback must be called with the new value when the user save his changes. However, it seems that I cannot add two fields to the directive scope :
return {
restrict: 'EA',
scope: {
value: '=value'/*,
onSave: '&onSave'*/
},
link: function(scope, element, attr) {
// ...
element.find('.save').click(function(){
save();
});
// Declaration of `save` as above.
}
}
If I uncomment onSave then the value is never received and onSave is undefined.
My question is, how can I give a value and a callback method to a directive ?
And, as bonus, how can I pass parameters to the callback ?
Thanks
You can pass 'n' number of fields in directives isolated scope.
If you want to pass a function use &. Keep this in mind if your property name is onSave then in the view use it like this on-save.
Your directive should look like below
app.directive('dir',function(){
return {
restrict: 'EA',
scope: {
onSave: '&'
},
link: function(scope, element, attr) {
// ...
debugger
console.log(scope.onSave)
scope.onSave();
// Declaration of `save` as above.
}
}
})
In the view you can pass the function like below
<dir on-Save='abc()'/>
OR
<dir on-save='abc()'/>

Unable to watch form $dirty (or $pristine) value in Angular 1.x

I have a scenario in an Angular 1.x project where I need to watch a controller form within a directive, to perform a form $dirty check. As soon as the form on a page is dirty, I need to set a flag in an injected service.
Here is the general directive code:
var directiveObject = {
restrict: 'A',
require: '^form',
link: linkerFn,
scope: {
ngConfirm: '&unsavedCallback'
}
};
return directiveObject;
function linkerFn(scope, element, attrs, formCtrl) {
...
scope.$watch('formCtrl.$dirty', function(oldVal, newVal) {
console.log('form property is being watched');
}, true);
...
}
The above only enters the watch during initialization so I've tried other approaches with the same result:
watching scope.$parent[formName].$dirty (in this case I pass formName in attrs and set it to a local var formName = attrs.formName)
watching element.controller()[formName] (same result as the above)
I've looked at other SO posts regarding the issue and tried the listed solutions. It seems like it should work but somehow the form reference (form property references) are out of scope within the directive and therefore not being watched.
Any advice would be appreciated.
Thank you.
I don't know why that watch isn't working, but as an alternative to passing in the entire form, you could simply pass the $dirty flag itself to the directive. That is:
.directive('formWatcher', function() {
restrict: 'A',
scope: {
ngConfirm: '&unsavedCallback', // <-- not sure what you're doing with this
isDirty: '='
},
link: function(scope, element, attrs) {
scope.watch('isDirty', function(newValue, oldValue) {
console.log('was: ', oldValue);
console.log('is: ', newValue);
});
}
})
Using the directive:
<form name="theForm" form-watcher is-dirty="theForm.$dirty">
[...]
</form>

Parent $scope in two way binding directive not updating correctly

I was simply trying to add functionality to an existing directive so I can track any changes that occur. I have a toggle control which is just used to toggle a boolean value. Here is my directive
app.directive('skToggle', function ($timeout) {
return {
replace: true,
restrict: 'A',
require: 'ngModel',
scope: {
ngModel: '=',
skcallback: '&callback',
disabled: '=',
emit: '#',
positive: '#',
negative: '#',
skTouched:'=?' //THIS IS NEW
},
template: '<div class="toggle" ng-class="{ disabled: disabled }" ng-click="disabled || toggle($event)">\
<div class="off">\
<span>{{neg}}</span>\
</div>\
<div class="on" ng-class="{ active: ngModel }">\
<span>{{pos}}</span>\
<div class="control"></div>\
</div>\
</div>',
link: function (scope, elem, attrs, ctrl) {
var hasCallback = angular.isDefined(attrs.callback);
scope.pos = scope.positive || "YES"
scope.neg = scope.negative || "NO";
scope.toggle = function (e) {
if (hasCallback) {
scope.skcallback({ event: e });
} else {
ctrl.$setViewValue(!ctrl.$viewValue);
}
if (scope.emit === 'true') {
$timeout(function () {
scope.$emit('toggle');
});
}
}
// THIS IS ALSO NEW
scope.$watch('ngModel', function(newVal, oldVal){
if(scope.skTouched && oldVal !== undefined && newVal !== oldVal){
scope.skTouched.UI.$dirty = true;
}
});
}
}
});
I have commented on which parts of the directive are new.. all I did was add a two-way binding on my directive that takes an object and updates the UI.$dirty property on it. The problem is when I print out the object on the screen, the UI object never gets updated on the parent $scope. I don't know if I'm just spacing on something easy or if I am doing something wrong but, my directive (child) scope is not updating the parent scope like it should be.
<div sk-toggle ng-model="feature.enabled" sk-touched="feature"></div>
So I realized the problem was with using $dirty as my variable. Angular must reserve $dirty for only use with form controllers, which explains why it wasn't showing up when I printed the entire object out on the page. Simply changing the variable to dirty made it work as expected
ngModel is a core angular directive. It may be that there is a conflict with your using ngModel as an attribute to use two-way databinding on.
You may be able to get around this by setting the priority of your directive to be higher than that of ngModel. Add a priority of something > 1 in your directive definition. See the docs for more info on priority.

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

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.

AngularJS - In a directive that changes the model value, why do I have to call $render?

I made a directive designed to be attached to an element using the ngModel directive. If the model's value matches something the value should then set to the previous value. In my example I'm looking for "foo", and setting it back to the previous if that's what's typed in.
My unit tests passed fine on this because they're only looking at the model value. However in practice the DOM isn't updated when the "put back" triggers. Our best guess here is that setting old == new prevents a dirty check from happening. I stepped through the $setViewValue method and it appears to be doing what it ought to. However it won't update the DOM (and what you see in the browser) until I explicitly call ngModel.$render() after setting the new value. It works fine, but I just want to see if there's a more appropriate way of doing this.
Code is below, here's a fiddle with the same.
angular.module('myDirective', [])
.directive('myDirective', function () {
return {
restrict: 'A',
terminal: true,
require: "?ngModel",
link: function (scope, element, attrs, ngModel) {
scope.$watch(attrs.ngModel, function (newValue, oldValue) {
//ngModel.$setViewValue(newValue + "!");
if (newValue == "foo")
{
ngModel.$setViewValue(oldValue);
/*
I Need this render call in order to update the input box; is that OK?
My best guess is that setting new = old prevents a dirty check which would trigger $render()
*/
ngModel.$render();
}
});
}
};
});
function x($scope) {
$scope.test = 'value here';
}
Our best guess here is that setting old == new prevents a dirty check from happening
A watcher listener is only called when the value of the expression it's listening to changes. But since you changed the model back to its previous value, it won't get called again because it's like the value hasn't changed at all. But, be careful: changing the value of a property inside a watcher monitoring that same property can lead to an infinite loop.
However it won't update the DOM (and what you see in the browser) until I explicitly call ngModel.$render() after setting the new value.
That's correct. $setViewValue sets the model value as if it was updated by the view, but you need to call $render to effectively render the view based on the (new) model value. Check out this discussion for more information.
Finally, I think you should approach your problem a different way. You could use the $parsers property of NgModelController to validate the user input, instead of using a watcher:
link: function (scope, element, attrs, ngModel) {
if (!ngModel) return;
ngModel.$parsers.unshift(function(viewValue) {
if(viewValue === 'foo') {
var currentValue = ngModel.$modelValue;
ngModel.$setViewValue(currentValue);
ngModel.$render();
return currentValue;
}
else
return viewValue;
});
}
I changed your jsFiddle script to use the code above.
angular.module('myDirective', [])
.directive('myDirective', function () {
return {
restrict: 'A',
terminal: true,
require: "?ngModel",
link: function (scope, element, attrs, ngModel) {
if (!ngModel) return;
ngModel.$parsers.unshift(function(viewValue) {
if(viewValue === 'foo') {
var currentValue = ngModel.$modelValue;
ngModel.$setViewValue(currentValue);
ngModel.$render();
return currentValue;
}
else
return viewValue;
});
}
};
});
function x($scope) {
$scope.test = 'value here';
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<h1>Foo Fighter</h1>
I hate "foo", just try and type it in the box.
<div ng-app="myDirective" ng-controller="x">
<input type="text" ng-model="test" my-directive>
<br />
model: {{test}}
</div>

Resources