I want to have an attribute directive that can be used like this:
<div my-dir="{items:m.items, type:'0'}"></div>
In my directive I can turn this into an object like:
function (scope, element, attributes, modelController)
{
var ops = $.extend({}, scope.$eval(attributes.myDir));
}
Now say m.items is an array. And somewhere down the line an ajax call replaces the array with a new one. I'd like to watch for this.
How can I watch m.items or whatever was typed in for the items property in the attribute? What should the watch expression be?
You should be able to use scope.$watch to watch the attribute. Angular will invoke the callback with the entire object when any of the values change.
function(scope, elem, attrs) {
scope.$watch(attrs.myDir, function(val) {
// Do something with the new value, val.
}, true);
}
http://plnkr.co/edit/JBGqW8uFzh1eJ1Ppq3fT
Anytime the model changes (by typing something in the input) the directive updates the elements html in the $watch.
Edited my answer: you want to include true as the last argument to compare object values, not references. Otherwise you will run digest more than 10 times and get an exception.
scope.$watchCollection (watchExpression, listener)
Refer to: https://docs.angularjs.org/guide/scope
Related
Trying to write a angular model that allows for a two way binding, I.E. where it has a ng-model variable, and if the controller updates it, it updates for the directive, if the directive updates it, it updates for the controller.
I have called the controller scope variable ng-model, and the directive model bindModel. In this case it sends an array of data, but that should not make a difference to how the binding works (I think at least).
So first the mapping, this is how the initial directive looks (link comes later).
return {
link: link,
scope: { cvSkills: '=',
bindModel:'=ngModel'},
restrict: 'E'
}
My understanding is (and I am uncertain about the exact scope at this moment) the directive will be aware of cvSkills (called cvSkills internally, and used to provide intial data), and bindModel which should pick up whats in ng-model).
The call in html is:
<skilltree cv-skills="cvskills" ng-model="CV._skillSet"></skilltree>
So the variables aren't actually (quite) in the directive yet. But there is an awareness of them, so I created two watchers (ignoring the cvskills one for now)
function link(scope,element, attr){
//var selected = ["55c8a069cca746f65c9836a3"];
var selected = [];
scope.$watch('bindModel', function(bindModel){
if (bindModel.length >> 0) {
console.log("Setting " + bindModel[0].skill)
selected = bindModel;
}
})
scope.$watch('cvSkills', function(cvSkills) {
So once the scope.$watch sees an update to bindModel, it picks it up and stores it to selected (same happens separately with cvSkills). In cvskills I then do updates to selected. So it will add additional data to selected if the user clicks buttons. All works and nothing special. My question is now. How do I then update bindModel (or ngModel) when there are updates to selected so that the controllers scope picks it up. Basically, how do I get the updates to propagate to ng-model="CV._skillSet"
And is this the right way to do it. I.E. make the scope aware, than pick up changes with scope.$watch and manually update the variable, or is the a more "direct" way of doing it?
=================== Fix using ngModel ===================
As per the article, if you add require: "ngModel" you get a fourth option to the function (and skip having a binding between ngModel and bindModel).
return {
link: link,
require: 'ngModel',
scope: { cvSkills: '='},
restrict: 'E'
}
Once you have done that, ngModel.viewValue will contain the data from ngModel= , in my case I am updating this in the code (which may be a bad idea). then call ngModel.setViewValue. It is probably safeish if you set the variable and then store it straight away (as follows)
function link(scope,element, attr, ngModel){
ngModel.$render = function() {
console.log("ngRender got called " + ngModel.$viewValue );
} ;
....
ngModel.$viewValue.push(newValue);
ngModel.$setViewValue(ngModel.$viewValue)
================= If you don't care about viewValue ==================
You can use modelValue instead of viewValue, and any updates to modelValue is propagated straight through.
function link(scope,element, attr, ngModel){
ngModel.$render = function() {
console.log("ngRender got called " + ngModel.$modelValue );
} ;
....
ngModel.$modelValue.push(newValue);
Using the require attribute of the directive you can have the controller API from ngModel directive. So you can update values.
Take a look at this very simple demo here : https://blog.hadrien.eu/2015/01/16/transformation-avec-ngmodel/
For more information here is the documentation : https://docs.angularjs.org/#!/api/ng/type/ngModel.NgModelController
I am experiencing a strange issue in that my directive seems to be executing on stale row.entities, meaning it does not get the new values as you scroll down or modify the sort of the grid. The initial ~20 rows render fine but past that the directives become disassociated with the rows.
See my very hacked together example here.
It looks like during sort values of the expression you pass to directive change, but the expression itself stays the same.
You should change scope & expression binding to = value binding (and access the value with scope.installs, without function call), then you will be able to track the changes.
// ...
scope: {
installs: '='
},
// ...
Then, to track the changes you can use scope.$watch and put your code inside.
link: function (scope, element, attrs) {
scope.$watch('installs', function(newValue) {
// your code, you can use newValue as current installs value
var installs = newValue;
// ...
});
}
Example here.
I am seeing inconsistent behavior with a directive when I $compile the element that contains the directive. In my case I have a directive that validates whether a password matches another password field. That directive looks like this:
app.directive('passwordMatches', function() {
return {
require: 'ngModel',
restrict: 'A',
scope: {
otherPasswordFieldValue: '=passwordMatches'
},
link: function (scope, elem, attrs, ngModelController) {
function validate(value) {
return value === scope.otherPasswordFieldValue;
}
//For DOM -> model validation
ngModelController.$parsers.unshift(function (value) {
var valid = validate(value);
ngModelController.$setValidity('password-matches', valid);
return valid ? value : undefined;
});
//For model -> DOM validation
ngModelController.$formatters.unshift(function (value) {
ngModelController.$setValidity('password-matches', validate(value));
return value;
});
scope.$watch(function() { return scope.otherPasswordFieldValue }, function () {
var valid = validate(ngModelController.$viewValue);
ngModelController.$setValidity('password-matches', valid);
});
}
};
});
This works fine alone. But I have another directive that is often used on the same element. The details of that directive aren't important because I've shown that the root cause of the issue is that that second directive compiles the element. As soon as I add this directive, the behavior changes. Without compiling the element, my passwordMatches directive works fine (the field becomes invalid if what I type doesn't match the other field and I can type whatever I want).
But as soon as I compile the element, I can type what I want until I make the fields match and it behaves normally up until that point. But once the values in the two fields match, if I type anything to make them not match, the field is completely blanked out. The easiest way to see this is in this jsbin: http://jsbin.com/IkuMECEf/12/edit. To reproduce, type "foo" in the first field and then try to type "fooo" (three o's) in the second field. As soon as you type the third "o" the field is blanked out. If you comment out the $compile, it works fine.
Thanks!
The second directive is compiling dom elements that have already been compiled by Angular. This second compile adds a second $watch, parser, etc because all the directive's linking functions are called again (here's a good detailed look at $compile) To confirm this you can put a console.log inside the $watch and you'll see that (with the second directive) it fires twice for every change- because of the duplicate $watch (remove the second directive and it fires only once- as expected). This second compilation step is not only causing the issue you're seeing but could cause other problems down the line.
If you have to recompile an Angular element then you first need to remove the existing one.
Here's one approach to this (explanation in the comments):
compile: function(compileElement) {
compileElement.removeAttr('another-directive');
return function(scope, element) {
// Create an "uncompiled" element using a copy of the current element's html
newe = angular.element(element.html());
// Remember where we were
parent= element.parent();
// Deleting the current "compiled" element
element.remove();
// Add the uncompiled copy
parent.append(news);
// Compile the copy
$compile(newe)(scope);
};
updated punker
Having read AngularJS : Difference between the $observe and $watch methods, and the implementation of ng-href/ng-src:
link: function(scope, element, attr) {
attr.$observe(normalized, function(value) {
if (!value)
return;
attr.$set(attrName, value);
// ...
}
}
I wonder why ng-href/ng-src are implemented by using attr.$observe instead of scope.$watch. By using scope.$watch, it looks like
link: function(scope, element, attr) {
scope.$watch(attr[normalized], function(newValue) {
// ...
})
}
then in view we could write <img ng-href="expressionFoo"> instead of <img ng-href="{{ expressionFoo }}">.
The possible reasons I could figure out are
attr.$observe makes a directive to work more like a normal DOM attribute. After link, I could affect the directive by attr.$set('ngHref', ...) in another directive.
interpolate is higher level than expression. It's easier to write plain strings and multiple expressions by using interpolate.
ng-href and ng-src both result in string attributes, so it's safe and easier to use interpolate hence attr.$observe here.
Any idea?
I think the scope isn't what you think.
Option scope in directives is false by default, which means we get parent scope, that could be anything, really.
If we want to watch for element's attribute change, we have to watch the element's attribute "ng-blah". The scope is simply not related.
If we were to use scope.watch, we probably wouldn't even have a property named after that attribute, and nothing would work.
I try to create a directive which should peform some actions when an input field is marked as invalid. For this example lets assume I have a directive which checks if the input is a prime number, and I want to create a directive which adds a class to the element when it's invalid:
<input type="text" ng-model="primeNumber" validate-prime invalid-add-class="error">
The validate-prime uses the parsers and formatters on ng-model to update the validity of the model.
Now I want the invalid-add-class directive to add the class "error" when the model is invalid, and to remove it when it is valid. In other words, it should watch the $valid (or $invalid) property of the model controller. However, I can't figure out how to get this working. I tried:
link : function(scope, element, attrs, ctrl) {
ctrl.$watch("$valid", function(newVal, oldVal) {
//never fired
});
}
Perhaps I could watch some variable on scope, but I don't know which variable to watch for.
So how can I be notified when the validity of a model changes?
If you have a <form>, add a name to it (lets assume 'myForm') and a name to your input (lets assume myInput). You should be able to $watch this by:
scope.$watch('myForm.myInput.$valid', function(validity) {})
If you don't have a form, you can always watch a function. This way:
scope.$watch(function() { return ctrl.$valid; }, function(validity){});
You can read more about the form approach here.
If you do not have a <form />you can easily get one:
In your directive definition:
require: '^form'
and then in your link function, the form is passed as the fourth parameter:
link: function (scope, element, attr, ctrl) {
Now you don't have to hard-code the form or the input field to perform the $watch:
scope.$watch(ctrl.$name + '.' + element.attr('name') + '.$valid',
function (validity) {});
Our goal, in general, should be to make a directive work independently of any one form or input. How can we allow it to read the local $valid property without imperatively binding it to a single specific form & input name?
Just use require: 'ngModel' as one of the properties of your directive config. This will inject the local ngModel controller as the fourth argument to the link function, and you can place a $watch directly upon $valid without needing to couple the directive's implementation to any particular form or input.
require: 'ngModel',
link: function postLink(scope, element, attrs, controller) {
scope.inputCtrl = controller;
scope.$watch('inputCtrl.$valid', handlerFunc)
}
The handler should consistently fire on changes to $valid with that structure. See this Fiddle, where the input is validated for the pattern of a U.S. Zip-Code or Zip+4. You'll get an alert each time validity changes.
EDIT 3/21/14: This post previously got hung up on a delusion of mine, fixating on the wrong cause of an implementation problem. My fault. The example above removes that fixation. Also, added the fiddle, showing that this approach does in fact work, and always did, once you add quotes around the watch expression.