<!DOCTYPE html>
<html>
<head>
    <base href="./" />
    <title>Angular 2 Custom Modal</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- application less -->
    <link href="app/_content/modal.less" rel="stylesheet/less" type="text/css" />
    <link href="app/_content/app.less" rel="stylesheet/less" type="text/css" />

    <!-- compiling less on client side for the example only, this should be done on the server in a production app -->
    <script src="//cdnjs.cloudflare.com/ajax/libs/less.js/2.5.3/less.min.js"></script>

    <!-- polyfill(s) for older browsers -->
    <script src="https://unpkg.com/core-js/client/shim.min.js"></script>

    <script src="https://unpkg.com/zone.js@0.6.23?main=browser"></script>
    <script src="https://unpkg.com/reflect-metadata@0.1.3"></script>
    <script src="https://unpkg.com/systemjs@0.19.27/dist/system.src.js"></script>

    <script src="systemjs.config.js"></script>
    <script>
        System.import('app').catch(function (err) { console.error(err); });
    </script>
</head>
<body>
    <app>Loading...</app>
</body>
</html>
/**
 * WEB ANGULAR VERSION
 * (based on systemjs.config.js in angular.io)
 * System configuration for Angular samples
 * Adjust as necessary for your application needs.
 */
(function (global) {
  System.config({
    // DEMO ONLY! REAL CODE SHOULD NOT TRANSPILE IN THE BROWSER
    transpiler: 'ts',
    typescriptOptions: {
      // Complete copy of compiler options in standard tsconfig.json
      "target": "es5",
      "module": "commonjs",
      "moduleResolution": "node",
      "sourceMap": true,
      "emitDecoratorMetadata": true,
      "experimentalDecorators": true,
      "removeComments": false,
      "noImplicitAny": true,
      "suppressImplicitAnyIndexErrors": true,
      "typeRoots": [
        "../../node_modules/@types/"
      ]
    },
    meta: {
      'typescript': {
        "exports": "ts"
      }
    },
    paths: {
      // paths serve as alias
      'npm:': 'https://unpkg.com/'
    },
    // map tells the System loader where to look for things
    map: {
      // our app is within the app folder
      app: 'app',

      // angular bundles
      '@angular/common': 'npm:@angular/common@2.2.1/bundles/common.umd.js',
      '@angular/compiler': 'npm:@angular/compiler@2.2.1/bundles/compiler.umd.js',
      '@angular/core': 'npm:@angular/core@2.2.1/bundles/core.umd.js',
      '@angular/forms': 'npm:@angular/forms@2.2.1/bundles/forms.umd.js',
      '@angular/http': 'npm:@angular/http@2.2.1/bundles/http.umd.js',
      '@angular/platform-browser': 'npm:@angular/platform-browser@2.2.1/bundles/platform-browser.umd.js',
      '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic@2.2.1/bundles/platform-browser-dynamic.umd.js',
      '@angular/router': 'npm:@angular/router@3.2.1/bundles/router.umd.js',

      // other libraries
      'rxjs':                      'npm:rxjs@5.0.0-rc.3',
      'ts':                        'npm:plugin-typescript@4.0.10/lib/plugin.js',
      'typescript':                'npm:typescript@2.0.3/lib/typescript.js',
      'underscore':                'npm:underscore@1.8.3',
      'jquery':                    'npm:jquery@3.1.1'
    },
    // packages tells the System loader how to load when no filename and/or no extension
    packages: {
      app: {
        main: './main.ts',
        defaultExtension: 'ts'
      },
      rxjs: {
        defaultExtension: 'js'
      }
    }
  });

  if (!global.noBootstrap) { bootstrap(); }

  // Bootstrap the `AppModule`(skip the `app/main.ts` that normally does this)
  function bootstrap() {

    // Stub out `app/main.ts` so System.import('app') doesn't fail if called in the index.html
    System.set(System.normalizeSync('app/main.ts'), System.newModule({ }));

    // bootstrap and launch the app (equivalent to standard main.ts)
    Promise.all([
      System.import('@angular/platform-browser-dynamic'),
      System.import('app/app.module')
    ])
    .then(function (imports) {
      var platform = imports[0];
      var app      = imports[1];
      platform.platformBrowserDynamic().bootstrapModule(app.AppModule);
    })
    .catch(function(err){ console.error(err); });
  }

})(this);
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app.module';

platformBrowserDynamic().bootstrapModule(AppModule);
import { Component } from '@angular/core';

@Component({
    moduleId: module.id.toString(),
    selector: 'app',
    templateUrl: 'app.component.html'
})

export class AppComponent { }
<!-- main app container -->
<div>
    <nav>
        <a [routerLink]="['/']">Home</a>
        <a [routerLink]="['/test-page']">Test Page</a>
    </nav>
    <router-outlet></router-outlet>
</div>

<!-- credits -->
<div class="credits">
    <p>
        <a href="http://jasonwatmore.com/post/2017/01/24/angular-2-custom-modal-window-dialog-box" target="_top">Angular 2 - Custom Modal Window / Dialog Box</a>
    </p>
    <p>
        <a href="http://jasonwatmore.com" target="_top">JasonWatmore.com</a>
    </p>
</div>
export * from './modal.service';
import * as _ from 'underscore';

export class ModalService {
    private modals: any[] = [];

    add(modal: any) {
        // add modal to array of active modals
        this.modals.push(modal);
    }

    remove(id: string) {
        // remove modal from array of active modals
        let modalToRemove = _.findWhere(this.modals, { id: id });
        this.modals = _.without(this.modals, modalToRemove);
    }

    open(id: string) {
        // open modal specified by id
        let modal = _.findWhere(this.modals, { id: id });
        modal.open();
    }

    close(id: string) {
        // close modal specified by id
        let modal = _.find(this.modals, { id: id });
        modal.close();
    }
}
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule }    from '@angular/forms';

import { AppComponent }  from './app.component';
import { routing }        from './app.routing';

import { ModalComponent } from './_directives/index';
import { ModalService } from './_services/index';
import { HomeComponent } from './home/index';
import { TestPageComponent } from './test-page/index';

@NgModule({
    imports: [
        BrowserModule,
        FormsModule,
        routing
    ],
    declarations: [
        AppComponent,
        ModalComponent,
        HomeComponent,
        TestPageComponent
    ],
    providers: [
        ModalService
    ],
    bootstrap: [AppComponent]
})

export class AppModule { }
import { Routes, RouterModule } from '@angular/router';

import { HomeComponent } from './home/index';
import { TestPageComponent } from './test-page/index';

const appRoutes: Routes = [
    { path: '', component: HomeComponent },
    { path: 'test-page', component: TestPageComponent },

    // otherwise redirect to home
    { path: '**', redirectTo: '' }
];

export const routing = RouterModule.forRoot(appRoutes);
import { Component } from '@angular/core';

@Component({
    moduleId: module.id.toString(),
    templateUrl: 'test-page.component.html'
})

export class TestPageComponent {
}
<div class="col-md-6 col-md-offset-3">
    <h1>Test Page</h1>
    <p>This one doesn't have any modals...</p>
</div>
export * from './test-page.component';
import { Component, OnInit } from '@angular/core';

import { ModalService } from '../_services/index';

@Component({
    moduleId: module.id.toString(),
    templateUrl: 'home.component.html'
})

export class HomeComponent implements OnInit {
    private bodyText: string;

    constructor(private modalService: ModalService) {
    }

    ngOnInit() {
        this.bodyText = 'This text can be updated in modal 1';
    }

    openModal(id: string){
        this.modalService.open(id);
    }

    closeModal(id: string){
        this.modalService.close(id);
    }
}
<div class="col-md-6 col-md-offset-3">
    <h1>Home</h1>
    <p>{{bodyText}}</p>
    <button (click)="openModal('custom-modal-1')">Open Modal 1</button>
    <button (click)="openModal('custom-modal-2')">Open Modal 2</button>
</div>

<modal id="custom-modal-1">
    <div class="modal">
        <div class="modal-body">
            <h1>A Custom Modal!</h1>
            <p>
                Home page text: <input type="text" [(ngModel)]="bodyText" />
            </p>
            <button (click)="closeModal('custom-modal-1');">Close</button>
        </div>
    </div>
    <div class="modal-background"></div>
</modal>

<modal id="custom-modal-2">
    <div class="modal">
        <div class="modal-body">
            <h1 style="height:1000px">A Tall Custom Modal!</h1>
            <button (click)="closeModal('custom-modal-2');">Close</button>
        </div>
    </div>
    <div class="modal-background"></div>
</modal>
export * from './home.component';
import { Component, ElementRef, Input, OnInit, OnDestroy } from '@angular/core';
import * as $ from 'jquery';

import { ModalService } from '../_services/index';

@Component({
    moduleId: module.id.toString(),
    selector: 'modal',
    template: '<ng-content></ng-content>'
})

export class ModalComponent implements OnInit, OnDestroy {
    @Input() id: string;
    private element: JQuery;

    constructor(private modalService: ModalService, private el: ElementRef) {
        this.element = $(el.nativeElement);
    }

    ngOnInit(): void {
        let modal = this;

        // ensure id attribute exists
        if (!this.id) {
            console.error('modal must have an id');
            return;
        }

        // move element to bottom of page (just before </body>) so it can be displayed above everything else
        this.element.appendTo('body');

        // close modal on background click
        this.element.on('click', function (e: any) {
            var target = $(e.target);
            if (!target.closest('.modal-body').length) {
                modal.close();
            }
        });

        // add self (this modal instance) to the modal service so it's accessible from controllers
        this.modalService.add(this);
    }

    // remove self from modal service when directive is destroyed
    ngOnDestroy(): void {
        this.modalService.remove(this.id);
        this.element.remove();
    }

    // open modal
    open(): void {
        this.element.show();
        $('body').addClass('modal-open');
    }

    // close modal
    close(): void {
        this.element.hide();
        $('body').removeClass('modal-open');
    }
}
export * from './modal.component';
/* EXAMPLE STYLES
-------------------------------*/
body {
    font-family: roboto;
    padding: 20px;
}

nav {
    margin-bottom: 20px;
    padding-bottom: 20px;
    border-bottom: 1px solid #ddd;
    
    a {
        margin-right: 8px;
    }
}

h1 {
    font-weight: normal;
    margin-top: 0;
}

input[type="text"] {
    display:block;
    width: 100%;
    font-family: roboto;
}

.credits {
    margin-top: 30px;
    border-top: 1px solid #ddd;
    text-align: center;
}

/* MODAL STYLES
-------------------------------*/
modal {
    /* modals are hidden by default */
    display: none;

    .modal {
        /* modal container fixed across whole screen */
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;

        /* z-index must be higher than .modal-background */
        z-index: 1000;
        
        /* enables scrolling for tall modals */
        overflow: auto;

        .modal-body {
            padding: 20px;
            background: #fff;

            /* margin exposes part of the modal background */
            margin: 40px;
        }
    }

    .modal-background {
        /* modal background fixed across whole screen */
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;

        /* semi-transparent black  */
        background-color: #000;
        opacity: 0.75;
        
        /* z-index must be below .modal and above everything else  */
        z-index: 900;
    }
}

body.modal-open {
    /* body overflow is hidden to hide main scrollbar when modal window is open */
    overflow: hidden;
}