This article is about creating a simple and re-usable modal component with angular2. Featuring @HostListener , ng-content , and some basic vanilla javascript code!

Usually a modal:

is a window layout on top of everything has variable content (for example a component) can be closed and opened at will

I always start writing angular2 components with the DOM I would like to see when I use it. It really helps picturing how to separate your concerns, for example:

<my-modal name="myModal"> <my-component [something]="input"></my-component> </my-modal> <button myOpenModal="myModal">Open myModal!</button>

Note that the my prefix is only there because it’s considered as a best practice. You should use your own prefix

Now let’s try to think a bit about this before going on with code. On the above snippet, we have:

a my-modal component, representing the window

component, representing the window a [myOpenModal] directive to open a given modal

directive to open a given modal a modal is referenced by a name and we need something to share modal states between the directive and the component

For the third point, let’s do a service that stores our modals! For this, I’ll simply use a Map inside a service:

@Injectable() export class MyModalService { map: Map<string, MyModalComponent> = new Map get(v: string): MyModalComponent { return this.map.get(v) } set(key: string, v: MyModalComponent): void { this.map.set(key, v) } }

Nothing complicated so far, let’s write the MyModalComponent . We will use ng-content to output our modal content. For accessibility purpose, the modal can be closed:

on click on the overlay with ESC key with a close button

@Component({ selector: 'my-modal', template: ` <div class="reveal-overlay" (click)="clickOverlay($event)" [hidden]="!show"> <div class="reveal"> <ng-content></ng-content> <button class="close-button" (click)="toggle()"> <span>×</span> </button> </div> </div> `, styles: [ '.reveal-overlay { background: rgba(0,0,0,0.6); position: fixed; top: 0; left: 0; right: 0; bottom: 0; }', '.reveal { background: white; width: 90%; margin: 40px auto; min-height: 70vh; position: relative; padding: 20px; }' '.close-button { position: absolute; right: 10px; top: 10px; }' ] }) export class MyModalComponent implements OnInit { @Input() name: string show: boolean = false constructor(private myModals: MyModalService) { } ngOnInit() { this.myModals.set(this.name, this) } clickOverlay(event: Event) { const target = (event.target as HTMLElement) // only close if we clicked on the `reveal-overlay` not on it's content if (target.classList.contains('reveal-overlay')) { this.toggle() } } toggle() { this.show = !this.show if (this.show) { document.addEventListener('keyup', this.escapeListener) } else { document.removeEventListener('keyup', this.escapeListener) } } private escapeListener = (event: KeyboardEvent) => { if (event.which === 27 || event.keyCode === 27) { this.show = false } } }

Now let’s write the directive that allows us to toggle the modal:

@Directive({ selector: '[myModalOpen]' }) export class MyModalOpenDirective { @Input() myModalOpen: string constructor(private myModals: MyModalService) { } @HostListener('click') onClick() { const modal = this.myModals.get(this.myModalOpen) if (!modal) { console.error('No modal named %s', this.myModalOpen) return } modal.toggle() } }

Let’s try this out! Here is a plunkr with the following HTML in the main App:

<my-modal name="hello world"> Hi </my-modal> <button myModalOpen="hello world">Open !</button>

This is great, now let’s go further by including a Component inside the modal! For demonstration purpose this is the component:

@Component({ selector: 'my-custom-component', template: '{{foo}}' }) export class MyCustomComponent { @Input() foo: string }

And the HTML markup:

<my-modal name="hello world"> <my-custom-component [foo]="foo"></my-custom-component> </my-modal> <button myModalOpen="hello world">Open !</button>

Thanks to ng-content this is still working great! Here’s a plunkr:

Now, a good addition to the modal would be to be able to listen to modalOpen / modalClose events. Indeed, from inside the modal, inside our my-custom-component we have to idea of the modal status. Sure, we could inject the myModalService and get the correct one to find a status. Though, the my-custom-component is used inside the modal, may be used elsewhere and therefore should not be dependent of myModalX .

For this to work I want to be able to do:

@Component({ selector: 'my-custom-component', template: '{{foo}}' }) export class MyCustomComponent { @Input() foo: string @HostListener('modalOpen') onModalOpen() { this.foo = this.foo.split('').reverse().join('') } }

This is easier then it looks, with angular you’ve always access to DOM elements on which you can dispatch events. Yes, a component is nothing more then a custom DOM element!

First, I need a reference to the content (what’s inside ng-content ). To do so, let’s review the MyModalComponent template and add a reference:

<div #modalContent> <ng-content></ng-content> </div>

This allows us to use ViewChild('modalContent') to use the DOM! Then, when the modal gets toggled, we will use the elements inside ng-content and dispatch a modalOpen or modalClose event. Note that this has to be done in the AfterContentChecked lifecycle to avoid unwanted behavior!

Here is substract of what changed in the MyModalComponent :

export class MyModalComponent implements OnInit, AfterContentChecked { @Input() name: string @ViewChild('modalContent') modalContent show: boolean = false // store elements to notify private notify: HTMLElements[] = [] // ... toggle() { this.show = !this.show if (this.show) { document.addEventListener('keyup', this.escapeListener) } else { document.removeEventListener('keyup', this.escapeListener) } // Those are the elements inside the `ng-content` this.notify = [].slice.call(this.modalContent.nativeElement.children) } // Dispatch events on the `DoCheck` lifecycle ngAfterContentChecked() { if (this.notify.length === 0) { return } const event = this.createEvent(this.show ? 'modalOpen' : 'modalClose') let toNotify while (toNotify = this.notify.shift()) { toNotify.dispatchEvent(event) } } private createEvent(name) { const event = document.createEvent('Events') event.initEvent(name, true, true) return event } }

And here is a preview: