Angularjs custom validation directive - javascript

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 );

Related

AngularJS: bind to parent ngChange [duplicate]

//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);
});
};
});

Is it possible to detect changes to ng-model in a directive without a deep $watch

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.

Add custom Directive to existing Input that already has angular directives [ng-model/ng-required]

I would like to use a standard input control that is decorated with ng-model and ng-required and then add my own custom attribute directive that provides uib-typeahead functionality to the control.
I used this link to get my directive partly working.
Add directives from directive in AngularJS
PLUNKR - The Version 2 of the directive does not work correctly with ng-model
My Directive does add typeahead functionality and that works quite well, but it is not binding the model on to the control after item is selected.
I have two version of my directive.
Version 1: is an element style directive and I have been using it successfully for a while, but it fell short when I wan't to have a bit more control over the input element, especially when I wanted to use ng-required='true' and other ng-message directives.
Version 2: is an attribute style directive, I went with this because I felt it was better to just add the typeahead functionality that I wanted to any standard HTML that can optionally use ng-required='true', ng-model etc...
While this directive is mostly working, it does not interact correctly with ng-model and I'm not sure how to get it working
angular.module(APP)
.directive('wkLocationSuggest', ['$compile', function ($compile) {
return {
restrict: 'A',
require: 'ngModel',
replace: false,
//terminal: true,
//priority: 0,
scope: {
wkApiModel: '=' // Provide access to the internal data that is returned via the API lookup
},
controller: 'LocationSuggestController',
link: function (scope, element, attrs, ngModelCtrl) {
if (!ngModelCtrl) {
return;
}
element.attr('typeahead', 'location as row.location for row in typeAhead($viewValue)');
element.attr('typeahead-wait-ms', '750');
element.attr('typeahead-on-select', 'onSelectInternal($item, $model, $label)');
element.attr('typeahead-min-length', '2');
element.attr('typeahead-focus-first', 'true');
element.removeAttr("wk-location-suggest"); //remove the location-suggest to avoid indefinite loop
element.removeAttr("data-wk-location-suggest"); //also remove the same attribute with data- prefix if it exists
// None of this is working
//// invoked when model changes from the outside
//ngModelCtrl.$render = function () {
// //scope.innerModel = ngModelCtrl.$modelValue;
//};
////// invoked when model changes from the inside
//scope.onChange = function (value) {
// ngModelCtrl.$setViewValue(scope.innerModel);
//};
scope.onSelectInternal = function ($item, $model, $label) {
// This fires, but it effects the ng-model on the first input,
// but not the input that this directive is attached too
ngModelCtrl.$setViewValue($item.location);
};
$compile(element)(scope);
}
};
}]);
These two images demonstrate part of the problem, may be better to test for yourself using PLUNKR above
I initially tried to dynamically add validators to your wk-location-suggest-new directive by implementing blur on the input element in combination with ngModel's $setValidity method; but don't know what exactly was preventing the event from firing.
Therefore, I turned to the other directive wk-location-suggest-old and tweaked it a bit to fit in both desired behaviors.
There, I noticed that you were missing a couple of things:
First of all, in order for a form element to glue with the form itself (wkProfileCompany in your case), and to work with ng-model, the element (in the directive template) needs a name.
Secondly, ng-required (or required) would work with the form only if it is added as an attribute to the element in the directive template, not the directive which compiles to the template containing the element.
Directive Definition
As you may notice, I've passed two properties from the outer scope to the directive's inner scope, namely:
the name of the input element,
and an isRequired flag as to specify whether the input is required or not.
.
.directive('wkLocationSuggestOld', [function () {
return {
restrict: 'E',
require: '?ngModel',
scope: {
name: '#', // <==
isRequired: '=' // <==
},
template: '<input name="{{name}}" type="text" class="{{innerClass}}" ng-model="innerModel"'
+ ' ng-change="onChange()" uib-typeahead="location as row.location for row in typeAhead($viewValue)" '
+ ' typeahead-wait-ms="750" typeahead-on-select="onSelectInternal($item, $model, $label)" '
+ ' typeahead-min-length="2" typeahead-focus-first="true" '
+ ' ng-required="isRequired">', // <== added ng-required here
controller: 'LocationSuggestController',
link: function (scope, element, attrs, ngModel) {
if (!ngModel) {
return;
}
...
}])
HTML
Finally, you can use the tweaked directive in your HTML as such:
<wk-location-suggest-old class="form-control" type="text" name="location2" ng-model="location2" is-required="true"></wk-location-suggest-old>
Plunker
Update
One of the possible reasons for ng-model not correctly binding in the wk-location-suggest-new directive to a provided value (i.e. location3) is that you are replacing the whole DOM element with a new custom DOM element which is compiled with the isolated scope of the directive itself.
Since the directive wk-location-suggest-new has an isolate scope, the scope is totally unaware of location3, because location3 (and all the other location values) are defined in the scope of MainCtrl and NOT the scope of the directive itself; therefore, you'll end up binding the input's value to an undefined property.
link: function (scope, element, attrs, ngModelCtrl) {
if (!ngModelCtrl) {
return;
}
...
$compile(element)(scope); // <== here
You need to update your model in setTimout() like below as you have an isolated scope in the directive.
setTimeout(function () {
scope.$apply(function () {
scope.location3 = 'Your selected value'
});
}, 2000);
Alternatively you can also utilize $timeout service to achieve the same result.

Value of ng-model undefined in custom directive

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.

$watch ngModel from inside directive using isolate scope

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

Categories

Resources