@delon/form 动态模式表单技能
这个技能帮助使用 @delon/form 的 SF(模式表单)组件创建动态表单。
核心原则
模式驱动表单
- JSON模式:使用JSON模式声明式定义表单
- 类型安全:模式定义的完整TypeScript支持
- 验证:内置验证和自定义验证器
- 动态渲染:基于表单状态的条件字段
关键特性
- 根据模式自动生成表单
- 用于特殊输入的自定义组件
- 异步数据加载(下拉列表、自动完成)
- 多步骤表单(向导)
- 响应式网格布局
- 国际化支持
基本模式表单
import { Component, signal, output } from '@angular/core';
import { SFSchema, SFComponent, SFUISchema } from '@delon/form';
import { SHARED_IMPORTS } from '@shared';
@Component({
selector: 'app-user-form',
standalone: true,
imports: [SHARED_IMPORTS, SFComponent],
template: `
<sf
[schema]="schema"
[ui]="ui"
[formData]="initialData()"
[loading]="loading()"
(formSubmit)="handleSubmit($event)"
(formChange)="handleChange($event)"
(formError)="handleError($event)"
/>
`
})
export class UserFormComponent {
loading = signal(false);
initialData = signal<any>({});
formSubmit = output<any>();
schema: SFSchema = {
properties: {
name: {
type: 'string',
title: '全名',
maxLength: 100
},
email: {
type: 'string',
title: '电子邮件',
format: 'email'
},
age: {
type: 'number',
title: '年龄',
minimum: 18,
maximum: 120
},
role: {
type: 'string',
title: '角色',
enum: ['admin', 'member', 'viewer'],
default: 'member'
}
},
required: ['name', 'email', 'role']
};
ui: SFUISchema = {
'*': {
spanLabel: 6,
spanControl: 18,
grid: { span: 24 }
},
$name: {
placeholder: '输入全名',
widget: 'string'
},
$email: {
placeholder: 'user@example.com',
widget: 'string'
},
$age: {
widget: 'number'
},
$role: {
widget: 'select',
placeholder: '选择角色'
}
};
handleSubmit(value: any): void {
console.log('表单提交:', value);
this.formSubmit.emit(value);
}
handleChange(value: any): void {
console.log('表单改变:', value);
}
handleError(errors: any): void {
console.error('表单错误:', errors);
}
}
常见组件
字符串输入
{
name: {
type: 'string',
title: '姓名',
ui: {
widget: 'string',
placeholder: '输入姓名',
prefix: 'User',
suffix: '@',
maxLength: 100
}
}
}
文本区域
{
description: {
type: 'string',
title: '描述',
ui: {
widget: 'textarea',
autosize: { minRows: 3, maxRows: 6 },
placeholder: '输入描述'
}
}
}
数字输入
{
amount: {
type: 'number',
title: '金额',
minimum: 0,
maximum: 1000000,
ui: {
widget: 'number',
precision: 2,
prefix: '$',
formatter: (value: number) => `$ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
}
}
日期选择器
{
birthDate: {
type: 'string',
title: '出生日期',
format: 'date',
ui: {
widget: 'date',
mode: 'date',
displayFormat: 'yyyy-MM-dd',
end: 'today' // 不能选择未来日期
}
}
}
日期范围
{
dateRange: {
type: 'string',
title: '日期范围',
ui: {
widget: 'date',
mode: 'range',
displayFormat: 'yyyy-MM-dd'
}
}
}
选择下拉列表
{
category: {
type: 'string',
title: '类别',
enum: [
{ label: '技术', value: 'tech' },
{ label: '商业', value: 'business' },
{ label: '科学', value: 'science' }
],
ui: {
widget: 'select',
placeholder: '选择类别',
allowClear: true,
showSearch: true
}
}
}
多选
{
tags: {
type: 'array',
title: '标签',
items: {
type: 'string',
enum: ['angular', 'react', 'vue', 'typescript']
},
ui: {
widget: 'select',
mode: 'multiple',
placeholder: '选择标签'
}
}
}
自动完成
{
city: {
type: 'string',
title: '城市',
ui: {
widget: 'autocomplete',
asyncData: () => this.loadCities(),
debounceTime: 300,
placeholder: '搜索城市'
}
}
}
private async loadCities(): Promise<any[]> {
return [
{ label: '纽约', value: 'ny' },
{ label: '洛杉矶', value: 'la' },
{ label: '芝加哥', value: 'chi' }
];
}
单选按钮
{
priority: {
type: 'string',
title: '优先级',
enum: [
{ label: '低', value: 'low' },
{ label: '中', value: 'medium' },
{ label: '高', value: 'high' }
],
default: 'medium',
ui: {
widget: 'radio',
styleType: 'button' // 或 'default'
}
}
}
复选框
{
agree: {
type: 'boolean',
title: '同意条款',
ui: {
widget: 'checkbox'
}
}
}
开关
{
isActive: {
type: 'boolean',
title: '激活状态',
ui: {
widget: 'switch',
checkedChildren: 'On',
unCheckedChildren: 'Off'
}
}
}
滑块
{
rating: {
type: 'number',
title: '评分',
minimum: 0,
maximum: 100,
ui: {
widget: 'slider',
marks: {
0: '0',
50: '50',
100: '100'
}
}
}
}
文件上传
{
avatar: {
type: 'string',
title: '头像',
ui: {
widget: 'upload',
action: '/api/upload',
accept: 'image/*',
limit: 1,
listType: 'picture-card'
}
}
}
异步数据加载
动态下拉选项
{
assignee: {
type: 'string',
title: '被分配人',
ui: {
widget: 'select',
asyncData: () => this.loadUsers(),
placeholder: '选择用户'
}
}
}
private async loadUsers(): Promise<any[]> {
const users = await this.userService.getUsers();
return users.map(u => ({
label: u.name,
value: u.id
}));
}
级联选择
{
country: {
type: 'string',
title: '国家',
ui: {
widget: 'select',
asyncData: () => this.loadCountries(),
change: (value: string) => this.onCountryChange(value)
}
},
city: {
type: 'string',
title: '城市',
ui: {
widget: 'select',
asyncData: () => this.loadCities(this.selectedCountry())
}
}
}
private selectedCountry = signal<string>('');
onCountryChange(value: string): void {
this.selectedCountry.set(value);
}
条件字段
基于值显示/隐藏
schema: SFSchema = {
properties: {
userType: {
type: 'string',
title: '用户类型',
enum: ['individual', 'company']
},
// 仅对公司显示
companyName: {
type: 'string',
title: '公司名称',
ui: {
visibleIf: {
userType: ['company']
}
}
},
// 仅对个人显示
firstName: {
type: 'string',
title: '名字',
ui: {
visibleIf: {
userType: ['individual']
}
}
}
}
};
自定义验证器
import { SFSchema } from '@delon/form';
schema: SFSchema = {
properties: {
password: {
type: 'string',
title: '密码',
ui: {
type: 'password',
validator: (value: string, formProperty: any, form: any) => {
if (value.length < 8) {
return [{ keyword: 'minLength', message: '密码至少8个字符' }];
}
if (!/[A-Z]/.test(value)) {
return [{ keyword: 'uppercase', message: '密码必须包含大写字母' }];
}
return [];
}
}
},
confirmPassword: {
type: 'string',
title: '确认密码',
ui: {
type: 'password',
validator: (value: string, formProperty: any, form: any) => {
if (value !== form.value.password) {
return [{ keyword: 'match', message: '密码必须匹配' }];
}
return [];
}
}
}
}
};
多步骤表单(向导)
import { Component, signal } from '@angular/core';
import { SFSchema } from '@delon/form';
@Component({
selector: 'app-wizard-form',
template: `
<nz-steps [nzCurrent]="currentStep()">
<nz-step nzTitle="基本信息" />
<nz-step nzTitle="地址" />
<nz-step nzTitle="确认" />
</nz-steps>
@switch (currentStep()) {
@case (0) {
<sf [schema]="basicInfoSchema" (formSubmit)="nextStep($event)" />
}
@case (1) {
<sf [schema]="addressSchema" (formSubmit)="nextStep($event)" />
}
@case (2) {
<div class="confirmation">
<h3>审核您的信息</h3>
<pre>{{ formData() | json }}</pre>
<button nz-button nzType="primary" (click)="submit()">提交</button>
</div>
}
}
`
})
export class WizardFormComponent {
currentStep = signal(0);
formData = signal<any>({});
basicInfoSchema: SFSchema = {
properties: {
name: { type: 'string', title: '姓名' },
email: { type: 'string', title: '电子邮件', format: 'email' }
},
required: ['name', 'email']
};
addressSchema: SFSchema = {
properties: {
street: { type: 'string', title: '街道' },
city: { type: 'string', title: '城市' },
zipCode: { type: 'string', title: '邮政编码' }
},
required: ['street', 'city']
};
nextStep(value: any): void {
this.formData.update(data => ({ ...data, ...value }));
this.currentStep.update(step => step + 1);
}
submit(): void {
console.log('最终数据:', this.formData());
}
}
网格布局
ui: SFUISchema = {
'*': {
spanLabel: 4,
spanControl: 20,
grid: { span: 12 } // 2列(24 / 12 = 2)
},
$description: {
grid: { span: 24 } // 满宽度
}
};
响应式布局
ui: SFUISchema = {
'*': {
grid: {
xs: 24, // 移动:满宽度
sm: 12, // 平板:2列
md: 8, // 桌面:3列
lg: 6 // 大屏:4列
}
}
};
表单操作
@Component({
template: `
<sf [schema]="schema" [button]="button">
<ng-template sf-template="button" let-btn>
<button
nz-button
[nzType]="btn.submit ? 'primary' : 'default'"
(click)="btn.submit ? handleSubmit() : handleReset()"
>
{{ btn.submit ? '提交' : '重置' }}
</button>
</ng-template>
</sf>
`
})
export class CustomButtonFormComponent {
button = {
submit_text: '提交',
reset_text: '重置',
submit_type: 'primary' as const,
reset_type: 'default' as const
};
}
清单
创建SF表单时:
- [ ] 使用适当的JSON模式类型
- [ ] 定义必填字段
- [ ] 设置验证规则(最小/最大,格式,模式)
- [ ] 使用适当的组件处理数据类型
- [ ] 处理异步数据加载
- [ ] 实现条件字段可见性
- [ ] 需要时添加自定义验证器
- [ ] 配置响应式网格布局
- [ ] 处理表单提交和错误
- [ ] 提供加载状态
- [ ] 测试表单验证
- [ ] 确保可访问性(标签,ARIA)