Angular13 CanDeactivate守卫无法检测Material下拉、自定义弹窗异常
问题根因排查
- mat-select变更无法被检测:模板中所有
mat-select使用的是[(value)]做双向绑定,Angular模板驱动表单(NgForm)只会识别注册了ngModel且带有name属性的表单控件,仅用value绑定的控件不会被纳入表单的脏值(dirty)状态计算,因此修改下拉选项后form.dirty始终为false,守卫不会触发拦截。 - 自定义模态框不生效:原生
confirm是阻塞式同步API,而Angular Material的MatDialog弹窗返回的是异步Observable流,原有canDeactivate方法只返回同步boolean值,路由守卫无法等待异步弹窗的用户选择结果,因此自定义弹窗逻辑无法正常运行。
修复实现方案
1. 修正模板中mat-select的绑定方式
把所有mat-select上的[(value)]替换为[(ngModel)],确保控件被NgForm接管,参与脏值检测。修正后的表单核心片段:
<form #form="ngForm" class="form-group"> <div class="row container-fluid px-5"> <div class="col-md-6"> <mat-form-field class="mx-md-5 w-100" appearance="standard"> <mat-label>Contract Type *</mat-label> <!-- 替换[(value)]为[(ngModel)] --> <mat-select [(ngModel)]="overallStructure.contractTypeId" class="product_type required" (selectionChange)="OnContractTypeSelect($event)" name="contractTypeId" tabindex="1"> <mat-option *ngFor="let item of contractType" [value]="item.id"> {{item.name}} </mat-option> </mat-select> </mat-form-field> <mat-form-field class="mx-md-5 w-100" appearance="standard"> <mat-label>Business Unit *</mat-label> <input [(ngModel)]="businessUnitName" matInput name="businessUnitName" tabindex="3"> </mat-form-field> <mat-form-field class="mx-md-5 w-100" appearance="standard"> <mat-label>Contract Period *</mat-label> <!-- 替换[(value)]为[(ngModel)] --> <mat-select [(ngModel)]="overallStructure.contractPeriod" class="product_type required" name="contractPeriod" tabindex="5"> <mat-option value="Accident Year">Accident Year</mat-option> <mat-option value="Policy Year">Policy Year</mat-option> </mat-select> </mat-form-field> </div> <p><br></p> </div> <!-- 其余按钮部分保持原有逻辑不变 --> </form>
2. 重写FormCanDeactivate抽象基类,支持异步自定义弹窗
修改canDeactivate方法的返回类型,支持返回Observable流,替换原生confirm为封装好的弹窗服务调用;注意浏览器beforeunload事件(关闭/刷新页面场景)受安全限制,无法唤起自定义弹窗,只能使用浏览器原生提示。修正后的基类代码:
import { Observable, of } from 'rxjs'; import { NgForm } from "@angular/forms"; import { PricingToolService } from '../../services/pricingtoolservice.service'; import { HostListener } from "@angular/core"; import { Injectable } from '@angular/core'; @Injectable() export abstract class FormCanDeactivate { abstract get form(): NgForm; protected constructor(protected dialogService: PricingToolService) {} canDeactivate(): Observable<boolean> | boolean { // 表单无未保存变更直接放行 if (!this.form.dirty) { return true; } // 返回自定义弹窗的Observable流,路由守卫会自动等待订阅结果 return this.dialogService.openConfirmationDialog("You have unsaved changes! If you leave, your changes will be lost."); } @HostListener('window:beforeunload', ['$event']) unloadNotification($event: BeforeUnloadEvent) { // 刷新/关闭页面场景仅支持原生提示,无法使用自定义弹窗 if (this.form.dirty) { $event.preventDefault(); $event.returnValue = "You have unsaved changes! If you leave, your changes will be lost."; } } }
3. 调整组件类构造函数,适配基类依赖
基类新增构造函数依赖后,子类组件需要在构造函数中传入弹窗服务实例,同时在取消/保存操作后重置表单脏状态,避免守卫误触发:
// 其余导入保持原有逻辑不变 @Component({ selector: 'app-overall-structure', templateUrl: './overall-structure.component.html', styleUrls: ['./overall-structure.component.scss'] }) export class OverallStructureComponent extends FormCanDeactivate implements OnInit { @ViewChild('form') form: NgForm; constructor( public service: PricingToolService, private activatedRoute: ActivatedRoute, private router: Router ) { super(service); } ngOnInit(): void { // 原有初始化逻辑不变 } onClickCancel(){ this.service.openConfirmationDialog("Cancel will remove any edit done on the Account Structure screen, Do you want to continue ?") .subscribe((response: boolean) => { if (response){ this.form.resetForm(); // 重置表单脏状态 this.ngOnInit(); } }); } // 保存成功后也要调用this.form.resetForm(),避免跳转时触发拦截 }
4. 确认CanDeactivate守卫配置
原有守卫代码已兼容异步返回值,仅需补充注入声明即可:
import { RouterStateSnapshot, ActivatedRouteSnapshot, CanDeactivate, UrlTree } from '@angular/router'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; export interface CanComponentDeactivate { canDeactivate: () => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree; } @Injectable({ providedIn: 'root' // 或在对应路由模块的providers数组中声明守卫 }) export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> { canDeactivate( component: CanComponentDeactivate, route: ActivatedRouteSnapshot, state: RouterStateSnapshot ) { return component.canDeactivate ? component.canDeactivate() : true; } }
注意事项
- 确保
PricingToolService中的openConfirmationDialog方法在用户点击确认时返回of(true),点击取消时返回of(false),弹窗关闭后流要正常完结,避免路由挂起。 - 页面刷新/关闭场景受浏览器安全策略限制,无法唤起自定义Angular Material弹窗,只能显示浏览器原生提示,属于通用前端限制,无框架层面的绕过方案。
内容的提问来源于stack exchange,提问作者JEuvin
相关产品推荐
相关产品推荐

