I am trying to watch my model value from inside my linking function.
scope.$watch(attrs.ngModel, function() {
console.log("Changed");
});
When I change the model value inside my controller, the $watch function is not triggered.
$scope.myModel = "ACT";
$timeout(function() {
$scope.myModel = "TOTALS";
}, 2000);
Fiddle: http://jsfiddle.net/dkrotts/BtrZH/4/
What am I missing here?
You'll need to watch a function that returns the $modelValue you're watching.
The following code shows a basic example:
app.directive('myDirective', function (){
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
scope.$watch(function () {
return ngModel.$modelValue;
}, function(newValue) {
console.log(newValue);
});
}
};
});
Here's a plunker of the same idea in action.
The problem is that you $watching attrs.ngModel which is equal to "myModel". You do not have "myModel" bound in your scope. You want to $watch "model". That is what is bound in the scope of your directive. See http://jsfiddle.net/BtrZH/5/
The proper way to do this is:
app.directive('myDirective', function () {
return {
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
ngModel.$render = function () {
var newValue = ngModel.$viewValue;
console.log(newValue)
};
}
};
});
Here is another way to do this:
app.directive('myDirective', function (){
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
attrs.$observe('ngModel', function(value){ // Got ng-model bind path here
scope.$watch(value,function(newValue){ // Watch given path for changes
console.log(newValue);
});
});
}
};
});
Doing it that way you will be able to listen value changes with binds like that
This is an extension of # Emmanuel's answer above to answer #Martin Velez, although I know it's pretty late! (Also I can't make comments yet, so if this isn't the right place for this, sorry!)
I'm not sure which version of Angular OP was using, but in Angular#1.2+ at least on the official docs https://docs.angularjs.org/api/ng/type/ngModel.NgModelController#$render, $render is listed like this:
Called when the view needs to be updated. It is expected that the user
of the ng-model directive will implement this method.
The $render() method is invoked in the following situations:
$rollbackViewValue() is called. If we are rolling back the view value
to the last committed value then $render() is called to update the
input control. The value referenced by ng-model is changed
programmatically and both the $modelValue and the $viewValue are
different from last time. Since ng-model does not do a deep watch,
$render() is only invoked if the values of $modelValue and $viewValue
are actually different from their previous value.
I interpret this to mean that the correct way to $watch an ngModel from a directive is to require ngModel and implement a link function that injects ngModelController. Then use the ngModel API that's built in to $render-on-change ($watch), or whatever else.
There are 2 ways to do it.
1) You can use $attrs.[any_attribute] and set on it any listener
2) You can have isolated scope with 2 ways binding variable and set a listener on it.If you want more,here is a cool article
http://www.w3docs.com/snippets/angularjs/bind-variable-inside-angularjs-directive-isolated-scope.html
Related
//main controller
angular.module('myApp')
.controller('mainCtrl', function ($scope){
$scope.loadResults = function (){
console.log($scope.searchFilter);
};
});
// directive
angular.module('myApp')
.directive('customSearch', function () {
return {
scope: {
searchModel: '=ngModel',
searchChange: '&ngChange',
},
require: 'ngModel',
template: '<input type="text" ng-model="searchModel" ng-change="searchChange()"/>',
restrict: 'E'
};
});
// html
<custom-search ng-model="searchFilter" ng-change="loadResults()"></custom-search>
Here is a simplified directive to illustrate. When I type into the input, I expect the console.log in loadResults to log out exactly what I have already typed. It actually logs one character behind because loadResults is running just before the searchFilter var in the main controller is receiving the new value from the directive. Logging inside the directive however, everything works as expected. Why is this happening?
My Solution
After getting an understanding of what was happening with ngChange in my simple example, I realized my actual problem was complicated a bit more by the fact that the ngModel I am actually passing in is an object, whose properties i am changing, and also that I am using form validation with this directive as one of the inputs. I found that using $timeout and $eval inside the directive solved all of my problems:
//main controller
angular.module('myApp')
.controller('mainCtrl', function ($scope){
$scope.loadResults = function (){
console.log($scope.searchFilter);
};
});
// directive
angular.module('myApp')
.directive('customSearch', function ($timeout) {
return {
scope: {
searchModel: '=ngModel'
},
require: 'ngModel',
template: '<input type="text" ng-model="searchModel.subProp" ng-change="valueChange()"/>',
restrict: 'E',
link: function ($scope, $element, $attrs, ngModel)
{
$scope.valueChange = function()
{
$timeout(function()
{
if ($attrs.ngChange) $scope.$parent.$eval($attrs.ngChange);
}, 0);
};
}
};
});
// html
<custom-search ng-model="searchFilter" ng-change="loadResults()"></custom-search>
The reason for the behavior, as rightly pointed out in another answer, is because the two-way binding hasn't had a chance to change the outer searchFilter by the time searchChange(), and consequently, loadResults() was invoked.
The solution, however, is very hacky for two reasons.
One, the caller (the user of the directive), should not need to know about these workarounds with $timeout. If nothing else, the $timeout should have been done in the directive rather than in the View controller.
And two - a mistake also made by the OP - is that using ng-model comes with other "expectations" by users of such directives. Having ng-model means that other directives, like validators, parsers, formatters and view-change-listeners (like ng-change) could be used alongside it. To support it properly, one needs to require: "ngModel", rather than bind to its expression via scope: {}. Otherwise, things would not work as expected.
Here's how it's done - for another example, see the official documentation for creating a custom input control.
scope: true, // could also be {}, but I would avoid scope: false here
template: '<input ng-model="innerModel" ng-change="onChange()">',
require: "ngModel",
link: function(scope, element, attrs, ctrls){
var ngModel = ctrls; // ngModelController
// from model -> view
ngModel.$render = function(){
scope.innerModel = ngModel.$viewValue;
}
// from view -> model
scope.onChange = function(){
ngModel.$setViewValue(scope.innerModel);
}
}
Then, ng-change just automatically works, and so do other directives that support ngModel, like ng-required.
You answered your own question in the title! '=' is watched while '&' is not
Somewhere outside angular:
input view value changes
next digest cycle:
ng-model value changes and fires ng-change()
ng-change adds a $viewChangeListener and is called this same cycle.
See:
ngModel.js#L714 and ngChange.js implementation.
At that time $scope.searchFilter hasn't been updated. Console.log's old value
next digest cycle:
searchFilter is updated by data binding.
UPDATE: Only as a POC that you need 1 extra cycle for the value to propagate you can do the following. See the other anwser (#NewDev for a cleaner approach).
.controller('mainCtrl', function ($scope, $timeout){
$scope.loadResults = function (){
$timeout(function(){
console.log($scope.searchFilter);
});
};
});
I'm writing a directive with custom validation logic to validate an object.
HTML:
<input type="hidden" name="obj" ng-model="vm.obj" validate-object />
JS:
angular
.module('myApp')
.directive('validateObject', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ngModelCtrl) {
ngModelCtrl.$validators.validateObject = myValidator;
function myValidator (modelValue, viewValue) {
return validateObject(modelValue);
}
function validateObject (obj) {
// Look inside the object
}
}
}
});
The problem is that the validator doesn't run when a property inside the object is changed.
I could add a $watch with objectEquality === true, and then manually $setCustomValidity with my validation logic. Something like this:
link: function (scope, element, attrs, ngModelCtrl) {
scope.$watch(attrs.ngModel, onModelChange, true);
function onModelChange (newValue) {
ngModelCtrl.$setCustomValidity('validateObject', validateObject(newValue))
}
function validateObject (obj) {
// Look inside the object
}
}
But I don't like using the old school way of manually using $setValidity, plus adding a manual $watch while NgModelController already has ways of registering inside the update process (like $formatters), and in addition the $watch being a deep one which can has performance issues.
Am I getting this wrong? Is there a better way?
From https://github.com/angular/angular.js/blob/master/src/ng/directive/ngModel.js#L699 :
if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) {
return;
}
ngModel performs a flat equality check against the older version of the model, so any changes inside an object would not be reflected on ngModel or ngChange.
The perferred approach would be to use immutable data, that means that every time you change the model (the object), create a new copy instead:
function changeModel(){
this.vm.name = "roy";
// Create a new object for ngModel;
this.vm = angular.copy(this.vm);
}
EDIT
I remember that I solved a previous issue before. You want to have a set of ng-models binded to properties on an object, and have 1 change listener for the entire object.
Here's my solution: http://plnkr.co/edit/6tPMrB8n1agINMo252F2?p=preview
What I did was to create a new directive "formModel" that must be placed on a form element. Angular has a form directive which has a controller.
NgModelController requires a parent form controller, which then it adds itself to the form (this is how you get validity on an entire form).
So in my directive, I decorated the form's $addControl method, and added a listener for every ngModelController that adds itself via $viewChangeListeners, and now on every change of ngModel inside the form, the formModel directive will duplicate the entire object and fire $setViewValue.
I am writing a custom directive which calls a legacy javascript date control we are using in other places.. The issue I am facing is the value of the ng-model I send in is always undefined.
This value is coming from a $http.get which is executed in the controller. The code for the directive seems to be hit before the controller code is run which is why this is happening. Is there a way for me to tell the directive only to run after the controller has been initialized?
app.directive('dateControl', function () {
return {
restrict: 'A',
require: '^ngModel',
scope: {
ngModel: '='
},
link: function (scope, element, attr) {
// scope.ngModel is undefined in here
alert(scope.ngModel);
addCalControlAngular(attr.id, scope.ngModel, attr.ngModel, attr.id);
}
}
});
<td date-Control ng-model="postAward.letterOfAwardDespatchDate" id="letterofaward">
The directive is not 'hit' before controller if it's inside controller, but if the value comes from http request then it's undefined until the model is updated with value (hope that makes sense), to assure that directive is executed after receiving data from http you can do a small trick with ng-if
<td procon-Date-Control ng-if="postAward.letterOfAwardDespatchDate" ng-model="postAward.letterOfAwardDespatchDate" id="letterofaward">
If you do it that way directive is going to be rendered in view only of the ng-if is not null/undefined or the expression is truthy i.e. ng-if="postAward.letterOfAwardDespatchDate == 'yesterday'"
I resolved this with a $watch in my link function
link: function (scope, element, attr) {
debugger;
// Watch for a value in ngModel and render that value, otherwise give a default
scope.$watch('ngModel', function (newVal) {
if (newVal) {
addCalControlAngular(attr.id, scope.ngModel, attr.isMandatory, attr.showDefault, attr.ngModel, attr.id);
} else {
addCalControlAngular(attr.id, c_NULL_DATE, attr.isMandatory, attr.showDefault, attr.ngModel, attr.id);
}
})
}
If you want to use ngModel try to add a argument in link function to add a injection like this:
link: function (scope, element, attr, ngModel)
After this, you can use code like ngModel.$render and so on, hope that helpful to you.
Hello I have a problem with my validation directive. First of all I have directive that creates input element, in which I have directives that are based on some parameters that tell if this calidation should be fired up or not... It looks like this:
app.directive('ngAttr', function($compile){
return {
scope:{},
link: function (scope, element, attrs){
var opts = scope.$eval(attrs.ngAttr);
if(opts.condition){
element.attr(opts.attrName, opts.condition)
}
}
};
});
It adds attribute based on condition passed to it... If I want to add a directive conditionally I do:
ng-attr="{condition: {{opts.validatePhone}}, attrName:'validate-phone'}"
as an attribute tu my previous directive that creates an input... And the problem is that the validate-phone directive is fired up only once when the directive is created it doen't react on input handlers... The code of validate directive is:
app.directive('validatePhone', function($compile){
return{
require: 'ngModel',
link: function(scope, element, attrs, ngModel){
function validate(val){
console.log(val)
}
scope.$watch(function() {
return ngModel.$viewValue;
}, validate);
}
};
});
Not it is simple but console.log() dosn't work when I change input.
I created a plunker so it will be easier to check it if someone has an idea... http://plnkr.co/edit/CgVCV58goFS9GKLBtRrw?p=preview
I don't believe the $watch function is set up correctly.
It would help if you could be a little more specific in what you wish
to do in the directive with the validation function.
Here is the link to the Angularjs docs on $watch.
[$watch][1]
Notice how the watch expression is a property of scope. You don't include anything in your $watch function. You are saying 'SCOPE' please 'WATCH' function.
What you should be saying is
'SCOPE' please 'WATCH' this property called 'enter scope property here' and execute this function when it changes, or some other custom logic.
I think it's mostly that your are not watching anything. You need to watch the ng-model(s) of whatever you with to update.
Change your function to this...
scope.$watch(attrs.ngModel, function( elementValue ) {
if( !elementValue ){ return; } // return if falsy.
if( validate( elementValue )){ return elementValue; }
}); // or something like this.
If you don't know the value of each ng-model, because perhaps they are created dynamically using ng-repeat, and unknown values from the server you could do something like this to watch something.
app.directive('mydirective', function(){
return {
restrict:'A', // Declares an Attributes Directive.
require: '?ngModel', // ? checks for parent scope if one exists.
link: function( scope, elem, attrs, ngModel ){
if( !ngModel ){ return };
scope.$watch(attrs.ngModel, function(value){
if( !value ) { return };
// do something with validation of ngModel.
});
}
}
});
Then in your HTML
<input type="text" ng-model="{{ dynamicValue }}" my-directive />
or
<input type="text" ng-model="{{ dynamicValue }}" my-directive="myFunctionName()" />
You would have to add a little to the directive to run the function though.
probably isolate the scope like
scope: {
myDirective: '&'
}
Then in the directive $watch you could run it like
scope.myDirective();
Something like this I think.
Also, there are many ways to validate using Angularjs. Here is another link.
Here is one way
There are several ways to connect the validation.
You can $watch its model,
You can add your function to fire on some event (blur, click, focus, etc...)
Or just run the function when the directive loads by just calling your validation function validate( value );
I have a directive which looks something like:
var myApp = angular.module('myApp',[])
.directive("test", function() {
return {
template: '<button ng-click="setValue()">Set value</button>',
require: 'ngModel',
link: function(scope, iElement, iAttrs, ngModel) {
scope.setValue = function(){
ngModel.$setViewValue(iAttrs.setTo);
}
}
};
});
The problem is that if I use this directive multiple times in a page then setValue only gets called on the last declared directive. The obvious solution is to isolate the scope using scope: {} but then the ngModel isn't accessible outside the directive.
Here is a JSFiddle of my code: http://jsfiddle.net/kMybm/3/
For this scenario ngModel probably isn't the right solution. That's mostly for binding values to forms to doing things like marking them dirty and validation...
Here you could just use a two way binding from an isolated scope, like so:
app.directive('test', function() {
return {
restrict: 'E',
scope: {
target: '=target',
setTo: '#setTo'
},
template: '<button ng-click="setValue()">Set value</button>',
controller: function($scope) {
$scope.setValue = function() {
$scope.target = $scope.setTo;
};
//HACK: to get rid of strange behavior mentioned in comments
$scope.$watch('target',function(){});
}
};
});
All you need to do is add scope: true to your directive hash. That makes a new inheriting child scope for each instance of your directive, instead of continually overwriting "setValue" on whatever scope is already in play.
And you're right about isolate scope. My advice to newbies is just don't use it ever.
Response to comment:
I understand the question better now. When you set a value via an expression, it sets it in the most immediate scope. So what people typically do with Angular is they read and mutate values instead of overwriting values. This entails containing things in some structure like an Object or Array.
See updated fiddle:
http://jsfiddle.net/kMybm/20/
("foo" would normally go in a controller hooked up via ngController.)
Another option, if you really want to do it "scopeless", is to not use ng-click and just handle click yourself.
http://jsfiddle.net/WnU6z/8/