記一個angular在路由配置中管理 Angular Material Dialog(實現動態元件的彈窗顯示)

HHepan發表於2023-03-16

背景描述

目標

我們的目標正如標題所言:在路由配置中管理 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的經典用法,經觀察和研究後我們可以看出它的大體運作流程,如下圖:
image.png
此流程中出現上述問題的地方主要集中在第三步驟。由於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:你想要彈出元件。具體流程如下圖:
image.png
這樣做,將實現彈窗的效果的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步中多寫幾行簡單的程式碼而已。這就是我們想要的,不用造重複的輪子,以及可以在複用起來輕鬆而簡便。

效果預覽

4.gif

參考文章

https://medium.com/ngconf/routing-to-angular-material-dialogs...

相關文章