Easy AngularJS Forms with angular-formly

Free Course

Getting Started with Angular 2

Angular 2 is the shiny new framework that comes with a lot of new concepts. Learn all the great new features.

A common problem developers face when creating AngularJS applications is that their HTML can tend to become bloated. HTML is great, but a frustrating part of working on AnguarJS apps for me is when I need to read through a page full of messy markup to figure out what’s happening in the app. This tends to especially show up in forms since a complete HTML form will have many tags, and with Angular can have many attributes to make use of Angular’s directives.

One way we can clean up our forms and have more readable HTML is to use an open source module called angular-formly which is maintained by Kent C. Dodds. It allows us to define our form elements in an array of objects instead of explicitly in an HTML document. This offers many benefits, including more maintainable and DRY code, easier validation, and a better way to accomplish custom form behavior.

The angular-formly site gives us a great example of how we can benefit right off the bat. Using the module we can go from this:

<form>
    <div class="form-group">
        <label for="exampleInputEmail1">Email address</label>
        <input type="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email" ng-model="vm.user.email">
    </div>
    <div class="form-group">
        <label for="exampleInputPassword1">Password</label>
        <input type="password" class="form-control" id="exampleInputPassword1" placeholder="Password" ng-model="vm.user.password">
    </div>
    <div class="form-group">
        <label for="exampleInputFile">File input</label>
        <input type="file" id="exampleInputFile" ng-model="vm.user.file">
        <p class="help-block">Example block-level help text here.</p>
    </div>
    <div class="checkbox">
        <label>
        <input type="checkbox" ng-model="vm.user.checked"> Check me out
        </label>
    </div>
    <button type="submit" class="btn btn-default" ng-click="vm.submit(vm.user)">Submit</button>
</form>

to this:

<formly-form model="vm.user" fields="vm.userFields">
    <button type="submit" class="btn btn-default" ng-click="vm.submit(vm.user)">Submit</button>
</formly-form> 

What We’ll Build

In this tutorial we’re going to be building a form for a car rental company to take reservation information from users. We’ll put a bunch of angular-formly’s features to use, including conditionally showing fields and doing custom validation.

angular-formly-1

Installing the Dependencies

To get started we will need to install angular-formly and some dependencies. I’ll be using npm to grab the modules, but you can also use bower if you like.

Create a new directory called angular-formly and then from the command line:

npm install angular-formly angular-formly-templates-bootstrap bootstrap api-check

This will create a node_modules folder in which will be the AngularJS framework, angular-formly, the angular-formly Bootstrap templates, Bootstrap and finally API Check. The angular-formly Bootstrap templates are custom field templates that allow us to plug directly into Bootstrap’s behavior for our forms. The module itself doesn’t ship with field templates, but they are available for a number of CSS frameworks. API Check gives us better feedback in the console about any errors we make when we create our forms.

Creating the File Structure

Next, let’s create our project files. We’ll need our standard AngularJS app.js file along with a MainController.js for our controller, a province.js for our service, a style.css for our custom styles and of course index.html for the main page. Your folder structure should look like this:

|-- angular-formly
    |-- css
        style.css
    |-- node_modules
        * all of our dependencies
    |-- scripts
        MainController.js
        province.js     
    app.js
    index.html

Setting Up the Initial Files

Let’s setup our app.js file in which we’ll declare formly and formlyBootstrap as our dependencies:

// app.js

(function() {

    'use strict';

    angular.module('formlyApp', ['formly', 'formlyBootstrap']);

})();

Next, let’s create our initial HTML file with the basic structure:

<!-- index.html -->

<!DOCTYPE html>
<html>
<head>
    <title>angular-formly</title>
    <link rel="stylesheet" type="text/css" href="css/style.css">
    <link rel="stylesheet" type="text/css" href="node_modules/bootstrap/dist/css/bootstrap.css">
</head>
<body ng-app="formlyApp" ng-controller="MainController as vm">

    <div class="container col-md-4 col-md-offset-4">

    </div>  

</body>

    <!-- Application Dependencies -->
    <script src="node_modules/api-check/dist/api-check.js"></script>
    <script src="node_modules/angular/angular.js"></script>
    <script src="node_modules/angular-formly/dist/formly.js"></script>
    <script src="node_modules/angular-formly-templates-bootstrap/dist/angular-formly-templates-bootstrap.js"></script>

    <!-- Application Scripts -->
    <script src="app.js"></script>
    <script src="scripts/MainController.js"></script>
    <script src="scripts/province.js"></script>
</html>

We’ll also need a controller which we’ll use to communicate data to our view. Let’s use MainController.js for this:

// scripts/MainController.js

(function() {

    'use strict';

    angular
        .module('formlyApp')
        .controller('MainController', MainController);

    function MainController() {

        var vm = this;

    }

})();

We’re going to need a list of Canadian provinces and territories for part of our form and this is best handled by using a service. Let’s make a province.js file that has one method, getProvinces, which is responsible for returning an array of province/territory objects:

// scripts/province.js
(function(){

    'use strict';

    angular
        .module('formlyApp')
        .factory('province', province);
        
        function province() {
            function getProvinces() {
                return [
                    {
                        "name": "Alberta",
                        "value":"alberta"
                    },
                    {
                        "name":"British Columbia",
                        "value":"british_columbia"
                    },
                    {
                        "name":"Manitoba",
                        "value":"manitoba"
                    },
                    {
                        "name":"New Brunswick",
                        "value":"new_brunswick"
                    },
                    {
                        "name":"Newfoundland and Labrador",
                        "value":"newfoundland_and_labrador"
                    },
                    {
                        "name":"Northwest Territories",
                        "value":"northwest_territories"
                    },
                    {
                        "name":"Nova Scotia",
                        "value":"nova_scotia"
                    },
                    {
                        "name":"Nunavut",
                        "value":"nunavut"
                    },              
                    {
                        "name":"Ontario",
                        "value":"ontario"
                    },
                    {
                        "name":"Prince Edward Island",
                        "value":"prince_edward_island"
                    },
                    {
                        "name":"Quebec",
                        "value":"quebec"
                    },
                    {
                        "name":"Saskatchewan",
                        "value":"saskatchewan"
                    },
                    {
                        "name":"Yukon",
                        "value":"Yukon"
                    },
                ];
            }

            return {
                getProvinces: getProvinces
            }
        }
        
})();

Finally, here’s some custom CSS that will give our form a nicer look:

/* css/style.css */

.container {
    background-color: #f2f2f2;
    border: 1px solid #ccc;
    padding: 20px;
    top: 20px;
    -webkit-box-shadow: 0 2px 2px 1px rgba(0,0,0,0.2);
    box-shadow: 0 2px 2px 1px rgba(0,0,0,0.2);
}

Setting Up the Form

To setup our angular-formly form, we only need a few lines of HTML. The angular-formly directive is accessed through a custom element called <formly-form> which is (optionally) placed inside a regular form element. Let’ set this up in our index.html file:

<!-- index.html -->

...

<form novalidate>
    <h1>Rent-a-Car, eh?</h1>
    <formly-form model="vm.rental" fields="vm.rentalFields" form="vm.rentalForm">
        <button type="submit" class="btn btn-primary" ng-disabled="vm.rentalForm.$invalid">Submit</button>
    </formly-form>
</form>

...

We start with a regular form element that encloses the special formly-form directive element. On formly-form we need to declare a few attributes, the first of which is model. This works similar to ng-model in that now anything we type into the form fields will populate an object called vm.rental. We can manipulate model to pre-fill fields by assigning properties to it in the controller, but we don’t want that in our case.

Next up is the fields attribute in which we reference an array of field objects in our controller that tell our formly-form what form fields we want. We’ll see how this works in more detail once we get to the controller.

We also want an attribute called form which is used to give our form a name. This form name can then be hooked into the submit button which is inside the formly-form so that we can check whether anything on the form is invalid and disable the button if that is the case. You’ll see that on the submit button we are using ng-disabled and passing to it the form name, vm.rentalForm, and calling the $invalid property on it. $invalid is part of Angular’s FormController and it is true if anything on the form is invalid and false if everything checks out.

That’s all the HTML we need! Let’s now move onto the controller.

Setting Up the Controller

We’ll need to add a couple things to our controller, the first being an object called vm.rental which is referenced on the formly-form tag. Since we don’t want to have any of the fields pre-populated, we’ll leave the object empty.

The next thing we’ll need is an array called vm.rentalFields which will have a number of objects that describe what fields we want our form to have. Our car rental form will require users to enter their first name, last name and email to proceed so let’s define those first.

// scripts/MainController.js

...

function MainController(province) {

    var vm = this;

    // The model object that we reference
    // on the  element in index.html
    vm.rental = {};
    
    // An array of our form fields with configuration
    // and options set. We make reference to this in
    // the 'fields' attribute on the  element
    vm.rentalFields = [
        {
            key: 'first_name',
            type: 'input',
            templateOptions: {
                type: 'text',
                label: 'First Name',
                placeholder: 'Enter your first name',
                required: true
            }
        },
        {
            key: 'last_name',
            type: 'input',
            templateOptions: {
                type: 'text',
                label: 'Last Name',
                placeholder: 'Enter your last name',
                required: true
            }
        },
        {
            key: 'email',
            type: 'input',
            templateOptions: {
                type: 'email',
                label: 'Email address',
                placeholder: 'Enter email',
                required: true
            }
        },
    ];
    
}

...

Let’s see what’s happening in the vm.rentalFields array:

  • key: Form field objects need a unique key which should generally describe what the field is for
  • type: Any of the form types such as input, textarea, select, radio etc. Types need to be registered with the formlyConfig service/provider (done for us already with the Bootstrap templates)
  • templateOptions: an object that has properties which further break down options for the field, including what kind of input the form element is going to take, what the label and placeholder should be and whether the field is required.

So far we’ve got three fields on our form. If you run the app in your browser, you should see this:

angular-formly-2

Hiding Form Fields

Declaring ng-if in our HTML to conditionally add or remove elements is pretty easy, but can get cumbersome. Let’s see how we can easily accomplish the same thing with angular-formly.

Our car renters will need to tell us whether they are under 25 years of age, and if they are, will need to provide extra insurance information. They will also need to tell us which province or territory they live in. Let’s only show these fields if the user has entered their email address so that we can get the feel of progressing through the form small bits at a time.

We’ll need a checkbox for asking the user if they are under 25, a select box to list the provinces and territories, and an input box for the extra insurance information.

// scripts/MainController.js

    ...
    
    {
        key: 'under25',
        type: 'checkbox',
        templateOptions: {
            label: 'Are you under 25?',
        },
        // Hide this field if we don't have
        // any valid input in the email field
        hideExpression: '!model.email'
    },
    {
        key: 'province',
        type: 'select',
        templateOptions: {
            label: 'Province/Territory',
            // Call our province service to get a list
            // of provinces and territories
            options: province.getProvinces()
        },
        hideExpression: '!model.email'
    },
    {
        key: 'insurance',
        type: 'input',
        templateOptions: {
            label: 'Insurance Policy Number',
            placeholder: 'Enter your insurance policy number'
        },
        hideExpression: '!model.under25 || !model.province',
    }

    ...

We’ve got a new property on the objects now called hideExpression. We can define an expression in the form of a string here that will apply an ng-if that will hide the form field if the expression is truthy and show it if it is falsey. In this case we need to check if the email field is filled out properly—our hideExpression is '!model.email' which essentially says if the email field isn’t filled out properly, hide the current form field.

We’ve created a select form to pick our province/territory from a drop-down and we need to populate it with options. We could do this in a number of ways, a common one being that we just declare an array of objects right on the options key, but what if we wanted to pull the data for the drop-down from an external source? To simulate that, we use our province service that we defined earlier and call the getProvinces() method on it. This method returns an array of province/territory objects that have a name and value key.

If you check back and fill in the email address field, you should see the checkbox and select box showing up. Also, if you check the box to say that you are under 25, the Insurance Policy Number field will show up.

angular-formly-3

Adding Custom Validation to Form Fields

Custom validation with angular-formly’s API is easy—we are able to define the custom validator in a function right on the field configuration object. We’re going to use the driver’s license input field to get a sense of how this works.

The pattern for driver’s license numbers differs from one provice/territory to the next and it would be nice if we could perform a validation check that is unique to each. It would be quite lengthy for this tutorial to apply this to every province and territory (plus I don’t know the patterns for them all), so we’re only going to deal with Ontario for now. The pattern for driver’s license numbers in Ontario is a letter followed by 14 numbers with dashes in between (A1234-12345-12345).

// scripts/MainController.js

    ...
    
    {
        key: 'license',
        type: 'input',
        templateOptions: {
            label: 'Driver\'s License Number',
            placeholder: 'Enter your drivers license number'
        },
        hideExpression: '!model.province',
        validators: {
            // Custom validator to check whether the driver's license
            // number that the user enters is valid or not
            driversLicense: function($viewValue, $modelValue, scope) {
                var value = $modelValue || $viewValue;
                if(value) {
                    // call the validateDriversLicense function
                    // which either returns true or false
                    // depending on whether the entry is valid
                    return validateDriversLicence(value)
                }
           }
        }
    },
    
    ...
    
    // Tests the input based on a helpful regular expression
    // gratefully borrowed from jQuery.formance by Omar Shammas
    // https://github.com/omarshammas/jquery.formance
    function validateDriversLicence(value) {
        return /[A-Za-z]\d{4}[\s|\-]*\d{5}[\s|\-]*\d{5}$/.test(value);
    }
            
    ...

Here we’ve created a custom validator called driversLicense on the validators key. The custom validator is a function which takes $viewValue, $modelValue and scope. Our form input is going to be equal to $modelValue (or $viewValue if the field is not yet valid). We need the custom validator to return true if the input is valid and false if it is invalid and to do this we call our validateDriversLicense function which runs the input value through a regular expression test to check for validity.

Now if our driver’s license input doesn’t match the pattern for an Ontario license, the form will show that what we’ve entered is invalid. Note: This tutorial won’t get into custom error messages, but angular-formly has excellent support for them.

angular-formly-4

Disabling Form Fields

Like we noted above, right now our form only knows the correct pattern for Ontario driver’s licenses so we should disable the driver’s license form for other provinces. We can do this easily with angular-formly by defining an expressionProperties key.

// scripts/MainController.js

    ...
    
    {
        key: 'license',
        type: 'input',
        templateOptions: {
            label: 'Driver\'s License Number',
            placeholder: 'Enter your drivers license number'
        },
        hideExpression: '!model.province',
        validators: {
           driversLicense: function($viewValue, $modelValue, scope) {
                var value = $modelValue || $viewValue;
                if(value) {
                    return validateDriversLicence(value)
                }
           }
        },
        expressionProperties: {
            // We currently only have a driver's license pattern for Ontario
            // so we need to disable this field if we've picked a province/territory
            // other than Ontario
            'templateOptions.disabled': function($viewValue, $modelValue, scope) {
                if(scope.model.province === 'ontario') {
                    return false;
                }
                return true;
            }
        }
    },
    
    ...

If we wanted to do a simple conditional to disable a form field, we would define a key called disabled on the templateOptions object. Since we are doing something dynamic here, we need to use expressionProperties and reference templateOptions.disabled as a string. This key has a function in which we tap into the value that is set on the currently selected province/territory. We access this value by calling scope.model.province and check wether the value is ‘ontario’. If it is we return false because we want the field to be enabled if the selected province is Ontario. In all other cases we return true to disable the field.

angular-formly-5

Wrapping Up

Hopefully through this tutorial you’ve been able to see the value of using angular-formly. It’s much easier to read an array of objects that represents our form configuration rather than a pile of messy HTML. We’ve drastically reduced the amount of HTML needed for our car rental form and have set ourselves up to have a much more maintable form.

The capabilities of angular-formly go well beyond what we’ve seen here. Check out the official angular-formly site to find out more and get in touch with the module’s maintainer, Kent C. Dodds for any questions or help.

Drop Me a Line!

If you’d like to get more AngularJS tutorials, feel free to head over to my website and signup for my mailing list. You should follow me on Twitter—I’d love to hear about what you’re working on!

Ryan Chenkie

Tech Writer at Auth0 where I create tutorials on the latest web technologies. I also write about Angular, Laravel and more on my site. Say hi to me Twitter!