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

Angular中取消文件选择器致Nebular Stepper对话框关闭的解决问询

Angular Nebular Stepper对话框在取消文件选择器时意外关闭

问题背景

使用Angular + Nebular Stepper组件开发数据收集页面,其中一个步骤通过自定义mat-file-upload组件实现PDF上传功能:

  • 文件上传功能正常,但取消系统文件选择器时,整个步进器对话框会被意外关闭,导致已填写数据丢失
  • 特殊场景:先添加附件再删除后,取消文件选择器不会触发对话框关闭
  • 已通过事件监听确认:文件输入框会触发cancel事件并向上冒泡到步进器组件
  • 尝试过阻止事件传播,但未生效

相关代码

stepper.component.ts

/**
 * Handler for the selectedFilesChanged event of mat-file-upload component
 * for Attachments.
 * @param {FileList} fileList List of File objects of the loaded files.
 */
onSelectedAttachmentsChanged(fileList: FileList): void {
  // White list for attachments - just PDF for now.
  if (fileList.item(0).type == "application/pdf") {
    getBase64EncodedFromFile(fileList.item(0)).subscribe({
      next: (result: string) => {
        const newAttachment: Attachment = {
          filename: fileList.item(0).name,
          fileExtension: FileExtension.pdf,
          data: result,
          mimeType: fileList.item(0).type,
        };

        this.changeRequest.attachments = [
          ...this.changeRequest.attachments,
          newAttachment,
        ];
      },
      error: (err: Error) => {
        console.log("An error has occurred: ", err);
      },
    });
  } else {
    this._dialogService.open(ConfirmDialogComponent, {
      context: {
        confirmHeader: "Unsupported File Type",
        confirmMessage: "Only PDFs can be attached here.",
        confirmBtnText: "Ok",
        confirmBtnStatus: "basic",
        confirmStatus: "info",
        confirmShowCancelBtn: false,
      },
    });
  }
}

stepper.component.html

<div fxLayout="row" fxLayoutAlign="center center" fxLayoutGap="1em">
  <mat-file-upload
    [acceptedTypes]="'.pdf'"
    [labelText]="''"
    [selectButtonText]="'Attach a PDF'"
    [showUploadButton]="false"
    [allowMultipleFiles]="false"
    (selectedFilesChanged)="onSelectedAttachmentsChanged($event)"
  ></mat-file-upload>
</div>

mat-file-upload.component.ts

import {
  Component,
  ElementRef,
  EventEmitter,
  Input,
  Output,
  ViewChild,
} from "@angular/core";

@Component({
  selector: "mat-file-upload",
  template: `
    <button
      nbButton
      [type]="selectFilesButtonType"
      [disabled]="disabled"
      color="primary"
      class="file-input-button"
      (click)="fileInput.click()"
      [attr.aria-label]="selectButtonText"
    >
      <nb-icon icon="plus-outline"></nb-icon>
      <span>{{ selectButtonText }}</span>
      <input
        #fileInput
        type="file"
        style="display: none"
        [accept]="acceptedTypes"
        [multiple]="allowMultipleFiles"
        (change)="filesChanged($event.target.files)"
      />
    </button>
    <button
      mat-raised-button
      [type]="uploadButtonType"
      color="primary"
      class="file-input-button"
      [disabled]="!selectedFiles || disabled"
      (click)="uploadFiles()"
      *ngIf="showUploadButton"
      [attr.aria-label]="uploadButtonText"
    >
      {{ uploadButtonText }}
    </button>
  `,
  styles: [
    ".file-input-button { margin-right: 8px !important }",
    ".file-input-text { font-size: 14px !important; margin-right: 8px !important }",
  ],
})
export class MatFileUploadComponent {
  @Input() labelText = "Select File(s)";
  @Input() selectButtonText = "Select File(s)";
  @Input() selectFilesButtonType: "button" | "menu" | "reset" | "submit" =
    "button";
  @Input() uploadButtonText = "Upload File(s)";
  @Input() uploadButtonType: "button" | "menu" | "reset" | "submit" = "button";
  @Input() allowMultipleFiles = false;
  @Input() showUploadButton = true;
  @Input() acceptedTypes = "*.*";
  @Input() customSvgIcon?: string = null;
  @Input() disabled = false;
  @Output() uploadClicked: EventEmitter<FileList> = new EventEmitter<FileList>();
  @Output() selectedFilesChanged: EventEmitter<FileList> = new EventEmitter<FileList>();

  @ViewChild("fileInput") fileInputRef: ElementRef;
  selectedFiles: FileList;
  selectedFileText = "";

  filesChanged(files?: FileList): void {
    this.selectedFiles = files;
    this.selectedFilesChanged.emit(this.selectedFiles);
    if (this.selectedFiles) {
      const numSelectedFiles = this.selectedFiles.length;
      this.selectedFileText =
        numSelectedFiles === 1
          ? this.selectedFiles[0].name
          : `${numSelectedFiles} files selected`;
    } else {
      this.selectedFileText = "";
      this.resetFileInput();
    }
  }

  uploadFiles(): void {
    this.uploadClicked.emit(this.selectedFiles);
    this.resetFileInput();
  }

  resetFileInput(): void {
    this.fileInputRef.nativeElement.value = "";
  }
}

解决方案

1. 直接捕获并阻止文件输入框的cancel事件

修改mat-file-upload.component.ts的模板,给文件输入框添加cancel事件处理器,阻止事件冒泡:

<input
  #fileInput
  type="file"
  style="display: none"
  [accept]="acceptedTypes"
  [multiple]="allowMultipleFiles"
  (change)="filesChanged($event.target.files)"
  (cancel)="onFileInputCancel($event)" <!-- 新增事件绑定 -->
/>

在组件类中添加事件处理方法:

onFileInputCancel(event: Event): void {
  event.stopPropagation(); // 阻止事件向上传递到步进器对话框
  event.preventDefault(); // 可选:取消默认行为
}

2. 阻止上传按钮的点击事件冒泡

修改mat-file-upload组件中的触发按钮,确保点击事件不会向上传递:

<button
  nbButton
  [type]="selectFilesButtonType"
  [disabled]="disabled"
  color="primary"
  class="file-input-button"
  (click)="fileInput.click(); $event.stopPropagation()" <!-- 新增阻止冒泡 -->
  [attr.aria-label]="selectButtonText"
>

3. 优化filesChanged方法的逻辑

取消文件选择器时,files参数为undefined,此时不需要触发selectedFilesChanged事件,避免触发意外的对话框关闭逻辑:

filesChanged(files?: FileList): void {
  this.selectedFiles = files;
  // 仅当有文件选中时才触发事件
  if (files) {
    this.selectedFilesChanged.emit(this.selectedFiles);
    const numSelectedFiles = this.selectedFiles.length;
    this.selectedFileText =
      numSelectedFiles === 1
        ? this.selectedFiles[0].name
        : `${numSelectedFiles} files selected`;
  } else {
    this.selectedFileText = "";
    this.resetFileInput();
  }
}

4. 可选:添加步进器对话框的关闭拦截

如果以上方法仍未解决问题,可以在步进器所在的对话框组件中添加关闭拦截,仅允许用户主动关闭:

import { NbDialogRef } from '@nebular/theme';

constructor(private dialogRef: NbDialogRef<YourStepperDialogComponent>) {
  // 拦截对话框关闭请求
  this.dialogRef.onBeforeClose().subscribe(() => {
    // 这里可以添加判断逻辑,比如检查是否是文件选择器触发的关闭
    // 如果是意外触发,返回false阻止关闭
    // 示例:仅当用户点击确认按钮时允许关闭
    return this.isUserInitiatedClose;
  });
}

内容的提问来源于stack exchange,提问作者92awdGSX

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 03:55:53