Weird behavior with directives and angular form validation - angularjs

I'm trying to create a custom directive to help me creating bootstrap forms and dealing with form validation using angular.
I've come up so far with the following directive:
directive('formGroup', ['$compile',function ($compile) {
var linker = function (scope, element, attrs) {
var fieldName = attrs.formGroup;
var formName = attrs.form;
if (formName === undefined || formName === null) {
//find form name
formName = $(element).closest('form').attr('name');
if (formName === undefined || formName === null) {
console.error('Could not find form name for error ' + fieldName);
}
}
var showErrorOn = formName + '.' + fieldName + '.$invalid';
$(element).addClass('form-group');
$(element).attr('ng-class', '{ \'has-error\' : ' + showErrorOn + ' }');
$(element).removeAttr('form-group');
$compile(element)(scope);
}
return {
restrict: 'A',
link: linker
}}]);
To be used with the following HTML:
<form name="testForm" novalidate>
<div class="row">
<div class="col-md-6" form-group="email">
<label>Email</label>
<input type="email" class="form-control" name="email" ng-model="email" required />
</div>
</div>
</form>
The point of the directive is a couple of things:
Finding the form name so I don't have to type it again for every field.
Add the class "form-group".
Add the class "has-error" whenever the field is invalid.
Everything seems to work just fine until I type a valid e-mail address and add a dot to the end (making the input invalid). After I do that, the input automatically clears out but I can't understand why.
Here's a fiddle: http://jsfiddle.net/kgwocdyu/2/
To reproduce type: "a#email" then type "." and the input will go blank.

Related

ngTagsInput with autocomplete in a form and add to db

I've been trying to use ngTagsInput with its Autocomplete feature (mbenford.github.io/ngTagsInput/demos) in a html form to submit recipes in Mongodb.
Basically, I'm using ngTagsInput with Autocomplete to query my ingredients db and display ingredients tags in the 'Ingredients in the recipe'.
It works fine, up until I save the recipe, and the ingredients are not saved.
I know where the problem is but I haven't found a solution yet.
Here is the 'ingredients' field of my add recipe page without ngTagsInput, just a normal text field:
<div class="form-group">
<label for="ingredients">List of ingredients</label>
<input type="text" class="form-control" id="ingredients" ng-model="form.ingredients">
</div>
And here is the 'ingredients' field using ngTagsInput (Working fine, but not saving):
<div class="form-group" ng-controller="recipeApiController">
<tags-input for="Ingredients" id="Ingredients" ng-model="tags" display-property="ingredientID" placeholder="Commence à taper le nom d'un ingrédient..." add-from-autocomplete-only="true">
<auto-complete source="loadTags($query)"></auto-complete>
</tags-input>
</div>
Because I'm replacing ng-model="form.ingredients" with ng-model="tags" required to use ngTagsInput, those ingredient tags are not saved when clicking my "Add recipe" button.
Here is the "save to db" part of my recipeApiController, used on the "add recipe" form page:
$scope.addToDatabase = function(){
RecipeApi.Recipe.save({}, $scope.form,
function(data){
$scope.recipe.push(data);
},
function(err){
bootbox.alert('Error: ' + err);
});
}
Do you have any idea how I could fix that, and save those tags?
Thanks in advance guys. I didn't want this post to be too long but if you need more info, code, I'll be super reactive to provide it. This would help me greatly.
I found a solution in another post:
https://stackoverflow.com/a/38383917/6858949
Basically, I couldn't get those tags to save because ng-model didn't work inside the <tags-input> tags. I therefore used this guy's directive to change the <tags-input> to an attribute:
<div elem-as-attr="tags-input"></div>
Here's the directive code:
app.directive('elemAsAttr', function($compile) {
return {
restrict: 'A',
require: '?ngModel',
replace: true,
scope: true,
compile: function(tElement, tAttrs) {
return function($scope) {
var attrs = tElement[0].attributes;
var attrsText = '';
for (var i=0; i < attrs.length; i++) {
var attr = attrs.item(i);
if (attr.nodeName === "elem-as-attr") {
continue;
}
attrsText += " " + attr.nodeName + "='" + attr.nodeValue + "'";
}
var hasModel = $(tElement)[0].hasAttribute("ng-model");
var innerHtml = $(tElement)[0].innerHTML;
var html = '<' + tAttrs.elemAsAttr + attrsText + '>' + innerHtml + '</' + tAttrs.elemAsAttr + '>';
var e = hasModel ? $compile(html)($scope) : html;
$(tElement).replaceWith(e);
};
}
}
});
I don't think this is optimal, but with my current knowledge of code, I'm thankful I found this solution. ✌🏼
EDIT:
I am now using ui-select: https://github.com/angular-ui/ui-select
And definitely recommend it
EDIT:
I put the code in the code box

ng-messages default error message

Is it possible to have ng-messages + ng-message display a default error message?
For instance:
<form name="myForm">
<input type="number" name="number" min="10" max="100" required ng-model="number" />
</form>
<div ng-messages="myForm.number.$error">
<div ng-message="required">Number is required</div>
<div ng-message>There's something wrong with number!</div>
</div>
I would like "There's something wrong with number!" to show up if there's any error besides required.
Is that possible?
<form name="myForm">
<input type="number" name="number" min="10" max="100" required ng-model="number" />
</form>
<div ng-show="myForm.number.$error">
<div ng-show="myForm.number.$error.required">Number is required</div>
<div ng-show="!myForm.number.$error.required && myForm.number.$error" >There's something wrong with number!</div>
</div>
Plnkr Demo:
http://plnkr.co/edit/aIJ1W8qTGiKHLJYJg0fe?p=preview
I wrote a directive that accomplishes a lot of the boiler plate stuff for validations. It includes allowing for a default message:
angular.module('validations').directive('showErrors', function() {
return {
restrict: 'A',
require: '^form',
link: function(scope, el, attrs, ngModelCtrl) {
var data = el.data();
var inputName = el.attr('name');
if (!inputName)
return;
var baseFormName = ngModelCtrl.$name;
var baseFieldName = baseFormName + '.' + inputName + '.$error.';
var validationMessages = '';
var validators = {};
var defaultValidationConditions = [];
var defaultValidationMessage = '';
angular.forEach(data, function (value, key) {
if (key.indexOf('se') === 0 && key[2] === key[2].toUpperCase()) {
var validationProperty = key.substring(2).toLowerCase();
// if this is the default message, just grab it and keep looping.
if (validationProperty === 'default') {
defaultValidationMessage = value;
return;
}
var condition = baseFieldName + validationProperty;
if (!validators[value]) {
validators[value] = [];
}
validators[value].push(condition);
// if we have a message for this error, we want to exclude it from the default
defaultValidationConditions.push('!' + condition);
}
});
defaultValidationConditions.push([baseFormName, '.', inputName, '.$invalid'].join(''));
angular.forEach(validators, function (conditions, message) {
var conditionString = conditions.join(' || ');
validationMessages += '<label ng-show="' + conditionString + '" class="error">' + message + '</label>';
});
// if a default validation message was specified, we need to create a label
if (defaultValidationMessage) {
validationMessages += '<label ng-show="' + defaultValidationConditions.join(' && ') + '" class="error">' + defaultValidationMessage + '</label>';
}
if (validationMessages) {
el.after(this.$compile('<div ng-show="' + baseFormName + '.' + inputName + '.$dirty">' + validationMessages + '</div>')(scope));
}
}
}
});
It's pretty simple to use:
<form name="myForm">
<input show-errors data-se-required="Field required"
data-se-default="Field Invalid"
name="myInput" ng-minlength="10" required />
</form>
The directive will automatically generate all of the html. It uses a div and labels, but can be adapted to use ngMessages if you want. JSFiddle is being a bit wonky at the moment. I'll try and add a working link tomorrow.
ngMessageExp is what you are after. This will show the 'other' error message when there is an error, but not the 'required' error.
<ng-messages for="myForm.number.$error">
<ng-message when="required">Number is required</ng-message>
<ng-message when-exp="['min', 'max']">There's something wrong with number!</ng-message>
</ng-messages>
You will have to add each error type to the list, but you should know what validations you are using. In this case, only min and max.
If you find yourself repeating that generic catch all error message then consider using ngMessageInclude that has the when-exp for all validation types that you use.

Share validation state for multiple fields against the same validator

I will start out stating that I have searched google and SO and I have not found an answer for this specific situation. Yes, there are other posts that sound the same but are more based on a "MoreThan / LessThan" mentality. This does not following that mentality at all so please do not mark this as a duplicate referring to them.
Check out the Plunker Example
I am attempting to make sure the user does not enter an address that already exists else where on the page. To do this I need to validate all the address fields since different locations may have the same street address. I need the validator to set all the related fields to valid if any are invalid once the address has been fixed to not be a duplicate. Currently it only sets the last field modified to valid and leaves the rest as invalid.
Plunker example demonstrates what is happening. I have tried many different approaches such as iterating through all fields and setting them to prestine and untouched and then setting them to dirty and touched to trigger validations again but I am having no luck getting this working.
Validator
angular.directive('ruleFunc', ['$parse', function($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function($scope, $element, $attrs, $ngModel) {
var validatorName = $attrs.ruleName;
var validatorFunc = $attrs.ruleFunc;
if (!angular.isDefined(validatorName)) {
throw Error("rule-name attribute must be defined.");
}
if (!angular.isDefined(validatorFunc)) {
throw Error("rule-func attribute must be defined.");
}
// in real code I passing a function call with the model as the param
// this example demonstrated the issue I am having though
var expressionHandler = $parse(validatorFunc);
// had to use viewChangeListener because changes to the model
// were not showing up correctly in the actual implementation
$ngModel.$viewChangeListeners.push(function() {
var valid = expressionHandler($scope);
$ngModel.$setValidity(validatorName, valid);
});
});
Form
<form name="AddressForm" novalidate>
<h1>Address Form</h1>
<div style="margin:20px">
<input id="Street" type="text" name="Street" placeholder="Street" data-ng-model="ctrl.address.street" rule-func="ctrl.checkVal()" rule-name="profileHasContact"> {{!AddressForm.Street.$error.profileHasContact}}
<br />
<input id="City" type="text" name="City" placeholder="City" data-ng-model="ctrl.address.city" rule-func="ctrl.checkVal()" rule-name="profileHasContact"> {{!AddressForm.City.$error.profileHasContact}}
<br />
<input id="State" type="text" name="State" placeholder="State" data-ng-model="ctrl.address.state" rule-func="ctrl.checkVal()" rule-name="profileHasContact"> {{!AddressForm.State.$error.profileHasContact}}
<br />
<input id="Zip" type="text" name="Zip" placeholder="Zip" data-ng-model="ctrl.address.zip" rule-func="ctrl.checkVal()" rule-name="profileHasContact"> {{!AddressForm.Zip.$error.profileHasContact}}
<br />
<div ng-if="(AddressForm.Street.$error.profileHasContact
|| AddressForm.City.$error.profileHasContact
|| AddressForm.State.$error.profileHasContact
|| AddressForm.Zip.$error.profileHasContact)">Address already exists in Main Contacts</div>
<button type="submit">Submit</button>
</div>
I did find a post that was close enough that I could hack together a solution.
Form validation - Required one of many in a group
Here is the updated plunker
Updated Validator
directive('ruleFunc', ['$parse', function($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function($scope, $element, $attrs, $ngModel) {
var validatorName = $attrs.ruleName;
var validatorFunc = $attrs.ruleFunc;
var groupName = $attrs.ruleGroup;
if (!angular.isDefined(validatorName)) {
throw Error("rule-name attribute must be defined.");
}
if (!angular.isDefined(validatorFunc)) {
throw Error("rule-func attribute must be defined.");
}
if(angular.isDefined(groupName)){
// setup place to store groups if needed
if (!$scope.__ruleValidationGroups) {
$scope.__ruleValidationGroups = {};
}
var groups = $scope.__ruleValidationGroups;
// setip group if needed
if(!groups[groupName]){
groups[groupName] = {};
}
var group = groups[groupName];
// assign model to group
group[$attrs.ngModel] = {
model: $ngModel
}
}
function updateValidity(valid){
if(angular.isDefined(groupName)){
// set all models in group to same validity
for(var prop in group){
if(group.hasOwnProperty(prop)){
group[prop].model.$setValidity(validatorName, valid);
}
}
}
else
{
// set this model validity if not in group
$ngModel.$setValidity(validatorName, valid);
}
}
var expressionHandler = $parse(validatorFunc);
$ngModel.$viewChangeListeners.push(function() {
var valid = expressionHandler($scope);
updateValidity(valid);
});
}
};
}]);

Angular Directive with dynamically generated input fields not able to display validation

After 3 days of scouring stackoverflow and other sites, I have found myself back at square one.
My task: I need to validate dynamically generated form fields.
The HTML:
<form name="myForm">
<form-field content="field" model="output[field.uniqueId]" ng-repeat="field in formFields"></form-field>
</form>
The controller:
var myApp = angular.module('myApp',[]);
function MyCtrl($scope) {
$scope.formFields = [
{
"fieldName": "Your Name",
"uniqueId": "your_name_0",
"fieldType": "text",
"isMandatory": true
},
{
"fieldName": "Description",
"uniqueId": "description_1",
"fieldType": "textarea",
"isMandatory": true,
}
];
$scope.output={};
}
The directive:
myApp.directive("formField",function($compile){
var templates = {
textTemplate:'<div class="form-group"><label for="{{content.uniqueId}}" >{{content.fieldName}}</label> <span ng-show="content.isMandatory" class="sub_reqText">*</span><span ng-show="form.content.fieldName.$invalid">Please check this field.</span><input type="text" ng-model="model" name="{{content.uniqueId}}" class="form-control" ng-required="content.isMandatory" id="{{content.uniqueId}}"/> </div><br>',
textareaTemplate:'<div class="form-group"><label for="{{content.uniqueId}}" >{{content.fieldName}}</label> <span ng-show="content.isMandatory" class="sub_reqText">*</span> <span ng-show="form.content.fieldName.$invalid">Please check this field.</span> <textarea ng-model="model" name="{{content.uniqueId}}" id="{{content.uniqueId}}" class="form-control" ng-required="content.isMandatory"></textarea> </div>'
};
var getTemplate = function(content, attrs){
var template = {};
template = templates[content.fieldType+"Template"];
if(typeof template != 'undefined' && template != null) {
return template;
}
else {
return '';
}
};
var linker = function(scope, element, attrs){
element.html(getTemplate(scope.content, attrs)).show();
$compile(element.contents())(scope);
}
return {
restrict:"E",
replace:true,
link:linker,
scope:{
content:'=',
model:'=?'
}
};
});
There is clearly some scope issue because I cannot access the form fields outside of the directive and I cannot access the form name inside the directive. I also know $scope.myForm.name property cannot be an angular binding expression but I am not sure how to rewrite it so that it works.
This is the jsfiddle: http://jsfiddle.net/scheedalla/57tt04ch/
Any guidance will be very useful, thank you!
While debugging the problem I found that, the name attribute is not properly compiled for form. It was showing {{content.uniqueId}} in name but actually it rendered properly on UI.
Eg.
For below html.
<input type="text" ng-model="model" name="{{content.uniqueId}}" class="form-control"
ng-required="content.isMandatory" id="{{content.uniqueId}}"/>
name rendered as name="your_name_0" but in form collection it was showing {{content.uniqueId}} with the interpolation directive.
Seems like name is not interpoluted properly.
Then found issue with AngularJS, "You can't set name attribute dynamically for form validation."
Note: Above mentioned issue has been fixed in Angular 1.3.(name
attributes interpolates properly)
& If you wanted to work them inside ng-repeat, then you should always use nested ng-form. Members inside ng-repeat will have their own form, and using that inner form you can handle your validation. Link For Reference
CODE CHANGE
var templates = {
textTemplate: '<ng-form name="form">'+
'<div class="form-group">'+
'<label for="{{content.uniqueId}}">{{content.fieldName}}</label> '+
'<span ng-show="content.isMandatory" class="sub_reqText">*</span>'+
'<span ng-show="form.input.$invalid">'+
'Please check this field.'+
'</span>'+
'<input type="text" ng-model="model1" name="input" class="form-control" ng-required="content.isMandatory" id="{{content.uniqueId}}" /> '+
'</div>'+
'</ng-form>'+
'<br>',
textareaTemplate: '<ng-form name="form">'+
'<div class="form-group">'+
'<label for="{{content.uniqueId}}">{{content.fieldName}}</label>'+
'<span ng-show="content.isMandatory" class="sub_reqText">*</span> '+
'<span ng-show="form.textarea.$invalid">Please check this field.</span>'+
'<textarea ng-model="model" name="textarea" id="{{content.uniqueId}}" class="form-control" ng-required="content.isMandatory"></textarea>'+
'</div>'+
'</ng-form>'
};
Only i changed the template html, basically added <ng-form></ng-form> for templates and handled the validation on basis it in inner form.
Here is your Working Fiddle
Hope this have cleared your understanding. Thanks.

How to add arbitrary attributes to an angular directive for data validation

I am attempting to create an angular directive that will be a custom tag for input fields in our application. Essentially what it will do is create the label, input field and the various bootstrap classes so there is a consistent look to them.
Along with that I would like it if I could add the various data validators that are appropriate for the particular input (such as required and custom validators) as attributes of the custom tag and then have those added to the input field and thus perform validation on that.
I have figured out a way that appears to put the attributes on the input field and the custom validator is getting called and properly evaluating the data, but the form never seems to think that the data is invalid. I think I am having a scope problem where the input being invalid is being set on the directive's scope rather than the parent scope but I'm not 100% sure about that and even if it is the problem I don't know how to fix it.
Here's a sample of what I'd like one of the tags to look like
<textinput ng-model="TestValue" name="TestValue" text="Label Text" config="GetConfigurationForm()" ngx-ip-address required></textinput>
which I want to generate something like
<div class="row">
<div class="form-group" ng-class="{ 'has-error': IsInvalid() }">
<label for="{{name}}" class="control-label">{{text}}</label>
<input id="{{name}}" type="text" class="form-control" ng-model="ngModel" name="{{name}}" ngx-ip-address required>
</div>
</div>
Note that the ngx-ip-address and required have been moved to the input field attributes.
My controller looks like the following (sorry it's so long)
var app = angular.module('test', []);
app.directive('ngxIpAddress', function()
{
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attributes, ngModel)
{
ngModel.$validators.ngxIpAddress = function(modelValue, viewValue)
{
// Value being blank is OK
if (ngModel.$isEmpty(modelValue))
return true;
// If the string starts with a character then
// this is not valid
if (isNaN(parseInt(viewValue[0])))
return false;
var blocks = viewValue.split(".");
if(blocks.length === 4)
{
return blocks.every(function(block)
{
return parseInt(block, 10) >= 0 && parseInt(block, 10) <= 255;
});
}
return false;
};
}
};
});
app.directive('textinput', function ()
{
return {
restrict: 'E',
scope: {
//# reads the attribute value, = provides two-way binding, & works with functions
ngModel: '=',
name: '#',
text: '#',
config: '&'
},
controller: function($scope) {
$scope.IsInvalid = function()
{
var getConfigurationFunction = $scope.config();
if (!getConfigurationFunction || !getConfigurationFunction[$scope.name])
return false;
return getConfigurationFunction[$scope.name].$invalid;
};
},
link: function(scope, element, attributes) {
var inputElement = element.find("input");
for (var attribute in attributes.$attr)
{
if (attribute !== "ngModel"
&& attribute !== "name"
&& attribute !== "text"
&& attribute !== "config")
{
inputElement.attr(attribute, attributes[attribute]);
}
}
},
template: '<div class="row">' +
'<div class="form-group" ng-class="{ \'has-error\': IsInvalid() }">' +
'<label for="{{name}}" class="control-label">{{text}}</label>' +
'<input id="{{name}}" type="text" class="form-control" ng-model="ngModel" name="{{name}}">' +
'</div>' +
'</div>'
};
});
app.controller(
"TestController",
[
"$scope",
function TestController(_scope)
{
_scope.TestValue = "TestTest";
_scope.GetConfigurationForm = function()
{
return _scope.ConfigurationForm;
};
}
]
);
If I put the attributes in the actual template then everything works as expected and the control turns red if the data isn't an ip address. When I add the attributes by moving them that doesn't work.
Here is a plunkr showing what I've got so far: http://plnkr.co/edit/EXkz4jmRif1KY0MdIpiR
Here is a plunkr showing what I'd like the end result to look like where I've added the tags to the template rather than the tag: http://plnkr.co/edit/mUGPcl1EzlHUiMrwshCr
To make this even more fun, in the future I will actually need to pass in a value to the data validation directives from the outside scope as well, but I'd like to get this working first.
Here you may find the correct answer.
The reasons of this issue are:
the attr will convert the attribute from ngxIpAddress to ngxipaddress, namely from the uppercase to lowercase, you can find this issue from this link. To solve it, just pass ngx-ip-address as parameter for function attr.
$compile(inputElement)(scope); need to be added into directive, when one directive is used in another directive. Here is one link.

Resources