如何创建可根据屏幕尺寸切换为Mat Dialog或Mat Bottom Sheet的Angular组件
解决Angular组件同时兼容Mat Dialog和Mat Bottom Sheet的依赖问题
问题说明
需要实现一个Angular组件:当屏幕宽度大于500px时以Mat Dialog形式展示,小于500px时切换为Mat Bottom Sheet形式。由于组件代码复杂(包含数百行HTML、SCSS和TS逻辑),不想维护两个几乎完全相同的组件,但同时注入Dialog和Bottom Sheet的依赖时会出现报错:
- 打开Bottom Sheet时提示
NullInjectorError: No provider for InjectionToken MatDialogData! - 打开Dialog时提示
Error: NullInjectorError: No provider for InjectionToken MatBottomSheetData!
移除其中一组依赖后对应模式可正常工作,但无法同时兼容两种展示方式,需要解决该依赖冲突问题。
现有代码
Dialog/Bottom Sheet 组件
export interface MyComponentData { firstName: string, lastName: string } @Component({ selector: "my-component", templateUrl: "./my-component.component.html", styleUrls: ["./my-component.component.scss"] }) export class MyComponent { constructor( @Inject(MAT_BOTTOM_SHEET_DATA) public bottomSheetData: MyComponentData, @Inject(MAT_DIALOG_DATA) public dialogData: MyComponentData, public bottomSheetRef: MatBottomSheetRef<MyComponent>, public dialogRef: MatDialogRef<MyComponent> ) {} }
主组件
@Component({ selector: "main-component", templateUrl: "./main-component.component.html", styleUrls: ["./main-component.component.scss"] }) export class MainComponent { constructor(private bottomSheet: MatBottomSheet, private dialog: MatDialog) {} onClick(temp: MyComponentData): void { if(window.innerWidth > 500) { this.dialog.open(MyComponent, {data: temp}) } else { this.bottomSheet.open(MyComponent, {data: temp}) } } }
解决方案
核心思路是使用@Optional()装饰器标记Dialog和Bottom Sheet的依赖,这样当组件以其中一种形式打开时,缺失的依赖会被注入为null而非触发报错,再统一封装数据和关闭逻辑,避免在业务代码中区分两种模式。
修改后的Dialog/Bottom Sheet组件
import { Component, Inject, Optional } from '@angular/core'; import { MAT_BOTTOM_SHEET_DATA, MatBottomSheetRef } from '@angular/material/bottom-sheet'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; export interface MyComponentData { firstName: string, lastName: string } @Component({ selector: "my-component", templateUrl: "./my-component.component.html", styleUrls: ["./my-component.component.scss"] }) export class MyComponent { // 统一对外的数据源,无需区分Dialog/Bottom Sheet public data: MyComponentData; // 统一的关闭引用 private closeRef: MatDialogRef<MyComponent> | MatBottomSheetRef<MyComponent>; constructor( @Optional() @Inject(MAT_BOTTOM_SHEET_DATA) bottomSheetData: MyComponentData, @Optional() @Inject(MAT_DIALOG_DATA) dialogData: MyComponentData, @Optional() bottomSheetRef: MatBottomSheetRef<MyComponent>, @Optional() dialogRef: MatDialogRef<MyComponent> ) { // 自动识别当前模式的数据源 this.data = dialogData || bottomSheetData; // 自动绑定当前模式的关闭引用 this.closeRef = dialogRef || bottomSheetRef; } // 统一的关闭方法,模板和业务逻辑直接调用即可 public close(result?: any): void { this.closeRef?.dismiss(result); } }
优化主组件(可选)
原代码使用window.innerWidth判断屏幕宽度,建议改用Angular CDK的BreakpointObserver,更符合Angular响应式设计,适配不同设备断点:
import { Component } from '@angular/core'; import { MatBottomSheet } from '@angular/material/bottom-sheet'; import { MatDialog } from '@angular/material/dialog'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { MyComponentData } from './my-component.component'; @Component({ selector: "main-component", templateUrl: "./main-component.component.html", styleUrls: ["./main-component.component.scss"] }) export class MainComponent { constructor( private bottomSheet: MatBottomSheet, private dialog: MatDialog, private breakpointObserver: BreakpointObserver ) {} onClick(temp: MyComponentData): void { // 使用预设的断点判断是否为移动端屏幕 const isMobile = this.breakpointObserver.isMatched(Breakpoints.Handset); if (!isMobile) { this.dialog.open(MyComponent, { data: temp }); } else { this.bottomSheet.open(MyComponent, { data: temp }); } } }
模板使用示例
修改后模板中直接使用统一的data和close()方法即可,无需区分当前是Dialog还是Bottom Sheet:
<div> <h2>Hello {{ data.firstName }} {{ data.lastName }}</h2> <button (click)="close()">关闭</button> </div>
内容的提问来源于stack exchange,提问作者Daniel Morales
相关产品推荐
相关产品推荐

