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

相关推荐
国服第二切图仔2 小时前
DevUI Design中后台产品的开源前端解决方案之DataTable 表格组件核心解析
前端
懒人村杂货铺2 小时前
FastAPI + 前端(Vue/React)Docker 部署全流程
前端·vue.js·fastapi
7***37452 小时前
前端技术的下一站:从“页面开发”走向“体验工程”
前端
哆啦A梦15882 小时前
商城后台管理系统 01,商品管理-搜索
前端·javascript·vue.js
苏小瀚2 小时前
[JavaEE] Spring Web MVC入门
前端·java-ee·mvc
Arvin_Rong2 小时前
回归传统?原生 JS + React 混合开发的实践与思考
javascript·react.js
前端不太难2 小时前
RN 构建包体积过大,如何瘦身?
前端·react native
小光学长2 小时前
基于web的影视网站设计与实现14yj533o(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
java·前端·数据库
vocoWone2 小时前
📰 前端资讯 - 2025年12月10日
前端