# 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→渲染)、表达式、校验器、数据源、自定义组件。