Published:

Angular 14 - Modal Popup (Dialog) Tutorial with Example

Built with Angular 14.2.12

Other versions available:

This tutorial shows how to implement a custom light-weight modal popup (dialog) in Angular 14 without any 3rd party plugins.

Angular Example App

The example app contains a page with some text and a couple of buttons to open two modal popups:

  • Modal #1 - contains an input field that allows you to edit the text displayed on the parent page, this demonstrates binding data directly from a component property to an element in a child modal component.
  • Modal #2 - is a tall popup to demonstrate that the modal is scrollable while keeping the page below locked in position.

Code on GitHub

The tutorial code is available on GitHub at https://github.com/cornflourblue/angular-14-modal-popup-example.

Here it is in action: (See on StackBlitz at https://stackblitz.com/edit/angular-14-modal-popup-example)


Running the Angular 14 Modal Example Locally

  1. Install Node.js and NPM from https://nodejs.org.
  2. Download or clone the project source code from https://github.com/cornflourblue/angular-14-modal-popup-example
  3. Install all required npm packages by running npm install or npm i from the command line in the project root folder (where the package.json is located).
  4. Start the application by running npm start from the command line in the project root folder.

NOTE: You can also start the app with 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.

For more info on setting up your local Angular dev environment see Angular - Setup Development Environment.


Angular 14 Project Structure

The Angular CLI was used to generate the base project structure with the ng new <project name> command, the CLI is also used to build and serve the application. For more info about the Angular CLI see https://angular.io/cli.

The app and code structure of the tutorial mostly follow the best practice recommendations in the official Angular Style Guide, with a few of my own tweaks here and there.

Folder Naming Convention

Shared/common code (components & services) are placed in folders prefixed with an underscore _ to easily differentiate them from features and group them together at the top of the folder structure. This example doesn't have any routed features (e.g. home, products etc) but if it did they would each have their own folder in the /app directory.

Barrel Files

The index.ts files in each folder are barrel files that group the exported modules from each folder together so they can be imported using only the folder path instead of the full module path, and to enable importing multiple modules in a single import (e.g. import { ComponentOne, ComponentTwo } from '../_components').

Here are the main project files that contain the application logic, I didn't include files that were generated by the Angular CLI ng new command that I left unchanged.

 

Modal Component Template

Path: /src/app/_components/modal.component.html

The modal component template contains the outer HTML for each modal on the page, it contains a couple of container divs for the modal and an Angular <ng-content> element.

Angular content projection

Content projection allows you to insert (or project) any content you want inside a child component using the <ng-content> element. The content you add inside a modal component (e.g. <jw-modal>my content...</jw-modal>) is rendered in the location of the <ng-content> element. For more info see https://angular.io/guide/content-projection.

<div class="jw-modal">
    <div class="jw-modal-body">
        <ng-content></ng-content>
    </div>
</div>
 

Modal Component LESS/CSS Styles

Path: /src/app/_components/modal.component.less

This stylesheet is the main file in the modal tutorial, the below CSS styles are what turn a couple of divs in the modal component template into a modal popup in your browser. The rest of the TypeScript code (in the modal service and modal component) controls opening, closing and managing modals.

Modal style rules

jw-modal - the root element of each modal component instance is set to hidden (closed) by default. The component element is defined by the selector in the modal component.

.jw-modal - the outer container div is fixed across the whole screen above all other elements with a semi-transparent black background and scrolling enabled in case the content is longer than the screen.

.jw-modal-body - the inner container div just has a white background and some padding.

body.jw-modal-open - this class is added to the HTML <body> tag when a modal is open. Scrolling is disabled on the underlying body content when a modal is open above.

Prefixed with jw-

I prefixed the modal element and classes with jw- to prevent name clashes with any 3rd party CSS libraries you might be using (e.g. Bootstrap).

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

    .jw-modal {
        /* modal container fixed across whole screen */
        position: fixed;
        inset: 0;

        /* z-index must be higher than everything else on the page */
        z-index: 10000;
        
        /* semi-transparent black background exposed by padding */
        background-color: rgba(0, 0, 0, .75);
        padding: 40px;

        /* enables scrolling for tall modals */
        overflow: auto;
    }

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

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

Modal Component

Path: /src/app/_components/modal.component.ts

The modal component controls the behaviour of a modal instance, it contains methods for opening and closing the modal, and performs a few steps when the component is initialized and destroyed.

The component selector sets the element for the modal component to <jw-modal>. Encapsulation is set to ViewEncapsulation.None to make the modal component styles apply globally, this is required to disable scrolling on the body when a modal is open because the style rule (body.jw-modal-open) targets to the root <body> element.

Unique id required

Each modal instance must have a unique id attribute (e.g. <jw-modal id="modal-1">), it is used to identify which modal you want to open when calling the modal service (e.g. modalService.open('modal-1')).

Modal component methods

ngOnInit() - adds the component instance to the modal service so it can be opened from anywhere. Moves the modal HTML element to be a direct child of the <body> to put it in the root stacking context of the page, this ensures the modal will appear above all other elements on the z-axis using the z-index style rule. Adds an event listener to close the modal on background click.

ngOnDestroy() - removes the component instance from the modal service. Removes the modal HTML element from the body, this isn't automatic because the element was moved on init.

open() - makes the modal appear by setting the display style to 'block'. Adds the jw-modal-open CSS class to the <body> tag. Sets the isOpen property to true.

close() - makes the modal disappear by setting the display style to 'none'. Removes the jw-modal-open CSS class from the <body> tag. Sets the isOpen property to false.

import { Component, ViewEncapsulation, ElementRef, Input, OnInit, OnDestroy } from '@angular/core';

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

@Component({
    selector: 'jw-modal',
    templateUrl: 'modal.component.html',
    styleUrls: ['modal.component.less'],
    encapsulation: ViewEncapsulation.None
})
export class ModalComponent implements OnInit, OnDestroy {
    @Input() id?: string;
    isOpen = false;
    private element: any;

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

    ngOnInit() {
        // add self (this modal instance) to the modal service so it can be opened from any component
        this.modalService.add(this);

        // 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: any) => {
            if (el.target.className === 'jw-modal') {
                this.close();
            }
        });
    }

    ngOnDestroy() {
        // remove self from modal service
        this.modalService.remove(this);

        // remove modal element from html
        this.element.remove();
    }

    open() {
        this.element.style.display = 'block';
        document.body.classList.add('jw-modal-open');
        this.isOpen = true;
    }

    close() {
        this.element.style.display = 'none';
        document.body.classList.remove('jw-modal-open');
        this.isOpen = false;
    }
}
 

Modal Popup Service

Path: /src/app/_services/modal.service.ts

The modal service is the middle man used to open and close modal popups from any component (or service) in the Angular app.

It manages a collection of active modal instances in the modals array. The add() and remove() methods are only used by modal component instances to add/remove themselves to the modals array.

Opening and closing modals

open() - the open method takes a modal id parameter and calls the open() method of the specified modal.

close() - the close method finds the currently open modal (isOpen: true) and calls the close() method of that modal.

import { Injectable } from '@angular/core';

import { ModalComponent } from '../_components';

@Injectable({ providedIn: 'root' })
export class ModalService {
    private modals: ModalComponent[] = [];

    add(modal: ModalComponent) {
        // ensure component has a unique id attribute
        if (!modal.id || this.modals.find(x => x.id === modal.id)) {
            throw new Error('modal must have a unique id attribute');
        }

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

    remove(modal: ModalComponent) {
        // remove modal from array of active modals
        this.modals = this.modals.filter(x => x === modal);
    }

    open(id: string) {
        // open modal specified by id
        const modal = this.modals.find(x => x.id === id);

        if (!modal) {
            throw new Error(`modal '${id}' not found`);
        }

        modal.open();
    }

    close() {
        // close the modal that is currently open
        const modal = this.modals.find(x => x.isOpen);
        modal?.close();
    }
}
 

App Component Template

Path: /src/app/app.component.html

The app component template contains some text and a couple of buttons to open two modal popups:

  • Modal #1 - contains an input field bound to the bodyText property of the app component, it allows you to edit the text near the top of the page (<p>{{bodyText}}</p>). This demonstrates binding data directly from a component property to an element in a child modal.
  • Modal #2 - is a tall popup to demonstrate that the modal is scrollable while keeping the page below locked in position.
<!-- main app container -->
<div>
    <div>
        <h1>Angular 14 - Modal Popup Example</h1>
        <p>{{bodyText}}</p>
        <button (click)="modalService.open('modal-1')">Open Modal 1</button>
        <button (click)="modalService.open('modal-2')">Open Modal 2</button>
    </div>
    
    <jw-modal id="modal-1">
        <h1>A custom modal popup</h1>
        <p>Home page text: <input type="text" [(ngModel)]="bodyText" /></p>
        <button (click)="modalService.close();">Close</button>
    </jw-modal>
    
    <jw-modal id="modal-2">
        <h1>A tall modal popup</h1>
        <p style="padding-bottom: 1500px;">Close with the button below or by clicking the background.</p>
        <button (click)="modalService.close();">Close</button>
    </jw-modal>
</div>
 

App Component

Path: /src/app/app.component.ts

The app component contains a bodyText string property which is used in the component template.

The modal service is provided by Angular dependency injection by declaring it as a parameter to the constructor, the protected modifier makes the service accessible in the component template as well as the component.

import { Component } from '@angular/core';

import { ModalService } from './_services';

@Component({ selector: 'app-root', templateUrl: 'app.component.html' })
export class AppComponent {
    bodyText = 'This text can be updated in modal 1';

    constructor(protected modalService: ModalService) { }
}
 

App Module

Path: /src/app/app.module.ts

The app module defines the root module of the application along with metadata about the module. For more info about angular modules see https://angular.io/docs/ts/latest/guide/ngmodule.html.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { ModalComponent } from './_components';

@NgModule({
    imports: [
        BrowserModule,
        FormsModule
    ],
    declarations: [
        AppComponent,
        ModalComponent
    ],
    bootstrap: [AppComponent]
})

export class AppModule { }
 

Main Index Html File

Path: /src/index.html

The main index.html file is the initial page loaded by the browser that kicks everything off. The Angular CLI (with Webpack under the hood) bundles all of the compiled javascript files together and injects them into the body of the index.html page so the scripts can be loaded and executed by the browser.

<!DOCTYPE html>
<html>
<head>
    <base href="/" />
    <title>Angular 14 - Modal Popup Tutorial with Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <app-root></app-root>
</body>
</html>
 

Angular Main File

Path: /src/main.ts

The main file is the entry point used by angular to launch and bootstrap the application.

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

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

if (environment.production) {
    enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.error(err));
 

Global LESS/CSS Styles

Path: /src/styles.less

The global styles file contains LESS/CSS styles that are applied globally throughout the application.

I just added a few things to pretty up the example a bit since it doesn't use Bootstrap or anything other CSS.

/* You can add global styles to this file, and also import other style files */
body {
    font-family: Arial, Helvetica, sans-serif;
    padding: 20px;
}

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

input[type="text"] {
    display: block;
    width: 100%;
    font-family: Arial, Helvetica, sans-serif;
    font-size: 1em;
    margin-top: 5px;
}

button {
    padding: 7px 10px;
    margin-right: 5px;
}

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

 


Subscribe or Follow Me For Updates

Subscribe to my YouTube channel or follow me on Twitter, Facebook or GitHub to be notified when I post new content.

Other than coding...

I'm currently attempting to travel around Australia by motorcycle with my wife Tina on a pair of Royal Enfield Himalayans. You can follow our adventures on YouTube, Instagram and Facebook.


Need Some Angular 14 Help?

Search fiverr to find help quickly from experienced Angular 14 developers.



Supported by