Cory Rylan

My name is , Google Developer Expert, Speaker, Software Developer. Building Design Systems and Web Components.

Follow @coryrylan
Angular

Build a Angular modal dialog with Angular Animate

Cory Rylan

- 5 minutes

Updated

This article has been updated to the latest version Angular 17 and tested with Angular 16. The content is likely still applicable for all Angular 2 + versions.

Angular is full fledged framework which provides a lot of functionality for developers right out of the box. In this post we are going to build our own modal dialog in the latest Angular. This will be simplistic example compared to a modal dialog from a UI kit like Kendo UI. In our example we will learn how to use Angular Animate, two way data binding, and ngContent for easy integration into our application. If you want to skip to the live code example check out the link at the bottom of the page. This post is slightly more advanced and assumes the basic knowledge of Angular (latest 2.x+) and its template syntax. Here is a brief look at what our final component will look like.

First lets start with our top level app component template. In this template we will have just a single button to toggle our dialog to open and close. Here we will also see how we interact with our custom dialog in other components.

<button (click)="showDialog = !showDialog" class="btn">Open</button>

<app-dialog [(visible)]="showDialog">
  <h1>Hello World</h1>
  <button (click)="showDialog = !showDialog" class="btn">Close</button>
</app-dialog>

Looking at our app template we have a single button that toggles a showDialog property on our app component. The next line is our app-dialog component. Notice the [(visible)]="showDialog" on our dialog component. We are using the two way data binding syntax or also known as "bananas in a box". This binds the value of the showDialog to the app-dialog and allows us to communicate to the app-dialog when to show and hide.

Inside the app-dialog we have the content we would like to be displayed in our dialog. This is a feature called ngContent. This allow content between component tags to be injected in specific parts of our app-dialog template.

Lets take a look at the app-dialog template.

<div [@dialog] *ngIf="visible" class="dialog">
  <ng-content></ng-content>
  <button
    *ngIf="closable"
    (click)="close()"
    aria-label="Close"
    class="dialog__close-btn"
  >
    X
  </button>
</div>
<div *ngIf="visible" class="overlay" (click)="close()"></div>

So our first line we see a interesting syntax [@dialog]. This is a special property syntax for the Angular Animations to target specific elements. We will come back to this in a bit. The next line is an ngIf to toggle the visibility of our dialog.

Next we have the ng-content tag. This is where the content in our app template is injected and displayed. This is the content that will be visible in our dialog. The last two parts are the close button and overlay of our dialog that when clicked toggle the visible property on the dialog component. Next lets look at the app-dialog component.

import {
  Component,
  OnInit,
  Input,
  Output,
  OnChanges,
  EventEmitter,
  trigger,
  state,
  style,
  animate,
  transition
} from '@angular/core';

@Component({
  selector: 'app-dialog',
  templateUrl: 'app/dialog.component.html',
  styleUrls: ['app/dialog.component.css'],
  animations: [
    trigger('dialog', [
      transition('void => *', [
        style({ transform: 'scale3d(.3, .3, .3)' }),
        animate(100)
      ]),
      transition('* => void', [
        animate(100, style({ transform: 'scale3d(.0, .0, .0)' }))
      ])
    ])
  ]
})
export class DialogComponent implements OnInit {
  @Input() closable = true;
  @Input() visible: boolean;
  @Output() visibleChange: EventEmitter<boolean> = new EventEmitter<boolean>();

  constructor() {}

  ngOnInit() {}

  close() {
    this.visible = false;
    this.visibleChange.emit(this.visible);
  }
}

So looking at our app-dialog component there is quite a bit going on the first few lines are describing where our CSS and template files are. In our CSS file we just have some CSS for the dialog box styles. The animations are controlled by Angular Animate. So lets take a look at the animations property on the component decorator.

Animation


animations: [
  trigger('dialog', [
    transition('void => *', [
      style({ transform: 'scale3d(.3, .3, .3)' }),
      animate(100)
    ]),
    transition('* => void', [
      animate(100, style({ transform: 'scale3d(.0, .0, .0)' }))
    ])
  ])
]

The Angular Animation is based on the Web Animation API instead of traditional CSS. This allows fine grain control of our animation timing in JavaScript and great performance. The first line we have a trigger value: dialog. This is the value that matches the [@dialog] property in our template. When the trigger is seen in the template Angular will apply the animation to that element.

Next is the transition function. This takes in a value of how the animation should occur. In our example we are using the * wildcard syntax which means in any state change of the applied element it should trigger the animation. The first transition uses the void state which is applied to elements not yet in the view. So when our *ngIf is active the element will enter into the view applying the transform style on the next line. The animate(100) applies the style function over a time span of 100 milliseconds.

The second transition is very similar but uses * => void to apply the second animation when the element leaves the view or is "void" of the view. Once applied we use the animate function and immediately apply the style transform to our dialog wrapping div.

Component Data binding

Now lets look at the last half of our app-dialog component.

export class DialogComponent implements OnInit {
  @Input() closable = true;
  @Input() visible: boolean;
  @Output() visibleChange: EventEmitter<boolean> = new EventEmitter<boolean>();

  constructor() {}

  ngOnInit() {}

  close() {
    this.visible = false;
    this.visibleChange.emit(this.visible);
  }
}

On our app-dialog we have a few properties. Using the Input and Output decorators we are defining the API for other components to interact with the app-dialog. The first input allows us to set the dialog to show or hide the close button, which is a common scenario for dialogs.

The input and output visible and visibleChange allows us to create custom two way binding so we can toggle the visibility of the dialog. Why two way? Could we not just set the visibility from the app component with just the [property] syntax? Well if we keep it two way using the EventEmitter we can set the app showDialog property when the close button is clicked. This allows the showDialog property to stay in sync whether its the app component or the dialog component setting it. The visibleChange follows the Angular convention adding Change to the end for properties that are for two way data binding. If you are unfamiliar with the Angular template syntax I recommend checking out the documentation.

Conclusion

The three main features we used, animations, ngContent, and two way data binding, when combined allows us to create easy to use feature rich components. Check out the live working demo below!

View Demo Code   
Twitter Facebook LinkedIn Email
 

No spam. Short occasional updates on Web Development articles, videos, and new courses in your inbox.

Related Posts

Angular

Creating Dynamic Tables in Angular

Learn how to easily create HTML tables in Angular from dynamic data sources.

Read Article
Web Components

Reusable Component Patterns - Default Slots

Learn about how to use default slots in Web Components for a more flexible API design.

Read Article
Web Components

Reusable Component Anti-Patterns - Semantic Obfuscation

Learn about UI Component API design and one of the common anti-patterns, Semantic Obfuscation.

Read Article