GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

Animating Modals in Angular.js

Modal dialogs are popular in rich client-side applications. When you have them, it's nice to have transitions between them. We could even take it a step further, and have separate transitions for opening/closing the modal, and another transition when we switch between them.

Fancy Directives

This is surprisingly simple to accomplish with Angular, using a handful of directives, and a service to encapsulate what we're currently displaying. We'll start by creating a "modal view" directive, with two options. One will control whether or not we're displaying the modal, and another will control what templateUrl to display.

modalView = ->
  restrict: 'E'

  scope:
    _modalShow: '&modalShow'
    _modalContent: '&modalSrc'

  templateUrl: '/templates/modal_view.html'

app.directive('modalView', modalView)

Our template would look like this:

.modal-backdrop(ng-if="_modalShow()")
.modal-container(ng-if="_modalShow()")
  .modal-window
    .modal-dialog
      .modal-content(ng-include="_modalContent()")

We make use of two directives that Angular provides. ngIf will remove the element from the DOM if its expression evaluates to false. ngInclude replaces its contents with the template at whatever URL its expression evaluates to.

Angular directives are small reusable snippets of HTML with some logic attached to them. Often times they simply exist to compose the built-in directives provided by Angular, which are incredibly powerful. Combined with Angular's bindings, they allow us to manipulate the DOM in large ways simply by changing a variables.

Next, we need a service to encapsulate what we're displaying, and a controller to expose that service to our application.

class Modal
  isOpen: => !!@src

  open: (src) => @src = src

  close: => @src = ''

app.service('Modal', Modal)

class ModalCtrl
  constructor: ($scope, Modal) ->
    $scope.modal = Modal

app.controller('ModalCtrl', ['$scope', 'Modal', ModalCtrl])

Both of these classes are incredibly simple. Our modal service exists to allow us to share one src variable across the application. Now in our layout, we would add the following.

%div(ng-controller="ModalCtrl")
  %modal-view(modal-show="modal.isOpen()" modal-src="modal.src")

Now we can change the modal's content by calling Modal.open('/whatever.html'). If the modal isn't already open, it'll place it on the DOM. If it is open, it'll change the content. Because these happen on different elements, we can apply separate animations depending on which element enters the page.

I'm basically a designer now.

Adding the animations requires the ngAnimate module. Once we've added that to our dependencies, we just need to use a little CSS to make the animations happen. In this example, we're using Sass and Bourbon to eliminate vendor prefixes, and structure things nicely. First, let's fade in the backdrop.

@mixin fade($opacity) {
  opacity: $opacity;

  &.ng-enter, &.ng-leave.ng-leave-active {
    opacity: 0;
  }
}

.modal-backdrop {
  @include fade(0.5);
  @include transition(0.15s linear all);
}

Next, we'll both fade and slide the modal container.

@mixin slide($x, $y) {
  @include transform(translate(0, 0));

  &.ng-enter, &.ng-leave.ng-leave-active {
    @include transform(translate($x, $y));
  }
}

.modal-container {
  @include fade(1);
  @include slide(0, -25%);
  @include transition(0.3s ease-out all);
}

Finally, we'll define a flip animation for changes in the content. This one is a bit more complex than the others.

@mixin flip-hidden {
  @include transform(rotateY(190deg) scale(1));
}

@mixin flip($time) {
  @include backface-visibility(hidden);

  &.ng-enter {
    @include animation($time flipIn);
    @include flip-hidden;
  }

  &.ng-leave {
    @include animation($time flipOut);

    &.ng-leave-active {
      @include flip-hidden;
    }
  }
}

@include keyframes(flipOut) {
  0% {
    @include transform(rotateY(0) scale(1));
    @include animation-timing-function(ease-out);
  }

  80% {
    @include transform(rotateY(170deg) scale(1));
    @include animation-timing-function(ease-out);
  }

  100% {
    @include flip-hidden;
    @include animation-timing-function(ease-in);
  }
}

@include keyframes(flipIn) {
  0% {
    @include flip-hidden;
    @include animation-timing-function(ease-in);
  }

  60% {
    @include transform(rotateY(360deg) scale(0.95));
    @include animation-timing-function(ease-in);
  }

  100% {
    @include transform(rotateY(360deg) scale(1));
    @include animation-timing-function(ease-in);
  }
}

.modal-dialog {
  @include perspective(1000);
  @include transform-style(preserve-3d);
}

.modal-content {
  @include flip(0.6s);
}

We'll skip the styles that actually make the modals into modals. It should be noted that the .modal-content needs to be position: absolute, or they will pop down to the bottom of the screen when the flip occurs (both the old modal and the new modal will be on the DOM at the same time)

And that's all there is to it. The end result is a beautiful transition, and 0 conditionals in our code to determine which one occurs.

modal transitions