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.
Related
I am struggling to figure out a way to trigger these AngularJS classes on a form I am trying to automatically fill with a chrome extension I am making. The form (specifically a textbox) has to be validated/modified before it will be validated and therefore submitted.
I originally tried using javascript to set the value of the textbox using the value property. This did not validate the form. I then tried using a dispatch event to send a key to the textbox, which resulted in nothing being input into the text box. How can I validate the form without requiring human input, or is this not possible?
Clarification, I am trying to replicate this action without user input by using a chrome extension.
Reference https://www.w3schools.com/angular/angular_validation.asp
Sounds like you need to create some events to simulate whatever angular is listening for, probably change or blur. Here's an example using click from mozilla:
https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events#Triggering_built-in_events
function simulateClick() {
var event = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
var cb = document.getElementById('checkbox');
var cancelled = !cb.dispatchEvent(event);
if (cancelled) {
// A handler called preventDefault.
alert("cancelled");
} else {
// None of the handlers called preventDefault.
alert("not cancelled");
}
}
How can I validate the form without requiring human input
Get the forms controls:
var controls = $scope.tdForm.$getControls();
Trigger their validators:
controls.forEach( _ => _.$validate() );
From the Docs:
$validate();
Runs each of the registered validators (first synchronous validators and then asynchronous validators). If the validity changes to invalid, the model will be set to undefined, unless ngModelOptions.allowInvalid is true. If the validity changes to valid, it will set the model to the last available valid $modelValue, i.e. either the last parsed value or the last value set from the scope.
For more information, see
AngularJS Form Controller API Reference
AngularJS ngModelController API Reference
When you type into the form, it updates the state of its controls (touched, dirty, etc.). According to how you define your fields validators (required, minLength...) the form will be valid or not after the user input.
In your submit method you should not proceed if any form fields are not valid. See AngularJS Developer Guide — Forms or Scotch Tutorials — AngularJS Form Validation you can have more details about AngularJS validation.
As Mike mentioned, you can use ngClass conditionally (see below) to apply some style classes only if a boolean condition occurr, for example the form is not valid.
<div ng-controller="ExampleController">
<form name="form" novalidate class="css-form">
<input type="text" ng-model="user.name" name="username" ng-class="{ 'error': !isValid }"/>
<div ng-show="form.$submitted>
<span ng-show="form.username.$error">Wrong Name</span></span>
</div>
<button ng-click="submit(user)"> Submit </button>
</form>
</div>
angular.module('formExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.isValid = true;
$scope.submit= function(user) {
if (user.name != 'Carl') {
$scope.isValid = false;
}
};
}]);
You can always programmatically change the form states if needed. For example to set the field to pristine:
$scope.form.$setPristine();
$scope.form.$setUntouched();
$setPristine sets the form's $pristine state to true, the $dirty state to false, removes the ng-dirty class and adds the ng-pristine class.
Additionally, it sets the $submitted state to false. This method will also propagate to all the controls contained in this form.
$setUntouched sets the form to its untouched state. This method can be called to remove the 'ng-touched' class and set the form controls to their untouched state (ng-untouched class).
Setting a form controls back to their untouched state is often useful when setting the form back to its pristine state.
UPDATE
Now it is clear what you are attempting to achieve. The two methods above can be used to set the form state, but if you want to validate it from code (this can be done passing the form to a service or directly in the controller for instance) then $validate() method will allow you to achieve that as mentioned by George.
What is the "Angular" recommended way to validate conditions that involve multiple fields on a form? Most or all of the validation examples that I have seen talk about custom validation directives that attach to a single textbox,select,etc. What about conditions that involve multiple fields on a form?
For example, I want to have an "empty" form validator: This will prevent submits on forms that have no required fields and all fields are empty.
Another example is, say I have a master/child one-to-many relationship on a page and the child relationship is a table of child records. What if I need to validate that at least one child record should exist IF 2 or 3 fields meet certain conditions?
One thought that I have is to built validation directives that attach to <form> tags as elements. Something like <form name="xxx" validate-not-empty > This directive will then set the $invalid property of the form, on submit. But I am concerned that this maybe is not the angular away to go as I have not seen this on any code samples I have seen. So I am looking for any alternatives to achieve this.
What is wrong with having your "not-empty" directive placed on each of your input field? (following this way of doing: http://angularjs.io/how-to-create-a-custom-input-validator-with-angularjs/)
Then you can just check anywhere if the form is valid, for example to disable the send button:
<button ng-disabled="!form.$valid">Send</button>
For it to work you must have all input with its ng-model.
I didn't quiet understand your master/child problem. It you could give an example...
I've used the basic validation, based on input type (email, tel, etc.) and the "required" attribute. But, more complex validation is typically handled through custom directives. Although, there may very well be a library that I'm not aware of that provides a common set.
As a basic example:
<form name="contactForm">
<input type="text" name="ContactName" ng-model="contact.name" required>
<button type="button" ng-click="submitForm(contactForm)" />
</form>
Then, in your controller:
$scope.submitForm = function (form) {
if (form.$valid) {
...
}
}
This W3Schools page describes Angular's validation fairly well.
AngularJS Form Validation
Toward the bottom of the article, they mention that custom validation requires a bit more effort. But, they provide examples.
To create your own validation function is a bit more tricky. You have
to add a new directive to your application, and deal with the
validation inside a function with certain specified arguments.
<form name="myForm">
<input name="myInput" ng-model="myInput" required my-directive>
</form>
<script>
var app = angular.module('myApp', []);
app.directive('myDirective', function() {
return {
require: 'ngModel',
link: function(scope, element, attr, mCtrl) {
function myValidation(value) {
if (value.indexOf("e") > -1) {
mCtrl.$setValidity('charE', true);
} else {
mCtrl.$setValidity('charE', false);
}
return value;
}
mCtrl.$parsers.push(myValidation);
}
};
});
</script>
Hope this helps.
What I am trying to do is take 4-5 inputs in a form, store to an object and when user selects 'save', temporarily store in session and be able to access to show that object in a row in a table on same page and then allow user to enter data form input again to add to next object.
So in the end, the user may enter 1 object or up to 6 and when done, allow user to submit all objects as an array of objects or something to that effect.
So I am not an angular power user, but figure it has this power somewhere. So my questions evolve around a good approach to this and maybe trying to find a good example or so. Any of you able to help on this?
Thanks much.
Well, this is actually quite easy if you have the table and the form on the same page. You can simply use one scope variable for the array of entered objects and one scope variable for the current object itself.
I'll now do an example without all of your requirements (e.g. that the user can enter up to six objects the most), but this should be quite straight forward to implement.
First take the HTML:
<form ng-controller="RolesController">
<label for="name">Name</label>
<input type="text" id="name" ng-model="current.name">
<label for="slug">Slug</label>
<input type="text" id="slug" ng-model="current.slug">
<label for="level">Level</label>
<input type="number" id="level" ng-model="current.level">
<!-- buttons -->
<button type="button" ng-click="storeTemp()">Store temporarily</button>
<button type="button" ng-click="storeAll()">Store all</button>
</form>
And the Controller:
angular.module('administration').controller('RolesController',
['$scope', '$http', function($scope, $http) {
$scope.current = {};
$scope.all = [];
$scope.storeTemp = function() {
/* do validation here or in html before ... */
$scope.all.push($scope.current);
$scope.current = {};
};
$scope.storeAll = function() {
/* maybe another validation ... */
$http.post('admin/roles', { roles: $scope.all });
};
});
Obviously you could also use the same button for both actions if you require your user to always input the same amount of new objects.
Ah, master-detail form!
You do not need to worry about the multiple inputs the user are savings. Just declare an array and push your objects into it every time the user click 'save'. As long as you do not destroy your controller (i.e no page reload, no switch of controllers, etc) your data are always there.
//master
$scope.toBeSentToServer = [];
//details, declare your object wherever according to your needs
$scope.formVariable = {
var1:"",
var2:"",
var3:""
};
$scope.clickSave = function(){
$scope.toBeSentToServer.push($scope.formVariable);
//reinstantiate your $scope.formVariable so that your form is empty
$scope.formVariable = {};
}
And now you can use our beloved ng-repeat to iterate through scope.toBeSentToServer inside the table.
<table>
<tr ng-repeat= "item in toBeSentToServer">
<td>{{item.var1}}</td>
<td>{{item.var2}}</td>
<td>{{item.var3}}</td>
</tr>
</table>
Of course, there will be times you would want to use the cookies / local-data-storage, there is a plethora of libraries available to do it. Angular has its own $cookies service but I would highly recommend angular-local-storage from Gregory Pike. (Sorry maybe bias)
But before all these, if you are looking for data persistent mechanism, maybe try using a factory or sevice in angularjs?
I am trying to find a simple solution to a required input type of scenario. I have multiple small forms that all send on one button save on the bottom of the page. What I am trying to accomplish is something like ngRequired, however across the whole controller, not just the individual forms. So the desired effect is pretty simple - if any of the inputs aren't filled out - set a boolean( or something) to false that disables the save button at the bottom.
So my first attempt is like this -
I have a model on each of the required items - there are 10 items
then I have a function that checks when you try to click the button how many are chcked like this
if($scope.modeltracker1){
//if there is anything inside model 1 add 1 to the tracker
$scope.modeltracker += 1;
}
and if the counter is not 10, don't do anything (all required are not filled out)
if($scope.modeltracker != 10){
//do nothing because all required are not filed out
}else{
//run save, all required all filed out
}
So - I feel like there should be a much easier solution than my first attempt here. Maybe something along the lines of checking if any individual one of these required fields is false, don't fire? I know that ngRequied would be great for this, but unfortunately the way this has to be structured, it cannot be one large form. There has to be a much easier way to accomplish this task with angular.
Any input would be much appreciated, thanks for reading!!
You can use ng-form to nest your multiple forms. It allows using nested forms and validating multiple forms as one form.
So, you need to nest your multiple forms in one root form.
<div ng-controller="demoController">
<form name="parentForm">
<ng-form name="firstForm">
<input type="text" ng-model="firstModel" required>
</ng-form>
<ng-form name="secondForm">
<input type="text" ng-model="secondModel" required>
</ng-form>
</form>
</div>
Then, all you need to do is to check parent form's validation status.
angular.module('formDemo', [])
.controller('demoController', ['$scope', function($scope) {
if($scope.parentForm.$valid) {
//run save, all required all filed out
} else {
//do nothing because all required are not filed out
}
}]);
you can use myForm.$invalid directive, as explained here: Disable submit button when form invalid with AngularJS
I am stuck with certain scenarios in a validation. I need to valiadte a field - "First Name". Validations logic i have been using is -
If the field is dirty then validate against regex
ng-show="aspnetForm.FirstName.$dirty && aspnetForm.FirstName.$error.nameValidate"
If the field is marked required (currently keeping the field required is entirely business dependent so i am reading the true/false value from a JSON) then user may try submitting the form as it is
ng-show="blankSubmit && aspnetForm.FirstName.$error.required"
where blankSubmit is just a scope variable i am setting true on submit button click.
Now 3rd scenario is the logic i am not getting that is if the user clicks on the firstname text box and then without dirtying it, just blurs out, then the validation message should be displayed if ng-required is set true.If i just place ng-show="aspnetForm.FirstName.$error.required" then on the page load itself the error message is displayed which i dont want as it gives user a bad UX.
I solely want error message to be displayed when the attribute ng-required is set true and user blurs out of the textbox.
One possible solution is to create a directive which marks a field as visited which you can then check in the ng-show:
.directive('visited', function() {
return{
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModel){
element.bind('blur', function(){
scope.$apply(function() {
ngModel.visited = true;
});
});
}
};
});
View:
ng-show='form.field.$error.required && (form.field.$dirty || form.field.visited)'
I would suggest you using ng-messages. Your HTML would look like this:
<div ng-messages="aspnetForm.FirstName.$error" role="alert">
<div ng-message="required">Please enter a value for this field.</div>
<div ng-message="nameValidate">This field must be a valid.</div>
...
</div>
In case you want to use required depending on some variables I would suggest you using this <input ... required={{shouldBeRequired}}/>. This should work, required field should be validated only when proper value is set to it.