I have made an attempt to create a custom validation directive to validate two items simultaneously at server side.
I have two values regNumber and regDate that have to be validated along with each other. So when user enters both of them correctly,they are validate. But, if one of them is entered incorrectly, they both have to be invalidated.
To accomplish the goal, I have written a the following directive base on this post.
Everything is working fine except When I enter both together regNumber and regDate incorrectly. Then, even If I change them both to correct values, still they are invalidated.
By "Everythin is working fine" I mean when I enter an invalid value for regNumber and a valid value for regDate, and change the regNumber back to a valid value, it works fine and vice versa(first regDate and then regNumber).
I think $setValidatity will be set for the latest input which has been changed and not for both of them but even if my guess is true, I don't know how to solve it. :D
Directive:
osiApp.directive('uniqueOrder', function ($http, $rootScope) {
var toId;
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elem, attr, ctrl) {
scope.$watch(attr.ngModel, function (value) {
if (scope.osiRequest.regDate) {
ctrl.$setValidity('uniqueOrder', true);
if (toId) clearTimeout(toId);
toId = setTimeout(function () {
$http({
method: 'GET',
url: $rootScope.baseAddress + '/ValidateOrderRegistrationNumber/Get',
params: {
orderRegistrationDate: scope.osiRequest.regDate ,
orderRegistrationNumber: scope.osiRequest.regNumber
}
}).success(function (isValid) {
ctrl.$setValidity('uniqueOrder', isValid);
});
}, 200);
}
});
}
}
});
HTML:
<div class="form-group" ng-class="myForm.regNumber.$error.uniqueOrder ||
myForm.regDate.$error.uniqueOrder ? 'has-error' : ''">
<input class="form-control"
name="regNumber" ng-model="osiRequest.regNumber" unique-order>
</div>
<div class="form-group" ng-class="myForm.regNumber.$error.uniqueOrder ||
myForm.regDate.$error.uniqueOrder ? 'has-error' : ''">
<input class="form-control"
name="regDate" ng-model="osiRequest.regDate " unique-order>
</div>
As mentioned by #NicolasMoise you have to be able to access both model in your directive, few ways to achieve this :
Adding both ng-model osiRequest.regDate & osiRequest.regNumber as your directive attributes unique-order. Check "scope" parameter for directive.
Access parent controller scope from directive by using $parent ( not ideal )
Related
I'm trying to make a field on a form valid by using the $valid class. I have the below HTML and JS code but even though the JS seems to work by modifying code editors, it doesn't validate the HTML. It doesn't seem to make it so $valid does anything but become true if the field has any text in it.
<input name="user" ng-model="user" ng-controller="SearchController" placeholder="User ID" required><br>
<span style="color:red" ng-show="searchForm.user.$dirty && searchForm.user.$invalid">A valid user ID is required.</span>
and
var searchApp = angular.module('searchApp', []);
searchApp.controller('SearchController', ['$scope', function ($scope) {
var users = ['11', '22', '33', '44']
return {
require: 'ngModel',
link: function (scope, element, attr, mCtrl) {
function idValidation(value) {
if (users.includes(value)) {
mCtrl.$setValidity('charE', true);
} else {
mCtrl.$setValidity('charE', false);
}
return value;
}
mCtrl.$parsers.push(idValidation);
}
};
}]);
The JS logic seems to work, but the HTML is not working correctly.
So you have a few minor errors in your code's logic to make this function correctly.
What you are attempting to do is create a custom validator. Your directive is using a $parser function, which is intended to parse a model's value for display. What you really want to be doing is working with a $validators function.
So, you would want to do something along the following:
var searchApp = angular.module('searchApp', []);
searchApp.directive('validUser', ['$scope', function ($scope) {
var users = ['11', '22', '33', '44']
return {
require: 'ngModel',
link: function (scope, element, attr, mCtrl) {
mCtrl.$validators.charE = function (value) {
return users.includes(value)
}
}
};
}]);
What happens is anytime the ngModel changes, it will run the value through the validators and determine if they are valid or invalid and automatically set ng-invalid-{name} or ng-valid-{name} (in this example, ng-invalid-charE and ng-valid-charE).
You can than use ngMessages to define your errors and they will show and hide as appropriate based on the validity.
Your ngController should more likely be a directive that adds the validator and your controller to contain the entire Search functionality/form HTML and be above your input to define your scope. Changing it to a directive would mean you'd remove ngController from the input and add an attribute for valid-user (as I've updated the name).
I have this custom validation directive:
/**
* Overwrites default url validation using Django's URL validator
* Original source: http://stackoverflow.com/questions/21138574/overwriting-the-angularjs-url-validator
*/
angular.module('dmn.vcInputUrl', [])
.directive('vcUrl', function() {
// Match Django's URL validator, which allows schemeless urls.
var URL_REGEXP = /^((?:http|ftp)s?:\/\/)(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|localhost|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?::\d+)?(?:\/?|[\/?]\S+)$/i;
var validator = function(value) {
if (!URL_REGEXP.test(value) && URL_REGEXP.test('http://' + value)) {
return 'http://' + value;
} else {
return value;
}
}
return {
require: '?ngModel',
link: function link(scope, element, attrs, ngModel) {
function allowSchemelessUrls() {
// Silently prefixes schemeless URLs with 'http://' when converting a view value to model value.
ngModel.$parsers.unshift(validator);
ngModel.$validators.url = function(value) {
return ngModel.$isEmpty(value) || URL_REGEXP.test(value);
};
}
if (ngModel && attrs.type === 'url') {
allowSchemelessUrls();
}
}
};
});
It works fine when you 'dirty' the input by typing or pasting, but I need it to run this validation, overwriting the default type="url" validation when the value is initially set in the ngModel.
I've tried adding ngModel.$formatters.unshift(validator); but it results in the 'http://' being added to input, which I need to avoid as user's changes are manually approved and it would be a waste of time to approve the addition of 'http://'.
Any help would be appreciated!
Set ng-model-options on the input type field, for example:
<input type="text"
ng-model-options="{ updateOn: 'default', debounce: {'default': 0} }"</input>
This will ensure your validator gets fired "when the value is initially set in the ngModel", as you have stated in the question.
See detailed AngularJs documentaion on ngModelOptions:enter link description here
validation of Url :
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<form name="form">
URL: <input type="url" ng-model="url.text" placeholder="Enter Link" name="fb_link"></input>
<span class="error" ng-show="form.fb_link.$error.url"></span>
</form>
Here's a jsfiddle example of what I'm trying to accomplish.
I'm trying to build a US phone number input where the view displays as (333) 555-1212, but the model binds to the numeric integer 3335551212.
My intention is to add custom validators to NgModelController which is why I have require: ng-model; there are simpler solutions without the isolate scope and NgModelController, but I need both.
You'll see an immediate error in the console: Error: Multiple directives [ngModel, ngModel] asking for 'ngModel' controller on: <input ng-model="user.mobile numeric" name="telephone" type="tel"> -- thought I was using an isolate scope here...
Thank you for looking #mimir137 but I appear to have solved it:
http://jsfiddle.net/hr121r18/8/
The directive was using replace: true, which ends up with this structure:
<form ng-controller="FooCtrl" class="ng-scope">
<p>Enter US phone number</p>
<input ng-model="user.mobile numeric" name="telephone" type="tel">
</form>
Both the template and the markup called for ng-model which led to the symptomatic error in the problem description. Once I removed that, it leads to this markup (note the wrapper element phone-number):
<form ng-controller="FooCtrl" class="ng-valid ng-scope ng-dirty ng-valid-parse" abineguid="BC0D9644F7434BBF80094FF6ABDF4418">
<p>Enter US phone number</p>
<phone-number ng-model="user.mobile" class="ng-untouched ng-valid ng-isolate-scope ng-dirty ng-valid-parse">
<input ng-model="numeric" name="telephone" type="tel" class="ng-valid ng-dirty ng-touched">
</phone-number>
</form>
But removing this required changes to $render; the elem passed into the link function is now phone-number and so you need to dig to grab the input inside it and set the value on that:
ngModel.$render = function () {
elem.find('input').val($filter('phonenumber')(ngModel.$viewValue));
};
There were a few other issues. $render() also needed to be called from the watcher.
Final:
var app = angular.module('myApp', []);
// i want to bind user.mobile to the numeric version of the number, e.g. 3335551212, but
// display it in a formatted version of a us phone number (333) 555-1212
// i am trying to make the directive's scope.numeric to have two-way binding with the controller's
// $scope.user.mobile (using isolate scope, etc.).
app.controller('FooCtrl', function ($scope) {
$scope.user = {
mobile: 3335551212
};
});
app.directive('phoneNumber', ['$filter', function ($filter) {
return {
restrict: 'E',
template: '<input ng-model="numeric" name="telephone" type="tel">',
require: 'ngModel',
scope: {
numeric: '=ngModel'
},
link: function (scope, elem, attrs, ngModel) {
// update $viewValue on model change
scope.$watch('numeric', function () {
ngModel.$setViewValue(scope.numeric);
ngModel.$render();
});
// $modelValue convert to $viewValue as (999) 999-9999
ngModel.$formatters.push(function (modelValue) {
return $filter('phonenumber')(String(modelValue).replace(/[^0-9]+/, ''));
});
// $viewValue back to model
ngModel.$parsers.push(function (viewValue) {
var n = viewValue;
if (angular.isString(n)) {
n = parseInt(n.replace(/[^0-9]+/g, ''));
}
return n;
});
// render $viewValue through filter
ngModel.$render = function () {
elem.find('input').val($filter('phonenumber')(ngModel.$viewValue));
};
}
};
}]);
app.filter('phonenumber', function () {
return function (number) {
if (!number) {
return '';
}
number = String(number);
var formattedNumber = number;
var c = (number[0] === '1') ? '1 ' : '';
number = number[0] === '1' ? number.slice(1) : number;
var area = number.substring(0, 3),
exchange = number.substring(3, 6),
subscriber = number.substring(6, 10);
if (exchange) {
formattedNumber = (c + '(' + area + ') ' + exchange);
}
if (subscriber) {
formattedNumber += ('-' + subscriber);
}
return formattedNumber;
}
});
HTML
<form ng-controller="FooCtrl">
<p>Enter US phone number</p>
<phone-number ng-model='user.mobile'></phone-number>
</form>
I created this fiddle that gets rid of most of your errors coming up in the console. Hopefully this will at least be able to put you on the right track.
I changed the template so that you can see that the filter is actually working.
It now has the typical {{ngModel | FilterName}} in plain text underneath the textbox.
The only real issue is displaying it in the textbox. I'm sure you will have no problem with that. I will check in the morning just in case you still have questions regarding this.
Edit: Alright it appears you have solved it already. Great job!
I am trying to wrap an input and some markup into an AngularJS directive.
However the fields are supposedly always validated which they shouldn't be.
Please check out my example # http://plnkr.co/edit/TivmuqQI4Y5K56gwcadW
here is the code for those who do not wish to look at Plunker
my directive
app.directive('myInput', function() {
return {
restrict: 'E',
require: '^ngModel',
templateUrl: 'form_control.html',
scope: {
label: '#',
placeholder: '#',
name: '#',
form: '=',
ngModel: '=ngModel'
},
}
});
this is my template
<div class="form-group" ng-class="{'has-error': form.{{name}}.$invalid && form.{{name}}.$dirty, 'has-success': form.{{name}}.$valid }">
<label for="{{name}}" class="col-sm-2 control-label">{{label}}</label>
<div class="col-sm-10">
<input type="{{type}}" class="form-control col-sm-10" id="{{name}}" name="{{name}}" ng-model="ngModel" placeholder="{{placeholder}}" ng-maxlength="10" ng-required="true">
</div>
</div>
and this is in my index.html
<my-input ng-model="website.url" name="url" label="URL" placeholder="http://example.com" form="form"></my-input>
Even though the input inside the template is required, the field is validated, which it shouldn't be if it's empty.
What am I doing wrong?
Required fields are immediately invalid on DOM load, as they are not valid until they have a value.
Also, in your plunker the top input is green, with an illusion of valid while the lower input is red showing invalid as it should.
You can't do this...
<div class="form-group" ng-class="{'has-error': form.{{name}}.$invalid && form.{{name}}.$dirty, 'has-success': form.{{name}}.$valid }">
The issue is with the form name field. When you do this...
<input type="{{type}}" class="form-control col-sm-10" id="{{name}}" name="{{name}}" ng-model="ngModel" placeholder="{{placeholder}}" ng-maxlength="10" ng-required="true">
name="{{name}} will actually compile to name: {{name}} in the form object. exmaple:
{{name}}: Constructor$dirty: false
$error: Object
$formatters: Array[1]
$invalid: true
$isEmpty: function (value) {
$modelValue: undefined
$name: "{{name}}"
$parsers: Array[1]...
This was taken from your plunker. Play around with console.log($scope.form) and look at the object.
under $error you will find that there are two inputs THIS form references, and neither of them are the expected input named 'url', they are in fact the 'title' input and your directive input '{{name}}' as seen here...
form: Constructor
$addControl: function (control) {
$dirty: false
$error: Object...
$name: "title"...
$name: "{{name}}"
Here is a forked plunker where I have already set up to console.log the form
Plunker
This shows that the form has no idea about any 'url' input. To get around this, you can write a custom directive for the input name field. Or just use the one I wrote below.
app.directive('myName', function(){
var myNameError = "myName directive error: "
return {
restrict:'A', // Declares an Attributes Directive.
require: 'ngModel', // ngModelController.
link: function( scope, elem, attrs, ngModel ){
if( !ngModel ){ return } // if no ngModel exists for this element
checkInputFormat(attrs); // check myName input for proper formatting.
var inputName = attrs.myName.match('^\\w+').pop(); // match upto '/'
assignInputNameToInputModel(inputName, ngModel);
var formName = attrs.myName.match('\\w+$').pop(); // match after '/'
findForm(formName, ngModel, scope);
} // end link
} // end return
function checkInputFormat(attrs){
if( !/\w\/\w/.test(attrs.myName )){ // should be in format "wordcharacters/wordcharacters"
throw myNameError + "Formatting should be \"inputName/formName\" but is " + attrs.myName
}
}
function assignInputNameToInputModel(inputName, ngModel){
ngModel.$name = inputName // adds string to ngModel.$name variable.
}
function addInputNameToForm(formName, ngModel, scope){
scope[formName][ngModel.$name] = ngModel; return // add input name and input object.
}
function findForm(formName, ngModel, scope){
if( !scope ){ // ran out of scope before finding scope[formName]
throw myNameError + "<Form name=" + formName + "'> element could not be found."
}
if( formName in scope){ // found scope[formName]
addInputNameToForm(formName, ngModel, scope)
return
}
findForm(formName, ngModel, scope.$parent) // recursively search through $parent scopes
}
});
// DIRECTIVE NOTES:
// This directive is used by simply including the following HTML element:
// <input my-name="email/profileForm">.
// In the example above, the input name is "email" and the form name
// that this input would be attached to would be named "profileForm"
// Like this...
// <form name="profileForm">
// <input my-name="email/profileForm">
// </form>
// Notice this "/" dividing the input name from the form name. This directive uses the '/'
// to separate the input name from the form name.
// Although it has not been tested yet, this directive should work with multi nested forms
// as well, as the recursive search only looks for the form name that was passed in, inwhich
// to bind the ngModel to.
// In this example, other form names would be skipped.
// <form name="profileForm">
// <form name="miniFormOne">
// <form name="miniFormTwo">
// <input my-name="email/profileForm">
// </form>
// </form>
// </form>
// The above example may not be the best behavior, but was just added as an example.
With this directive, you could use your nested directive input like this
<input type="{{type}}" class="form-control col-sm-10" id="{{name}}" my-name="{{ variableName + '/' + yourformnamevariable }}" ng-model="ngModel" placeholder="{{placeholder}}" ng-maxlength="10" ng-required="true">
The myName directive will get this: "url/form" in your case, then it will add the $name variable which will be 'url' to the ngModel for THAT input, as well as search up scope for the form named "form" and attach the ndModel to THAT form. The directive uses the '/' to separate names. The search is recursive and will travel until it finds the form name or error out with a message.
Of course when you create dynamic input names, how are you to call out
formName.dynamicInputName.$valid? as you don't know what the dynamic input names ARE!!!
So using form.{{name}}.$invalid && form.{{name}}.$dirty doesn't work unless you do this...
form.$valid && form.$dirty
But that won't work for each individual input. You can figure out your own way to do that...
I use another directive inside the input
Then in my-errors I just listen to the ngModel like after requiring ngModel, you can now call ngModel.$invalid then set a variable to change the CSS. If I get some time, I'll make a jsFiddle and link it here at the bottom. However, you can just use the class input.ng-invalid { border-color: red; } and the inputs will be red until they are valid.
If this doesn't clear things up, ask some more questions... We'll get it.
Hope this helpy's
For my project, i'm currently developping custom form / inputs directive.
For example, I have the following directive :
angular.module('myApp').directive("textField", function() {
return {
restrict: 'E',
replace: true,
templateUrl : "/common/tpl/form/text-field.html",
scope : {
label : "#",
model : "="
}
};
});
with the associated template :
<div class="form-group">
<label for="{{fieldId}}" class="col-lg-2 control-label">{{label |translate}}</label>
<div class="col-lg-10">
<input type="text" class="form-control" id="{{fieldId}}" ng-model="model" placeholder="{{label|translate}}">
</div>
</div>
I have many more custom fields ( date, select, double select, and so on... )
The usage is simple :
<text-field label="app.myLabel" model="myObj.myAttribute"></text-field>
The idea is to cleanup the main template by avoiding to verbosely add labels on every fields. Very common need i believe.
Now the problem :
Now I need to add custom validation to my input models.
I did a naive approach which was to create a validation directive :
directive('myValidation', function (){
return {
require: 'ngModel',
link: function(scope, elem, attr, ngModel) {
ngModel.$parsers.unshift(function (value) {
// do some validation
return value;
});
}
};
});
and then to use it like this :
<text-field label="app.myLabel" model="myObj.myAttribute" myValidation="validationOptions"></text-field>
But of course this doesnt work, simple because the text-field directive which is replace=true "erases" the validation directive on it's element.
Can someone tell one what is the correct approach to do "custom input with presentation" directive, while allowing the validation part to be declared on the directive ( here text-field ) and used on the directive's input ?
Per example, is there a way to say "attributes on my element directive will be 'copied' to inside my directive ?"
aka :
<text-field label="app.myLabel" model="myObj.myAttribute" **myValidation="validationOptions"**></text-field>
would result in :
<div class="form-group">
<label for="{{fieldId}}" class="col-lg-2 control-label">{{label |translate}}</label>
<div class="col-lg-10">
<input type="text" class="form-control" id="{{fieldId}}" ng-model="model" **myValidation="validationOptions"** placeholder="{{label|translate}}">
</div>
</div>
Or am I simply missing something ?
I would like to avoid using transclusion to resolve this issue, because this would oblige the form template to look like this :
<field label="myLabel">
<input type="text" class="form-control" id="{{fieldId}}" ng-model="model" placeholder= {{label|translate}}">
</field>
which is just uselessly verbose in my opinion. But i'm starting to ask myself if there really is another option ?
Maybe the trick can be done in the pre (or post ?) directve link function, where I would copy attributes/ directive from the text-field tag to it's child (input) tag ?
Could someone just light the way for me there ?
Could you try this:
Write a validate directive. This will have a controller that exposes an addValidationFunction(fn) and a getValidationFunction() methods.
Have the myValidation directive require the validate controller and call ctrl.addValidationFunction(myValidationImplementation) where myValidationImplementation is a function implementing the validation logic for this specific directive.
Write another directive, validateInner. This will require optionally the validate controller from its parent. This directive will also require the ngModel controller. If it finds the validate controller, it calls ctrl.getValidationFunction() and registers the function with the ngModel i.e.:
require: ["^?validate", "ngModel"],
link: function(scope,el,attrs,ctrls) {
if( ctrls[0] != null ) {
var validationFn = ctrls[0].getValidationFunction();
// register validationFn with ngModel = ctrls[1]
}
...
}
In the template of your textField:
<input validate-inner type="text" class="form-control" id="{{fieldId}}" ng-model="model" placeholder="{{label|translate}}">
Usage:
<text-field label="app.myLabel" model="myObj.myAttribute"
validate my-validation="validationOptions"></text-field>
NOTE 1: I am not sure if a replace:true directive wipes the other directives. If so, it is not consistent behaviour.
NOTE 2: The myValidation directive gets called as <xxx my-validation> (note camelCase → dash-case). If your code above is not a typo, then this is why <text-field> seems to wipe myValidation.