# dynamicform
**Repository Path**: kingecg/dynamicform
## Basic Information
- **Project Name**: dynamicform
- **Description**: 动态表单实现.....
- **Primary Language**: Unknown
- **License**: MIT
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-08-19
- **Last Updated**: 2026-09-01
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# dynamicform
基于 Angular 13 的动态表单库:通过 JSON 定义驱动表单渲染、校验、表达式与数据源,并提供可视化编辑器(拖拽设计 → JSON → 实时渲染)。
## 架构
Monorepo(Angular Workspace),四个库 + 示例应用:
| 库 | 说明 |
|---|---|
| `@dyf/core` | 数据模型(FormDefinition/Field/Cell/Row)、组件注册表、表达式引擎、校验器框架、数据源服务、字段组件基类 |
| `@dyf/components` | 标准字段组件(text/textarea/number/date/select/checkbox/array)+ 注册模块 |
| `@dyf/material` | 基于 `@angular/material` 的字段组件库:覆盖标准组件 + radio/switch/slider |
| `@dyf/renderer` | 表单渲染器:定义 → FormGroup → 实时渲染(表达式/校验/数据源/布局) |
| `@dyf/editor` | 可视化编辑器:托盘 + 画布 + 属性面板,拖拽设计与属性编辑 |
| `dynamicform` | 示例应用(`src/app/examples/*`),演示完整链路 |
设计文档见 [docs/DESIGN.md](docs/DESIGN.md),任务拆解见 [docs/TASKS.md](docs/TASKS.md)。
## 安装
```bash
npm install
```
## 构建与测试
```bash
npm run build # 构建四库 + 示例应用
npm run build:lib # 仅构建五个库(core → components → material → editor → renderer)
npm run build:material # 仅构建 @dyf/material
npm run test # 四库 + 应用单元测试
npm run lint # 全仓 ESLint
npm run storybook # 组件/渲染器/编辑器/Material Storybook
```
## 快速开始
### 1. 渲染器(只读表单)
```ts
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { DyfStandardComponentsModule } from '@dyf/components';
import { DyfRendererModule } from '@dyf/renderer';
import { createFieldDefinition, createRowDefinition } from '@dyf/core';
import type { FormDefinition } from '@dyf/core';
const definition: FormDefinition = {
version: '1.0',
layout: 'list',
rows: [createRowDefinition(1), createRowDefinition(1)],
fields: [
createFieldDefinition({
name: 'name',
label: '姓名',
component: 'text',
props: { placeholder: '请输入姓名' },
validators: [{ name: 'required', message: '姓名必填' }],
}),
createFieldDefinition({
name: 'age',
label: '年龄',
component: 'number',
props: { min: 0, max: 150 },
}),
],
};
@NgModule({
imports: [BrowserModule, DyfStandardComponentsModule, DyfRendererModule],
})
export class AppModule {}
```
```html
```
### 2. 编辑器(可视化设计)
```ts
import { DyfEditorModule } from '@dyf/editor';
@NgModule({
imports: [BrowserModule, DyfStandardComponentsModule, DyfEditorModule],
})
export class AppModule {}
```
```html
```
编辑器输出的定义可直接交给渲染器消费(保证 JSON 可序列化)。
## 字段分组
`FormDefinition.groups` 可把字段划分为若干分组,每个分组拥有组标签(`label`)与**独立的组内布局**(`list` 或 `grid` 列布局):
```ts
import { createGroupDefinition } from '@dyf/core';
import type { FormDefinition } from '@dyf/core';
const definition: FormDefinition = {
name: 'user',
version: '1.0',
layout: 'list',
// 顶层 rows 即“默认分组”:存在 groups 时,所有未分组字段默认属于默认分组
rows: [{ id: 'r1', cells: [{ id: 'c1', fieldId: 'f-name' }] }],
groups: [
{
id: 'g-contact',
label: '联系方式',
layout: 'grid', // 组内列布局
grid: { defaultColumns: 2, columnTemplate: '1fr 1fr' },
rows: [{ id: 'gr1', cells: [{ id: 'gc1', fieldId: 'f-phone' }, { id: 'gc2', fieldId: 'f-email' }] }],
},
createGroupDefinition('备注信息', 'list'), // 工厂函数快速创建空分组
],
fields: [/* ... */],
};
```
约定:
- `groups` 缺省或为空数组时,行为与无分组完全一致(向后兼容)
- 存在 `groups` 时,顶层 `rows` 作为默认分组(`DYF_DEFAULT_GROUP_ID = 'default-group'`),承载所有未分组字段
- 每个分组内可独立选择 `list` 或 `grid`(列)布局,`grid` 支持分组级 `columnTemplate` / `rowGap` / `columnGap`,行级 `rowProps` 优先级更高
- 渲染器按「默认分组 → 命名分组顺序」渲染,无字段的分组自动跳过;组标签渲染在区块顶部
- 所有分组内字段共享同一个 `FormGroup`(字段名全局唯一),表达式、校验、数据源行为与未分组字段一致
### 分组渲染方式:`renderGroupAs`
`FormDefinition.renderGroupAs` 决定**存在分组时**的渲染形态,可选值 `'according'`(折叠面板)与 `'tab'`(标签页),**默认 `according`**:
```ts
const definition: FormDefinition = {
name: 'user',
version: '1.0',
layout: 'list',
renderGroupAs: 'tab', // 'according' | 'tab',缺省 according
rows: [/* 默认分组 */],
groups: [/* ... */],
fields: [/* ... */],
};
```
行为说明:
- **`according`**:每个分组渲染为可点击折叠的面板,默认全部展开,点击组头收起/展开
- **`tab`**:分组渲染为标签页,顶部为 tab 头,默认激活第一个区块,点击 tab 切换
- **无 `groups` 时 `renderGroupAs` 不生效**,仍按顺序平铺(DOM 与旧版本一致)
- 未知取值回退为 `according`
- 折叠/未激活的分组**只做视觉隐藏,DOM 与 `FormControl` 均保留**,因此隐藏分组内的值不丢失、校验照常参与整表 `validate()`
- 分组内存在校验错误时,tab 头 / 折叠组头会显示红色错误标记,便于定位到被收起的分组
- 渲染器还提供 `groupRenderMode` 输入,可在不改定义的前提下覆盖 `renderGroupAs`:
```html
```
### 编辑器中的分组操作
画布提供以下分组交互:
- **+ 添加分组**:画布底部按钮,新增分组(默认标签 `分组 N`,继承默认分组布局);首次添加分组时自动写入 `renderGroupAs: 'according'`
- **组标签编辑**:组头输入框内直接改名
- **切换布局**:组头按钮,在组内 `list` ↔ `grid`(列布局)间切换,字段自动按列数重排
- **拖动分组排序**:拖拽组头左侧手柄(⣿)上下调整分组顺序
- **跨组拖拽字段**:把字段拖到目标分组区域即可移入该分组末尾;从托盘拖到分组区域可直接在该分组新增字段
- **删除分组**:组内字段自动归还默认分组,不会丢失
- **分组渲染方式**:右侧属性面板「布局」区块中,存在分组时可选择 `折叠面板(according)` / `标签页(tab)`
## 标准组件库(`@dyf/components`)
`DyfStandardComponentsModule` 注册以下字段组件(原生 HTML 实现,零第三方 UI 依赖):
| 组件名 | 说明 | 主要 props | 值类型 |
|---|---|---|---|
| `text` | 单行文本 | `placeholder` / `maxlength` / `minlength` | `string` |
| `textarea` | 多行文本 | `placeholder` / `rows` / `maxlength` | `string` |
| `number` | 数字 | `min` / `max` / `step` / `precision` | `number \| ''` |
| `date` | 日期 | `format` / `minDate` / `maxDate` | `'yyyy-MM-dd'` 字符串 |
| `select` | 下拉选择 | `multiple` / `allowClear` / `showSearch` | 选中项(多选为数组) |
| `checkbox` | 复选框 | `labelPosition`(`before` / `after`,默认 `after`) | `boolean` |
| `array` | 数组(元素为简单类型或对象) | `showIndex` / `addLabel` | `any[]` |
`checkbox` 说明:勾选输出 `true`、取消勾选输出 `false`;传入值按真值语义归一化(`true` / `'true'` / `1` 视为勾选),因此 JSON Schema 的 `type: boolean` 可直接映射到该组件。
```ts
createFieldDefinition({
name: 'agree',
label: '同意用户协议',
component: 'checkbox',
type: 'boolean',
props: { labelPosition: 'after' },
validators: [{ name: 'required', message: '必须同意用户协议' }],
});
```
## Material 风格组件库(`@dyf/material`)
`@dyf/material` 基于 [@angular/material](https://material.angular.io) 13 提供 Material Design 风格的字段组件,与 `@dyf/components` 输入输出契约完全一致,可直接替换或与之混用。
### 组件清单
| 组件名 | 说明 | Material 实现 | 值类型 |
|---|---|---|---|
| `text` | 单行文本(覆盖标准组件) | `matInput` | `string` |
| `textarea` | 多行文本(覆盖标准组件) | `matInput` + `cdkTextareaAutosize` | `string` |
| `number` | 数字(覆盖标准组件) | `matInput[type=number]` | `number \| ''` |
| `date` | 日期(覆盖标准组件) | `mat-datepicker` | `'yyyy-MM-dd'` 字符串 |
| `select` | 下拉选择(覆盖标准组件) | `mat-select` | 选中项(多选为数组) |
| `array` | 数组(元素为简单类型或对象) | 复用 `@dyf/components` 实现 | `any[]` |
| `checkbox` | 复选框(覆盖标准组件) | `mat-checkbox` | `boolean` |
| `radio` | 单选按钮组 | `mat-radio-group` | 选项 `value` |
| `switch` | 开关 | `mat-slide-toggle` | `boolean` |
| `slider` | 滑块 | `mat-slider` | `number` |
### 使用
```bash
npm install @angular/material @angular/animations
```
```ts
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { DyfMaterialComponentsModule } from '@dyf/material';
import { DyfRendererModule } from '@dyf/renderer';
@NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule, // Material 动画必需
DyfMaterialComponentsModule,
DyfRendererModule,
],
})
export class AppModule {}
```
`styles.scss` 中引入一个 Material 主题:
```scss
@import '@angular/material/prebuilt-themes/indigo-pink.css';
```
### 与标准组件混用 / 覆盖
组件注册遵循「后注册覆盖先注册」:
```ts
// Material 版覆盖全部同名标准组件(text/textarea/number/date/select)
imports: [DyfStandardComponentsModule, DyfMaterialComponentsModule]
```
若只想在**局部**使用 Material 组件(不影响其他页面的标准组件),把 Provider 放在组件级注入器上:
```ts
import { DyfComponentRegistry } from '@dyf/core';
import { DYF_MATERIAL_COMPONENTS_PROVIDERS } from '@dyf/material';
@Component({
selector: 'app-material-page',
template: ``,
// 组件级注入器:Material 注册仅在本组件子树生效
providers: [DyfComponentRegistry, ...DYF_MATERIAL_COMPONENTS_PROVIDERS],
})
export class MaterialPageComponent {}
```
也可按需只注册部分组件(如仅使用 slider):
```ts
import { DYF_COMPONENT } from '@dyf/core';
import { DYF_MAT_SLIDER } from '@dyf/material';
providers: [{ provide: DYF_COMPONENT, useValue: DYF_MAT_SLIDER, multi: true }]
```
### Material 专属 props
除各组件自身属性外,`mat-form-field` 类组件(text/textarea/number/date/select)支持:
| prop | 取值 | 默认 |
|---|---|---|
| `appearance` | `legacy` / `standard` / `fill` / `outline` | `outline` |
| `floatLabel` | `auto` / `always` / `never` | `auto` |
| `color` | `primary` / `accent` / `warn` | `primary` |
| `hint` | 提示文本 | — |
其它专属属性:`text` 的 `prefixIcon` / `suffixIcon`,`number` 的 `prefix` / `suffix`,`textarea` 的 `autosize` / `minRows` / `maxRows`,`date` 的 `startView` / `touchUi`,`checkbox`/`switch` 的 `labelPosition`,`radio` 的 `vertical`,`slider` 的 `thumbLabel` / `tickInterval` / `showValue`。
### 自定义 Material 组件
继承 `DyfMatFieldBase`(内部已把 `value`/`disabled`/`errors` 桥接到 `control`,并让 `` 自动显示):
```ts
import { Component } from '@angular/core';
import { DyfMatFieldBase } from '@dyf/material';
@Component({
selector: 'app-mat-color',
template: `
{{ field.label }}
{{ e.message }}
`,
})
export class MatColorComponent extends DyfMatFieldBase {
// 需要值转换时覆盖 transformIn / transformOut
}
```
## 数组类型字段
字段定义支持数组类型:`type: 'array'` + `items` 声明元素形态。元素既可以是**简单类型**(`string` / `number` / `boolean` / `date`),也可以是**复杂对象**(`object`,含子字段)。渲染由 `array` 组件负责(`@dyf/components` 与 `@dyf/material` 均已注册)。
### 简单类型元素
```ts
createFieldDefinition({
name: 'tags',
label: '标签',
component: 'array',
type: 'array',
items: { type: 'string' }, // string | number | boolean | date
validators: [{ name: 'minItems', params: { min: 1 } }],
});
// 值形态:{ tags: ['营销', '运营'] }
```
### 复杂对象元素
`items.type = 'object'` 时用 `items.fields` 声明每行的子字段(结构与普通 `DyfFieldDefinition` 一致,可复用任意已注册组件、`props`、`dataSource`、`validators`):
```ts
createFieldDefinition({
name: 'addresses',
label: '地址',
component: 'array',
type: 'array',
items: {
type: 'object',
fields: [
createFieldDefinition({ name: 'city', label: '城市', component: 'text' }),
createFieldDefinition({
name: 'postcode',
label: '邮编',
component: 'text',
validators: [{ name: 'required', message: '邮编必填' }],
}),
],
defaultValue: { city: '', postcode: '' }, // 可选:新增一行的初始值
},
validators: [{ name: 'minItems', params: { min: 1 } }],
});
// 值形态:{ addresses: [{ city: '北京', postcode: '100080' }] }
```
### 行为说明
- **增删与排序**:每行提供「删除 / 上移 / 下移」;顶部提供添加按钮(文案可用 `props.addLabel` 覆盖,`props.showIndex` 控制序号显示)
- **条数校验**:`minItems` / `maxItems` 标准校验器(`params.min` / `params.max`),达到边界时对应按钮自动禁用
- **子字段校验**:对象元素的子字段校验错误会**聚合上抛**并注入外层控件(键前缀 `dyf:inner:`),因此整表 `validate()` / `statusChange` 的 `valid` 会正确反映数组内部错误;错误信息同时在对应行内展示
- **禁用透传**:外层 `disabled`(含 `disabled` 表达式)会传递给行内所有子字段与操作按钮
- **编辑器支持**:右侧属性面板在字段为数组类型时显示「数组定义」区块,可配置元素类型、对象子字段(name / label / 组件)与元素默认值
## JSON Schema 支持
`@dyf/core` 提供 **JSON Schema(draft-07)⇄ FormDefinition 双向转换**,纯函数、无 Angular 依赖:
```ts
import { jsonSchemaToFormDefinition, formDefinitionToJsonSchema } from '@dyf/core';
```
### JSON Schema → FormDefinition
**产物固定为 `list` 布局**:一个字段占一行一 cell,行顺序即 `properties` 顺序。
```ts
const schema = {
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'object',
title: '用户表单',
properties: {
name: { type: 'string', title: '姓名', minLength: 2, maxLength: 20 },
age: { type: 'integer', title: '年龄', minimum: 0, maximum: 150 },
city: { type: 'string', title: '城市', enum: ['bj', 'sh'], enumNames: ['北京', '上海'] },
tags: { type: 'array', title: '标签', items: { type: 'string' }, minItems: 1 },
contact: { // 对象属性 → 分组(组内也是 list 布局)
type: 'object',
title: '联系方式',
properties: { phone: { type: 'string', title: '手机' } },
required: ['phone'],
},
},
required: ['name', 'age'],
};
const definition = jsonSchemaToFormDefinition(schema, { name: 'user' });
// definition.layout === 'list',可直接交给
```
映射规则:
| JSON Schema | FormDefinition |
|---|---|
| `type: string` | `text`(`maxLength > 255` 或 `format: textarea` → `textarea`) |
| `type: number` / `integer` | `number`(`integer` 附加整数校验器) |
| `type: boolean` | `checkbox` |
| `format: date` / `date-time` | `date` |
| `enum` | `select` + `static` 数据源(`enumNames` / `x-enumNames` 为显示文本) |
| `type: array` | `array` 组件 + `items`(`items.enum` → 多选 `select`) |
| `items: { type: object }` | `items.type = 'object'` + `items.fields` 子字段 |
| `type: object` 属性 | **分组**(`groupObjects: false` 时按点路径 `parent.child` 平铺) |
| `required` | `required` 校验器 |
| `minLength`/`maxLength`/`pattern` | `props.minlength`/`maxlength` + `expression` 校验器 |
| `minimum`/`maximum`/`multipleOf` | `props.min`/`max`/`step` + `expression` 校验器 |
| `exclusiveMinimum`/`exclusiveMaximum` | `expression` 校验器 |
| `minItems`/`maxItems`/`uniqueItems` | `minItems`/`maxItems` 校验器 + `expression` 校验器 |
| 其它关键字(`default`/`format`/`description`/`oneOf`…) | 原样存入 `props['x-json-schema']`(残留桶) |
> `boolean` 默认映射为 `checkbox`,`@dyf/components`(标准组件库)与 `@dyf/material` 均已注册该组件,无需额外配置。若想改用其它组件(如 Material 的开关),通过 `componentMap` 覆盖映射:
>
> ```ts
> jsonSchemaToFormDefinition(schema, { componentMap: { boolean: 'switch' } });
> ```
>
> 各组件库清单:`@dyf/components` = text / textarea / number / date / select / checkbox / array;`@dyf/material` 额外提供 radio / switch / slider。
由约束派生的校验器均为 `expression` 校验器,**空值一律放行**(是否必填交由 `required` 决定),并在 `params` 中记录 `origin: 'json-schema'` / `keyword` / `value`,便于反向精确还原。
转换选项:
```ts
jsonSchemaToFormDefinition(schema, {
name: 'user', // 表单名,缺省取 x-dyf-form.name → $id 末段 → 自动生成
groupObjects: false, // 对象属性不建分组,按点路径平铺(默认 true)
componentMap: { boolean: 'switch' }, // 覆盖类别 → 组件名映射
generateValidators: false, // 不生成约束校验器(props 仍映射,默认 true)
longTextThreshold: 500, // 长文本阈值(默认 255)
});
```
### FormDefinition → JSON Schema
```ts
const schema = formDefinitionToJsonSchema(definition, {
$id: 'https://example.com/schemas/user.json',
includeDyfExtensions: true, // 写入 x-dyf / x-dyf-form 扩展(默认 true)
nestByDotPath: true, // 点路径字段名 a.b → 嵌套对象 schema(默认 true)
$schema: null, // 传 null 时不输出 $schema
});
```
- 字段按 `rows` → `groups` 的**展示顺序**输出到 `properties`(未被任何 cell 引用的字段按 `fields` 顺序补齐)
- `required` 校验器 → 所属对象层级的 `required` 数组
- 约束派生校验器(`origin: 'json-schema'`)与 `props`(`minlength`/`max`/`step`…)→ 还原为 schema 关键字
- `props['x-json-schema']` 残留关键字合并回 schema
### `x-dyf` / `x-dyf-form` 扩展(保证双向无损)
JSON Schema 无法表达组件选择、表达式、分组等表单信息,这些统一放在扩展关键字中:
```jsonc
{
"type": "object",
"properties": {
"remark": {
"type": "string",
"title": "备注",
"x-dyf": {
"component": "textarea", // 覆盖推导出的组件
"label": "备注信息", // 覆盖 title
"type": "string", // 字段值类型
"props": { "rows": 5 }, // 附加/覆盖 props
"hidden": "data.name === \"\"", // 隐藏表达式
"disabled": "data.locked", // 禁用表达式
"validators": [{ "name": "expression", "message": "不能含空格", "params": { "expression": "!/\\s/.test(current)" } }],
"dataSource": { "type": "url", "url": "/api/options" }, // 非 enum 数据源
"group": { "id": "g1", "label": "补充信息" } // 归入指定分组
}
}
},
"x-dyf-form": {
"name": "user_table", // 表单名(后台存储表名)
"version": "1.0",
"renderGroupAs": "tab", // 分组渲染方式
"indexes": [{ "name": "idx_name", "fields": [{ "fieldname": "name", "order": "asc" }], "uniq": true }]
}
}
```
`x-dyf` 优先级高于所有推导结果。因此:
```ts
// 往返稳定(幂等)
const once = formDefinitionToJsonSchema(jsonSchemaToFormDefinition(schema));
const twice = formDefinitionToJsonSchema(jsonSchemaToFormDefinition(once));
// once 与 twice 深度相等
```
若需要产出给第三方消费的“纯净” Schema,传 `includeDyfExtensions: false`(代价是反向还原会丢失组件、表达式等表单信息)。
## 表达式
字段的 `hidden` / `disabled` / expression 校验器支持表达式,语法:
- `data.`:读取其他字段当前值
- 内置方法:`Number()` / `String()` / `Boolean()`
- 示例:`data.age >= 18`、`data.toggle === "yes"`
```ts
createFieldDefinition({
name: 'extra',
label: '额外字段',
component: 'text',
hidden: 'data.toggle === "yes"',
});
```
## 数据源
`select` 等组件可通过 `dataSource` 配置选项:
```ts
createFieldDefinition({
name: 'city',
label: '城市',
component: 'select',
dataSource: {
type: 'static', // static | expression | url
options: [
{ value: 'beijing', label: '北京' },
{ value: 'shanghai', label: '上海' },
],
mapping: { value: 'value', label: 'label' },
},
});
```
## 扩展自定义组件
自定义组件需继承 `DyfFieldBase`(`@dyf/core`),并通过 `DYF_COMPONENT` 多值 Provider 注册:
```ts
// slider.component.ts
import { Component } from '@angular/core';
import { DyfFieldBase } from '@dyf/core';
@Component({
selector: 'app-slider',
template: `
`,
})
export class SliderComponent extends DyfFieldBase {
onInput(value: string): void {
this.onControlChange(value);
}
}
```
```ts
// 注册描述
import type { DYFComponent } from '@dyf/core';
const SLIDER: DYFComponent = {
name: 'slider',
label: '滑块',
component: SliderComponent,
props: [
{ name: 'min', label: '最小值', type: 'number' },
{ name: 'max', label: '最大值', type: 'number' },
],
defaultValue: { min: 0, max: 100 },
};
@NgModule({
providers: [{ provide: DYF_COMPONENT, useValue: SLIDER, multi: true }],
})
export class AppModule {}
```
注册机制(DESIGN §5.2):**后注册覆盖先注册**——外部注册同名组件可覆盖标准组件(如覆盖 `text` 的 label / props)。
## 示例应用
```bash
npm start
```
导航切换示例:完整链路(编辑→JSON→渲染)、表达式、校验器、数据源、自定义组件。