ngx-light-modal
Version:
Lightweight and dependency-free Angular modal package using standalone components. Supports stacking, backdrops, dynamic injection, and custom content components.
244 lines (236 loc) • 13.5 kB
JavaScript
import { NgClass } from '@angular/common';
import * as i0 from '@angular/core';
import { inputBinding, createComponent, Injectable, inject, input, ViewContainerRef, HostListener, ViewChild, Component } from '@angular/core';
import { Subject, filter as filter$1 } from 'rxjs';
import { filter } from 'rxjs/operators';
var CloseState;
(function (CloseState) {
CloseState["EXITED"] = "EXITED";
CloseState["BACKDROP_DISMISS"] = "BACKDROP_DISMISS";
CloseState["CLOSE_BUTTON"] = "CLOSE_BUTTON";
CloseState["ESCAPE"] = "ESCAPE";
CloseState["SUBMITTED"] = "SUBMITTED";
CloseState["CANCELLED"] = "CANCELLED";
})(CloseState || (CloseState = {}));
class ModalRef {
componentInstance;
id;
_afterClosed = new Subject();
_beforeClosed = new Subject();
_afterOpened = new Subject();
afterClosed$ = this._afterClosed.asObservable();
beforeClosed$ = this._beforeClosed.asObservable();
afterOpened$ = this._afterOpened.asObservable();
contentRef;
constructor(componentInstance, id) {
this.componentInstance = componentInstance;
this.id = id;
}
onSubmitted() {
return this.getStateObservable(CloseState.SUBMITTED);
}
onEscaped() {
return this.getStateObservable(CloseState.ESCAPE);
}
onBackdropDismiss() {
return this.getStateObservable(CloseState.BACKDROP_DISMISS);
}
onStateClose(state) {
return this.getStateObservable(state);
}
onCancelled() {
return this.getStateObservable(CloseState.CANCELLED);
}
onUserDismissed() {
return this.afterClosed$.pipe(filter((result) => [CloseState.BACKDROP_DISMISS, CloseState.ESCAPE, CloseState.CLOSE_BUTTON].includes(result.state)));
}
close(result, state = CloseState.EXITED) {
this._beforeClosed.next();
this._beforeClosed.complete();
this._afterClosed.next({ state, data: result });
this._afterClosed.complete();
}
notifyAfterOpened() {
if (this._afterOpened.closed)
return;
this._afterOpened.next();
this._afterOpened.complete();
}
getStateObservable(state) {
return this.afterClosed$.pipe(filter((result) => result.state === state));
}
}
class ModalService {
injector;
appRef;
environmentInjector;
modalStack = [];
contentComponentRegistry = new WeakMap();
constructor(injector, appRef, environmentInjector) {
this.injector = injector;
this.appRef = appRef;
this.environmentInjector = environmentInjector;
}
open(component, data, config = {}) {
const zIndex = 1000 + (this.modalStack.length + 1) * 100;
const bindings = [
inputBinding('showBackdrop', () => config.showBackdrop ?? true),
inputBinding('dismissOnBackdropClick', () => config.dismissOnBackdropClick ?? true),
inputBinding('showCloseButton', () => config.showCloseButton ?? true),
inputBinding('escapeKeyClose', () => config.escapeKeyClose ?? true),
inputBinding('modalClass', () => config.modalClass || ''),
inputBinding('backdropClass', () => config.backdropClass || ''),
inputBinding('zIndex', () => zIndex),
];
const modalContainerRef = createComponent(ModalContainerComponent, {
environmentInjector: this.environmentInjector,
elementInjector: this.injector,
bindings,
});
const contentRef = modalContainerRef.instance.createComponent(component);
if (contentRef.instance && data) {
Object.assign(contentRef.instance, data);
}
const modalRef = new ModalRef(contentRef.instance, this.generateModalId());
this.modalStack.push({ containerRef: modalContainerRef, modalRef });
this.contentComponentRegistry.set(contentRef.instance, modalRef);
// Configure modal container
Object.assign(modalContainerRef.instance, {
modalRef: modalRef,
});
document.body.appendChild(modalContainerRef.location.nativeElement);
this.appRef.attachView(modalContainerRef.hostView);
modalContainerRef.instance.focus();
modalRef.afterClosed$.subscribe((result) => {
this.cleanupModal(modalRef.id);
this.closeSource.next(result);
});
Promise.resolve().then(() => modalRef.notifyAfterOpened());
return modalRef;
}
generateModalId() {
return Date.now() + Math.floor(Math.random() * 1000);
}
getModalRef(contentComponent) {
return this.contentComponentRegistry.get(contentComponent);
}
cleanupModal(modalId) {
const index = this.modalStack.findIndex((m) => m.modalRef.id === modalId);
if (index === -1)
return;
const [modal] = this.modalStack.splice(index, 1);
this.contentComponentRegistry.delete(modal.modalRef.componentInstance);
// Destroy content component
if (modal.modalRef.contentRef) {
modal.modalRef.contentRef.destroy();
}
// Destroy container
this.appRef.detachView(modal.containerRef.hostView);
modal.containerRef.destroy();
// Update z-index and top status for remaining modals
this.modalStack.forEach((m) => {
m.containerRef.changeDetectorRef.detectChanges();
});
// Focus new top modal
if (this.modalStack.length > 0) {
const topModal = this.modalStack[this.modalStack.length - 1];
topModal.containerRef.instance.focus();
}
}
getTopModal() {
return this.modalStack[this.modalStack.length - 1]?.modalRef;
}
isTopModal(modalRef) {
const top = this.getTopModal();
console.log(modalRef);
return top?.id === modalRef.id;
}
closeSource = new Subject();
// Listen to all closes
onClose$() {
return this.closeSource.asObservable();
}
// Listen to specific states
onStateClose$(state) {
return this.closeSource.pipe(filter$1((result) => result.state === state));
}
// Helper methods for common states
onSubmitted$() {
return this.onStateClose$(CloseState.SUBMITTED);
}
onDismissed$() {
return this.onStateClose$(CloseState.BACKDROP_DISMISS);
}
onEscaped$() {
return this.onStateClose$(CloseState.ESCAPE);
}
notifyClose(result) {
this.closeSource.next(result);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ModalService, deps: [{ token: i0.Injector }, { token: i0.ApplicationRef }, { token: i0.EnvironmentInjector }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ModalService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ModalService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [{ type: i0.Injector }, { type: i0.ApplicationRef }, { type: i0.EnvironmentInjector }] });
class ModalContainerComponent {
modalContent;
modal;
modalRef;
contentComponent;
modalService = inject(ModalService);
showBackdrop = input.required();
dismissOnBackdropClick = input.required();
showCloseButton = input.required();
escapeKeyClose = input.required();
modalClass = input.required();
backdropClass = input.required();
zIndex = input.required();
focus() {
this.modal.nativeElement.focus();
}
handleEscape(event) {
if (this.escapeKeyClose() && this.modalService.isTopModal(this.modalRef)) {
this.close(CloseState.ESCAPE);
}
}
createComponent(component) {
this.modalContent.clear();
return this.modalContent.createComponent(component);
}
onBackdropClick(event) {
if (this.dismissOnBackdropClick() && event.target === this.modal.nativeElement) {
this.close(CloseState.BACKDROP_DISMISS);
}
}
closeWithButton() {
this.close(CloseState.CLOSE_BUTTON);
}
close(state) {
this.modalRef?.close(undefined, state);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ModalContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: ModalContainerComponent, isStandalone: true, selector: "app-modal-container", inputs: { showBackdrop: { classPropertyName: "showBackdrop", publicName: "showBackdrop", isSignal: true, isRequired: true, transformFunction: null }, dismissOnBackdropClick: { classPropertyName: "dismissOnBackdropClick", publicName: "dismissOnBackdropClick", isSignal: true, isRequired: true, transformFunction: null }, showCloseButton: { classPropertyName: "showCloseButton", publicName: "showCloseButton", isSignal: true, isRequired: true, transformFunction: null }, escapeKeyClose: { classPropertyName: "escapeKeyClose", publicName: "escapeKeyClose", isSignal: true, isRequired: true, transformFunction: null }, modalClass: { classPropertyName: "modalClass", publicName: "modalClass", isSignal: true, isRequired: true, transformFunction: null }, backdropClass: { classPropertyName: "backdropClass", publicName: "backdropClass", isSignal: true, isRequired: true, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: true, transformFunction: null } }, host: { listeners: { "document:keydown.escape": "handleEscape($event)" } }, viewQueries: [{ propertyName: "modalContent", first: true, predicate: ["modalContent"], descendants: true, read: ViewContainerRef, static: true }, { propertyName: "modal", first: true, predicate: ["modal"], descendants: true, static: true }], ngImport: i0, template: " <div\r\n #modal\r\n [ngClass]=\"{ 'ngx-backdrop-active': showBackdrop() }\"\r\n [class]=\"'ngx-modal-backdrop ' + backdropClass()\"\r\n (click)=\"onBackdropClick($event)\"\r\n tabindex=\"-1\"\r\n [style.zIndex]=\"zIndex()\"\r\n >\r\n <div class=\"ngx-modal-content\" [ngClass]=\"modalClass()\" [style.zIndex]=\"zIndex() + 1\">\r\n @if (showCloseButton()) {\r\n <button class=\"ngx-close-button\" (click)=\"closeWithButton()\">×</button>\r\n }\r\n\r\n <ng-container #modalContent></ng-container>\r\n </div>\r\n </div>\r\n", styles: [".ngx-modal-backdrop{position:fixed;top:0;left:0;width:100dvw;height:100dvh;display:flex;justify-content:center;align-items:center}.ngx-modal-backdrop.ngx-backdrop-active{background-color:#00000080}:host{position:fixed;top:0;left:0;width:100dvw;height:100dvh;min-height:100vh}.ngx-modal-backdrop:focus{outline:none}.ngx-modal-content{background:#fff;padding:20px;border-radius:4px;position:relative;max-width:90dvw;max-height:90dvh;overflow:auto}.ngx-close-button{position:absolute;top:10px;right:10px;background:none;border:none;font-size:1.2rem;cursor:pointer;z-index:100}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ModalContainerComponent, decorators: [{
type: Component,
args: [{ selector: 'app-modal-container', imports: [NgClass], standalone: true, template: " <div\r\n #modal\r\n [ngClass]=\"{ 'ngx-backdrop-active': showBackdrop() }\"\r\n [class]=\"'ngx-modal-backdrop ' + backdropClass()\"\r\n (click)=\"onBackdropClick($event)\"\r\n tabindex=\"-1\"\r\n [style.zIndex]=\"zIndex()\"\r\n >\r\n <div class=\"ngx-modal-content\" [ngClass]=\"modalClass()\" [style.zIndex]=\"zIndex() + 1\">\r\n @if (showCloseButton()) {\r\n <button class=\"ngx-close-button\" (click)=\"closeWithButton()\">×</button>\r\n }\r\n\r\n <ng-container #modalContent></ng-container>\r\n </div>\r\n </div>\r\n", styles: [".ngx-modal-backdrop{position:fixed;top:0;left:0;width:100dvw;height:100dvh;display:flex;justify-content:center;align-items:center}.ngx-modal-backdrop.ngx-backdrop-active{background-color:#00000080}:host{position:fixed;top:0;left:0;width:100dvw;height:100dvh;min-height:100vh}.ngx-modal-backdrop:focus{outline:none}.ngx-modal-content{background:#fff;padding:20px;border-radius:4px;position:relative;max-width:90dvw;max-height:90dvh;overflow:auto}.ngx-close-button{position:absolute;top:10px;right:10px;background:none;border:none;font-size:1.2rem;cursor:pointer;z-index:100}\n"] }]
}], propDecorators: { modalContent: [{
type: ViewChild,
args: ['modalContent', { read: ViewContainerRef, static: true }]
}], modal: [{
type: ViewChild,
args: ['modal', { static: true }]
}], handleEscape: [{
type: HostListener,
args: ['document:keydown.escape', ['$event']]
}] } });
/*
* Public API Surface of ngx-modal
*/
/**
* Generated bundle index. Do not edit.
*/
export { CloseState, ModalContainerComponent, ModalRef, ModalService };
//# sourceMappingURL=ngx-light-modal.mjs.map