Is this really the right way to do angular form validation? - angularjs

I'm trying to learn angularjs, and at the moment am looking at forms and form validation. By following the docs on the angular site, I have a template html file as follows:
<form role="form" name="fnolForm" novalidate autocomplete="off" data-ng-submit="submit()">
<fieldset class="form-group">
<legend>Information about you</legend>
<label data-ng-class="{'has-error' : fnolForm.submitError && fnolForm.name.$invalid}">
Your name
<input class="form-control" type="text" data-ng-model="fnol.insuredParty.name" name="name" required/>
<span class="error" data-ng-show="fnolForm.submitError && fnolForm.name.$error.required">Please provide your name</span>
</label>
<label data-ng-class="{'has-error' : fnolForm.submitError && fnolForm.phoneNumber.$invalid}">
Contact number
<input class="form-control" type="tel" data-ng-model="fnol.insuredParty.phoneNumber" name="phoneNumber" required/>
<span class="error" data-ng-show="fnolForm.submitError && fnolForm.phoneNumber.$error.required">Please provide your contact number</span>
</label>
<label data-ng-class="{'has-error' : fnolForm.email.$error.email || (fnolForm.submitError && fnolForm.email.$invalid)}">
Email address
<input class="form-control" type="email" data-ng-model="fnol.insuredParty.email" name="email" required/>
<span class="error" data-ng-show="fnolForm.submitError && fnolForm.email.$error.required">Please provide your email address</span>
<span class="error" data-ng-show="fnolForm.email.$error.email">Please enter a valid email address</span>
</label>
</fieldset>
and in my controller I am doing:
$scope.submit = function() {
if ($scope.fnolForm.$valid) {
// client side validation has passed, do something ....
} else {
// client side validation has failed
$scope.fnolForm.submitError = true;
}
};
Whilst my validation works fine, it doesn't feel right on a number of counts:
Business logic (validation rules) is mixed up with view concerns
The validation rules are not (obviously) testable
Client side validation is easy to work/hack around - needs to be backed up by server-side validation
I'm sure there's a solution to the last point (validation rules being backed up by server side validation); but the first two worry me
Is this really the right way to do client side validation in angular? Do we really have to write the rules within the template like that? And if we do, how on earth are we supposed to unit test the validation rules?
Would be interested in anyone's views (no pun intended :)) on this;
Cheers
Nathan
Edit 31/03/14 19:50
Having read the comments and re-read the code I've pasted, I think I originally had difficulty accepting the fnolForm.submitError && fnolForm.name.$invalid type logic within the view.
However, on reflection I can now see that this is not actually the business logic that determines form validation, it is purely presentation sugar. IE. we can strip all of that out and the form still has the same validation rules:
<form role="form" name="fnolForm" novalidate autocomplete="off" data-ng-submit="submit()">
<fieldset class="form-group">
<legend>Information about you</legend>
<label>
Your name
<input class="form-control" type="text" data-ng-model="fnol.insuredParty.name" name="name" required/>
</label>
<label>
Contact number
<input class="form-control" type="tel" data-ng-model="fnol.insuredParty.phoneNumber" name="phoneNumber" required/>
</label>
<label>
Email address
<input class="form-control" type="email" data-ng-model="fnol.insuredParty.email" name="email" required/>
</label>
</fieldset>
In the above case, my form's validation rules are determined by standard html5 attributes (required, type="email" etc) which angularjs is providing the cross browser logic for, so I don't need to unit test that. And I know that angularjs provides custom additional field validators for mix/max length etc (directives), so I don't to test those either.
But I still can't help thinking that I need to test my form as a unit, in particular whether it's valid or not.
For example, the domain business rules might say that the form can only be submitted if the name, phone number and email fields are all filled out, and that the email address field contains a valid (formatted) email address.
In this case I would want to write a series of unit tests where I populate the form objects properties with various combinations of data, and assert the validity of the form.
Maybe the problem is that I'm coming from a java background, and the above pattern re: unit testing is certainly what I'm used to.

OK, after considering the various options, comments and answers offered (thanks guys), I've gone with a solution based around writing a validation service, and injecting that into the controller.
This seems to ticks the most boxes for me as it means I have a clear definition of what constitutes a valid form, and its testable. But in addition it also uses angularjs directives etc to decorate the form (as per my original markup) - the presentation sugar :)
So, this is my final solution. This is my view template as before:
<form role="form" name="fnolForm" novalidate autocomplete="off" data-ng-submit="submit()">
<fieldset class="form-group">
<legend>Information about you</legend>
<label data-ng-class="{'has-error' : fnolForm.submitError && fnolForm.name.$invalid}">
Your name
<input class="form-control" type="text" data-ng-model="fnol.insuredParty.name" name="name" required/>
<span class="error" data-ng-show="fnolForm.submitError && fnolForm.name.$error.required">Please provide your name</span>
</label>
<label data-ng-class="{'has-error' : fnolForm.submitError && fnolForm.phoneNumber.$invalid}">
Contact number
<input class="form-control" type="tel" data-ng-model="fnol.insuredParty.phoneNumber" name="phoneNumber" required/>
<span class="error" data-ng-show="fnolForm.submitError && fnolForm.phoneNumber.$error.required">Please provide your contact number</span>
</label>
<label data-ng-class="{'has-error' : fnolForm.email.$error.email || (fnolForm.submitError && fnolForm.email.$invalid)}">
Email address
<input class="form-control" type="email" data-ng-model="fnol.insuredParty.email" name="email" required/>
<span class="error" data-ng-show="fnolForm.submitError && fnolForm.email.$error.required">Please provide your email address</span>
<span class="error" data-ng-show="fnolForm.email.$error.email">Please enter a valid email address</span>
</label>
</fieldset>
I have written a service as follows:
fnolService.factory("FnolFormValidator",
[
function() {
return function(fnol) {
return !!(fnol &&
fnol.insuredParty &&
fnol.insuredParty.name &&
fnol.insuredParty.phoneNumber &&
fnol.insuredParty.email &&
fnol.insuredParty.email.match(/^[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,4}$/i) !== null);
}
}
]
);
And I inject it into my controller and use it as follows:
fnolControllers.controller("ReportAccidentController",
[
"$scope",
"$modal",
"fnol",
"FnolFormValidator",
function($scope, $modal, fnol, FnolFormValidator) {
$scope.submit = function() {
if ($scope.fnolForm.$valid && FnolFormValidator($scope.fnol)) {
// client side validation has passed, do something ....
} else {
// client side validation has failed
$scope.fnolForm.submitError = true;
}
};
I quite like this solution because it allows me to unit test the validation rules of the form (ie. the FnolFormValidator service) and also the controller logic. And if I want to test the presentation sugar, then I could write something with Selenium or similar.
To me it feels like the right level of separation of concerns.

I guess it's just how you look at it. I understand where the concern comes in about business logic. I guess I see I don't see it that way though. It's simply just displaying feedback to the user. The logic for this to happen is actually located within the form object. So you are just calling expressions for it to be evaluated elsewhere.
I personally like to keep it there within the view. It feels like it belongs there (although it does start to make simple forms feel a bit bloated). But if you'd rather, you could implement it within the controller. $scope.fnolForm.submitError etc. However, if you wanted real-time feedback, you'd need to put an ng-change() on the input to mimic it. And then set a flag to have your ng-show/hide to evaluate against.

To answer your first point, it's true that it can seem a little mixed up with form validation.
But, all the business logic for setting those error values is taken care of behind the view, and the view is only looking at them or at combinations of them to decide how to display the errors.
But it is alot of code in the view, so my suggestion would be to use a directive for your inputs. See http://docs.angularjs.org/guide/directive .
A directive can wrap up your inputs into 1 'element' and you can just pass in the important pieces. And the directive handles all the code and logic for errors, so you don't need to do that directly in the view. You will just need to require a form element as a parent of your directive and then you can pull the controller for that in as the 4th parameter on your directive's link function. The documentation on directives is extensive and will cover all of that.
Hope that helps!

That's how I am doing the client side validation too. Let me try to answer your questions.
Business logic (validation rules is mixed up with view concerns)
If you also work on the server-side validation, actually angular form validation do not interfere with business logic but they just decide how to show errors or warnings to users, which is totally front-end.
The validation rules are not (obviously) testable
You cannot probably test these validation rules with unit testing but you can definitely test them with e2e or functional testing using selenium drivers. I think it's better to test front-end stuff with e2e testing.
As you noticed, you should always use server-side validation even though you have these nice angular-style client side validation.

Related

Deletion Error with Email Validation in Angular

So I've recently taken over an Angular Giving Form Application. I am running validation on the email field using ng-pattern and displaying the errors on blur with ngMessages. The validation works great, however once the validation passes as $valid if the user decides they need to make a change in their email and begin to delete part of the first deletion deletes the last character of the email as expected, but the second deletion deletes the entire field forcing the user to start from scratch.
The regex for ng-pattern is being set in the controller scope with the variable $scope.emailre
The files are much to large to place here but here is the link to the site I am working on for my client.
https://epiqa.moodyglobal.org/corporate/
Snippet of Angular controller:
myApp.controller('mainCtrl', function($scope, localStorageService, $http) {
$scope.emailre = /^(([^<>()\[\]\\.,;:\s#"]+(\.[^<>()\[\]\\.,;:\s#"]+)*)|(".+"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
Snippet of HTML Form:
<div class="row form-group">
<div class="col-sm-6">
<div>
<label class="label" for="txt_donorEmail">E-mail:</label>
<input ng-class="{ 'submitted-error' : givingForm.email.$invalid && submitted }" ng-model="email" type="text" id="email" name="email" class="textbox required full form-control" maxlength="50" ng-pattern="emailre" required />
</div>
<div ng-messages="givingForm.email.$error" ng-if="givingForm.email.$touched || submitted">
<div class="errorText" ng-message="required">This field is required</div>
<div class="errorText" ng-message="pattern">Enter a valid email</div>
</div>
</div>
</div>
I have tried changing the input type from type="text" to type="email" but when doing that any time the user types two (.) periods the field gets immediately deleted.
Please help any ideas are very welcome.
The behavior is caused by this section
$scope.$watch('email', function(value){
localStorageService.set('email',value);
$scope.emailValue = localStorageService.get('email');
});
By Angular documentation
The default behaviour in ngModel is that the model value is set to undefined when the validation determines that the value is invalid. By setting the allowInvalid property to true, the model will still be updated even if the value is invalid.
I'm not sure whether you want to save the invalid email into localStorage, though. Maybe you can add a check only update when the value is valid.

Why name attribute required in AngullarJs validation

Friends, I am new to angularjs please explain why name attribute required for angularjs validation
<form name="lform" novalidate>
<input type="text" name="userName" ng-model="userName" required novalidate/>
<span style="color:red" ng-show="lform.userName.$dirty && lform.userName.$invalid">
<span ng-show="lform.userName.$error.required">Username is required.</span>
</span>
<br/>
</form>
If I remove name="userName" code not working, please explain this.
Update: If I remove "ng-model" it is not working, but I can change "ngmodel='newName'" is working, please explain this reason also.
This is just how HTML form works, name describe the parameter that will be sent to the server.
Angular's approach is to extend HTML and its behaviour, instead of inventing the wheel. Angular encapsulates "form" (see ngForm directive) and extend it. Because name is the unique id of an input in a form, it is also the unique id of the input in ngForm's collection.
ng-model is another directive that bind the value of the input into a variable in the current scope, it doesn't have to hold the same value as name.
Let's take your example and change ng-model:
<form name="lform" novalidate>
<input type="text" name="userName" ng-model="object.name" required novalidate/>
<span style="color:red" ng-show="lform.userName.$dirty && lform.userName.$invalid">
<span ng-show="lform.userName.$error.required">Username is required.</span>
</span>
<br/>
</form>
The validation will work, but the variable that will be updated in your scope is "object.name".
To sum it up, lform.userName holds the metadata and object.name will hold the actual data.
The answer is in your code.
<span ng-show="lform.userName.$error.required">Username is required.</span>
Angular is using the input name to store the error related information.
lform – your form name
username – your input name

AngularJS form validation testing

Sometimes forms become very complicated and it is impossible to test every case manually after code changes.
I already have unit testing with karma on the project.
Is there any tools or best practices how to test AngularJS form validation with jasmine and karma?
For example how can I test such form with jasmine and karma automatically?
<form name="appForm" novalidate>
<div>
Username: <input type="text" ng-model="data.username" name="username" ng-maxlength="15" required />
</div>
<div>
Email: <input type="email" ng-model="data.email" name="email" required />
</div>
<div>
Age: <input type="number" ng-model="data.age" name="age" />
</div>
<div>
<button ng-click="submit()" ng-disabled="appForm.$invalid">Submit</button>
</div>
</form>
It depends on what you actually want to make sure when testing form validation.
If you want to be sure invalid form will not be submited, then it is one case. And I don't see problems with this case.
If you want to be sure that appropriate messages are displayed for invalid fields, then, for example, you can make a directive, that is aware of all your possible field restrictions ('required', 'ng-maxlength', 'url', etc.) and is responsible for displaying appropriate error messages. So you will need to create tests only for this directive.
Example:
<input type="text" ng-model="data.username" my-directive name="username" ng-maxlength="15" required />
myDirective is aware of required and ng-maxlength restrictions, that were put on the field, & it is responsible for displaying appropriate error messages for invalid state of the field.

Angular form that contains nested directive not validating

I have a very simplified bit of html like so:
<call-panel>
<form name="hotListForm" >
<twilio>
<input type="text" name="phoneOtherText" id="phoneOtherText" class="form-control" required="required" ng-model="phoneNumber" ng-pattern="validation" ng-trim="false"/>
<span class="error" ng-show="hotListForm.phoneOtherText.$error.pattern">Not a valid phone number</span>
</twilio>
</form>
</call-panel>
But this doesn't work. Is there a special way to do this when in a nested directive. I have verified pattern is available.
Screenshot of it definitely being in the form and of form structure:
It is not working probably because you have the id set to "phoneOtherText" as well, try changing the id and try

angularjs ng-minlength validation is not working, form still being submitted

I am trying to submit the form on only successful validation.
validation is working for required but not working for ng-minlength
form input is invalid but form is still being submitted.
<form name="myForm" ng-submit="count = count + 1" ng-init="count=0" ng-app>
<div class="control-group" ng-class="{error: myForm.mobile.$invalid}">
<label class="control-label" for="mobile">Mobile</label>
<div class="controls">
<input type="text" name="mobile" placeholder="07XXXXXXXXX" ng-model="mobile" ng-minlength="11" required />
<span ng-show="myForm.mobile.$error.required" class="help-inline">Required</span>
<span ng-show="myForm.mobile.$error.minlength" class="help-inline">Mobile number should be minimum 11 character starting from 07</span>
</div>
</div>
<div class="control-group">
<div class="controls">
<input class="btn" type="submit" value ="submit" />
</div>
count: {{count}}<br />
<tt>myForm.$invalid = {{myForm.$invalid}}</tt><br/>
</div>
</form>
http://jsfiddle.net/pMMke/9/
what am I doing wrong.
I don't want to use submit button disable method.
This is what you are doing wrong: you are mixing two concepts, Angular validators and
HTML5 validators.
The required HTML5 validators, for instance, states that:
When present, it specifies that an input field must be filled out before submitting the form.
So, if you try to submit a form that has an input with this attribute, it will show a message explaining this to the user, and it will prevent the form from being sent. This is the behavior you want. Why isn't working for ng-minlength? Because ng-minlength is an Angular validator (you can tell because it begins with ng-), and it doesn't add any special behavior to the form. It simply set the input where it is located to invalid (and hence, the form), and let you decide what to do with it.
You have an option: you can use the pattern HTML5 validator, to specify the field requires at least 11 characters. It would like this:
<input type="text" pattern=".{11,}">
So when you submit a form containing this input, it will no be sent if the user has enter less than 11 characters.
But since we are it, and you are already using the pattern validator, you could use the regular expression in its full potential, and define something like:
<input type="text" pattern="07[0-9]{9}" />
Which will only admit values of 11 characters, that start by "07" and that contains only digits. I have modified your fiddle to show you how it would work: http://jsfiddle.net/helara/w35SQ/
I mistakenly used ngMaxlength="12" ngMinlength="6" instead of ng-minlength="6" ng-maxlength="12", it's working fine now.
Both ng-minlength & mg-maxlength works in AngularJS.
I've tested this in AngularJS version 1.3.
Make sure to use novalidate with <form> to disable browser's native validation.
This should work:
To enter mobile number
ng-show="myForm.mobile.$touched && myForm.mobile.$error.required"
For minimum length
ng-show="myForm.mobile.$touched && myForm.mobile.$error.minlength"
For maximum length
ng-show="myForm.mobile.$touched && myForm.mobile.$error.maxlength"
This work for me guys
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input ng-minlength="11" class="mdl-textfield__input" type="text" name="cpf" id="cpf" ng-model="avaliacao.cpf" ng-required="true" ng-pattern="/^\d+$/">
<label class="mdl-textfield__label" for="cpf">CPF *</label>
</div>
<p style="color: #d50000;" ng-show="myForm.cpf.$error.required && myForm.cpf.$dirty">Field Required</p>
<p style="color: #d50000;" ng-show="myForm.cpf.$error.pattern">Only numbers</p>
<p style="color: #d50000;" ng-show="myForm.cpf.$error.minlength">Min 11 Chars</p>
I'm facing the same issue, and I think you can only disable the button or ignore the entered value by yourself. You can also check the $valid property in your controller and ignore the value... It is not so nice, but I found no other way.

Resources