Angular Form Validation Basics

Angular Form Validation Basics

Form validation refers to the ability to check the status of the inputs your user gave you to make sure everything is as expected. For example, did they try to submit a phone number of "1" instead of a valid phone number? Does the email address have the required @ symbol? Have they filled out all of the required fields?

Form validation outside of Angular can be a bit daunting. Lots of your code-writing time is spent checking the length of the input they gave you, making sure the data they gave you is valid, etc. Then you have to add some kind of message or error class to show the user they did it wrong, then remove that class/message when they fix it, etc.

In Angular, we're given some excellent tools to help make this process a lot smoother. This post will cover some of those tools.

To start, you may want to check out Scotch.io's excellent follow-alongs on AngularJS Form Validation and Form Validation w/ ngMessages. Much of what we cover here is adapted from their content.

What we're building

We'll be building a basic sign-up form. It will have the following fields and requirements:

  • First name - optional
  • Last name - optional
  • Email address - required, must be valid email address
  • Phone Number - optional
  • Password - required, must be longer than 7 characters
  • Password confirmation - required, must match the first password
  • Button for submitting the info, which should just log to the console all of the person's info. (Obviously not what you'd really want to do with this info, but that's the topic for another post.)

We'll also be using bootstrap for our styling and layout so our form looks good.

Starting Point

Let's get some code set up as a starting point. Everything in here is basic HTML form stuff and the generic Angular setup. If you're unfamiliar with anything in this boilerplate code, you should probably study up on HTML forms and/or Angular first before moving on.

index.html:
<!DOCTYPE html>
<html lang="en" ng-app="ValidationPractice">

<head>
    <meta charset="UTF-8">
    <title>Signup Form</title>
    
    <!--  Bring in Bootstrap from the CDN  -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
    
    <!--  Add our own styles  -->
    <link rel="stylesheet" href="styles.css">
</head>

<body ng-controller="FormController" class="container">
    <section class="row">
        <div class="col-md-4 col-md-offset-4 well">
            <form name="validateForm" novalidate> <!-- We add "novalidate" so HTML won't validate for us automatically. -->
                <div class="form-group col-md-12">
                    <input type="text" name="firstName" class="form-control" placeholder="First Name" ng-model="person.firstName">
                </div>

                <div class="form-group col-md-12">
                    <input type="text" name="lastName" class="form-control" placeholder="Last Name" ng-model="person.lastName">
                </div>

                <div class="form-group col-md-12">
                    <input type="email" name="email" class="form-control" placeholder="Email" ng-model="person.email" required>
                </div>

                <div class="form-group col-md-12">
                    <input type="tel" name="phone" class="form-control" placeholder="Phone Number" ng-model="person.phone">
                </div>

                <div class="form-group col-md-12">
                    <input type="password" name="password" class="form-control" placeholder="Password" ng-model="person.password" required>
                </div>

                <div class="form-group col-md-12">
                    <input type="password" name="passwordRepeat" class="form-control" placeholder="Password Repeat" ng-model="person.passwordRepeat" required>
                </div>

                <button class="btn btn-primary btn-lg center-block" ng-click="addBadge(person)">Submit</button>
            </form>
        </div>
    </section>

    <!--  AngularJS from the CDN  -->
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.js"></script>
    
    <!--  Our own JS  -->
    <script src="app.js"></script>
</body>

</html>

######app.js:
var app = angular.module("ValidationPractice", []);

app.controller("FormController", ["$scope", function($scope) {
    
    $scope.logPersonInfo = function(person) {
        // More to come
    }
    
}]);

A few important points about the above code:

  • We added novalidate to the form. This prevents the default HTML5 built-in validations from running, since we'll be making our own.
  • ng-model is already applied to each input. It is building a person object with the appropriate properties on the fly for us.
  • The email, password, and passwordRepeat fields are required with the built-in HTML required attribute added to the element.

Important Angular form and input properties

Angular automagically sets properties on each input type that allows us to access more information about the state of that input and the state of the form based on the user's interaction with the form. In other words, Angular gives us a way to know more information about how the user has interacted with the form, such as "is the current input valid according to your rules?", "has the user typed anything into this input box yet?", and "has the user entered and left this input box already?". These give us tons of flexibility to set our styles and messages at exact the times we want.

Here are the properties Angular adds to the form and each input, along with their descriptions:

  • $valid - Boolean that tells you if the form or input is valid based on the rules you've set
  • $invalid - Boolean that tells you if the form or input isn't valid based on the rules you've set. Simply the exact opposite of $valid.
  • $dirty - Boolean that tells you if the user has already interacted with the form or a individual input (typed something in, checked a checkbox, etc.). Remains true even if they delete what they typed or uncheck the box, etc.
  • $pristine - Boolean that tells you if the user hasn't yet interacted with the form or individual input. It will remain false even if they delete what they typed or uncheck the box, etc. Simply the exact opposite of $dirty.
  • $touched - Boolean turns to true if the input undergoes a "blur" event, meaning the user has clicked into the input box and then clicked out of it, whether they type something or not.
Accessing these properties:

In vanilla JS, we're used to using something like document.<form name> or document.getElementById("my-form"). In Angular, they make it slightly simpler by allowing you to just access the form with <form name> and access the input fields with <form name>.<input name>. So you need to make sure to give your form and inputs a name property to make this easier.

Once you've accessed the form or input, you just add the property listed above to get the boolean value of that property. For example:

myForm.firstName.$touched will tell you whether or not the first name field sent off a blur event at some point.

Quick validation rules

Angular gives us some directives to make certain validation rules very easy for us to implement:

  • ng-minlength sets the minimum length allowed for an input, meaning as long as the user's input is shorter than the specified minlength, it is not valid. (E.g.: We can add ng-minlength="7" on our password field, which sets the $invalid property to true (and the $valid property to false) until 7 or more characters are typed into the input box, at which time it flips those.
<!-- index.html -->
...

<!-- Password input field -->
<input type="password" name="password" class="form-control" placeholder="Password" ng-model="person.password" ng-minlength="7" required>

Cool things you can do now that you have the right tools

Disable the submit button

Because we set some validation rules (email address is required and must be valid, password must be 7 characters or longer), our whole form (and those specific fields) are considered "invalid" until they are fixed. This means that our validateForm.$invalid is set to true until the user fixes those issues. So we can use the ng-disabled directive to disable our button until the issues are fixed, preventing the user from submitted incomplete or invalid data.

<!-- index.html -->
...

<!-- Submit Button -->
<button class="btn btn-primary btn-lg center-block" ng-click="addBadge(person)" ng-disabled="validateForm.$invalid">Submit</button>

disabled submit button

Add helpful messages when the user did it wrong

Because we can test the current state of the form and the input fields, we can show and hide helpful messages to the user when they're putting in the information incorrectly.

Using Angular's ng-show directive and the validation properties set on our form and inputs, we can make it so a help message only shows to the user when something seems wrong. For example, if the email field is invalid.

However, it'd be a bit annoying to have the messages show up immediately when you load the page. To avoid this, we can add a conditional to the ng-show telling it to only show the message if the email field is invalid and if they've already focused in on the field and then blurred away (remember $touched?), since that would likely indicate they think they did everything right and they're moving on to the next input. Once $touched flips to true and the email field is invalid, then we feel the obligation to show them the error/help message. And because of our strict && adding the text, if they go back and fix the problem (make the input valid), the message will go away.

We can put any kind of conditional checks in the ng-show directive, including checking that $scope properties are doing what we want them to. For example, we can check that the user input the password repeat correctly by checking that person.passwordRepeat is the

<!-- index.html -->
...

<!-- Email Input Field -->
<div class="form-group col-md-12">
    <input type="email" name="email" class="form-control" placeholder="Email" ng-model="person.email" required>
    <p ng-show="validateForm.email.$invalid && validateForm.email.$touched" class="help-block">Email is required.</p>
</div>

...

<!-- Password and Password Repeat Input Fields  -->
<div class="form-group col-md-12">
    <input type="password" name="password" class="form-control" placeholder="Password" ng-model="person.password" ng-minlength="7" required>
    <p ng-show="validateForm.password.$error.minlength && validateForm.password.$touched" class="help-block">Your password must be at least 7 characters long.</p>
</div>

<div class="form-group col-md-12">
    <input type="password" name="passwordRepeat" class="form-control" placeholder="Password Repeat" ng-model="person.passwordRepeat" required>
    <p ng-show="person.passwordRepeat !== person.password && validateForm.passwordRepeat.$touched" class="help-block">Passwords don't match</p>
</div>
Change the CSS of the input boxes based on their validation properties

Above we talked about how to access the properties added to the form and input objects, which are accessible by using <form-name>.<input-name>.property. Beyond adding those properties, Angular also adds corresponding ng- CSS classes to the HTML element itself, giving us greater flexibility in styling. So when something is $pristine, that element also has a CSS class of ng-pristine as well. When $pristine becomes false, the ng-pristine class gets removed.

This allows us to make style changes to our input boxes, since those classes get added automatically for us. So we can do something like this:

.ng-invalid.ng-touched {
    border-color: red;
}

In other words, if both the invalid and the touched classes are on an element at the same time (which, remember, happens when they've blurred the input box and the stuff inside didn't satisfy our conditions), then add a red border around the input box to make it extra obvious that they have to fix it.

Conditionally adding CSS classes on your own

We don't have to rely solely on the ng-pristine, etc. classes that Angular adds for us. Instead, we can write our own classes and add them based upon the conditions found in the form or input properties discussed above using Angular's ng-class directive. The syntax looks like this:

ng-class="{ 'class-name' : conditional expression evaluating to true/truthy or false/falsy }"

If the conditional evaluates to true (or something truthy), it applies the class specified. Otherwise it removes it. So with our email field, we can use the same conditions we used to apply the help text and apply the built-in Bootstrap .has-error class to the entire div surrounding the input, which will style our input box with a red border and our help text with red colored text:

<div class="form-group col-md-12" ng-class="{'has-error' : validateForm.email.$invalid && validateForm.email.$touched}">
    <input type="email" name="email" class="form-control" placeholder="Email" ng-model="person.email" required>
    <p ng-show="validateForm.email.$invalid && validateForm.email.$touched" class="help-block">Email is required.</p>
</div>

Awesome! Have you noticed what your JS file looks like so far? We haven't even had to touch the JS file yet because Angular has built in so many great validation features for us. We've done all of our validation in the HTML, which makes sense since the only purpose of our validation is to change what gets displayed to our user in the view based on the interaction they're having with our form. Now, someone reading through our HTML file can get all the contextual information about what should be happening from the HTML page.

Here's what our complete HTML page looks like right now:

<!DOCTYPE html>
<html lang="en" ng-app="ValidationPractice">

<head>
    <meta charset="UTF-8">
    <title>Signup Form</title>
    
    <!--  Bring in Bootstrap from the CDN  -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
    
    <!--  Add our own styles  -->
    <link rel="stylesheet" href="styles.css">
</head>

<body ng-controller="FormController" class="container">
    <section class="row">
        <div class="col-md-4 col-md-offset-4 well">
            <form name="validateForm" novalidate> <!-- We add "novalidate" so HTML won't validate for us automatically. -->
                <div class="form-group col-md-12">
                    <input type="text" name="firstName" class="form-control" placeholder="First Name" ng-model="person.firstName">
                </div>

                <div class="form-group col-md-12">
                    <input type="text" name="lastName" class="form-control" placeholder="Last Name" ng-model="person.lastName">
                </div>

                <div class="form-group col-md-12" ng-class="{'has-error' : validateForm.email.$invalid && validateForm.email.$touched}">
                    <input type="email" name="email" class="form-control" placeholder="Email" ng-model="person.email" required>
                    <p ng-show="validateForm.email.$invalid && validateForm.email.$touched" class="help-block">Email is required.</p>
                </div>

                <div class="form-group col-md-12">
                    <input type="tel" name="phone" class="form-control" placeholder="Phone Number" ng-model="person.phone">
                </div>

                <div class="form-group col-md-12">
                    <input type="password" name="password" class="form-control" placeholder="Password" ng-model="person.password" ng-minlength="7" required>
                    <p ng-show="validateForm.password.$error.minlength && validateForm.password.$touched" class="help-block">Your password must be at least 7 characters long.</p>
                    <p ng-show="validateForm.password.$touched && validateForm.password.$invalid" class="help-block">Password is required.</p>
                </div>

                <div class="form-group col-md-12">
                    <input type="password" name="passwordRepeat" class="form-control" placeholder="Password Repeat" ng-model="person.passwordRepeat" required>
                    <p ng-show="person.passwordRepeat !== person.password && validateForm.passwordRepeat.$touched" class="help-block">Passwords don't match</p>
                </div>

                <button class="btn btn-primary btn-lg center-block" ng-click="addBadge(person)" ng-disabled="validateForm.$invalid">Submit</button>
            </form>
        </div>
    </section>

    <!--  AngularJS from the CDN  -->
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.js"></script>
    
    <!--  Our own JS  -->
    <script src="app.js"></script>
</body>

</html>

Conclusion

We'll be writing a second post to continue exploring even more Angular form validation abilities, but for now this should give you an excellent head-start on doing some very useful client-side validation to your forms.