Custom validation via child control - angularjs

So I'm working inside a directive that contains it's own form element and buttons, however all the controls must be transcluded through. The model for this particular view contains a property for total capacity, and a property that is a collection of compartments (separate entity). Each compartment has it's own capacity. I already have a function that will show an error on the view if/when the Total Capacity is not equal to the combined capacity of all compartments. The problem here is, since all my controls are transcluded through (and I'm not supposed to modify the parent directive) I have no clue if/how I can use that same function to mark the form as invalid to disable the save button. I was wondering if there is a solution (hopefully one that doesn't involve custom directives or services) that would allow me to set the parent form invalid if an expression returns true.
** UPDATE **
Sorry guys, I think I explained it backwards the first time. So this would be a good representation of what is going on in the html. (Also I haven't used stackoverflow much before this so bear with me)
edit-page-directive:
<div>
<form name="editForm">
<ng-transclude>
</ng-transclude>
<a class="btn btn-success">Save</a>
<a class="btn btn-danger">Cancel</a>
</form>
</div>
View for this particular edit:
<edit-page>
<uib-tabset>
<uib-tab>
<!--Total Capacity input-->
<input type="text" numeric="{min:1, format:'#,###.#'}" ng-model-options="{updateOn: 'blur'}" class="form-control" id="tcCapacity" name="tcCapacity" data-ng-required="true" ng-model="vm.dataContext.entity.TotalCapacity" />
<!--End Total Capacity-->
</uib-tab>
<uib-tab>
<table>
<tr><thead><th>...</th><th>Capacity</th><th>(Buttons for compartment add/remove)</th></thead></tr>
<tr ng-repeat="compartment in vm.dataContext.entity.TrailerConfigCompartments">
<td width="200">{{compartment.Sequence}}</td>
<!--Important input under this-->
<td><input type="text" numeric="{min:0, format:'#,###.#'}" class="form-control" ng-model="compartment.Capacity" data-ng-required="true" /></td>
<!--Important input above-->
<td align="right" style="padding-right:30px;">
<a class="btn" style="padding: .7em; color: black;" ng-click="vm.addCompartment(compartment.Sequence + 1)">
<span uib-tooltip="New compartment at sequence {{compartment.Sequence + 1}}" class="btn-edit" style='margin-left:5px'><span class="glyphicon glyphicon-plus" style="margin-top:3px"></span></span>
</a>
<a class="btn" style="padding: .7em; color: black;" ng-click="vm.removeCompartment(compartment)">
<span uib-tooltip="Remove compartment" class="btn-edit" style='margin-left:5px'><span class="glyphicon glyphicon-minus" style="margin-top:3px"></span></span>
</a>
</td>
</tr>
</table>
</uib-tab>
</uib-tabset>
</edit-page>

If I understand you correctly you have something like
HTML
<div data-ng-controller="FormController as vm">
<form class="foo form">
<input type="text"> // some inputs
<input type="text"> // some inputs
<transcluded-directive>
<button class="foo button-to-disable">Do something</button> // button that should be disabled
</transcluded-directive>
</form>
</div>
JS
.controller("FormController", function($scope) {
var vm = this;
vm.validateTotalCapacity = function () {
// validation stuff
}
});
So I think you can do something like:
HTML
<div data-ng-controller="FormController as vm">
<form class="foo form {{vm.validateTotalCapacity() ? '' : 'form-has-errors'}}" >
<input type="text"> // some inputs
<input type="text"> // some inputs
<transcluded-directive>
<button class="foo button-to-disable">Do something</button> // button that should be disabled
</transcluded-directive>
</form>
</div>
Look I put your form validator in <form class="foo form"> and make condition for error class
CSS
.form-has-errors .button-to-disable {
pointer-events: none;
cursor: default;
opacity: 0.5
// or your custom disabled styles
}
UPDATE
I see, but I believe you could try this:
HTML
<div>
<form name="editForm" class="{{editForm.$valid ? '' : 'form-has-errors '}}">
<ng-transclude>
</ng-transclude>
<a class="btn btn-success">Save</a>
<a class="btn btn-danger">Cancel</a>
</form>
</div>

So I realized I had misinterpreted the customValidation piece of angularjs. I thought any directive I'd have to create for validation would have to be added to the form element itself. Just as well I thought it would be alot harder to set up than it actually is.
For future reference:
1.) Create a directive and restrict it to an attribute
2.) Require ngModel for this directive
3.) Set up your link function:
link: function(scope, elem, attrs, ngModel) {....}
4.) Add a function to the $validators object of the control you want to validate. Do this INSIDE of your link function. Ex:
link: function(scope, elem, attrs, ngModel) {
ngModel.$validators.validationFn = function(value) {
//Where value is the current value of the control
//In my case, where I want to compare value to the combined value of other
//compartments I would send in whatever data I wanted via the scope property of
//this directive and compare the two in this function
}
}
5.) Return true if control is valid and vice versa
And that's it.
If you want to access this validator to display an error message just:
ng-show="vm.arbitraryInput.$error.validationFn"
Keep in mind that now if it returns true, then the input is invalid.

Related

add new angularJs event to a button on input value change using controller in angularJs

I m trying to add angularJs event to a button if value of input field changes. But i cannot find a solution how can i do it. I know how to detect change but do not know how to add a new event using controller.
Let me illustrate by example:
<form name="addNewCourse">
<input ng-model="courseNameEdit" ng-value="courseName" ng-pattern="regex" ng-trim="false" name="addNewCourseField" ng-minlength="5" ng-maxlength="60" required>
<div ng-messages="addNewCourse.addNewCourseField.$error" style="text-align: left !important; color:red;">
<div ng-message="required">This field is required*</div>
<div ng-message="pattern">Must be only digits or numbers</div>
<div ng-message="maxlength">Must not exceed 60 characters</div>
<div ng-message="minlength">Must not be smaller than 5 characters</div>
</div>
<button class="btn btn-primary" ng-click="saveCourseNameFunc(courseNameEdit)">Save</button>
</form>
what i want is to add ng-disabled="addNewCourse.$invalid" into the button if value change using controller. Thanks in Advance.
If I'm not wrong here, You are about to validate your form while field value changes via the controller. Try below solution and let me know if this will not work for you.
FORMOBJECT.$setSubmitted();
You can also try triggering change event of that field via controller like:
angular.element('#INPUT_ID').trigger('change');
Hope you will get the solution from one of the given above.
How about using ng-change and a boolean in your ng-disabled expression activate the disability simply.
<form name="addNewCourse">
<input ng-change="changed()" ng-disabled="addNewCourse.$invalid && activeDisable" ng-model="courseNameEdit" ng-value="courseName" ng-pattern="regex" ng-trim="false" name="addNewCourseField" ng-minlength="5" ng-maxlength="60" required>
<div ng-messages="addNewCourse.addNewCourseField.$error" style="text-align: left !important; color:red;">
<div ng-message="required">This field is required*</div>
<div ng-message="pattern">Must be only digits or numbers</div>
<div ng-message="maxlength">Must not exceed 60 characters</div>
<div ng-message="minlength">Must not be smaller than 5 characters</div>
</div>
<button class="btn btn-primary" ng-click="saveCourseNameFunc(courseNameEdit)">Save</button>
</form>
and your controller must contain something like this:
$scope.activeDisable = false;
$scope.change = function(){
$scope.activeDisable = true;
}

How to repeat duplicate objects using ng-repeat in AngularJs?

This is my code.
$scope.data=[];
$scope.data=[{"label":"name","type":"string"},{"label":"email","type":"string"}];
$scope.addFields = function (field) {
$scope.data.push(field);
};
This is my html:-
<div ng-repeat="eachItem in data">
<input type="button" value="add" ng-click="addFields(eachItem)"/>
<label>{{eachItem.label}}</label>
<input type="text" ng-model="fieldValue"/>
</div>
when i click add button push one more object into $scope.data array like
$scope.data=[{"label":"name","type":"string"},{"label":"email","type":"string"},{"label":"name","type":"string"}];
In the above i got an error
angular.min.js:102 Error: [ngRepeat:dupes] http://errors.angularjs.org/1.3.14/ngRepeat/dupes?p0=nestedField%20in%20fieā€¦%2C%22type%22%3A%22string%22%2C%22%24%24hashKey%22%3A%22object%3A355%22%7D
at Error (native)
I have duplicate objects after adding. because i want to repeat label names using ng-repeat in angularjs.First i have output like this
OutPut:-
name textbox
email textbox
After add button click Output:-
name textbox
email textbox
name textbox
use track by $index
var app = angular.module("app",[])
app.controller('ctrl',['$scope', function($scope){
$scope.data=[];
$scope.data=[{"label":"name","type":"string"},{"label":"email","type":"string"}];
$scope.addFields = function (field) {
$scope.data.push(field);
};
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.22/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<div class="item item-checkbox">
<div ng-repeat="eachItem in data track by $index">
<input type="button" value="add" ng-click="addFields(eachItem)"/>
<label>{{eachItem.label}}</label>
<input type="text" />
</div>
</div>
Use track by for this purpose.
<div ng-repeat="eachItem in data track by $index">
<input type="button" value="add" ng-click="addFields(eachItem)"/>
<label>{{eachItem.label}}</label>
<input type="text" ng-model="eachItem.value" />
</div>
You also able to use track by with your custom filed, like id, or whatever
Important: It's better to use track by in each ng-repeat, cause it's improve ng-repeat's performance (read more).
But avoid to use track by in ng-options and other cases when you use select as .. for ... construction (read more)
JsFiddle here
You have to ensure that items in the array have an unique key. If that is not possible you can use track by $index in the ng-repeat.
Check the details here

angular JS dynamic tooltip creation

EDIT:
It seems like the Error or wrong handling is because the scope does not gets updated when the email field is not valid... is there a way to chang that?
this is my first question on stackoverflow so i hope i will do it right.
I am pretty new to angular js and i am creating some basics at the moment.
in my demo app i created a normal form in a bootstrap style and i was planing to create a directive to show the errors in a bootstrap way. so far so good. that was working and my next step was to create a angular js bootstrap tooltip directive when the form is not valid. the thing is, that i wanna do this dynamic.
i post some code to explain it better:
<b>HTML:</b>
<div class="container" ng-controller="LoginCtrl as vm">
<form id="login" name="vm.loginForm" class="form-signin" ng-submit="vm.login()" novalidate>
<div show-errors>
<input type="email" name="username" ng-model="vm.credentials.username" class="form-control" placeholder="Email address" required autofocus>
</div>
<div show-errors>
<input type="password" name="password" ng-model="vm.credentials.password" class="form-control" placeholder="Password" required>
</div>
<button class="btn btn-lg btn-primary btn-block" id="submit" type="submit">
Login
<span class="spinner"><i class="fa fa-refresh fa-spin"></i></span>
</button>
</form>
</div>
<b>showError Directive:</b>
(function () {
angular.module('testapp.Validate', []).directive('showErrors', validationDirective);
/**
* #name validate directive
*/
function validationDirective($compile) {
return {
require: '^form',
restrict: 'A',
link: function (scope, el, attrs, formCtrl) {
var inputNgEl = angular.element(el[0].querySelector("[name]"));
var inputName = inputNgEl.attr('name');
inputNgEl.bind('blur', function () {
toogle(inputNgEl, el, formCtrl[inputName]);
});
scope.$on('show-errors-check', function () {
toogle(inputNgEl, el, formCtrl[inputName]);
});
}
}
}
function toogle(inputNgEl, fromGroup, inputField) {
fromGroup.toggleClass('has-feedback has-error', inputField.$invalid);
if (inputField.$invalid && !fromGroup[0].querySelector(".glyphicon-remove")) {
inputNgEl.after('<span class="glyphicon form-control-feedback glyphicon-remove"></span>');
} else if (!inputField.$invalid) {
if (fromGroup[0].querySelector(".glyphicon-remove") != null) {
fromGroup[0].querySelector(".glyphicon-remove").remove();
}
}
}
})();
That is working so far . it just adds a has-feedback,has-error class to the parent div and a span with an error-icon after the input.
but back to my plan, now I also want to create a dynamic tooltip for the input. so I planed to add something like that in the "toogle" function
inputNgEl.attr('uib-tooltip',"aaaa");
inputNgEl.attr('tooltip-placement',"right");
inputNgEl.attr('tooltip-trigger',"mouseenter");
inputNgEl.attr('tooltip-class',"testTool");
But that is not working because the input field got already compiled before.
so I asked google about it and there I found some solutions with $compile
$compile(inputNgEl)(scope);
But when I am using it, and I type in a valid email address the field gets reset. aaa#aaa (still working) but after I add the aaa#aaa. (the field gets reseted - I guess compiled again). the tooltip would work btw.
Can anybody help me with that or is there a better solution to create a dynamic angular bootstrap tooltip?
Maybe you need to add a $watch on the input element, and see if it changes, add a tooltip in the input element and compile it

bind not working for required input field

I've this AngularJS HTML using Bootstrap:
<div class="col-sm-6" ng-app ng-controller="MyController">
<br/><br/>
<form name="myForm">
<div class="input-group">
<input type="text" name="input" class="form-control" ng-model="input" maxlength="{{inputMaxLength}}" ng-minlength="{{inputMaxLength}}" ng-maxlength="{{inputMaxLength}}" placeholder="Type input.." aria-describedby="basic-addon2" required />
<span class="input-group-addon" id="basic-addon2" ng-bind="{{inputMaxLength-input.length}}"></span>
</div>
<div class="btn-group">
<button type="button" class="btn btn-default btn-primary" ng-disabled="myForm.$invalid">Submit</button>
</div>
</form>
</div>
and this controller:
function MyController($scope) {
$scope.input = "";
$scope.inputMaxLength = 18;
}
The Bootstrap addon in the field, should always count the remaining characters. The Submit button should be disabled as long the form is not valid.
The button works as aspected, but the "count down" in the add on is always 18.
Why?
See this JSFiddle.
You have a typo in ng-min-length, you should have:
ng-minlength="{{inputMinLength}}"
instead of
ng-minlength="{{inputMaxLength}}"
Oh and you should lose the curly braces on ng-bind, you can use one or the other but not both
So either:
<span class="input-group-addon" id="basic-addon2" ng-bind="inputMaxLength-input.length"></span>
or
<span class="input-group-addon" id="basic-addon2">{{inputMaxLength-input.length}}</span>
(same applies for ng-minlength="{{inputMaxLength}}" ng-maxlength="{{inputMaxLength}}", no need for interpolation here, use ng-minlength="inputMaxLength" ng-maxlength="inputMaxLength" instead)
Note that while the input does not fulfill the requirements ie. larger than minLength and shorter than maxLength input will not have a value.
In this case you can get the value using myForm.input.$viewValue
I have updated you fiddle http://jsfiddle.net/29m5tdsc/9/
This can't work : your ng-validation (ng-minlength) will set $scope.input to null. So your counter wont work.
Besides, you wrote :
ng-bind="{{inputMaxLength - input.length}}"
When angular will work he will replace variables with values. You should wrote :
ng-bind="(inputMaxLength - input.length)"
JS Fiddle

ng-model and form inside ng-repeat

I am creating a form for each item in my $scope. ng-model is not linking with the data on my form submit.
<li ng-repeat="item in favourites">
<form ng-submit="DeleteFavourite()" class="form-horizontal" id="frmSpec">
<input ng-model="item.Description"/>
<input ng-model="item.Refno"/>
<button type="submit"class="btn">{{item.Description}}
<span class="glyphicon glyphicon-remove"></span>
</button>
</form>
</li>
The issue is very closely related to the comment by #DavidBeech . In an angular controller the scope is seen as a hierarchy object.
So for example if you have the following:
<div ng-controller="SomeCtrl">
<li ng-repeat="item in favourites">
<form ng-submit="DeleteFavourite()" class="form-horizonatal" id="frmSpec">
<input ng-model="item.Description"/>
<input ng-model="item.Refno"/>
<button type="submit"class="btn">{{item.Description}}
<span class="glyphicon glyphicon-remove"></span>
</button>
</form>
</li>
</div>
When that controller is injected into the div and that new scope instance is created it only sees what is at that level and the scopes of it's parents. It has no knowledge of its children's scopes. Therefore, when you call DeleteFavourite() since it is a method attached to the scope of the controller it will not have the context of the ng repeat. So as David stated you will need to do something like DeleteFavorite(item) in order for it to have knowledge of what you are submitting otherwise you will not have knowledge of what item in the iteration you are submitting.
Feel free to comment if you want an example and I can put together a fiddle with an example of scope inheritance.

Resources