Angular 2+ modal окно c помощью ng-template
Когда несколько месяцев назад передо мной встала задача — как реализовать модальное окно на новом Angular, то в поисках ответа я перерыла интернет и пришла к нескольким мыслям:
- Существующие в виде пакетов решения слишком громоздки, так что нужно свое, реализующие только то, что необходимо.
- Кажется все будет не так уж просто в реализации, хотя на словах все не так страшно.
Что такое модальное/диалоговое окно или проще говоря popup ?
Это некий html код, который появляется на странице. У него есть определенные стили, представляющие его всплывающим окном.
В рамках нового Angular это компонент. То есть нам надо вставить в наше дерево компонентов новый компонент.
Так же нам необходим механизм закрытия/скрытия модального окна.
Таким образом, нужны:
- компонент, который будет реализовывать функционал вставки
- компонент, который мы будет вставлять
- сервис, с помощью которого все наши потенциальные компоненты на вставку/открытие будут общаться.
PopupService
Начнем с сервиса.
Я его реализовала с помощью ReplaySubject. ReplaySubject похож на BehavierSubject (про который писала в посте Angular 2 сервис с observable).
Но он может хранить более одного переданного значения. В данном случае нам это дает возможность ‘поймать’ не только последний открытый/закрытый попап, но и большее количество. При желании.
Метод asObservable() делает наш объект только читаемым, таким образом мы защищаем нашу систему от нежелательного вмешательства в стороннем коде.
Теперь через объект popupDialog$ нельзя будет испускать (передавать всем подписчикам) значение, которое нам не нужно. Следовательно, подписчики могут получить либо ‘open’, либо ‘close’. Значения, которые popupDialog испускает в методах сервиса open и close.
import {Injectable} from '@angular/core'; import { Subject } from 'rxjs/Subject'; import { Observable } from 'rxjs/Observable'; import {ReplaySubject} from 'rxjs/Rx'; @Injectable() export class PopupService { private popupDialog = new ReplaySubject<{popupEvent: string, component?: any, options?: {}}>(); public popupDialog$ = this.popupDialog.asObservable(); open(component: any, options?: any) { this.popupDialog.next({popupEvent: 'open', component: component, options: options}); } close() { this.popupDialog.next({popupEvent: 'close'}); } }
Компонент, вставляющий модальное окно
В шаблоне данного компонента мы будет использовать директиву — popup-host, размещенную в компоненте ng-template, который нам как раз и позволяет вставлять один компонент в другой. А пометка-директива popup-host дает понять куда именно вставлять.
import { Component, AfterViewInit, ComponentFactoryResolver, OnDestroy, ViewContainerRef, ViewChild } from '@angular/core'; import {Subscription} from 'rxjs/Subscription'; import {PopupService} from './popup.service'; import {PopupDirective} from './popup.directive'; export interface PopupComponent { data: any; close: any; } @Component({ selector: 'xp-popup', template: ` <div class="modal-block"> <ng-template popup-host></ng-template> </div> ` }) export class XpPopupComponent implements AfterViewInit, OnDestroy { subscription: Subscription; /* инжектим child PopupDirective как свойство popupHost в этом родительском контейнере */ @ViewChild(PopupDirective) popupHost: PopupDirective; constructor(private popupService: PopupService, private componentFactoryResolver: ComponentFactoryResolver, private viewContainerRef: ViewContainerRef) { } ngAfterViewInit() { this.subscription = this.popupService.popupDialog$.subscribe((data) => { if (!!data && data.popupEvent === 'open') { this.open(data); } else if (data && ( data.popupEvent === 'close') ) { this.close(); } }); } ngOnDestroy() { this.subscription.unsubscribe(); } open(data) { let componentFactory = this.componentFactoryResolver.resolveComponentFactory(data.component); /* получаем доступ к контейнеру, куда будем вставлять компонент */ this.viewContainerRef = this.popupHost.viewContainerRef; // Удаляем любые уже открытые диалоговые окна this.viewContainerRef.clear(); /* инжектим компонент в контейнер*/ let componentRef = this.viewContainerRef.createComponent(componentFactory); // передаем опции в вставляемый компонент, // которые будут доступны в его шаблоне (например заголовок попапа) (<PopupComponent>componentRef.instance).data = data.options; } close() { this.viewContainerRef.detach(0); } }
Сама директива очень простая
import { Directive, ViewContainerRef, Inject } from '@angular/core'; @Directive({ selector: '[popup-host]' }) export class PopupDirective { constructor(public viewContainerRef: ViewContainerRef) { } }
А вот пример компонента на показ в диалоговом окне
import { Component, Input } from '@angular/core'; import { PopupService } from './../popup.service'; export interface PopupComponent { data: any; } @Component({ selector: 'success-popup', template: ` <div class="ui-modal"> <h1>{{ data.title }}</h1> <div class="modal-message">{{ data.message }}</div> <button ng-click="close()" class="btn btn__success"></button> </div> ` }) export class SuccessModalComponent implements PopupComponent { @Input() data: {}; constructor( private _popupService: PopupService) {} close() { this._popupService.close(); } }
А попроще?
Спустя время после реализации попапного модуля (со всем вышеперечисленным) я пришла к значительно более простому решению. О нем в посте Angular 2+ модальное окно с помощью router-outlet
Здравствуйте Front Nika. Я взял — за основу именно это модальное окно, но у меня никак не получается его довести до ума. SuccessModalComponent вставляется в компонент XpPopupComponent- но само модальное окно не показывается. Есть ли ссылка на полный пример?
Григорий, здравствуйте! Полный пример показать не могу, но если код компонента вставился, но не отображается, то стоит смотреть что с css стилями. Если же не вставился, то можно проверить все ли подключено в app.module.