DevUI modal 弹窗表单联动实战:表格编辑功能完整实现

最近在做一个用户管理模块,需要在表格中点击"编辑"按钮弹出表单弹窗来修改数据。刚开始用 d-modal 组件直接写,结果各种问题,后来发现官方推荐用 DialogService,这才算解决了。记录一下踩坑过程。

前言

弹窗表单是后台管理系统里最常见的交互模式了。用户列表页面,点击编辑按钮,弹出一个表单弹窗,修改完数据保存,更新列表。听起来简单,但实际做起来还是有不少细节要注意的。

我一开始想当然地直接用 d-modal 组件,结果发现控制显示隐藏、数据回填、表单验证这些地方都挺麻烦的。后来看了官方文档,发现用 DialogService 动态创建弹窗会更简单。这篇文章就记录一下怎么用 DialogService 实现弹窗表单联动。

一、DialogService 的正确打开方式

DevUI 提供了两种弹窗使用方式:一种是直接用 d-modal 组件,另一种是用 DialogService 动态创建。对于表单弹窗这种场景,官方推荐用 DialogService

首先,需要在 app.config.ts 中提供 DialogService 和它的依赖:

typescript 复制代码
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideAnimations } from '@angular/platform-browser/animations';
import { DialogService } from 'ng-devui/modal';
import { OverlayContainerRef } from 'ng-devui/overlay-container';
import { DevConfigService } from 'ng-devui/utils';
import { DocumentRef } from 'ng-devui/window-ref';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    provideAnimations(),
    DialogService,
    OverlayContainerRef,
    DevConfigService,
    DocumentRef,
  ],
};

这里有个坑:DialogService 依赖 OverlayContainerRef,而 OverlayContainerRef 又依赖 DocumentRef,这些都需要手动提供。如果漏了哪个,运行时会报 NullInjectorError

二、创建表单组件

表单组件不需要包含 d-modal 标签,只需要表单内容。DialogService 会自动帮你套上弹窗的外壳。

typescript 复制代码
// user-edit-modal.component.ts
import { Component, OnInit, Input } from '@angular/core';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { FormModule } from 'ng-devui/form';
import { TextInputModule } from 'ng-devui/text-input';
import { SelectModule } from 'ng-devui/select';
import { RadioModule } from 'ng-devui/radio';

@Component({
  selector: 'app-user-edit-modal',
  standalone: true,
  imports: [
    CommonModule,
    ReactiveFormsModule,
    FormModule,
    TextInputModule,
    SelectModule,
    RadioModule,
  ],
  templateUrl: './user-edit-modal.component.html',
  styleUrl: './user-edit-modal.component.scss',
})
export class UserEditModalComponent implements OnInit {
  @Input() data: any; // DialogService 会通过 data 传递数据

  userForm!: FormGroup;

  constructor(private fb: FormBuilder) {}

  ngOnInit(): void {
    this.initForm();
    // 从 data 中获取数据
    if (this.data?.userData) {
      this.loadUserData(this.data.userData);
    }
  }

  initForm(): void {
    this.userForm = this.fb.group({
      name: ['', [Validators.required, Validators.minLength(2)]],
      age: [null, [Validators.required, Validators.min(18)]],
      gender: ['', Validators.required],
      email: ['', [Validators.required, Validators.email]],
      department: ['', Validators.required],
    });

    // 监听表单变化,更新按钮状态
    this.userForm.valueChanges.subscribe(() => {
      if (this.data?.canConfirm) {
        this.data.canConfirm(this.userForm.valid);
      }
    });
  }

  loadUserData(data: any): void {
    // 数据格式转换
    const formData = {
      name: data.name || '',
      age: data.age || null,
      gender: data.gender === '男' ? 'male' : 'female',
      email: data.email || '',
      department: this.getDepartmentKey(data.department),
    };
    this.userForm.patchValue(formData);
  }

  onSubmit(): void {
    if (this.userForm.valid) {
      const submitData = {
        ...this.userForm.value,
        gender: this.userForm.value.gender === 'male' ? '男' : '女',
        department: this.departmentOptions.find(
          d => d.key === this.userForm.value.department
        )?.value || '',
      };
      // 调用回调函数
      if (this.data?.onSave) {
        this.data.onSave(submitData);
      }
    } else {
      // 标记所有字段为 touched,显示验证错误
      Object.keys(this.userForm.controls).forEach(key => {
        this.userForm.get(key)?.markAsTouched();
      });
    }
  }
}

关键点:

  • 组件通过 @Input() data 接收 DialogService 传递的数据
  • 表单验证通过后,调用 data.onSave 回调函数
  • 表单变化时,通过 data.canConfirm 更新弹窗按钮的禁用状态

三、在表格组件中使用 DialogService

表格组件中注入 DialogService,点击编辑按钮时调用 open 方法:

typescript 复制代码
// table.component.ts
import { Component } from '@angular/core';
import { DialogService } from 'ng-devui/modal';
import { UserEditModalComponent } from '../user-edit-modal/user-edit-modal.component';

@Component({
  selector: 'app-table',
  standalone: true,
  imports: [DataTableModule, PaginationModule, ButtonModule, CommonModule],
  templateUrl: './table.component.html',
})
export class TableComponent {
  constructor(private dialogService: DialogService) {}

  // 打开编辑弹窗
  openEditModal(user: any): void {
    const results = this.dialogService.open({
      id: 'user-edit-dialog',
      width: '600px',
      title: '编辑用户',
      content: UserEditModalComponent,
      backdropCloseable: true,
      data: {
        userData: user,
        onSave: (userData: any) => {
          // 更新表格数据
          this.updateUserData(user.id, userData);
          results.modalInstance.hide();
        },
        canConfirm: (value: boolean) => {
          // 更新保存按钮的禁用状态
          results.modalInstance.updateButtonOptions([{ disabled: !value }]);
        }
      },
      buttons: [
        {
          cssClass: 'primary',
          text: '保存',
          disabled: true,
          handler: ($event: Event) => {
            if (results.modalContentInstance?.onSubmit) {
              results.modalContentInstance.onSubmit();
            }
          },
        },
        {
          cssClass: 'common',
          text: '取消',
          handler: () => {
            results.modalInstance.hide();
          },
        },
      ],
    });
  }
}

dialogService.open() 返回的对象包含:

  • modalInstance:弹窗实例,可以调用 hide() 关闭弹窗
  • modalContentInstance:表单组件实例,可以调用组件的方法

四、样式优化

弹窗内容区域的样式需要注意,要确保背景透明,和弹窗背景一致:

scss 复制代码
.modal-content {
  padding: 0;
  background: transparent !important;
  
  form {
    background: transparent !important;
    
    d-form-item {
      margin-bottom: 20px;
      display: flex;
      align-items: flex-start;
      background: transparent !important;
      
      d-form-label {
        margin-right: 12px;
        margin-top: 8px;
        min-width: 60px;
        flex-shrink: 0;
      }
      
      d-form-control {
        flex: 1;
        max-width: 200px;
      }
    }
  }
}

// 覆盖 DevUI 默认样式
:host ::ng-deep {
  form[dForm] {
    background: transparent !important;
    border: none !important;
  }
}

!important::ng-deep 是为了覆盖 DevUI 的默认样式。如果不加,可能会看到表单区域有背景色或边框,和弹窗不协调。

五、常见问题

1. 按钮状态更新

保存按钮默认是禁用的,只有当表单验证通过时才启用。这需要在表单的 valueChanges 中调用 canConfirm

typescript 复制代码
this.userForm.valueChanges.subscribe(() => {
  if (this.data?.canConfirm) {
    this.data.canConfirm(this.userForm.valid);
  }
});

2. 数据格式转换

表格数据和表单数据格式可能不一样。比如表格里性别是"男"/"女",表单里是"male"/"female"。需要在 loadUserDataonSubmit 中做转换。

3. 表单验证

表单提交时,如果验证不通过,需要标记所有字段为 touched,这样错误信息才会显示出来。

总结

DialogService 实现弹窗表单,比直接用 d-modal 组件要简单很多。不需要手动管理显示隐藏,不需要 ViewChild 和生命周期钩子,代码更清晰。唯一需要注意的是要提供所有依赖的服务,以及处理好数据格式转换。

如果遇到问题,先检查 app.config.ts 里的服务提供者是否齐全,然后看看数据格式转换是否正确。基本上这两点解决了,功能就能正常工作了。

参考资源

DevUI官网:https://devui.design/home

相关推荐
Ticnix22 分钟前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人25 分钟前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl29 分钟前
OpenClaw 深度技术解析
前端
崔庆才丨静觅32 分钟前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人41 分钟前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼44 分钟前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空1 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust
Mr Xu_1 小时前
Vue 3 中计算属性的最佳实践:提升可读性、可维护性与性能
前端·javascript
jerrywus1 小时前
我写了个 Claude Code Skill,再也不用手动切图传 COS 了
前端·agent·claude
玖月晴空1 小时前
探索关于Spec 和Skills 的一些实战运用-Kiro篇
前端·aigc·代码规范