Published: February 09 2023
Last updated: March 27 2023

Getting the Bootstrap 5 Modal and Angular 15 to play nicely together

Built with Angular 15.1.4 and Bootstrap 5.2.2

This tutorial shows how to open and close (show/hide) Bootstrap Modal popups with Angular 15.

The solution uses Bootstrap CSS only and not Boostrap JS. Bootstrap's JavaScript is not required because all modal behaviour is controlled with Angular.

Angular Bootstrap Modal Example

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

  • Angular + Bootstrap 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.
  • Angular + Bootstrap 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/bootstrap-5-modal-angular-15.

Here it is in action: (See on StackBlitz at https://stackblitz.com/edit/bootstrap-5-modal-example-with-angular-15)


Run the Bootstrap Modal with Angular Example

  1. Install Node.js and NPM from https://nodejs.org.
  2. Download or clone the project source code from https://github.com/cornflourblue/bootstrap-5-modal-angular-15
  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 + Bootstrap 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').

Bootstrap 5 CSS

The CSS from Bootstrap 5.2.2 is included in the main index.html file from a CDN. The documentation on Bootstrap's Modal CSS classes is available at https://getbootstrap.com/docs/5.2/components/modal/, just ignore anything about Bootstrap's JavaScript modal plugin because we're using Angular in place of the that here.

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.

 

Bootstrap 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 the bootstrap modal container divs (modal, modal-dialog, modal-content) wrapped around an Angular <ng-content> element. The semi-transparent modal background is created by the bootstrap modal-backdrop 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. <modal>my content...</modal>) is rendered in the location of the <ng-content> element. For more info see https://angular.io/guide/content-projection.

<div class="modal" style="display: block;">
    <div class="modal-dialog">
        <div class="modal-content">
            <ng-content></ng-content>
        </div>
    </div>
</div>
<div class="modal-backdrop show"></div>
 

Modal Component LESS/CSS Styles

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

We only need a couple of styles rules since all the modal styling is handled by Bootstrap CSS.

Modal style rules

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.

body.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.

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

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

Angular + Bootstrap Modal Component

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

The angular modal component controls the behaviour of a bootstrap 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 <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.modal-open) targets to the root <body> element.

Unique id required

Each modal instance must have a unique id attribute (e.g. <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 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 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: '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 === '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('modal-open');
        this.isOpen = true;
    }

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

Angular Modal Popup Service

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

The modal service is the middle man used to open and close Bootstrap modals 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 Bootstrap 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:

  • Angular + Bootstrap 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.
  • Angular + Bootstrap #2 - is a tall popup to demonstrate that the modal is scrollable while keeping the page below locked in position.

Both modals include a Bootstrap modal-header, modal-body and modal-footer element, each header contains a modal title and a close icon, and each modal footer has a close button.

<!-- main app container -->
<div>
    <div class="m-3">
        <h1>Bootstrap 5 Modal Example with Angular 15</h1>
        <p>{{bodyText}}</p>
        <button class="btn btn-primary me-2" (click)="modalService.open('modal-1')">Open Modal 1</button>
        <button class="btn btn-primary" (click)="modalService.open('modal-2')">Open Modal 2</button>
    </div>
    
    <modal id="modal-1">
        <div class="modal-header">
            <h5 class="modal-title">Angular + Bootstrap Modal #1</h5>
            <button type="button" class="btn-close" (click)="modalService.close();"></button>
        </div>
        <div class="modal-body">
            <p>This bootstrap modal was opened with Angular!</p>
            <p>
                <label class="form-label">Page text:</label>
                <input type="text" class="form-control" [(ngModel)]="bodyText" />
            </p>
        </div>
        <div class="modal-footer">
            <button type="button" class="btn btn-secondary" (click)="modalService.close();">Close</button>
        </div>
    </modal>
    
    <modal id="modal-2">
        <div class="modal-header">
            <h5 class="modal-title">Angular + Bootstrap Modal #2</h5>
            <button type="button" class="btn-close" (click)="modalService.close();"></button>
        </div>
        <div class="modal-body">
            <p style="padding-bottom: 1500px;">This is a tall modal to demonstrate scrolling modal content.</p>
        </div>
        <div class="modal-footer">
            <button type="button" class="btn btn-secondary" (click)="modalService.close();">Close</button>
        </div>
    </modal>
</div>
 

Angular App Component

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

The app component contains a bodyText string property which is used in the component template and bound to the text input of Bootstrap modal #1.

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.

Bootstrap 5 CSS

The CSS stylesheet from Bootstrap 5.2.2 is included here via a CDN.

<!DOCTYPE html>
<html>
<head>
    <base href="/" />
    <title>Bootstrap 5 Modal Example with Angular 15</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- bootstrap css -->
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
</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));

 


Need Some Angular Help?

Search fiverr for freelance Angular developers.


Follow me for updates

On Twitter or RSS.


When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Comments


Supported by