背景描述
目標
我們的目標正如標題所言:在路由配置中管理 Angular Material Dialog,從而更簡便(程式碼量更少,可複用性,可擴充性、可維護性更強)地實現動態元件的彈窗顯示。不難看出,我們的目標由兩個部分組成,動態元件和彈窗。其實二者的單獨實現都並不困難,angular官網所推薦的眾多UI庫中都有二者對應的demo。但是,要將二者健壯地結合起來並不是一件容易的事情。
我曾經嘗試過兩種方案:一是bootstrap->model實現彈窗+Material->Cdk->Portal實現動態元件;二是Material->Dialog實現彈窗+向openDialog()方法中傳引數以控制要載入的元件實現動態元件。對兩種方案經過嘗試後會發現,兩個方案都有問題且大同小異:對使用的元件(C層、V層)程式碼侵入過多,且部分程式碼需要多次複寫,這對於開發而言並不是好的現象。
後來經過老師引導,最終發現以路由配置去管理Material Dialog實現動態元件彈窗顯示的方案更為合適。這就是我們下面所要研究的。
Material Dialog使用方法
首先我們需要對Material->Dialog基礎的使用方法有一定的瞭解,具體詳情請參考官方文件:
https://material.angular.io/components/dialog/overview,在此就不過多贅述。
如何實現以路由配置去管理
實現思路
首先我們要理解好官方提供的dialog的經典用法,經觀察和研究後我們可以看出它的大體運作流程,如下圖:
此流程中出現上述問題的地方主要集中在第三步驟。由於openDialog()方法是直接寫在元件C層中的,這就導致程式碼入侵這個問題無法避免。而倘若以向方法中傳引數來控制要載入的元件實現動態元件的方式,這無疑會讓本就侵入的程式碼量變得更多了,而且還有部分程式碼需要多次複寫;最最讓人受不了的還是:每個想要這樣用的元件,都得來上這麼一套,實在折磨。所以現在問題進一步轉化成了:如何在Material->Dialog經典用法的基礎上,將原本需要寫在元件C層中的openDialog()方法抽離出來,變得可複用,易維護。
經老師指導和上網查閱資料,最終以路由配置去管理的方式實現了我們想要的效果,再回過頭來總結它具體的實現思路:V層button不再直接繫結openDialog()方法,而是繫結路由(routerLink)且要加上路由內容輸出語句(<router-outlet></router-outlet>);在routing.module檔案中給所繫結子路由的component設定為DialogEntryComponent(翻譯過來為彈窗入口元件,沒錯這是我們新建的dialog-entry.component.ts檔案,這時候需要將openDialog()方法及其他相關方法從原元件C層中挪動到這裡面來,這樣原元件C層將不會出現程式碼侵入的問題);然後在routing.module檔案中給所繫結路由的data中設定component:你想要彈出元件。具體流程如下圖:
這樣做,將實現彈窗的效果的openDialog()方法及其他相關方法抽離到DialogEntryComponent中;將元件的動態渲染交由路由檔案中向DialogEntryComponent傳入目標元件來完成。完美解決了上文中提到的一系列問題。
程式碼實現
檔案目錄準備:1.Term目錄下需要有term-index元件,term-add元件,term-edit元件,每個元件中都有C層、V層、CSS檔案、測試檔案;還需要有term.module.ts、term-routing.module.ts檔案。2.dialog-entry目錄下需要有dialog-entry.component.ts、dialog-entry.module.ts檔案。
term-index的C層什麼都不需要
term-index的V層
<!--基於Material Dialogs的彈窗顯示動態元件demo-->
<button mat-raised-button routerLink="add">add</button>
<button mat-raised-button routerLink="edit/3">edit</button>
<router-outlet></router-outlet>
term-add的C層
export class TermAddComponent implements OnInit {
constructor(
public dialogRef: MatDialogRef<TermAddComponent>,
) {}
ngOnInit(): void {
}
onNoClick(): void {
this.dialogRef.close();
}
onOkClick(): void {
this.dialogRef.close();
}
}
term-add的V層
<h1 mat-dialog-title>Hi! term-add works</h1>
<div mat-dialog-actions>
<button mat-button (click)="onNoClick()">No Thanks</button>
<button mat-button (click)="onOkClick()" cdkFocusInitial>Ok</button>
</div>
term-edit的C層與term-add的基本相同
只需將建構函式中MatDialogRef<>填入自己的元件即可。
term-edit的V層與term-add的基本相同
只需將h1標籤中填入可辨識此元件的內容即可。
term-routing.module.ts
import { NgModule } from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {TermIndexComponent} from './term-index/term-index.component';
import {TermAddComponent} from './term-add/term-add.component';
import {TermEditComponent} from './term-edit/term-edit.component';
import {DialogEntryComponent} from '../../common/dialog-entry/dialog-entry.component';
const routes: Routes = [
{
path: '',
component: TermIndexComponent,
children: [
{
path: 'add',
component: DialogEntryComponent,
data: {
component: TermAddComponent
}
},
{
path: 'edit/:TermId',
component: DialogEntryComponent,
data: {
component: TermEditComponent
}
}
]
}
];
@NgModule({
imports: [
RouterModule.forChild(routes)
],
exports: [RouterModule]
})
export class TermRoutingModule { }
dialog-entry.component.ts
import {Component} from '@angular/core';
import {MatDialog} from '@angular/material/dialog';
import {ActivatedRoute, Router} from '@angular/router';
@Component({
template: ''
})
export class DialogEntryComponent {
url: string | undefined;
constructor(public dialog: MatDialog,
private router: Router,
private route: ActivatedRoute) {
this.url = this.route.snapshot.url[0].path;
this.openDialog();
}
openDialog(): void {
const dialogRef = this.dialog.open(this.route.snapshot.data.component, {
width: '250px'
});
const relativeBackUrl = this.getRelativeBackUrl();
dialogRef.afterClosed().subscribe(result => {
this.router.navigate([relativeBackUrl], { relativeTo: this.route });
});
}
private getRelativeBackUrl(): string {
if (this.url === 'add') {
return '../';
} else if (this.url === 'edit') {
return '../../';
} else {
return '';
}
}
}
dialog-entry.module.ts
import {NgModule} from '@angular/core';
import {MatDialogModule} from '@angular/material/dialog';
import {DialogEntryComponent} from './dialog-entry.component';
@NgModule({
declarations: [
DialogEntryComponent
],
imports: [
MatDialogModule
],
})
export class DialogEntryModule {
}
最後在使用時需要記得在模組中引入DialogEntryModule。此demo為term模組則有
term.module.ts
@NgModule({
declarations: [TermIndexComponent, TermAddComponent, TermEditComponent],
imports: [
CommonModule,
TermRoutingModule,
ReactiveFormsModule,
MatButtonModule,
DialogEntryModule,
MatDialogModule
],
providers: [
{ provide: MatDialogRef, useValue: {} },
],
})
總結
至此,我們的目標終於達成了,之後其他模組想要複用的話,只需要做以下3項工作即可:
1.在module檔案中importers我們的DialogEntryModule;
2.給主元件的V層button中繫結轉跳的路由資訊;
3.在routing.module檔案中設定對應路由所要轉跳的元件。
眾所周知,第2第3步工作就算不使用動態元件彈窗也是無法避免的。而像本文這般配置好後,之後其他模組再想要複用時,多出的工作內容只有第1步和第3步中多寫幾行簡單的程式碼而已。這就是我們想要的,不用造重複的輪子,以及可以在複用起來輕鬆而簡便。
效果預覽
參考文章
https://medium.com/ngconf/routing-to-angular-material-dialogs...