JavaScript Best Practices: Part 5 – Angular UI Router Modals

/ April 27, 2016 in 

Many web apps use Bootstrap modals to draw the user’s attention to important information or to provide user prompts. The most common way to use Angular modals is to use the UI Bootstrap library. This provides a $uibModal service that can be used to open Angular modals and handle any user responses. To open an Angular modal, use the $uibModal.open() function.


$uibModal.open({
  controller: ‘ModalController’,
  controllerAs: ‘modalController’,
  templateUrl: ‘modal.html’
});

In smaller applications, this allows the developer to quickly integrate Bootstrap modals into an Angular app.

But, what happens when you have a large application that opens many modals from many different pages? The $uibModal.open() function calls start spreading throughout your application and become more difficult to manage.

Anuglar UI Router to the Rescue

Angular UI Router is a router library for Angular that is created by the same team that created UI Bootstrap. Angular UI Router allows you to organize an Angular app as a series of states to which the user can navigate like pages in the app. Since the only difference between a modal and a regular page is that the modal is displayed in a dialog box, we can therefore represent the modals as states and navigate to them just like any standard UI Router state.

Integrating Modals as States in Angular UI Router

To show how to integrate modals as states in Angular UI Router, here’s a sample application that can be found at Plunker. This example uses a simple store application to demonstrate the use of modals.

The first step to using states for modals is to create two named sibling views in your base template. A standard Angular UI router view is declared using ui-view.

<ui-view/>

To create two named views, just provide the views with names so we can target them using the router.


<div ui-view="modal"></div>
<div ui-view="store"></div>

Then, in your router, create two sibling states that target those views. The store state will be the parent state for all non-modal states in our application, while all modal states will be child-states of the modal state.


$stateProvider
.state('store', {
   url: '/',
   views: {
     'store': {
       template: ''
     }
   }
 })
.state('modal', {
   abstract: true,
   views: {
     'modal': {
       templateUrl: 'modal.html'
     }
   }
 });

The modal.html template will contain the elements necessary to create a modal in Bootstrap as well as a ui-view that can be used by all child modal states to show the modal content. When any child route is activated, the modal template will get inserted into the DOM and cause the modal to open.


<div class="modal modal-open fade in store--modal">
   <div class="modal-backdrop fade in"></div>
   <div class="modal-dialog">
      <div class="modal-content">
         <a class="modal--close" title="Close">×</a>
      <div>
   </div>
</div>

The default modal view has two issues. First, the modal is still hidden by default. Bootstrap uses JavaScript to show and hide the modal, however, we can fix this with CSS to manually set the modal’s display property to block. Also, the modal-dialog component was being shown behind the modal-backdrop. The z-index of the modal-backdrop is set at 1040 in bootstrap. So, to put the modal-dialog on top, set its z-index to 1050.


.store--modal {
  display: block;
}
.modal-dialog {
  z-index: 1050;
}

To add a modal, simply create a child-state of the modal state in Angular UI Router, a controller for the modal, and a template that contains the modal’s content.


.state('modal.itemDetail', {
    controller: 'ItemDetailController',
    controllerAs: 'itemDetailController',
    templateUrl: 'item-detail-modal.html',
    params: {
      product: null
    }
 })

<div class="modal-header">
   <h1>Item Detail</h1>
</div>
<div class="modal-body">
   <h2>{{itemDetailController.product.name}}</h2>
</div>
<div class="modal-footer"></div>

Any other states are then added to the application as child-states of the store state, complete with their own templates and controllers. Now, to activate a modal, instead of using $uibModal.open(), just use $state.go('modal.itemDetail'), or it can be activated in a link like any other state.


<a ui-sref="modal.itemDetail">Open Modal</a>

Deep State Redirects

The next piece of the puzzle is how to go back to the position in the app when the modal closes. You can do this in two different ways by using the Angular UI Router Extras library. In the UI Router Extras library, we will use Sticky States to allow multiple states to run concurrently. This will allow the modal to activate while not navigating away from the standard store states. The Deep State Redirect feature will be used to navigate back to the activated root states from the parent state. The first things to do are to configure the root store state with both the sticky and deepStateRedirect (dsr) properties. The deepStateRedirect value can be either a simple boolean or a config object to describe default states and other advanced features.


$stateProvider
 .state('store', {
   url: '/',
   views: {
     'store': {
       template: ''
     }
   },
   sticky: true,
   dsr: {
     default: {
       state: 'store.home'
     }
   }
}

With the store route set as both sticky and deepStateRedirect, you can exit any modal and return to the previous state in the app by navigating to the root store state.

$state.go('store') or <a ui-sref="store.home">Close Modal</a>

This will work as long as the deepStateRedirect is just set to true (dsr: true). However, if you use the other dsr features, such as default states, the deepStateRedirect will always go to the default if you go to the root state. To get around this, you can retrieve the deep state that was previously activated by using the $stickyState service. The getInactiveStates() function will return an array with the previously active child-state as the first item in the array.


var inactiveStates = $stickyState.getInactiveStates();
var state = 'store'; // Default State
if (inactiveStates.length > 0) {
   state = inactiveStates[0].name;
}
return state;

Angular Modal Responses

The last remaining feature of $uibModal to implement is the ability to handle user input in the Angular modal. In this case, we can just add approve and reject buttons to the modal. These buttons can then navigate back to the previous state and pass the response as a parameter, which we called modalResponse. To handle the response in the target controller, just add the parameter to the state’s params object. Once the state is activated, you can then check the params to see if the modalResponse has been entered.


.state('store.list', {
    url: 'list',
    controller: 'ListController',
    controllerAs: 'listController',
    templateUrl: 'list.html',
    params: {
      modalResponse: null
    }
})

By using states for Angular modals, you can keep all configurations of states in one central router instead of handling the states in multiple $uibModal.open() calls spread throughout the application. This will make the use of modals in Angular apps both easier to implement and maintain.

I hope you’ve found the information in this blog series helpful. The JavaScript best practices, JavaScript tools, JavaScript frameworks and libraries we’ve discussed should help you develop reliable and maintainable enterprise web applications. Additionally, tips like the use of Bootstrap modals with Angular UI Router can help you cleanly integrate popular design patterns into your application. So feel free to reach out and continue the discussion with us via questions or comments on LinkedIn or Twitter.


Read All Posts in the JavaScript Best Practices Series:

Previous

JavaScript Best Practices: Part 4 – JavaScript Tools

Next

DevIgnition – Emerging Technologies Conference

Close Form

Enjoy our Blog?

Then stay up-to-date with our latest posts delivered right to your inbox.

Or catch us on social media

Stay in Touch

Whether we’re honing our craft, hanging out with our team, or volunteering in the community, we invite you to keep tabs on us.