angularjs validators not working when used with another directive - javascript

I have a "uniqueCheck" directive which checks if the value is already present in a list or not and accordingly validates the ngModel. This directive when used on say an input tag works as expected but when used on a directive which renders an input tag the result is not as expected.
The validator function inside the directive is getting called but it doesn't validate or invalidate the ngModel of the input.
You can view the complete code of the directives on the plnkr link provided
Plnkr Link : plnkr
html is as follows :
<--! when used with a directive -->
<my-wrapper ng-model="values.abc" unique-check="" list="list" prop="name"> </my-wrapper>
<--! when used on an input tag-->
<div ng-form="myform">
<input type="text" unique-check
list="list" prop="name"
name="myfield"
ng-model="values.pqr"/>
<span>isDuplicate:{{myform.myfield.$error.isDuplicate}}</span>
</div>

You're creating 2 separate ngModel instances, that are both updated when the input's changed.
The first is created by the <input> itself, which is the one assigned to 'myform'. This is the one that the <span> error message within my-wrapper is bound too.
The second one is the one created by the my-wrapper directive - which is the one that has the validator attached to it.
If you check the console (for the plnkr below) and inspect the values being output by the validator when the input is changed, you can see that the ngModel associated with the validator, is not the same ngModel that's associated with the form. But that both are actually being updated when the input's changed.
Clear the console once the page has loaded and then check the output when you change the first input.
http://plnkr.co/edit/nz6ODOVpn6lJlb055Svs?p=preview
Why is this happening?
Because both ng-model directives get passed the same string ('values.abc'), which are then evaluated against scope to determine which object property they should watch and update - i.e two way binding.
So when you change the input you're changing the value of scope.values.abc through the inputs ngModel instance. This change is picked up by the my-wrapper ngModelinstance - as it's watching the same object property - that then validates itself.
You can't solve the problem in this way, as the ngModel directive expects a string, not another ngModelinstance.
Solution
You could transfer the attributes from my-wrapper to the input at compile:
app.directive("myWrapper", function(){
var templateFn = function(element, attrs){
return '<div ng-form="myform">'+
'<input type="text" name="myfield"/>'+
'<span>(inside directive) : isDuplicate:{{myform.myfield.$error.isDuplicate}}</span>'
'</div>';
}
return {
restrict :'E',
template : templateFn,
require: 'ngModel',
scope: true,
compile: function(element, attrs) {
var attr;
angular.forEach(element.find('input'), function(elem) {
elem = angular.element(elem)
for(attr in attrs.$attr) {
elem.attr(attrs.$attr[attr], attrs[attr]);
}
});
for(attr in attrs.$attr) {
element.removeAttr(attrs.$attr[attr]);
}
}
}
});
http://plnkr.co/edit/m2TV4BZKuyHz3JuLjHrY?p=preview

Dont use scope in your myWrapper directive, it creates a separate scope of variables. Also, you need to use element.ngModel, not just a string 'ngModel' as the ng-model.
Change your myWrapper directive like this to work:
app.directive("myWrapper", function(){
var templateFn = function(scope, element, attrs){
return '<div ng-form="myform">'+
'<input type="text" name="myfield" ng-model="'+element.ngModel+'"/>'+
'<span>isDuplicate:{{myform.myfield.$error.isDuplicate}}</span>'
'</div>';
}
return {
restrict :'E',
template : templateFn,
//require: 'ngModel',
//scope: {'ngModel' : '='}
}
});

Related

Angular ngMessages - Accessing Multiple Form Values

I am trying to add a custom validation function to Angular's ngMessages.
Specifically, I want the value of a few inputs (the number of inputs will be dynamic, but for now stick with 2) to total 100.
I have created a new directive called totalOneHundred which is triggering on a form change, but I cannot figure out how to access other form values from the link: call back.
I have posted my code below. Is there something I am missing? Also, if there is a better way to accomplish this (a sum() function in the controller and an ng-show, for example) please call me out.
Thanks for your help.
The form:
<!-- input box to be validated -->
<input type="number" class="form-control" name="lowBound" ng-model="ctrl.lowBound" total-one-hundred required>
<!-- validation messages -->
<div ng-messages="form['lowBound'].$error" role="alert">
<div ng-message="required">Cannot be empty</div>
<div ng-message="totalOneHundred">Sum of tasks must = 100</div>
</div>
<!-- input box to be validated -->
<input type="number" class="form-control" name="highBound" ng-model="ctrl.highBound" total-one-hundred required>
<!-- validation messages -->
<div ng-messages="form['highBound'].$error" role="alert">
<div ng-message="required">Cannot be empty</div>
<div ng-message="totalOneHundred">Sum of tasks must = 100</div>
</div>
The directive:
return {
restrict: "A",
require: ["^^form", "ngModel"],
link: function(scope, element, attributes, controllers) {
// At first, form is assigned the actual form controller...
const form = controllers[0];
const model = controllers[1];
model.$validators.totalOneHundred = function (modelValue, form, element, scope) {
// however, the value of form here is "1".
// modelValue is the value of the triggering input,
// but how can I access the other form inputs?
return true;
};
}
};
Initially I took your code and implemented this fiddle. A sum() method of the parent controller calculates the total (simple in the fiddle, but since the parent controller knows the entire, dynamic model, it is doable in the real case too). The total-one-hundred takes the sum as argument, i.e.:
<input type="number" class="form-control" name="lowBound" ng-model="ctrl.lowBound"
total-one-hundred="ctrl.sum()" required />
Alas, it doesn't work correctly! Problem: each input displays the "Sum of tasks must = 100" error. If you change a field and the total becomes correct, that field becomes valid and stops displaying the message. But the other fields do not!
EDIT: Well, it can work even this way. The secret is to add a watch on the sum for each validation directive and re-apply validation on that field; the new link function:
link: function(scope, element, attributes, controllers) {
const model = controllers[0];
var totalEvaluator = $parse(attributes['totalOneHundred']);
scope.$watch(totalEvaluator, function(newval, oldval) {
if( newval !== oldval ) {
model.$validate();
}
})
model.$validators.totalOneHundred = function (modelValue) {
return totalEvaluator(scope) === 100;
};
}
(NOTE that this costs an extra watch per field!)
Now however, the sum() function (which may potentially be expensive) is called many times. Watching the inputs of this function and calling it only when they change, may improve the situation.
An updated fiddle: https://jsfiddle.net/m8ae0jea/1/ (I still prefer model validation -see last paragraph- but it is good to be aware of all alternatives and their side-effects.)
This is a conceptual problem with cross-field validations. Where does the validation belong? If you can refactor your model so what gets validated is an entire object, then you can use Angular's custom controls as in this fiddle.
Now the model looks like:
this.model = {
lowBound: <a number>,
highBound: <a number>
};
And there is an editor for the entire model, complete with its own messages:
<model-editor name="entireModel" ng-model="ctrl.model" form="form"
total-one-hundred="ctrl.sum()"></model-editor>
<div ng-messages="form['entireModel'].$error" role="alert">
<div ng-message="totalOneHundred">Sum of tasks must = 100</div>
</div>
As you can see the total validation applies to the entire model.
The second example works correctly, if you can live with just a single message for the entire "total" validation. But I do not like it...
Angular's validation is (IMHO) a quick and dirty solution suited for simple things. Say a field must not be empty, another field must comply with a regular expression and so on. For complex things (like this case) I find it inappropriate to define business logic in the view. I prefer doing model validation and binding the validation results with Angular. To that extent, I created egkyron which is well suited for such things.

AngularJS validate other field when this field changes

So, I've got a select menu with some options, and a text field that takes in a number. I need the text field to have certain ranges depending on the value of the select menu, which I have all set up with a custom validation. What I can't figure out is, how do I force that validation to run when the option of the select menu changes?
I tried directly in the HTML to do:
<select ng-change="$scope.my_form.numberField.$validate()">
This didn't work, so I created a created a function in the controller to run:
$scope.myFunction = function(){
$scope.my_form.numberField.$validate();
}
Then changed the select to
<select ng-change="myFunction()">
I then tried changing the function code to:
console.log( $scope.my_form.numberField.$validate() );
This is spits out an "undefined"
The HTML text field looks like this:
<input type="text" name="numberField" ng-model="numberField" number-validate>
I really have no idea where else to go from here. The validation works perfectly as I update the field directly, but I can't figure out how to get it to update when the select field changes. Any ideas?
Here is my current solution. I still feel like calling $validation() should work and that I missed something, so if anyone knows what the problem with that is, please share.
I changed my directive to add a $watchCollection and included both of the fields that I wanted to track.
Originally I had something like this:
App.directive('numberValidate', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
ctrl.$parsers.unshift(function(viewValue) {
var selectField = scope.my_form.selectField
I changed that to something like this:
App.directive('numberValidate', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
scope.$watchCollection('[my_form.numberField, my_form.selectField]', function (data) {
var numberField = data[0];
var selectField = data[1];
This still requires having number-validate in the html:
<input type="text" name="numberField" ng-model="my_form.numberField" number-validate>
I'm fairly new to Angular, so I'm not sure if this is the best option, but it's working right now.

ngModel - How to deal with its different behavior in different browsers?

I'm trying to deal with different behavior of ngModel in different browsers.
My directive wraps jqueryUI autocomplete and on its select event it calls ngModel.$setViewValue(selectedItem.id). Autocomplete allows user to select item by mouse click or by pressing enter on the keyboard.
If suggested item is:
{
"name": "Apple",
"id": "1000"
}
I expect after selecting it, the ngModel value will be selected item's id - 1000.
In Chrome it works OK - it sets $viewValue and $modelValue correctly ($modelValue=1000).
In Firefox it sets model as in Chrome ($modelValue=1000), but when I click somewhere else - make blur (then browser probably fires change event), model changes and it becomes same as visible input value ($modelValue='Apple').
In IE 11 it sets model correct only when I select item with mouse click. If I select it by pressing enter, the model becomes visible input value ($modelValue='Apple')
Here is plunkr: http://plnkr.co/edit/o2Jkgprf8EakGqnpu22Y?p=preview
I'd like to reach the same behavior in every browser. How to deal with that problems?
This seems to be related to http://bugs.jqueryui.com/ticket/8878
As pointed out in the above link, the change event is triggered only in Firefox and not in Chrome. So in your case, $setViewValue is again triggered when clicked outside and the model value is set to "Apple".
There is change callback for autocomplete jquery ui widget. To handle both the case/browsers may be you would have to explicitly set view value again on this call back (and it works).
http://plnkr.co/edit/GFxhzwieBJTSL8zjSPSZ?p=preview
link: function(scope, elem, attrs, ngModel) {
elem.on('change', function(){
// This will not be printed in Chrome and only on firefox
console.log('change');
});
select: function(event, ui) {
ngModel.$setViewValue(ui.item.data.id);
scope.$apply();
},
// To handle firefox browser were change event is triggered
// when clicked outside/blur
change: function(event, ui) {
ngModel.$setViewValue(ui.item.data.id);
scope.$apply();
}
Ok, I think I've made it. The solution is based on Yoshi's comment and it uses local model to keep selected data.
When user selects something, local model is set to selected Object and $viewValue is set to the text value of selected item. Then parser sets id property of local model as $modelValue.
select: function(event, ui) {
if(ui.item && ui.item.data){
model = ui.item.data
ngModel.$setViewValue(model.name);
scope.$apply();
}
}
ngModel.$parsers.push(function(value) {
if(_.isObject(model) && value!==model.name){
model = value;
return model;
}
return model.id;
});
Parser function do also one important thing. Because it's run when user type something or on the change event (that was the problem in firefox!), it checks if the value is same as current local model's text value, and if not it changes local model into this value. It means that if parser function is run by change event value would be the same as text value, so $modelValue is not changed, but if user type something model is updated to the typed value (it becomes String).
Validator function checks if local model is an Object. If not it means that field is invalid so by default its $modelValue disappears.
Here is the plunkr:
http://plnkr.co/edit/2ZkXFvgLIwDljfJoyeJ1?p=preview
(In formatter function I return that what comes, so $viewValue is temporarily an Object but then in $render method I call $setViewValue to set $viewValue and $modelValue correctly, so it becomes String. I heard $setViewValue should not be run in $render method, but I don't see other way to set correct $modelValue when something comes from outside).
I had similiar fights with the ngModelController and $setViewValue.
Eventually I looked for alternative solutions.
One approach that I found which worked pretty well was to create a new element as component directive which includes the input tag as a transcluded element.
app.directive('fruitAutocomplete', function($http) {
return {
restrict: 'E',
require: 'ngModel',
transclude: true,
template: '<ng-transclude></ng-transclude>',
link: function(scope, elem, attrs, ngModelController) {
var $input = elem.find('input');
$input.autocomplete({
...
});
}
}
})
In the HTML:
<fruit-autocomplete name="fruit" ng-model="model.fruit">
<input ng-disabled="inputDisabled" placeholder="input fruit"/>
</fruit-autocomplete>
Here is a working Plunker
With this proposed solution you can isolate the ngModelController and the jQueryUI modal interplay to its own custom element and it does not interfere with the "normal" <input> tag and you are not concerned by the jQueryUI bug.
By using the <input> tag as transcluded element you can still benefit from most the Angular input goodies like ng-disabled, placeholder, etc...

Angularjs input field focus event?

i use angularjs and i have created a normal input field like this:
<input type="text" style="border: none" ng-model="model" >
i want do the following:
if someone clicks in the input field, i want call for example method A. Then he writes text in this field and if the person clicks somewhere in my page so that the input field is no longer focused it should call method B. is this possible with angularjs ? if yes, how can i do that ?
ng-focus is only active at the input event but not at the output..
i want lock something with this, method A sets only a value to true and method B sets the same value to false. but i must know when the input field is active and when not.
You are looking at ng-focus and ng-blur.
<input type="text" style="border: none" ng-model="model" ng-focus="A()" ng-blur="B()">
On a side note, use css classes instead of inline styles.. :)
Or just call the same method with argument and set the value acc:-
<input type="text" style="border: none" ng-model="model" ng-focus="A(true)" ng-blur="A(false)">
If you are using functionality that you may wish to apply to fields throughout your application, you could put the it into a directive.
Here is an example that adds and removes a css class based on the focus or blur of a field:
angular.module('myApp').directive('inputFocus', function () {
var FOCUS_CLASS = 'input-focused';
return {
restrict: 'A',
priority: 1,
require: 'ngModel',
link: function (scope, element, attrs, ctrl) {
element.bind('focus',function () {
element.parent().addClass(FOCUS_CLASS);
}).bind('blur', function () {
element.parent().removeClass(FOCUS_CLASS);
});
}
};
});
You can bind method B to angular's ng-blur directive to detect when an input loses focus
<input type='text' ng-focus='methodA()' ng-blur='methodB()' ng-model='model'>

How to create an AngularJS directive that binds to a variable?

I want to build a directive for showing datepicker textboxes, i.e regular textboxes which have JQuery UI's datepicker used on them, so when the user clicks them, a datepicker box opens to let them pick the date, etc.
I want to bind this directive somehow to a property on my scope. E.g if it were a normal textbox and I did ng-model='myDate' then $scope.myDate would be updated if the user typed in a new date. In the same way, I want to bind this field from the directive so when the user picks a date, it updates the scope property its bound to.
The problem is, I want to display the directive using something like this:
<datepicker name='something' value='2013-07-20' model='myProperty' />
And have the directive replace it with the <input type="text" /> etc. So I can't use ng-model.
How else can I bind the model property to the directive so that it updates whenever the user changes it?
See if this is what you want:
HTML
<div ng-app="app" ng-controller="Ctrl">
<foo model="property"></foo>
<input type="text" ng-model="property">
</div>
Javascript
angular.module('app', [])
.directive('foo', function() {
return {
restrict: 'E',
replace: true,
scope: { model: '=' },
template: '<input type="text" ng-model="model">'
};
})
.controller('Ctrl', function($scope) {
$scope.property = 'Foobar';
});
jsFiddle
In order to use ng-model instead of model, you'll need to wrap the input in a container tag. Here's another jsFiddle script that illustrates it.
Finally, there's a date picker control in Angular UI Bootstrap. Perhaps it already does what you need.

Categories