Friday, November 29, 2019

Angular 8 - Custom Modal Window | Dialog Box

In this tutorial we'll cover how to implement modal windows (dialog boxes) in Angular 8 with TypeScript. The example is a custom modal without the need for any 3rd party libraries.

The tutorial code is available on GitHub at
https://github.com/cornflourblue/angular-8-custom-modal.
There are plenty of plugins and libraries out there that include modal windows, in the past I used them myself when I needed to add a modal to a new project. The main issue I have with 3rd party plugins is that they usually contain a lot of features I don't need which adds unnecessary bloat to my Angular app, so a while ago I took some time to implement a custom modal window to see how difficult it would be and also to remove the magic & mystery I had in my mind about exactly how modals work.
Angular 8 - Custom Modal Window | Dialog Box

When I finished I was pleasantly surprised at the relatively small amount of code required to implement a custom modal window, most of the modal 'magic' is done with a handful of CSS styles (see modal.component.less) while Angular / TypeScript is just used for showing and hiding the modal windows.
Here it is in action: (See on StackBlitz at https://stackblitz.com/edit/angular-8-custom-modal-dialog-codeandroid)

Running the Angular 8 Modal Dialog Locally
Install NodeJS and NPM from https://nodejs.org/en/download/.
Download or clone the project source code from https://github.com/cornflourblue/angular-8-custom-modal
Install all required npm packages by running npm install from the command line in the project root folder (where the package.json is located).
Start the application by running npm start from the command line in the project root folder.
NOTE: You can also run the app directly using the Angular CLI command ng serve --open. To do this first install the Angular CLI globally on your system with the command npm install -g @angular/cli.
Adding Custom Modals to Your Angular 8 App
To add modals to your Angular 8 application you'll need to copy the /src/app/_modal folder and contents from the example project, the folder contains the modal module and associated files, including:
modal.model.less - LESS/CSS styles for displaying modal dialogs, this is where the modal "magic" happens.
modal.component.html - modal component template that contains the wrapper html for displaying modal dialogs.
modal.component.ts - modal component with the logic for displaying modal dialogs.
modal.module.ts - modal module that encapsulates the modal component so it can be imported by the app module.
modal.service.ts - modal service that can be used by any angular component to open and close modal dialogs.
index.ts - barrel file that re-exports the modal module and service so they can be imported using only the folder path instead of the full path to each file, and also enables importing from multiple files with a single import.
Import the Modal Module into your App Module
To make the modal component available to your Angular 8 application you need to add the ModalModule to the imports array of your App Module (app.module.ts). See the app module from the example app below, the modal module is imported on line 5 and added to the imports array of the app module on line 16.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { ModalModule } from './_modal';
import { appRoutingModule } from './app.routing';
import { AppComponent } from './app.component';
import { HomeComponent } from './home';
import { TestPageComponent } from './test-page';
@NgModule({
    imports: [
        BrowserModule,
        FormsModule,
        ModalModule,
        appRoutingModule
    ],
    declarations: [
        AppComponent,
        HomeComponent,
        TestPageComponent
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }
Add the <jw-modal> tag to pages where you want to display modals
To add a modal dialog to any page simply add the <jw-modal id="[insert unique id]"></jw-modal> tag along with the content for the modal. You can put any content you like inside the <jw-modal> element. You can also update the modal LESS/CSS if you want to change the styles of the modals, e.g to make them smaller or add CSS animation transitions.
IMPORTANT: A unique id is required for each modal on a page, it can be any string e.g. 'custom-modal-1'. The id string is used by the modal service to keep track of each active modal in the angular app, so the service knows which modal to open/close based on the id passed to the modalService.open() and modalService.close() methods e.g. modalService.open('custom-modal-1').

Here is the home component template from the example app (/src/app/home/home.component.html) that contains two modals, each is opened by a button click, and the first modal contains an input text field that allows you to edit the bodyText displayed in the template.

<div>
    <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>
<jw-modal id="custom-modal-1">
    <h1>A Custom Modal!</h1>
    <p>Home page text: <input type="text" [(ngModel)]="bodyText" /></p>
    <button (click)="closeModal('custom-modal-1');">Close</button>
</jw-modal>
<jw-modal id="custom-modal-2">
    <h1 style="height:1000px">A Tall Custom Modal!</h1>
    <button (click)="closeModal('custom-modal-2');">Close</button>
</jw-modal>

Opening & Closing Angular 8 Modal Dialogs
To open a modal call the modalService.open() method with the id of the modal you want to open, e.g. modalService.open('custom-modal-1'). To close a modal call the modalService.close() method with the id of the modal you want to close, e.g. modalService.close('custom-modal-1').

By default modals are closed on background click, to disable this remove the chunk of code in the modal component (/src/app/_modal/modal.component.ts) located directly below the comment // close modal on background click.

Here is the home component from the example app (/src/app/home/home.component.ts), it contains methods for opening and closing modals (openModal() and closeModal()) that call the corresponding methods of the modal service.

import { Component, OnInit } from '@angular/core';
import { ModalService } from '../_modal';
@Component({ templateUrl: 'home.component.html' })
export class HomeComponent implements OnInit {
    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);
    }
}
Breakdown of the Angular 8 Custom Modal Code
Below is a breakdown of the pieces of code used to implement custom modal dialogs in Angular 8 & TypeScript, you don't need to know the details of how it all works to use the modals in your project, it's only if you're interested in the nuts and bolts or if you want to modify the underlying code or behaviour.
LESS/CSS Styles for Angular 8 Modal Dialogs
These are the styles applied to the custom modal dialogs in this example, they could also be used in non-angular projects as it's just pure LESS/CSS.

I prefixed the modal element and classes with jw- to prevent conflicts with 3rd party css libraries such as Bootstrap.
/* MODAL STYLES
-------------------------------*/
jw-modal {
    /* modals are hidden by default */
    display: none;
    .jw-modal {
        /* modal container fixed across whole screen */
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        /* z-index must be higher than .jw-modal-background */
        z-index: 1000;
     
        /* enables scrolling for tall modals */
        overflow: auto;
        .jw-modal-body {
            padding: 20px;
            background: #fff;
            /* margin exposes part of the modal background */
            margin: 40px;
        }
    }
    .jw-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 .jw-modal and above everything else  */
        z-index: 900;
    }
}
body.jw-modal-open {
    /* body overflow is hidden to hide main scrollbar when modal window is open */
    overflow: hidden;
}
Angular 8 Modal Service
The Angular 8 modal service manages the communication that's required between page components and modal components. It maintains a list of available modals on the page and exposes methods for interacting with those modals.
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
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
        this.modals = this.modals.filter(x => x.id !== id);
    }
    open(id: string) {
        // open modal specified by id
        const modal = this.modals.find(x => x.id === id);
        modal.open();
    }
    close(id: string) {
        // close modal specified by id
        const modal = this.modals.find(x => x.id === id);
        modal.close();
    }
}
Angular 8 Modal Component
The custom modal component is used to add modal windows anywhere in your angular application by using the <jw-modal> tag. Each modal instance adds itself to the modal service when it loads by calling modalService.add(this) from the ngOnInit Angular lifecycle method, and removes itself from the modal service when it is destroyed by calling modalService.remove(this.id) from the ngOnDestroy Angular lifecycle method.
import { Component, ViewEncapsulation, ElementRef, Input, OnInit, OnDestroy } from '@angular/core';
import { ModalService } from './modal.service';
@Component({
    selector: 'jw-modal',
    templateUrl: 'modal.component.html',
    styleUrls: ['modal.component.less'],
    encapsulation: ViewEncapsulation.None
})
export class ModalComponent implements OnInit, OnDestroy {
    @Input() id: string;
    private element: any;
    constructor(private modalService: ModalService, private el: ElementRef) {
        this.element = el.nativeElement;
    }
    ngOnInit(): void {
        // 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
        document.body.appendChild(this.element);
        // close modal on background click
        this.element.addEventListener('click', el => {
            if (el.target.className === 'jw-modal') {
                this.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 component is destroyed
    ngOnDestroy(): void {
        this.modalService.remove(this.id);
        this.element.remove();
    }
    // open modal
    open(): void {
        this.element.style.display = 'block';
        document.body.classList.add('jw-modal-open');
    }
    // close modal
    close(): void {
        this.element.style.display = 'none';
        document.body.classList.remove('jw-modal-open');
    }
}
Angular 8 Modal Component Template
The modal component template contains just a couple of wrapper divs for the modal content and a div for the modal background. The <ng-content> element is replaced by Angular with the contents you set inside the <jw-modal> element, this is called Angular content projection.

<div class="jw-modal">
    <div class="jw-modal-body">
        <ng-content></ng-content>
    </div>
</div>
<div class="jw-modal-background"></div>
source : https://jasonwatmore.com/post/2019/07/12/angular-8-custom-modal-window-dialog-box

No comments:

Post a Comment