Panther Software

A Developer's Adventures in Coding

A Reusable Bootstrap Modal AngularJS Directive for Forms

Getting Started – The Form Directive.

My basic goal is to create an AngularJS Directive that:

  • presents a consistent ui interface
  • has configurable attributes for the form’s submit, cancel, title and body content
  • modals are instantiated and displayed when required via a click event

Modal Form Template.

templates/form_modal.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div class="modal hide fade">
  <div class="modal-header">
    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
    <h4>{{title}}</h4>
  </div>
  <form name="form" ng-submit="submit()" style="margin-bottom: 0;">
    <div class="modal-body">
      <div ng-include="template"></div>
    </div>
    <div class="modal-footer">
      <button class="btn" ng-click="close()">Cancel</button>
      <input type="submit" id="submit" class="btn btn-primary" value="{{okButtonText}}"/>
    </div>
  </form>
</div>

templates/form_modal.html contains a standard Bootstrap Modal.

Note:

  • form submit is via ng-submit="submit()" is first consumed by the directive and then delegated to a controller
  • <div class="modal-header"> is configurable via {{title}}
  • the form’s submit button display value="{{okButtonText}}" is configurable
  • <div class="modal-body"> is specified by a partial via <div ng-include="template"></div>

Modal Form Content.

I’ll use a login partial as the form’s content:

sessions/login.html
1
2
3
4
5
6
7
8
9
<fieldset>
  <legend>Login</legend>

  <label>Username</label>
  <input type="text" ng-model="formObject.username" placeholder="Your Username" required/>

  <label>Password</label>
  <input type="password" ng-model="formObject.password" placeholder="Your Password" required/>
</fieldset>

My goal is to include sessions/login.html inside templates/form_modal.html and instantiate and present the login form when a user clicks a login link or button.

The Directive.

As a reminder, the goal of this directive is to process the following:

form-modal directive in use
1
2
3
4
5
6
7
8
9
10
11
12
  <div ng-controller="someController">
    ...
    <a href="#" form-modal
                template="sessions/login.js"
                title="Login"
                ok-button-text="Login"
                form-submit="login()"
                form-object="user">
      Login
    </a>
    ...
  </div>

When a user clicks on Login link, the directive will:

  • load the partial from sessions/login.js and wrap around it the bootstrap modal form template defined in templates/form_modal.html
  • the form’s model is bound to someController.user as specified by the form-object="user" attribute
  • when the form is submitted, the directive will delegate the submit to someController.login() function as specified by the form-submit="login()" attribute

Directive Challenge 1 – Scope Isolation.

By default, directives inherit the current controller’s scope. Normally this isn’t an issue but it can become undesirable in instances where re-usable components are to be defined. The main reason for this is that the directive could inadvertently clobber any similarly named variables in the parent scope chain.

This is why we’ll be isolating the directive’s scope:

form-modal Directive
1
2
3
4
5
6
7
8
9
10
11
12
angular.module('yourAppDep').directive('formModal', [function() {
  return {
    scope: {
      formObject: '=',
      formErrors: '=',
      title: '@',
      template: '@',
      okButtonText: '@',
      formSubmit: '&'
    }
  }
}]);
  • formObject and formErrors attributes define two-way binding between the parent controller and the directive.
  • title and okButtonText define replacement strings for the form’s template
  • template points to a URL which defines the modal’s body content
  • formSubmit defines a function delegate

Directive Challenge 2 – Loading the Directive’s Template.

We can’t use the directive’s template nor templateUrl object definition as templates/form_modal.html has content that itself needs evaluating (namely: {{title}}; value="{{okButtonText}}" on the submit button; and the ng-include template for the modal’s body ).

From AngularJS Directive page:

The compile function deals with transforming the template DOM.

The modified Directive with a Compile function:

form-modal Directive
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
angular.module('yourAppDep').directive('formModal', ['$http', function($http) {
  return {
    scope: {
      formObject: '=',
      formErrors: '=',
      title: '@',
      template: '@',
      okButtonText: '@',
      formSubmit: '&'
    },
    compile: function(element, cAtts){
      var template;

      $http.get('templates/form_modal.html')
        .success(function(response) {
          template = response;
        });

    }
  }
}]);

Note the injected $http service, which will be used to fetch the directive’s base template.

The directive now has the template. Now what?

Directive Challenge 3 – Compiling and Linking the Template

A compile function can have a return value which can be either a function or an object. Returning a function – is equivalent to registering the linking function via the link property of the config object when the compile function is empty.

Update the directive to return a linking function, which will transform the template, inject the requested template="sessions/login.js" into the modal form and finally instantiate the modal.

The returned Link function will process templates/form_modal.html to:

  • define the submit() function in <form name="form" ng-submit="submit()">
  • define the close() function in <button class="btn" ng-click="close()">Cancel</button>
  • instantiate and show the bootstrap modal form

Let’s add these:

form-modal Directive
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
angular.module('yourAppDep').directive('formModal', ['$http', '$compile', function($http, $compile) {
  return {
    scope: {
      ...
    },
    compile: function(element, cAtts){
      var template,
        $element,
        loader;

      loader = $http.get('templates/form_modal.html')
        .success(function(data) {
          template = data;
        });

      //return the Link function
      return function(scope, element, lAtts) {
        loader.then(function() {
          //compile templates/form_modal.html and wrap it in a jQuery object
          $element = $( $compile(template)(scope) );
        });

        //called by form_modal.html cancel button
        scope.close = function() {
          $element.modal('hide');
        };

        //called by form_modal.html form ng-submit
        scope.submit = function() {
          var result = scope.formSubmit();

          if (Object.isObject(result)) {
            result.success(function() {
              $element.modal('hide');
            });
          } else if (result === false) {
            //noop
          } else {
            $element.modal('hide');
          }
        };

        element.on('click', function(e) {
          e.preventDefault();
          $element.modal('show');
        });
      };
    }
  }
}]);

Note:

  • the loader variable holds the $http get promise. We will compile the fetched template once the promise is resolved in the Link function.
    • $compile the template using the directive’s scope in $compile(template)(scope)
    • wrap it in a jQuery object and assign it to $element
  • add the scope.close function. This is referenced from <button class="btn" ng-click="close()">Cancel</button> in templates/form_modal.html
  • add the scope.submit function. This is referenced from <form name="form" ng-submit="submit()"> in templates/form_modal.html
    • scope.formSubmit() is defined in <a ... form-submit="login()" ...>Login</a>. The login() method is defined inside someController
    • capture the return value from scope.formSubmit() in result to further determine whether the Modal should stay visible (to display errors for example). If scope.formSubmit() returns false or an errored promise then the modal will remain visible
    • if result is undefined or contains a promise that is successful then the modal is destroyed via $element.modal('hide')
  • The last part of the puzzle is capturing the element’s click event to show the modal. This is achieved via element.on('click', fn).

Finally, a Real World Example

I’ve extracted the following from parlmnt.com (github source), which I am in the process of migrating from Backbone to AngularJS.

The site’s navbar contains the following snippet for user registration and logins.

navbar snippet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<ul class="nav pull-right" ng-controller="sessionsController">
  <li ng-show="loggedIn">
    ...
  </li>
  <li ng-hide="loggedIn">
    <ul class="nav">
      <li>
        <a href="#" form-modal template="/templates/sessions/register.html"
                                 title="Register"
                                 ok-button-text="Register"
                                 form-submit="register()"
                                 form-object="user"
                                 form-errors="errors">
          Register
        </a>
      </li>
      <li>
        <a href="#" form-modal template="/templates/sessions/login.html"
                                 title="Login"
                                 ok-button-text="Login"
                                 form-submit="login()"
                                 form-object="user"
                                 form-errors="errors">
            Login
        </a>
      </li>
    </ul>
  </li>
</ul>

For the sake of brevity I will only show /templates/sessions/login.html:

/templates/sessions/login.html
1
2
3
4
5
6
7
8
9
10
11
<fieldset>
  <label>Username</label>
  <input type="text" ng-model="formObject.username" placeholder="Your Username" required/>

  <label>Password</label>
  <input type="password" ng-model="formObject.password" placeholder="Your Password" required/>

  <div class="alert alert-error" ng-show="formErrors.extra">
    <strong>{{formErrors.extra}}</strong>
  </div>
</fieldset>

sessionsController is defined as:

sessionsController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
angular.module('parlmntDeps').controller('sessionsController',
  ['$scope', 'user',  function($scope, user) {

  $scope.loggedIn = false;
  $scope.user = {};
  $scope.errors = {};

  $scope.login = function() {
    _clearErrors();
    return user.login($scope.user)
      .success(_registered)
      .error(_errored);
  };

  ///// TEH PRIVATE

  function _clearErrors(){
    $scope.errors = null;
    $scope.errors = {};
  }

  function _addError(field, message) {
    $scope.errors[field] = message;
  }

  function _errored(response) {
    if (response.errors) {
      Object.each(response.errors, function(field, errors) {
        _addError(field, errors.first())
      });

      if (response.errors.base && Object.isString(response.errors.base)) {
        _addError('extra', response.errors.base)
      }
    }
  }

  function _registered(response) {
    $scope.loggedIn=  true;
    $scope.user = {name: response.username};
  }
}]);

Points of interest:

  • sessionsController.login simply delegates the login to the user service, which returns a promise with either a success or error with code 422 if the login was unsuccessful
  • the directive examines this promise and if it is successful the modal is destroyed. Otherwise an error message is displayed

Conclusion

This is my first week wih AngularJS and the library lives up to its heroic promise.

As a new AngularJS user my first learning curve is gaining an in-depth knowledge of Directives. This is also where I’ve spent most of my time this week.

I do foresee my directive war chest growing as I encapsulate most of common tasks and boilerplate code into reusable components, which will speed up future app development.

Comments