You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 22:21:45