Take me home

Asynchronous and flexible modal popups in AngularJS

Published March 11, 2013

This post is a description of the technique I'm using to show modeal popups in AngularJS.

The end result is a <div> in the body that contains an AngularJS enabled template. That's it. There's no actual rendering of a popup, such as keeping the popup centred on the page, drawing a transparent background above the rest of the page, etc.

Here's the full tl;dr of the code.

// You need to fetch $compile from the dependency injector

// This is how you create a raw AngularJS template. You can use
// anything you want here, such as ng-repeat, etc etc.
// In this case, all we need is an angular template that includes
// the template we actually want to show inside the popup. This
// lets Angular handle the loading of the template etc. for us.
var popupTemplate = document.createElement("div");
popupTemplate.setAttribute("ng-include", "'/my-popup-contents-template.html'");

// We give the popup a new scope, inherited from the current one.
var popupScope = $scope.$new();
popupScope.someValue = Math.random();
var popupLinker = $compile(popupTemplate);
var popupElement = popupLinker(popupScope);

// popupElement is now a div that contains your template,
// fully AngularJS enabled. How you display it as a popup
// is up to you.
myShowPopupFunction(popupElement);

popupScope.$on("finished", function () {
  myHidePopupFunction(popupElement);

  // Avoid leaks and nasty stuff. Destroying the scope when
  // the popup is hidden is absolutely vital.
  popupScope.$destroy();
});

You get a DOM element that is your popup contents. It's up to you how you display it. The contents of the popup is a fully "Angularized" template, where you can do all the usual AngularJS stuff.

Adding a controller

I like to make my templates self-contained, and declare the controller in them. In this example, the file /my/template.html could contain the following:

<!-- This is the file: /my-popup-contents-template.html -->

<p>Works outside the controller: {{someValue}}</p>

<div ng-controller="MyPopupContentsController">
   <p>.. stuff ...</p>
   <p>{{ 5 + 5 }}</p>
   <p>And works inside the controller: {{someValue}}</p>
</div>

Since the popupTemplate is just a regular AngularJS template, you're also free to set ng-controller there, in a div that wraps the ng-include.

The "finished" event

I typically have a close button in my popup that emits the "finished" event. When this event is emitted, the code that displayed the popup will hide it.

It's also important to destroy the scope that was created with $scope.$new(); to avoid leaks. The popup controller might add watch statements and other things, that will be nuked when you $destroy() the scope. So pairing the hiding code with the teardown code makes sense, so it's impossible/difficult to hide the popup without also destroying the scope.

Why the extra scope?

ng-include will create a new scope for us. Why do we need the extra popupScope?

The answer is teardown and garbage collection. As mentioned above, our popup might add watch statements and do other nasty side effecty things. And we don't actually have direct access to the scope created by ng-include. So we create our own scope that we can control, that will destroy all its child scopes - the ng-include scope and any ng-controller scopes, and so on.

What is $compile?

This is angular terminology. Compile takes an AngularJS template in the form of DOM elements. Calling compile on a DOM element parses the DOM as an AngularJS template and returns a linker. The linker is a reusable function that takes a scope and returns a DOM element that hooks up the template and the scope. This allows you to use a linker with any scope, as many times as you want. This is essentially how directives work, and it's also the reason directives have a "link" method.

To get a hold of $compile, just use the normal dependency injection techniques.

MyApp.controller("MyController" ,
  ["$scope", "$compile", function ($scope, $compile) {
    $scope.showAPopup = function () {
      // Same as the code block at the beginning of this post.
      var popupTpl = document.createElement("div");
      popupTpl.setAttribute("ng-include", "'/my-popup-contents-template.html'");
      var popupScope = $scope.$new()
      myShowPopupFunction($compile(popupTpl)(popupScope))
      popupScope.$on("finished", function () {
        myHidePopupFunction();
        popupScope.$destroy();
      });
    };
  }]);
<!-- Template -->
<a ng-click="showAPopup()"></a>

Go render some popups!

This technique lets you have as many popups as you want on a single page, and it lets you render the popups any way you want to. You don't have to, say, put the popup HTML in your angular template, and ng-hide it until you want to show it. The popup is only initialized when you want to, and you can display it with any API that takes a DOM element and displays it as a popup (hint: most jQuery plugins out there).


Questions or comments?

Feel free to contact me on Twitter, @augustl, or e-mail me at august@augustl.com.