I have a directive that makes some value prices validation, and I thought that it was working well, until I notice a bug, for example if the next value price I'm validating in the input is the same of the previous one, the next one is not validated, I don't understand why, if it was different value it worked with no issues, but if the value is the same of the previous, the directive is not fired up, I have no idea if there is any relation with the '$asyncValidators', above I leave my custom directive.
Directive:
(function() {
'use strict';
angular
.module('myApp')
.directive('priceValidator', priceValidator);
priceValidator.$inject = ['$q', 'ServerValidatorService'];
/* #ngInject */
function priceValidator($q, ServerValidatorService) {
var directive = {
link: link,
restrict: 'A',
require: 'ngModel',
scope: {
costPrice: '=',
currentPvp: '='
}
};
return directive;
function link(scope, element, attrs, ctrl) {
ctrl.$asyncValidators.costPrice = function(costPrice) {
var pvp = scope.currentPvp;
scope.currentPrice = costPrice;
var cost = String(costPrice).replace(",", ".");
return ServerValidatorService.validateCostPrice(pvp, cost)
.then(function() {
return true;
},
function(response) {
ctrl.$setDirty();
if (response.data.errors.maximum[0])
scope.costPrice = response.data.errors.maximum[0];
return $q.reject(response.data.message);
});
};
}
}
})();
// Controller function that fill form with each product info:
function getMainProduct(productLine) {
if (typeof productLine !== "undefined") {
vm.discountId = productLine.discount_id;
vm.discountPercentage = productLine.discount_percentage;
vm.valuePrice = productLine.value_price;
}
}
HTML
<input type="text" class="form-control"
name="discount" ng-model="form.discPrice"
ng-model-options="{updateOn: 'default blur', debounce: {'default': 300, 'blur': 0} }"
ng-required="formCtrl.isP()" cost-price="form.maxP"
current-pvp="form.pvp" num-type="decimal"
ng-change="formCtrl.check()" price-validator>
Related
I'm trying to implement an asynchronous validation by using a custom directive.
This is the directive
moduloArea.directive('uniqueName', function($http, $q) {
return {
require : 'ngModel',
link: function($scope, element, attrs, ngModel) {
ngModel.$asyncValidators.nombre = function(modelValue, viewValue) {
return $http.get('/checkUsernameAvailability/'+viewValue).then(
function(response) {
if (!response.data.validUsername) {
return $q.reject(response.data.errorMessage);
}
return true;
}
);
};
}
};
});
The result in console
As you can see when the root username is typed the return is an JSON object because this username is already taken.
But in the HTML the form in $invalid when the directive unique-name is inserted.
<form name="registerUsernameForm" novalidate="novalidate">
<input type="text" name="username" data-ng-model="person.userName" data-unique-name="" required="required"/>
<span data-ng-show="registerUsernameForm.username.$error.uniqueName">This username is already taken.</span>
<button type="submit" data-ng-disabled="registerUsernameForm.$invalid || registerUsernameForm.$pending" data-ng-click="registerPerson(person)"> Save Person </button>
</form>
I'm using the data-unique-name="" (="") because if I don't then thymeleaf generate the following error:
Attribute name "data-unique-name" associated with an element type
"input" must be followed by the ' = ' character.
What do you think can be wrong?
You are validating nombre not uniqueName. For this reason, uniqueName is still stay invalid.
moduloArea.directive('uniqueName', function($http, $q) {
return {
require : 'ngModel',
link: function($scope, element, attrs, ngModel) {
ngModel.$asyncValidators.uniqueName= function(modelValue, viewValue) {
var value = modelValue || viewValue;
return $http.get('/checkUsernameAvailability/'+value ).then(
function resolved(response) {
if (response.data && !response.data.validUsername) {
return $q.reject(response.data.errorMessage);
}
return true;
}, function rejected() {
//username does not exist, therefore this validation passes
return true;
}
);
};
}
};
});
something similar may have been answered (ng-pattern + ng-change) but all responses were unable to fix this issue.
I have two imbricated directives for creating a form input, a parent directive to control name, label, validator etc. and a child directive to set pattern and input type specific stuff.
However, when setting a pattern, the value on my model is set to undefined when ng-pattern return false.
Directives:
<input-wrapper ng-model="vm.customer.phone" name="phone" label="Phone number">
<input-text type="tel"></input-text>
</input-wrapper>
Generated HTML:
<label for="phone">Phone number:</label>
<input type="text" name="phone"
ng-model="value"
ng-model-options="{ updateOn: \'blur\' }"
ng-change="onChange()"
ng-pattern="/^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/">
JS:
angular.module('components', [])
.directive('inputWrapper', function() {
return {
restrict: 'E',
require: 'ngModel',
scope: true,
link: function (scope, element, attrs, ngModel) {
scope.name = attrs.name;
scope.label = attrs.label;
scope.onChange = function () {
ngModel.$setViewValue(scope.value);
};
ngModel.$render = function () {
scope.value = ngModel.$modelValue;
};
}
}
})
.directive('inputText', function() {
return {
restrict: 'E',
template: '<label for="{{name}}">{{label}}:</label><input type="text" name="{{name}}" ng-model="value" ng-model-options="{ updateOn: \'blur\' }" ng-change="onChange()" ng-pattern="pattern">',
link: function (scope, element, attrs) {
if (attrs.type === 'tel') {
scope.pattern = /^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/;
}
}
}
});
angular.module('app',['components'])
.controller('ctrl',function($scope){
var vm = this;
vm.customer = {
phone: '010203040506'
};
});
What am I doing wrong ?
Codepen for use case: https://codepen.io/Yosky/pen/yVrmvw
By default in angular if a validator fail, undefined value assigned to ng-model, You can change this setting as follow :
<div ng-model-options="{ allowInvalid: true}">
read here for detail docs
I had some requirements that meant that I really really didn't want ng-model to write out undefined to the scope when validation was invalid, and I didn't want the invalid value either, so allowInvalid didn't help. In stead I just wanted ng-model do not write anything, but I couldn't find any option for this.
So I couldn't see any way forward except for doing some monkey patching of the ng-model controller.
So first I required ngModel in the component I was building require: { model: 'ngModel' } and then I did this in the $onInit hook:
const writeModelToScope = this.model.$$writeModelToScope;
const _this = this;
this.model.$$writeModelToScope = function() {
const allowInvalid = _this.model.$options.getOption('allowInvalid');
if (!allowInvalid && _this.model.$invalid) {
return;
}
writeModelToScope.bind(this)();
};
I also didn't want to take in a new model value while the value was invalid and the component had focus, so I did:
const setModelValue = this.model.$$setModelValue;
this.model.$$setModelValue = function(modelValue) {
_this.lastModelValue = modelValue;
if (_this.model.$invalid) {
return;
}
if (_this.hasFocus) {
return;
}
setModelValue.bind(this)(modelValue);
};
element.on('focus', () => {
this.hasFocus = true;
});
element.on('blur', (event) => {
this.hasFocus = false;
const allowInvalid = this.model.$options.getOption('allowInvalid');
if (!allowInvalid && this.model.$invalid) {
this.value = this.lastModelValue;
}
event.preventDefault();
});
Feel free to judge me, just know that I already feel dirty.
I created an angular validator module for form validation without the need to include ngMessages in the form and everything is working as expected.
However I discovered a bug which I have been trying to fix. It has something to do with $compile.
So I have a directive that adds attributes to form elements and this is achieved by using $compile service however, it seems the $compile service causes an unwanted behaviour to ng-click so when ng-clicklocated inside the form is called it fires twice;
Here is the directive and what I am doing:
angular.module('app',[])
.directive('validateForm',['$compile',
function($compile)
{
var addErrors = function(rules, form, ctrls, scope, config){
//code
};
return {
restrict: 'A',
require: ['^form'],
link: {
post: function(scope, element, attrs, ctrls){
var form = ctrls[0];
var config = scope.validator;
if(typeof config != 'object') return;
var rules = config['rules'];
var errors = [];
//-----
},
pre: function(scope, element, attrs, ctrls){
var elm = element.find('select, input, textarea').attr('validate-field','');
element.removeAttr("validate-form"); //remove the attribute to avoid indefinite loop
element.removeAttr("data-validate-form");
$compile(element.contents())(scope);
}
}
};
}
])
.controller('UserController',['$scope', function($scope){
$scope.title = 'Form Validator';
$scope.clickThings = function(value){
alert(value); //pops up twice means ng-click fires twice
}
}]);
Form markup:
<div ng-controller="UserController">
<form novalidate="" validate-form name="form" role="form">
<div class="form-group">
<input type="text" class="form-control" ng-model="first_name" name="first_name" />
</div>
<div class="form-group">
<input type="text" class="form-control" ng-model="last_name" name="last_name" />
</div>
<div class="form-group">
<input type="text" class="form-control" ng-model="email" name="email" />
</div>
<div class="form-group">
<input type="password" class="form-control" ng-model="password" name="password" />
</div>
<div class="form-group">
<input type="text" class="form-control" ng-model="country" name="country" />
</div>
<a type="button" class="btn btn-success" ng-click="clickThings('Submit Clicked')">Submit</a>
</form>
</div>
I have created a plunker:
http://embed.plnkr.co/uIid4gczKxKI4rPOHqx7
After trying different things I realized ng-click which is already compile is compiled a second time when $compile(element.contents())(scope) is called.
To resolve this issue, only need to compile the altered/affected elements i.e
elem = element.find('select, input, textarea').attr('validate-field','');
by replacing $compile(element.contents())(scope) with $compile(elem)(scope);
So I ended up with this:
angular.module('app',[])
.directive('validateForm',['$compile',
function($compile)
{
var addErrors = function(rules, form, ctrls, scope, config){
//code
};
return {
restrict: 'A',
require: ['^form'],
link: {
post: function(scope, element, attrs, ctrls){
var form = ctrls[0];
var config = scope.validator;
if(typeof config != 'object') return;
var rules = config['rules'];
var errors = [];
//-----
},
pre: function(scope, element, attrs, ctrls){
var elem = element.find('select, input, textarea').attr('validate-field','');
element.removeAttr("validate-form"); //remove the attribute to avoid indefinite loop
element.removeAttr("data-validate-form");
$compile(elem)(scope);
}
}
};
}
])
.controller('UserController',['$scope', function($scope){
$scope.title = 'Form Validator';
$scope.clickThings = function(value){
alert(value); //pops up twice means ng-click fires twice
}
}]);
Working plunker here:
What is the purpose for using the $compile in a pre-link function? if u just want to do template transformation before the directive element get linked, u should put ur code in directive's compile and take out the $compile. Or if you would like to $compile your element after child elements are linked, u should put ur code in post-link. Putting $compile in pre-link will cause the child nodes of your child elements get linked twice.
Either choose:
angular.module('app',[])
.directive('validateForm',['$compile',
function($compile)
{
var addErrors = function(rules, form, ctrls, scope, config){
//code
};
return {
restrict: 'A',
require: ['^form'],
compile:function(element, attrs, ctrls){
// the code will be executed before get complied
var elm = element.find('select, input, textarea').attr('validate-field','');
return function(scope, element, attrs, ctrls){
var form = ctrls[0];
var config = scope.validator;
if(typeof config != 'object') return;
var rules = config['rules'];
var errors = [];
//-----
}
}
};
}
])
Or
angular.module('app',[])
.directive('validateForm',['$compile',
function($compile)
{
var addErrors = function(rules, form, ctrls, scope, config){
//code
};
return {
restrict: 'A',
require: ['^form'],
link: {
post: function(scope, element, attrs, ctrls){
var form = ctrls[0];
var config = scope.validator;
if(typeof config != 'object') return;
var rules = config['rules'];
var errors = [];
//-----
var elm = element.find('select, input, textarea').attr('validate-field','');
element.removeAttr("validate-form"); //remove the attribute to avoid indefinite loop
element.removeAttr("data-validate-form");
$compile(element.contents())(scope);
},
pre: function(scope, element, attrs, ctrls){
}
}
};
}
])
EDIT
Just eliminate the $compile clause will also do the trick. But these three ways have some difference.
For more information u should refer to the official document
I'm trying to unite the AngularJS validation model with the Bootstrap form validation display.
If a user loads an empty form, I don't want the form to display error message right away. I want to wait until the user interacts with the form.
If a user submit the form with required fields not filled out, I also want to display an error message.
If a user starts typing in the field, I want error messages to show up right away.
So I have to check myForm.$submitted, myForm.<fieldName>.$dirty as well as myForm.<fieldName>.$touched.
However, it makes a lot of duplicated code with very few variation.
I've tried to make a directive to fix this issue but I can't seem to find the right way to wrap this complexity away.
HTML:
<div class="form-group required" ng-class="{ 'has-error': myForm.firstname.$invalid && (myForm.firstname.$dirty || myForm.$submitted || myForm.firstname.$touched) }">
<label for="firstname" class="control-label" translate>Profile.FirstName</label>
<input type="text" class="form-control" id="firstname" name="firstname" required ng-model="vm.profile.firstName"/>
<p class="help-block" ng-if="myForm.firstname.$error.required" translate>Forms.Default.Required</p>
</div>
I want to take the whole ng-class attribute and replace it by something more succinct. The directive seemed like the way to go so tried this:
(function(){
'use strict';
angular.module('app')
.directive('hasError', [function(){
return {
restrict: 'A',
scope: {
form: '=bsForm',
control: '=bsControl'
},
link: function(scope, element){
scope.$watch('form', function(){
var isInvalid = scope.control.$invalid && scope.control.$dirty;
element.toggleClass('has-error', isInvalid);
});
}
};
}]);
})();
Usage:
<div class="form-group required" has-error bs-form="myForm" bs-control="myForm.firstname">
...
</div>
This however was not refreshing when properties of form changed.
What am I missing?
So... I managed to make a directive work properly for exactly my usage.
If there is a better way, please prove me wrong.
(function(){
'use strict';
angular.module('app')
.directive('hasError', [function(){
return {
restrict: 'A',
scope: {
form: '=bsForm',
control: '=bsControl'
},
link: function(scope, element){
scope.$watchGroup(['control.$invalid', 'control.$dirty', 'control.$touched', 'form.$submitted'], function(){
var isInvalid = scope.control.$invalid && (scope.control.$dirty || scope.form.$submitted || scope.control.$touched);
element.toggleClass('has-error', isInvalid);
});
}
};
}]);
})();
I did something like this one. My solution took a slightly different approach, but it may be helpful here (you can view the gist on Github).
Essentially, what I do is wrap all my form data inside a single object and I assign that object to a <form> attribute. I then watch that object and any time it changes, I select all elements with the ng-dirty and ng-invalid classes (this selector could be changed to whatever you like). I then loop through each of these elements and update messages for each of them.
Here's the code:
(function() {
"use strict"
angular.module('app')
.directive('formValidator', function() {
return {
require: '^form',
scope: {
formData: '=',
validateAll: '='
},
link: function(scope, element, attrs, ctrls) {
window.frm = ctrls;
var selector = '.ng-dirty.ng-invalid';
function validate() {
$(".formValidator-input-validation-error-message").remove();
element.find(selector).each(function(index, el) {
$el = $(el);
var messages = [];
var classes = $el.attr('class').match(/[\d\w-_]+/g);
for (var i in classes) {
var lastIndex = classes[i].lastIndexOf('-invalid-');
if (lastIndex != -1) {
var validationMessageAttr = "data-" + classes[i].substr(lastIndex + 9) + "-validation-message";
var msg = $el.attr(validationMessageAttr);
if (!msg) {
msg = element.attr(validationMessageAttr);
if (!msg) {
msg = "Invalid!";
}
}
messages.push("<div class='validator'>" + msg + "</div>");
}
}
$(el).after("<div style='position:absolute;' class='formValidator-input-validation-error-message'>" + messages.join() + "</div>");
});
}
scope.$watch(function() {
return scope.formData;
}, function() {
validate();
}, true);
scope.$watch('validateAll', function(newValue, oldValue) {
selector = !!newValue ? '.ng-invalid' : '.ng-dirty.ng-invalid';
validate();
});
}
};
})
})();
i have this directive
angular.module('tutors.components')
.directive('formValidator', function() {
return {
restrict: 'A'
link: function (scope, element, attrs) {
function nameValidator (input) {
var regExp : "/^[a-zA-Z']*$/";
var validator : regExp.test(input);
return validator;
}
};
});
And this html
<input form-Validator
type="text"
required
name="firstName"
class="form-control"
ng-maxlength="20"
placeholder="First name"
ng-model="user.firstName"
ng-disabled="vm.isLoading"
tooltip="{{requiredMsg}}"
tooltip-placement="top"
tooltip-trigger="focus"
tooltip-enable="signupForm.firstName.$error.required && signupForm.firstName.$touched">
and i want to run the directive so it validates the text entered in the input, i have tried using an ng-pattern like this ng-pattern="/[a-zA-Z]+/" but that didn't work (i still prefer using it over the directive)
Your directive should be be having small case form-validator to get call to the directive by compiler.
Additionally you need to change your nameValidator function code to below.
.directive('formValidator', function() {
return {
restrict: 'A',
require: 'ngModel', //to get ngModelController
link: function(scope, element, attrs, ngModel) {
function nameValidator(input) {
var regExp = "/^[a-zA-Z]$/";
var validator = regExp.test(input);
return validator;
}
ngModel.$validators.push(nameValidator); //should pushed to validators to validate
}
}
})
OR ng-pattern should be fine, only it should have ^(start) & $(end)
ng-pattern="/^[a-zA-Z]*$/"
Demo Plunkr