从源码看angular/material2 中 dialog模块的实现方法

自己准备作为模板的弹窗内容组件

在需要使用的组件内注入 MatDialog 服务

调用 open 方法创建弹窗,并支持传入配置、数据,以及对关闭事件的订阅

深入源码

进入material2的源码,先从 MatDialog 的代码入手,找到这个 open 方法:

open<T>( componentOrTemplateRef: ComponentType<T> | TemplateRef<T>, config?: MatDialogConfig ): MatDialogRef<T> { // 防止重复打开 const inProgressDialog = this.openDialogs.find(dialog => dialog._isAnimating()); if (inProgressDialog) { return inProgressDialog; } // 组合配置 config = _applyConfigDefaults(config); // 防止id冲突 if (config.id && this.getDialogById(config.id)) { throw Error(`Dialog with id "${config.id}" exists already. The dialog id must be unique.`); } // 第一步:创建弹出层 const overlayRef = this._createOverlay(config); // 第二步:在弹出层上添加弹窗容器 const dialogContainer = this._attachDialogContainer(overlayRef, config); // 第三步:把传入的组件添加到创建的弹出层中创建的弹窗容器中 const dialogRef = this._attachDialogContent(componentOrTemplateRef, dialogContainer, overlayRef, config); // 首次弹窗要添加键盘监听 if (!this.openDialogs.length) { document.addEventListener('keydown', this._boundKeydown); } // 添加进队列 this.openDialogs.push(dialogRef); // 默认添加一个关闭的订阅 关闭时要移除此弹窗 // 当是最后一个弹窗时触发全部关闭的订阅并移除键盘监听 dialogRef.afterClosed().subscribe(() => this._removeOpenDialog(dialogRef)); // 触发打开的订阅 this.afterOpen.next(dialogRef); return dialogRef; }

总体看来弹窗的发起分为三部曲:

创建一个弹出层(其实是一个原生DOM,起宿主和入口的作用)

在弹出层上创建弹窗容器组件(负责提供遮罩和弹出动画)

在弹窗容器中创建传入的弹窗内容组件(负责提供内容)

弹出层的创建

对于其他组件,仅仅封装模板以及内部实现就足够了,最多还要增加与父组件的数据、事件交互,所有这些事情,单使用angular Component就足够实现了,在何处使用就将组件选择器放到哪里去完事。

但对于弹窗组件,事先并不知道会在何处使用,因此不适合实现为一个组件后通过选择器安放到页面的某处,而应该将其作为弹窗插座放置到全局,并通过服务来调用。

material2也要面临这个问题,这个弹窗插座是避免不了的,那就在内部实现它,在实际调用弹窗方法时动态创建这个插座就可以了。要实现效果是:对用户来说只是在单纯调用一个 open 方法,由material2内部来创建一个弹出层,并在这个弹出层上创建弹窗。

找到弹出层的创建代码如下:

create(config: OverlayConfig = defaultConfig): OverlayRef { const pane = this._createPaneElement(); // 弹出层DOM 将被添加到宿主DOM中 const portalHost = this._createPortalHost(pane); // 宿主DOM 将被添加到<body>末端 return new OverlayRef(portalHost, pane, config, this._ngZone); // 弹出层的引用 } private _createPaneElement(): HTMLElement { let pane = document.createElement('div'); pane.id = `cdk-overlay-${nextUniqueId++}`; pane.classList.add('cdk-overlay-pane'); this._overlayContainer.getContainerElement().appendChild(pane); // 将创建好的带id的弹出层添加到宿主 return pane; } private _createPortalHost(pane: HTMLElement): DomPortalHost { // 创建宿主 return new DomPortalHost(pane, this._componentFactoryResolver, this._appRef, this._injector); }

其中最关键的方法其实是 getContainerElement() , material2把最"丑"最不angular的操作放在了这里面,看看其实现:

getContainerElement(): HTMLElement { if (!this._containerElement) { this._createContainer(); } return this._containerElement; } protected _createContainer(): void { let container = document.createElement('div'); container.classList.add('cdk-overlay-container'); document.body.appendChild(container); // 在body下创建顶层的宿主 姑且称之为弹出层容器(OverlayContainer) this._containerElement = container; }

弹窗容器的创建

跳过其他细节,现在得到了一个弹出层引用 overlayRef。material2接下来给它添加了一个弹窗容器组件,这个组件是material2自己写的一个angular组件,打开弹窗时的遮罩部分以及弹窗的外轮廓其实就是这个组件,对于为何要再套这么一层容器,有其一些考虑。

动画效果的保护

这样动态创建的组件有一个缺点,那就是其销毁是无法触发angular动画的,因为一瞬间就销毁掉了,所以material2为了实现动画效果,多加了这么一个容器来实现动画,在关闭弹窗时,实际上是在播放弹窗的关闭动画,然后监听容器的动画状态事件,在完成关闭动画后才执行销毁弹窗的一系列代码,这个过程与其为难用户来实现,不如自己给封装了。

注入服务的保护

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wywsgs.html