├── .eslintrc.cjs ├── .gitignore ├── .npmrc ├── .vscode └── extensions.json ├── LICENSE ├── README.EN.md ├── README.md ├── demo.jpg ├── docs ├── .vitepress │ ├── config.mts │ └── theme │ │ └── index.ts ├── api │ ├── DynamicForm.md │ ├── DynamicFormOptions.md │ ├── global.md │ ├── internal-controls.md │ └── internal-form.md ├── assets │ └── root.scss ├── examples │ ├── BasicUseageDoc.vue │ ├── BasicUseageDoc1.vue │ ├── BasicUseageDoc2.vue │ ├── BasicUseageDoc3.vue │ ├── BasicUseageDoc4.vue │ ├── BasicUseageDoc5.vue │ ├── BasicUseageDoc6.vue │ ├── BasicUseageDoc7.vue │ ├── DocHome.vue │ └── ingrate │ │ ├── AntDesgin.ts │ │ ├── ArcoDesgin.ts │ │ ├── ElementPlus.ts │ │ ├── IngrateDemoAntDesgin.vue │ │ ├── IngrateDemoArcoDesgin.vue │ │ ├── IngrateDemoElementPlus.vue │ │ └── MyCheckBox.vue ├── guide │ ├── about.md │ ├── basic-useage.md │ ├── custom-control.md │ ├── custom-render.md │ ├── form-funs.md │ ├── form-linkage.md │ ├── form-nest.md │ ├── getting-started.md │ ├── ingrate-ant-design.md │ ├── ingrate-arco-design.md │ ├── ingrate-element.md │ ├── register-controls.md │ └── tab.md ├── index.md └── vite.config.ts ├── library ├── DynamicForm.ts ├── DynamicForm.vue ├── DynamicFormBasicControls │ ├── Form.tsx │ ├── FormContext.tsx │ ├── FormItem.tsx │ ├── Layout │ │ ├── Col.tsx │ │ └── Row.tsx │ ├── Utils │ │ ├── ArrayUtils.ts │ │ └── ObjectUtils.ts │ └── index.ts ├── DynamicFormInner.vue ├── DynamicFormInternal.ts ├── DynamicFormItem.vue ├── DynamicFormItemControls │ ├── BaseCheck.ts │ ├── BaseCheck.vue │ ├── BaseDivider.ts │ ├── BaseDivider.vue │ ├── BaseInput.ts │ ├── BaseInput.vue │ ├── BaseRadio.ts │ ├── BaseRadio.vue │ ├── BaseSelect.ts │ ├── BaseSelect.vue │ ├── BaseTextArea.ts │ ├── BaseTextArea.vue │ ├── FormArrayGroup.ts │ ├── FormArrayGroup.vue │ ├── FormArrayGroupItem.vue │ ├── FormCustomLayout.vue │ ├── FormGroup.ts │ ├── FormGroup.vue │ └── index.ts ├── DynamicFormItemNormal.vue ├── DynamicFormItemRenderer │ ├── DynamicFormItemRegistry.ts │ └── DynamicFormItemRenderer.vue ├── DynamicFormTab │ ├── DynamicFormTab.vue │ └── DynamicFormTabPage.vue ├── Scss │ ├── BaseControl.scss │ ├── Color.scss │ └── Form.scss ├── main.ts ├── tsconfig.json └── vite.config.ts ├── package-lock.json ├── package.json ├── public └── favicon.ico └── tsconfig.json /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 'latest' 13 | }, 14 | rules: { 15 | 'vue/multi-word-component-names': 'off', 16 | 'vue/no-reserved-component-names': 'off', 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | temp/ 20 | types/ 21 | 22 | # Editor directories and files 23 | .vscode/* 24 | !.vscode/extensions.json 25 | .idea 26 | *.suo 27 | *.ntvs* 28 | *.njsproj 29 | *.sln 30 | *.sw? 31 | docs/.vitepress/cache/ 32 | docs/.vitepress/dist/ 33 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.com/ 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 梦欤 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.EN.md: -------------------------------------------------------------------------------- 1 | # vue-dynamic-form 2 | 3 | A data driven form component for vue3. 4 | 5 | ## Documention 6 | 7 | [Documention](https://docs.imengyu.top/vue-dynamic-form-docs)。 8 | 9 | ![demo](./demo.jpg) 10 | 11 | ## Install 12 | 13 | ```shell 14 | # npm 15 | npm i @imengyu/vue-dynamic-form 16 | 17 | # or yarn 18 | yarn add @imengyu/vue-dynamic-form 19 | ``` 20 | 21 | ## Introduction 22 | 23 | In the management system development, we often use form submission data, and form submission data takes up most of the development time. 24 | When the form is very large, manually writing the form components can be a hassle. vue-dynamic-form was written to solve this problem, 25 | vue-dynamic-form allows you to dynamically generate forms using JSON data. You only need to pass in a JSON containing various description information to render a complete form. 26 | 27 | vue-dynamic-form is not required for development, it is just a widget to help you speed up development. 28 | 29 | The management system projects used by the author's company have all used dynamic forms, saving 80% of the time of layout forms, and the development efficiency has been greatly improved (you can spend more time to touch the fish 🤭). vue-dynamic-form is now open source, hoping to facilitate your development. 30 | 31 | > **This project is still in the early release stage, and there may be many problems. If you encounter any problems, please feel free to submit a issue in [Github](https://github.com/imengyu/vue-dynamic-form/issues), I'll try to fix it for you!** 32 | 33 | ## Author's other project 34 | 35 | * [vue3-context-menu A context menu component for Vue3](https://github.com/imengyu/vue3-context-menu/) 36 | * [vue-dock-layout A Vue editor layout component that like Visual Studio](https://github.com/imengyu/vue-dock-layout) 37 | * [vue-code-layout A Vue editor layout component that like VSCode](https://github.com/imengyu/vue-code-layout) 38 | 39 | ## License 40 | 41 | [MIT](./LICENSE) 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-dynamic-form 2 | 3 | [English](./README.EN.md) | 中文 4 | 5 | vue-dynamic-form 一个用数据驱动的 Vue3 动态表单组件。 6 | 7 | ## 文档 8 | 9 | [查看文档](https://docs.imengyu.top//vue-dynamic-form-docs)。 10 | 11 | ![demo](./demo.jpg) 12 | 13 | ## 安装 14 | 15 | ```shell 16 | # npm 17 | npm i @imengyu/vue-dynamic-form 18 | 19 | # or yarn 20 | yarn add @imengyu/vue-dynamic-form 21 | ``` 22 | 23 | ## 简介 24 | 25 | 设计参考了 [阿里的 XRender](https://xrender.fun/form-render)。 26 | 27 | 在中后台开发中,我们经常会使用表单提交数据,表单提交数据占据开发的多半时间, 28 | 当表单特别多时,手动写表单组件是一件非常麻烦的事情。vue-dynamic-form就是为了解决这个问题而写的, 29 | vue-dynamic-form 支持你使用 JSON 数据动态生成表单,只需要传入一个包含各种描述信息的 JSON,就能渲染出一个完整的表单。 30 | 31 | vue-dynamic-form 并不是开发中所必须的,它只是一个帮助你加快开发的小组件。 32 | 33 | 作者公司使用的中后台项目已经全部使用动态表单,省去了80%布局表单的时间,开发效率得到了不少提升(可以用更多时间摸鱼啦🤭)。现在将 vue-dynamic-form 开源,希望可以为你的开发提供便利。 34 | 35 | > **本项目目前还处于早期发布阶段,可能会存在不少问题,如果遇到问题,欢迎在 [Github](https://github.com/imengyu/vue-dynamic-form/issues) 提出 Issue,我会尽量为你解决!** 36 | 37 | ## 支持 38 | 39 | 作者开发不易,如果这个项目对您有帮助,希望你可以帮我点个 ⭐ ,这将是对我极大的鼓励。谢谢啦 (●'◡'●) 40 | 41 | ## 作者的其他项目 42 | 43 | * [vue3-context-menu 一个简洁美观简单的Vue3右键菜单组件](https://github.com/imengyu/vue3-context-menu/) 44 | * [vue-code-layout A Vue editor layout component that like VSCode](https://github.com/imengyu/vue-code-layout) 45 | 46 | ## License 47 | 48 | [MIT](./LICENSE) 49 | -------------------------------------------------------------------------------- /demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imengyu/vue-dynamic-form/67c458aaab05acf2e99d19512893f92b1d373148/demo.jpg -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitepress"; 2 | import MarkdownPreview from 'vite-plugin-markdown-preview' 3 | 4 | export default defineConfig({ 5 | title: 'vue-dynamic-form', 6 | description: 'A data driven form component for vue3.', 7 | base: '/vue-dynamic-form-docs/', 8 | themeConfig: { 9 | socialLinks: [ 10 | { 11 | icon: { 12 | svg: '' 13 | }, 14 | link: 'https://gitee.com/imengyu/vue-dynamic-form' 15 | }, 16 | { icon: 'github', link: 'https://github.com/imengyu/vue-dynamic-form' }, 17 | ], 18 | footer: { 19 | message: 'Released under the MIT License.', 20 | copyright: 'Copyright © 2024 imengyu.top' 21 | }, 22 | nav: [ 23 | { text: '教程', link: '/guide/about' }, 24 | { text: 'API 参考', link: '/api/global' }, 25 | { text: '更新日志', link: 'https://github.com/imengyu/vue-dynamic-form/CHANGELOG.md' } 26 | ], 27 | sidebar: { 28 | '/guide/': [ 29 | { 30 | text: '起步', 31 | items: [ 32 | { text: '介绍', link: '/guide/about' }, 33 | { text: '开始使用', link: '/guide/getting-started' }, 34 | { text: '绑定组件', link: '/guide/register-controls', items: [ 35 | { text: 'Ant Design Vue', link: '/guide/ingrate-ant-design' }, 36 | { text: 'Arco Design Vue', link: '/guide/ingrate-arco-design' }, 37 | { text: 'Element Plus', link: '/guide/ingrate-element' }, 38 | ] }, 39 | 40 | { text: '基础用法', link: '/guide/basic-useage' }, 41 | ] 42 | }, 43 | { 44 | text: '高级用法', 45 | items: [ 46 | { text: '自定义渲染', link: '/guide/custom-render' }, 47 | { text: '自定义组件', link: '/guide/custom-control' }, 48 | { text: '表单联动', link: '/guide/form-linkage' }, 49 | { text: '表单方法', link: '/guide/form-funs' }, 50 | { text: '表单嵌套', link: '/guide/form-nest' }, 51 | { text: '表单标签页', link: '/guide/tab' }, 52 | ] 53 | }, 54 | ], 55 | '/api/': [ 56 | { 57 | text: 'API 参考', 58 | items: [ 59 | { text: '全局函数', link: '/api/global' }, 60 | { text: 'DynamicForm', link: '/api/DynamicForm' }, 61 | { text: 'DynamicFormOptions', link: '/api/DynamicFormOptions' }, 62 | { text: '内置Form', link: '/api/internal-form' }, 63 | { text: '内置组件', link: '/api/internal-controls' }, 64 | ] 65 | }, 66 | ] 67 | } 68 | }, 69 | vite: { 70 | plugins: [ MarkdownPreview() as any ], 71 | }, 72 | }); -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import '@arco-design/web-vue/dist/arco.css'; 2 | import 'ant-design-vue/dist/reset.css'; 3 | import 'element-plus/dist/index.css' 4 | import '../../assets/root.scss' 5 | import DefaultTheme from 'vitepress/theme'; 6 | import { registerAllFormComponents } from '../../examples/ingrate/ArcoDesgin' 7 | 8 | export default { 9 | ...DefaultTheme, 10 | async enhanceApp(ctx) { 11 | DefaultTheme.enhanceApp(ctx); 12 | if (!import.meta.env.SSR) { 13 | const plugin = await import('vue-codemirror') 14 | ctx.app.use(plugin.default) 15 | registerAllFormComponents(); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /docs/api/DynamicForm.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API 参考 - DynamicForm 3 | --- 4 | 5 | # API 参考 - DynamicForm 6 | 7 | 动态表单组件。 8 | 9 | ## Props 10 | 11 | | 属性 | 描述 | 类型 | 默认值 | 12 | | :----: | :----: | :----: | :----: | 13 | | model | 表单数据模型 | `Object` | — | 14 | | options | 表单相关定义 | [IDynamicFormOptions](./DynamicFormOptions.md) | — | 15 | 16 | ## Events 17 | 18 | | 事件名 | 描述 | 参数 | 19 | | :----: | :----: | :----: | 20 | | finish | 提交表单且数据验证成功后回调事件 | - | 21 | | finishFailed | 提交表单且数据验证失败后回调事件 | - | 22 | | submit | 数据验证成功后回调事件 | - | 23 | | ready | 表单初始化完成,实例引用已经就绪时发出事件 | - | 24 | 25 | ## Slots 26 | 27 | | 插槽名 | 描述 | 参数 | 28 | | :----: | :----: | :----: | 29 | | formCeil | 表单条目自定义渲染插槽 | 参数见下方 | 30 | | formArrayButtonAdd | 数组条目的添加按钮渲染插槽 | `{ onClick: () => void }` | 31 | | formArrayButtons | 数组条目的删除/上移/下移按钮自定义渲染插槽 | `{ onDeleteClick: () => void, onUpClick: () => void, onDownClick: () => void }` | 32 | | empty | 当前表单没有条目时显示的空插槽 | - | 33 | | endButton | 当前表单末尾渲染插槽,通常用于横向布局的表单末尾按钮 | - | 34 | 35 | ### formCeil 插槽参数 36 | 37 | | 属性 | 描述 | 类型 | 默认值 | 38 | | :----: | :----: | :----: | :----: | 39 | | name | 表单项的完整路径 | `string` | — | 40 | | item | 表单项定义 | `IDynamicFormItem` | — | 41 | | model | 表单项当前的值 | `Object` | — | 42 | | onModelUpdate | 用于双向绑定数据回调 | `(v: unknown) => void` | — | 43 | | rawModel | 整个 form 的值 (最常用,当两个关联组件距离较远时,可以从顶层的 rawModel 里获取) | `Object` | — | 44 | | parentModel | 父表单元素的值 (上一级的值,只在列表场景的使用,例如列表某个元素的父级就是整个 item) | `Object` | — | 45 | | disabled | 是否禁用当前表单项 | `boolean` | `false` | 46 | | rule | 当前条目校验数据 | `Object` | — | 47 | 48 | ## IDynamicFormRef 49 | 50 | 表单实例方法。 51 | 52 | | 函数名 | 描述 | 53 | | :----: | :----: | 54 | | `getFormRef: () => T` | 获取表单组件的 Ref | 55 | | `getFormItemControlRef: (key: string) => T` | 获取指定表单项组件的 Ref | 56 | | `submit: () => void` | 触发提交。同 getFormRef().submit() | 57 | | `setValueByPath: (path: string, value: unknown) => void` | 外部修改指定单个 field 的数据 | 58 | | `getValueByPath: (path: string) => unknow` | 外部获取指定单个 field 的数据 | 59 | -------------------------------------------------------------------------------- /docs/api/DynamicFormOptions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API 参考 - IDynamicFormOptions 3 | --- 4 | 5 | # API 参考 - IDynamicFormOptions 6 | 7 | 动态表单配置项参考。 8 | 9 | | 属性 | 描述 | 类型 | 必填 | 10 | | :----: | :----: | :----: | :----: | 11 | | formItems | 表单条目数据 | [`IDynamicFormItem[]`](#idynamicformitem) | 是 | 12 | | formRules | 表单的校验规则 | `Record` | — | 13 | | formLabelCol | 表单label栅格宽度 | `{ span: number, offset?: number }` | — | 14 | | formLabelWidth | 表单label宽度 | `number|string` | — | 15 | | formWrapperCol | 表单组件栅格宽度 | `{ span: number, offset?: number }` | — | 16 | | formAdditionaProps | 表单组件附加属性 | `Record` | — | 17 | | formAdditionalEvents | 表单组件附加事件绑定 | `Record` | — | 18 | | widgets | 自定义重写表单控件。你可以重写内置控件,在这个表单中会以此重写列表为先查找表单组件。 | [`Record`](./global.md#dynamicformitemregistryitem) | — | 19 | | internalWidgets | 自定义重写内置表单控件 Form FormItem。你可以使用其他组件库的组件例如 elemnent-ui 或者 ant-desgin-vue | [`IDynamicFormInternalWidgets`](#idynamicforminternalwidgets) | — | 20 | | disabled | 表单是否禁用 | `boolean` | — | 21 | 22 | ## IDynamicFormItem 23 | 24 | 表单条目 25 | 26 | | 属性 | 描述 | 类型 | 必填 | 27 | | :----: | :----: | :----: | :----: | 28 | | type | 当前表单类型 | `string` | 是 | 29 | | hidden | [联动回调] 显是否隐藏当前表单项 | `boolean` or `IDynamicFormItemCallback` | — | 30 | | disabled | [联动回调] 是否禁用当前表单项 | `boolean` or `IDynamicFormItemCallback` | — | 31 | | additionalProps | [联动回调] 附加组件属性。支持动态回调(只支持第一级传入回调)。当传入值是函数时,请使用 additionalDirectProps。 | `string` | — | 32 | | additionalEvents | 附加组件事件绑定 | `Record` | — | 33 | | additionalDirectProps | 当前表单类型 | `string` | — | 34 | | type | 附加组件属性。此属性直接应用到目标渲染组件上,没有联动回调。 | `Record` | — | 35 | | formProps | 附加 FormItem 组件属性 | `unknown` | — | 36 | | watch | 监听当前表单数据更改 | `(oldValue: unknown, newValue: unknown) => void` | — | 37 | | name | 当前表单项名称。 | `string` | 是 | 38 | | label | [联动回调] 当前表单说明文字。支持动态回调。 | `string` or `IDynamicFormItemCallback` | — | 39 | | children | 子条目。仅在 'object','array-single','array-object','group-object' 或者其他容器条目中有效。 | `IDynamicFormItem[]` | — | 40 | | newChildrenObject | 当子对象为数组时,可设置这个自定义回调。用于添加按钮新建一个对象,如果这个函数为空,则没有添加按钮。 | `(arrayNow: unknown[]) => unknown` | — | 41 | | deleteChildrenCallback | 当子对象为数组时,可设置这个自定义回调。删除按钮回调,可选,不提供时默认操作为将 | `(arrayNow: unknown[], deleteObject: unknown) => unknown` | — | 42 | | childrenColProps | 子条目的 Col 配置属性(应用到当前条目的所有子条目上)。仅在 object 或者其他容器条目中有效。 | `ColProps` | — | 43 | | colProps | 条目的 Col 配置属性(应用到当前条目上)。仅在 object 或者其他容器条目中有效。 | `ColProps` | — | 44 | | rowProps | 条目的 Row 配置属性(应用到当前条目上)。仅在 object 或者其他容器条目中有效。 | `RowProps` | — | 45 | 46 | ## IDynamicFormInternalWidgets 47 | 48 | 自定义重写内置表单控件 Form FormItem 定义。 49 | 50 | ```ts 51 | { 52 | Form: { 53 | /** 54 | * 组件实例 55 | */ 56 | component: unknown, 57 | /** 58 | * 属性的名称修改 59 | * 每个UI框架的属性名称有点不一样,你需要根据对应的文档重新写对应的属性名称 60 | * 右边是对应的属性名称 61 | */ 62 | propsMap: { 63 | rules?: string, 64 | model?: string, 65 | labelCol?: string, 66 | labelWidth?: string, 67 | wrapperCol?: string, 68 | //提交成功事件 69 | onFinish?: string, 70 | //提交失败事件 71 | onFinishFailed?: string, 72 | //submit事件 73 | onSubmit?: string, 74 | }, 75 | }, 76 | FormItem: { 77 | /** 78 | * 组件实例 79 | */ 80 | component: unknown, 81 | /** 82 | * 属性的名称修改 83 | */ 84 | propsMap: { 85 | name?: string, 86 | label?: string, 87 | }, 88 | }, 89 | } 90 | ``` 91 | 92 | ## 表单联动属性回调定义 (IDynamicFormItemCallback) 93 | 94 | | 属性 | 描述 | 类型 | 必填 | 95 | | :----: | :----: | :----: | :----: | 96 | | model | 当前表单条目的值 | `unknown` | 是 | 97 | | rawModel | 整个 form 的值 (最常用,当两个关联组件距离较远时,可以从顶层的 rawModel 里获取) | `unknown` | 是 | 98 | | parentModel | 父表单元素的值 (上一级的值,只在列表场景的使用,例如列表某个元素的父级就是整个 item) | `unknown` | 是 | 99 | | item | 当前表单条目信息 | `IDynamicFormItem` | 是 | 100 | | formRules | 当前条目校验数据 | `Record` | 是 | 101 | -------------------------------------------------------------------------------- /docs/api/global.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API 参考 - 全局函数 3 | --- 4 | 5 | # API 参考 - 全局函数 6 | 7 | ## configDefaultDynamicFormOptions(options) 8 | 9 | 配置默认的动态表单属性,配置后将会对所有动态表单生效。 10 | 11 | |参数|必填|类型|说明| 12 | |--|--|--|--| 13 | |options|是|[IDynamicFormOptions](./DynamicFormOptions.md)|参数| 14 | 15 | ## DynamicFormItemRegistry 16 | 17 | 用于管理注册动态表单的条目组件。 18 | 19 | ### DynamicFormItemRegistry.register(type, componentInstance, additionalProps, valueName) 20 | 21 | 注册自定义表单控件 22 | 23 | |参数|必填|类型|说明| 24 | |--|--|--|--| 25 | |type|是|string|唯一类型名称| 26 | |componentInstance|是|unknown|组件类| 27 | |additionalProps|是|`Record`|组件的附加属性,将会设置到渲染函数上| 28 | |valueName|是|string|用于指定表单子组件的双向绑定值属性名称,默认是 value, 当你的组件主 modelValue 名称不一致时,可以重新指定。| 29 | 30 | ### DynamicFormItemRegistry.unregister(type) 31 | 32 | 取消注册自定义表单控件 33 | 34 | |参数|必填|类型|说明| 35 | |--|--|--|--| 36 | |type|是|string|唯一类型名称| 37 | 38 | ### DynamicFormItemRegistry.findDynamicFormItemByType(type) 39 | 40 | 查找已注册的表单组件,如果未找到,则返回 null 41 | 42 | |参数|必填|类型|说明| 43 | |--|--|--|--| 44 | |type|是|string|唯一类型名称| 45 | 46 | 返回值:表单组件信息 47 | 48 | 类型:DynamicFormItemRegistryItem 49 | 50 | ## DynamicFormItemRegistryItem 51 | 52 | |参数|类型|说明| 53 | |--|--|--| 54 | |componentInstance|unknown|组件的类实例| 55 | |valueName|string|组件的双向绑定属性名称| 56 | |additionalProps|object|组件的默认值| 57 | -------------------------------------------------------------------------------- /docs/api/internal-controls.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 内置组件参考 3 | --- 4 | 5 | # 内置组件参考 6 | 7 | ## Row 8 | 9 | 栅格布局行组件。 10 | 11 | ### Props 12 | 13 | | 属性 | 描述 | 类型 | 默认值 | 14 | | :----: | :----: | :----: | :----: | 15 | | gutter | 列元素之间的间距(单位为 px) | `number` | 0 | 16 | | justify | 主轴对齐方式,可选值为 'flex-start' , 'flex-end' , 'center' , 'space-between' , 'space-around' , 'space-evenly' | `string` | — | 17 | | gutter | 交叉轴对齐方式,可选值为 "center" , "flex-start" , "flex-end" , "stretch" , "baseline" | `number` | — | 18 | | wrap | 是否自动换行 | `boolean` | true | 19 | 20 | ## Col 21 | 22 | 栅格布局列组件。 23 | 24 | ### Props 25 | 26 | | 属性 | 描述 | 类型 | 默认值 | 27 | | :----: | :----: | :----: | :----: | 28 | | offset | 列元素偏移距离 | `number` | 0 | 29 | | span | 列元素宽度 | `number` | 0 | 30 | -------------------------------------------------------------------------------- /docs/api/internal-form.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 内置 Form 参考 3 | --- 4 | 5 | # 内置 Form 参考 6 | 7 | 下面参考仅用于默认表单,如果你更换了其他 UI库 的表单,则请以其文档为准。 8 | 9 | ## Form 10 | 11 | 表单组件。 12 | 13 | ### FormProps 14 | 15 | | 属性 | 描述 | 类型 | 默认值 | 16 | | :----: | :----: | :----: | :----: | 17 | | model | 表单数据对象 | `object` | - | 18 | | rules | 表单验证规则 | `Record` | - | 19 | | colon | 配置 Form.Item 的 colon 的默认值 | `boolean` | - | 20 | | hideRequiredMark | 隐藏所有表单项的必选标记 | `boolean` | - | 21 | | labelAlign | 统一设置左侧文本对齐 | `'left'` or `'center'` or `'right'` | - | 22 | | labelCol | 统一设置左侧文本的flex占比 | `number` | 2 | 23 | | wrapperCol | 统一设置输入框的flex占比 | `number` | - | 24 | | validateTrigger | 统一设置字段校验规则 onBlur 文本框失去焦点时校验/onValueChange 数值更改时校验/onSubmit 提交时校验(默认) | `ValidTrigger` | false | 25 | | showLabel | 是否显左边标题 | `boolean` | true | 26 | | name | 表单的名称 | `string` | - | 27 | 28 | ### Events 29 | 30 | | 事件名 | 描述 | 参数 | 31 | | :----: | :----: | :----: | 32 | | finish | 提交表单且数据验证成功后回调事件 | - | 33 | | finishFailed | 提交表单且数据验证失败后回调事件 | - | 34 | | submit | 数据验证成功后回调事件 | - | 35 | 36 | ### Form 37 | 38 | 实例方法。 39 | 40 | #### clearValidate 41 | 42 | 清空指定属性的校验结果。 43 | 44 | | 参数 | 描述 | 类型 | 必填 | 45 | | :----: | :----: | :----: | :----: | 46 | | name | 指定属性名称,不填为整个表单 | `string|string[]` | 否 | 47 | 48 | #### resetFields 49 | 50 | 重置属性的值。 51 | 52 | | 参数 | 描述 | 类型 | 必填 | 53 | | :----: | :----: | :----: | :----: | 54 | | name | 指定属性名称,不填为整个表单 | `string|string[]` | 否 | 55 | 56 | #### validate 57 | 58 | 对指定属性进行校验。 59 | 60 | | 参数 | 描述 | 类型 | 必填 | 61 | | :----: | :----: | :----: | :----: | 62 | | name | 指定属性名称,不填为整个表单 | `string|string[]` | 否 | 63 | 64 | #### scrollToField 65 | 66 | 滚动表单页面至指定属性。 67 | 68 | | 参数 | 描述 | 类型 | 必填 | 69 | | :----: | :----: | :----: | :----: | 70 | | name | 属性名称 | `string` | 是 | 71 | 72 | #### submit 73 | 74 | 手动提交表单。 75 | 76 | | 参数 | 描述 | 类型 | 必填 | 77 | | :----: | :----: | :----: | :----: | 78 | | valid | 是否需要校验表单,默认 true | `valid` | 否 | 79 | 80 | ## FormItem 81 | 82 | 表单条目组件。 83 | 84 | ### FormItemProps 85 | 86 | | 属性 | 描述 | 类型 | 默认值 | 87 | | :----: | :----: | :----: | :----: | 88 | | label | 输入框左侧文本 | `string` | - | 89 | | name | 名称,作为提交表单时的标识符 | `object` | - | 90 | | disabled | 是否禁用输入框 | `boolean` | false | 91 | | center | 是否内容垂直居中 | `boolean` | true | 92 | | colon | 是否在 label 后面添加冒号 | `boolean` | true | 93 | | required | 是否必填 | `boolean` | false | 94 | | showRequiredBadge | 是否显示表单必填星号 | `boolean` | false | 95 | | labelAlign | 左侧文本对齐 | `'left'` or `'center'` or `'right'` | - | 96 | | labelCol | 左侧文本的flex占比 | `{ span?: number, offset?: number }` | - | 97 | | wrapperCol | 输入框的flex占比 | `{ span?: number, offset?: number }` | - | 98 | | labelStyle | 左侧文本的样式 | `object` | - | 99 | | labelColor | 左侧文本的颜色 | `string` | - | 100 | | labelDisableColor | 左侧文本的禁用颜色 | `string` | - | 101 | | validateTrigger | 设置字段校验的时机 onBlur 文本框失去焦点时校验/onValueChange 数值更改时校验/onSubmit 提交时校验(默认) | `ValidTrigger` | - | 102 | | showLabel | 是否显左边标题 | `boolean` | true | 103 | | noBottomMargin | 是否去掉底部编辑 | `boolean` | false | 104 | 105 | ## FormContext 106 | 107 | 用于表单校验。仅用于默认表单,如果你更换了其他 UI库 的表单,则请以其文档的自定义表单为准。 108 | 109 | ### useInjectFormItemContext() 110 | 111 | 用于表单项的注册,用法: 112 | 113 | ```vue 114 | 123 | 124 | 150 | ``` 151 | -------------------------------------------------------------------------------- /docs/assets/root.scss: -------------------------------------------------------------------------------- 1 | .demo-index { 2 | margin-top: 40px; 3 | border-radius: 5px; 4 | border: 1px solid var(--vp-c-divider); 5 | color: var(--vp-c-text-2); 6 | } 7 | .demo-row { 8 | display: flex; 9 | flex-direction: row; 10 | } 11 | .demo-col { 12 | display: flex; 13 | flex-direction: column; 14 | width: 50%; 15 | 16 | &.padding { 17 | padding: 10px; 18 | } 19 | } 20 | .demo-alert { 21 | width: 100%; 22 | padding: 8px 16px; 23 | margin: 0; 24 | box-sizing: border-box; 25 | border-radius: 4px; 26 | position: relative; 27 | background-color: var(--vp-code-block-bg); 28 | overflow: hidden; 29 | opacity: 1; 30 | display: block; 31 | 32 | &.success { 33 | background-color: #cef1bc; 34 | color: #67c23a; 35 | } 36 | &.error { 37 | background-color: #f1cdcd; 38 | color: #f56c6c; 39 | } 40 | } 41 | .demo-result { 42 | min-height: 100px; 43 | padding: 10px; 44 | background-color: var(--vp-code-block-bg); 45 | border: none; 46 | border-radius: 5px; 47 | font-size: 13px; 48 | font-family: var(--vp-font-family-mono); 49 | 50 | h5 { 51 | margin: 0; 52 | font-size: 15px; 53 | } 54 | } 55 | div.demo-result { 56 | white-space: pre; 57 | } 58 | 59 | @media (max-width: 700px) { 60 | .demo-index { 61 | margin-top: 30px; 62 | } 63 | .demo-row { 64 | display: flex; 65 | flex-direction: column; 66 | } 67 | .demo-col { 68 | width: 100%; 69 | } 70 | } -------------------------------------------------------------------------------- /docs/examples/BasicUseageDoc.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /docs/examples/BasicUseageDoc1.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /docs/examples/BasicUseageDoc2.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /docs/examples/BasicUseageDoc3.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /docs/examples/BasicUseageDoc4.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /docs/examples/BasicUseageDoc5.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /docs/examples/BasicUseageDoc6.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /docs/examples/BasicUseageDoc7.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /docs/examples/DocHome.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | -------------------------------------------------------------------------------- /docs/examples/ingrate/AntDesgin.ts: -------------------------------------------------------------------------------- 1 | import { markRaw } from "vue"; 2 | import { 3 | Alert, Checkbox, DatePicker, Form, 4 | FormItem, Image, Input, InputNumber, 5 | Rate, Switch, Textarea, TimePicker 6 | } from "ant-design-vue"; 7 | import { DynamicFormItemRegistry, IDynamicFormOptions, configDefaultDynamicFormOptions } from "@imengyu/vue-dynamic-form"; 8 | 9 | export const defaultConfig = { 10 | internalWidgets: { 11 | Form: { 12 | component: markRaw(Form), 13 | propsMap: { 14 | rules: 'rules', 15 | wrapperCol: 'wrapperCol', 16 | labelCol: 'labelCol', 17 | }, 18 | }, 19 | FormItem: { 20 | component: markRaw(FormItem), 21 | propsMap: { 22 | name: 'name', 23 | wrapperCol: 'wrapperCol', 24 | labelCol: 'labelCol', 25 | }, 26 | }, 27 | }, 28 | } as IDynamicFormOptions 29 | 30 | export function registerAllFormComponents() { 31 | configDefaultDynamicFormOptions(defaultConfig); 32 | 33 | DynamicFormItemRegistry.register('text', markRaw(Input), {}, 'modelValue') 34 | .register('password', markRaw(Input.Password), {}, 'modelValue') 35 | .register('number', markRaw(InputNumber), {}, 'modelValue') 36 | .register('text-area', markRaw(Textarea), {}, 'modelValue') 37 | .register('switch', markRaw(Switch), {}, 'modelValue') 38 | .register('check-box', markRaw(Checkbox), {}, 'modelValue') 39 | .register('rate', markRaw(Rate)) 40 | .register('date', markRaw(DatePicker), {}, 'pickerValue') 41 | .register('time', markRaw(TimePicker), {}, 'modelValue') 42 | .register('date-time', markRaw(DatePicker), { showTime: true }) 43 | .register('alert', markRaw(Alert)) 44 | .register('static-image', markRaw(Image), {}, "src"); 45 | } -------------------------------------------------------------------------------- /docs/examples/ingrate/ArcoDesgin.ts: -------------------------------------------------------------------------------- 1 | import { markRaw } from "vue"; 2 | import { 3 | Alert, Checkbox, DatePicker, Form, FormItem, Button, 4 | Image, Input, InputNumber, Rate, Switch, Textarea, TimePicker, 5 | Tabs, 6 | TabPane, 7 | Select 8 | } from "@arco-design/web-vue"; 9 | import { DynamicFormItemRegistry, IDynamicFormOptions, configDefaultDynamicFormOptions } from "@imengyu/vue-dynamic-form"; 10 | import MyCheckBox from "./MyCheckBox.vue"; 11 | 12 | export const defaultConfig = { 13 | internalWidgets: { 14 | Form: { 15 | component: markRaw(Form), 16 | propsMap: { 17 | rules: 'rules', 18 | wrapperCol: 'wrapper-col-props', 19 | labelCol: 'label-col-props', 20 | }, 21 | }, 22 | FormItem: { 23 | component: markRaw(FormItem), 24 | propsMap: { 25 | name: 'field', 26 | }, 27 | }, 28 | Tab: { 29 | component: markRaw(Tabs), 30 | propsMap: { 31 | activeKey: 'activeKey', 32 | defaultActiveKey: 'defaultActiveKey', 33 | }, 34 | }, 35 | TabPage: { 36 | component: markRaw(TabPane), 37 | propsMap: { 38 | title: 'title', 39 | disabled: 'disabled', 40 | }, 41 | }, 42 | }, 43 | } as IDynamicFormOptions 44 | 45 | export function registerAllFormComponents() { 46 | configDefaultDynamicFormOptions(defaultConfig); 47 | 48 | DynamicFormItemRegistry.register('text', markRaw(Input), {}, 'modelValue') 49 | .register('password', markRaw(Input.Password), {}, 'modelValue') 50 | .register('number', markRaw(InputNumber), {}, 'modelValue') 51 | .register('text-area', markRaw(Textarea), {}, 'modelValue') 52 | .register('select', markRaw(Select), {}, 'modelValue') 53 | .register('switch', markRaw(Switch), {}, 'modelValue') 54 | .register('check-box', markRaw(Checkbox), {}, 'modelValue') 55 | .register('rate', markRaw(Rate)) 56 | .register('date', markRaw(DatePicker), {}, 'pickerValue') 57 | .register('time', markRaw(TimePicker), {}, 'modelValue') 58 | .register('date-time', markRaw(DatePicker), { showTime: true }) 59 | .register('alert', markRaw(Alert)) 60 | .register('static-image', markRaw(Image), {}, "src") 61 | .register('button', markRaw(Button), {}, 'text'); 62 | 63 | //这是注册自己的自定义组件 64 | DynamicFormItemRegistry.register('my-check', markRaw(MyCheckBox), {}, "value"); 65 | } -------------------------------------------------------------------------------- /docs/examples/ingrate/ElementPlus.ts: -------------------------------------------------------------------------------- 1 | import { markRaw } from "vue"; 2 | import { 3 | ElAlert, ElCheckbox, ElDatePicker, ElForm, 4 | ElFormItem, ElImage, ElInput, ElInputNumber, 5 | ElRate, ElSwitch, ElTimePicker 6 | } from "element-plus"; 7 | import { DynamicFormItemRegistry, IDynamicFormOptions, configDefaultDynamicFormOptions } from "@imengyu/vue-dynamic-form"; 8 | 9 | export const defaultConfig = { 10 | internalWidgets: { 11 | Form: { 12 | component: markRaw(ElForm), 13 | propsMap: { 14 | rules: 'rules', 15 | //element 不支持设置wrapper宽度 16 | //wrapperCol: 'wrapper-col-props', 17 | labelCol: 'label-width', 18 | }, 19 | }, 20 | FormItem: { 21 | component: markRaw(ElFormItem), 22 | propsMap: { 23 | name: 'prop', 24 | labelCol: 'label-width', 25 | }, 26 | }, 27 | }, 28 | } as IDynamicFormOptions 29 | 30 | export function registerAllFormComponents() { 31 | configDefaultDynamicFormOptions(defaultConfig); 32 | 33 | DynamicFormItemRegistry.register('text', markRaw(ElInput), {}, 'modelValue') 34 | .register('password', markRaw(ElInput), { showPassword: true }, 'modelValue') 35 | .register('number', markRaw(ElInputNumber), {}, 'modelValue') 36 | .register('text-area', markRaw(ElInput), { type: "textarea", rows: 2 }, 'modelValue') 37 | .register('switch', markRaw(ElSwitch), {}, 'modelValue') 38 | .register('check-box', markRaw(ElCheckbox), {}, 'modelValue') 39 | .register('rate', markRaw(ElRate)) 40 | .register('date', markRaw(ElDatePicker), {}, 'pickerValue') 41 | .register('time', markRaw(ElTimePicker), {}, 'modelValue') 42 | .register('date-time', markRaw(ElDatePicker), { showTime: true }) 43 | .register('alert', markRaw(ElAlert)) 44 | .register('static-image', markRaw(ElImage), {}, "src"); 45 | } -------------------------------------------------------------------------------- /docs/examples/ingrate/IngrateDemoAntDesgin.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /docs/examples/ingrate/IngrateDemoArcoDesgin.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /docs/examples/ingrate/IngrateDemoElementPlus.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /docs/examples/ingrate/MyCheckBox.vue: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | -------------------------------------------------------------------------------- /docs/guide/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 介绍 3 | --- 4 | 5 | 8 | 9 | # 介绍 10 | 11 | ## 什么是 vue-dynamic-form? 12 | 13 | vue-dynamic-form 是一个中后台表单组件,可以用动态数据驱动生成表单组件。 14 | 并不是开发中所必须的,它只是一个帮助你加快开发的小组件。 15 | 设计参考了 [阿里的 XRender](https://xrender.fun/form-render)。 16 | 17 | 在中后台开发中,我们经常会使用表单提交数据,表单提交数据占据开发的多半时间, 18 | 当表单特别多时,手动写表单组件是一件非常麻烦的事情。vue-dynamic-form就是为了解决这个问题而写的, 19 | vue-dynamic-form 支持你使用 JSON 数据动态生成表单,只需要传入一个包含各种描述信息的 JSON,就能渲染出一个完整的表单。 20 | 21 | **TODO: vue-dynamic-form 还支持你使用可视化编辑器,通过鼠标拖拉来生成表单。** 22 | 23 | 作者公司使用的中后台项目已经全部使用动态表单,省去了80%布局表单的时间,开发效率得到了不少提升(可以用更多时间摸鱼啦🤭)。现在将 vue-dynamic-form 开源,希望可以为你的开发提供便利。 24 | 25 | ::: warning 26 | **本项目目前还处于早期发布阶段,可能会存在不少问题,如果遇到问题,欢迎在 [Github](https://github.com/imengyu/vue-dynamic-form/issues) 提出 Issue,我会尽量为你解决!** 27 | ::: 28 | 29 | ## 效果 30 | 31 | 32 | 33 | 上面的效果是使用了默认的组件,功能比较简陋,仅用于测试和展示。 34 | 实际你可以在表单中嵌入使用你喜欢的 UI 组件库,例如 [Ant Design Vue](https://www.antdv.com/docs/vue/getting-started-cn)、[arco.design Vue](https://arco.design/vue/docs/start)、[Element plus](https://element-plus.gitee.io/zh-CN/guide/installation.html) 等等,嵌入其他UI组件库的最终效果你可以[查看这里](./register-controls.md#案例)。 35 | 36 | ## 开始之前 37 | 38 | 文档中所示案例,你都可在 [Github 仓库](https://github.com/imengyu/vue-dynamic-form/tree/master/src/example/views) 中找到完整的源代码。 39 | 40 | 作者开发不易,如果这个项目对您有帮助,希望你可以去 [Github](https://github.com/imengyu/vue-dynamic-form) 或者 [Gitee](https://gitee.com/imengyu/vue-dynamic-form) 帮我点个 ⭐ ,这将是对我极大的鼓励。谢谢啦 (●'◡'●) 41 | 42 | 如果你准备好了,那我们就开始吧~ 43 | 44 | [👉 立即开始](./getting-started.md) 45 | -------------------------------------------------------------------------------- /docs/guide/basic-useage.md: -------------------------------------------------------------------------------- 1 | # 基础用法 2 | 3 | 7 | 8 | ## 编写表单结构+数据 9 | 10 | 绑定组件完成后,你就可以开始写动态表单了,你需要写的有两个部分,结构 与 数据。 11 | 12 | * 数据是你最终提交到服务端的数据。 13 | * 结构是你渲染表单结构的数据结构。 14 | 15 | 一般的数据如下所示,你只需要声明其为响应式数据,并将其传入 DynamicForm 的 model 属性中即可使用。 16 | 17 | ```js 18 | const formModel = reactive({ 19 | stringProp: '', 20 | stringProp2: '', 21 | numberProp: 2, 22 | numberProp2: 3, 23 | booleanProp: false, 24 | }); 25 | ``` 26 | 27 | ### 表单结构 28 | 29 | 表单由 formItems 和 formRules 两部分组成,最重要的是 formItems,用于描述表单的结构。 30 | 31 | formItems 由数组组成,每个条目表示一个表单项,例如下面的表单项显示一个 基础 input: 32 | 33 | ```ts 34 | const formOptions = ref({ 35 | formLabelCol: { span: 6 }, 36 | formWrapperCol: { span: 18 }, 37 | formItems: [ 38 | { 39 | type: 'base-text', 40 | label: '文本', 41 | name: 'stringProp', 42 | additionalProps: { placeholder: '请输入文本' } 43 | }, 44 | ] 45 | }); 46 | ``` 47 | 48 | 其中: 49 | 50 | * type 指定当前表单项使用的组件,这与你在 [绑定组件](#绑定组件) 章节中绑定时使用的名称一致。 51 | * name 指定当前表单项绑定的数据属性,上面这个input绑定了 `formModel.stringProp` 这个属性。 52 | * label 指定说明文字 53 | * additionalProps 这个用于为组件设置特殊属性,这个可以传入什么属性是由你使用的组件决定的,例如 input 有 placeholder 这个属性,所以我可以覆盖这个属性,指定 input 的水印文字。 54 | 同样的,还有 additionalEvents 用于设置或者覆盖默认事件。 55 | 56 | 最终显示效果如下所示: 57 | 58 | 59 | 60 | 一个完整的表单就是由上面的表单描述数据多条组合而成的,因此,只要你的脑海中有了表单的结构数据,一个 61 | 表单就可以快速搭建出来。 62 | 63 | #### 手写结构 64 | 65 | 对于初学者来说记住结构里所有的字段和使用方式并非易事。为了让大家能够快速上手,建议以以下的顺序尝试。 66 | 67 | 1. 下方以及后续章节有基础用法、高级用法的完整样例,建议阅读后参考再开始使用。 68 | 2. 玩转一下 表单设计器,拖拖拽拽导出数据,丢到代码里生成可用表单。本质上这是一个可视化的表单生成器,支持导入 & 导出。 69 | 3. 建议使用 Typescript,当你拼写时可以给出代码提示。 70 | 71 | ### 表单验证 72 | 73 | 动态表单本身不提供任何验证方法,验证是由其中的 Form 组件实现的,所以 formRules 仅仅是把值传给了 Form。 74 | 75 | 动态表单内置的 Form 组件是使用 async-validator 进行验证的,具体验证参数请参考 [async-validator文档](https://github.com/yiminghe/async-validator)。默认表单组件会在提交时自动验证。 76 | 77 | 如果你将表单默认的 Form 替换了,使用的其他组件库的 Form 组件,传入的 formRules 请参考其对应的文档。 78 | 79 | ### 表单提交 80 | 81 | 表单可以手动验证提交,也可以通过插入一个 type="submit" 的按钮触发提交 (具体行为由你使用的 Form 组件定义) 82 | 83 | ```vue 84 | 95 | 96 | 97 | 124 | ``` 125 | 126 | ### 嵌套表单结构 127 | 128 | 表单支持嵌套,你可以嵌套对象或者数组。 129 | 130 | 具体请参考 [嵌套表单结构](./form-nest.md)。 131 | 132 | ### 表单其他属性 133 | 134 | 表单数据还支持设置 label 与 wrapper 占比。 135 | 136 | ```ts 137 | const formOptions = ref({ 138 | //formLabelWidth: '100px', //单独设置宽度 139 | formLabelCol: { span: 12 }, 140 | formWrapperCol: { span: 12 }, 141 | //... 142 | }); 143 | ``` 144 | 145 | 效果: 146 | 147 | 148 | 149 | 表单数据还支持设置 Form 组件的其他属性,如果你使用自定义 Form 组件,可以使用 formAdditionaProps 设置自定义属性。 150 | 151 | ```ts 152 | const formOptions = ref({ 153 | formAdditionaProps: { 154 | //自定义属性 155 | layout: 'inline', 156 | }, 157 | //... 158 | }); 159 | ``` 160 | 161 | ## 完整的最简单 Demo 162 | 163 | ```vue preview 164 | 173 | 174 | 248 | ``` 249 | 250 | ## 高级用法 251 | 252 | * [👉 自定义表单组件](./custom-control.md) 253 | * [👉 表单联动](./form-linkage.md) 254 | * [👉 表单嵌套](./form-nest.md) 255 | -------------------------------------------------------------------------------- /docs/guide/custom-control.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 自定义组件 3 | --- 4 | 5 | # 自定义组件 6 | 7 | 自定义组件功能使表单拥有很好扩展性,可能的应用场景如下: 8 | 9 | * 我需要写一个异步加载的搜索输入框(普适性不高/难以用 schema 描述的组件) 10 | * 我需要在表单内部写一个 excel 上传按钮(完全定制化的需求) 11 | 12 | 自定义组件可以选择局部注册,也可选择全局注册: 13 | 14 | * 局部注册:仅在某个表单中使用。 15 | * 全局注册:可以在全部的表单中使用,通常是重复使用的组件。 16 | 17 | ## 编写组件 18 | 19 | 组件与普通表单组件基本一致,但是有以下这些表单属性会被传入: 20 | 21 | |属性|类型|说明| 22 | |--|--|--| 23 | |item|IDynamicFormItem|当前表单项的数据定义| 24 | |disabled|boolean|当前表单项是否禁用| 25 | |rawModel|IDynamicFormObject|整个表单的值| 26 | |parentModel|IDynamicFormObject|父表单元素的值 (上一级的值,只在列表场景的使用,例如列表某个元素的父级就是整个 item)| 27 | |`[双向绑定主属性名称]`|unknown|当前双向绑定变量的值,“双向绑定主属性名称” 是 `DynamicFormItemRegistry.register` 注册时指定的 | 28 | |name|string|当前表单项的数据键值名称| 29 | 30 | 另外,用户可以指定 additionalProps、additionalEvents 和 additionalDirectProps, 这些属性与事件都会被绑定到你的组件上。 31 | 32 | 事件: 33 | 34 | 为了实现数据双向绑定,发出 `'update:[双向绑定主属性名称]'` 事件用于数据更改事件。(“双向绑定主属性名称” 是 `DynamicFormItemRegistry.register` 注册时指定的)。 35 | 36 | 如下,这是一个自定义 check-box 的示例: 37 | 38 | ```vue 39 | BaseCheck.vue 40 | 54 | 55 | 86 | ``` 87 | 88 | ## 全局注册 89 | 90 | 注:全局注册在运行中只需要注册一次即可。 91 | 92 | ```js 93 | import BaseCheck from 'BaseCheck.vue'; 94 | 95 | DynamicFormItemRegistry.register('base-check', markRaw(BaseCheck), {}, 'value');//指定传入主属性是 “value” 96 | ``` 97 | 98 | ## 局部注册 99 | 100 | 在表单属性的 widgets 中注册,这个组件可以在当前表单中使用,参数与 DynamicFormItemRegistry.register 一致: 101 | 102 | ```ts 103 | import BaseCheck from 'BaseCheck.vue'; 104 | import { type IDynamicFormOptions, makeWidget } from "@imengyu/vue-dynamic-form"; 105 | 106 | const formOptions = ref({ 107 | widgets: { 108 | 'base-check': makeWidget(markRaw(BaseCheck), {}, 'value'), 109 | }, 110 | formItems: [ 111 | ... 112 | ] 113 | }); 114 | ``` 115 | 116 | ## 一些情况下不需要包装自定义组件 117 | 118 | 自定义组件就是普通的 Vue 组件,唯一的要求是要有一个双向绑定值。所以如果现成的组件已经有了双向绑定的主属性,就可以直接拿来用,只需要在注册时指定主属性的名称。 119 | 120 | 举例来说:现在我们需要使用“级联选择”组件,这时打开 arco desgin 文档,我们看到 cascader 默认使用了 model-value (v-model),那就直接拿来用吧: 121 | 122 | ```ts 123 | import { Cascader } from "@arco-design/web-vue"; 124 | 125 | DynamicFormItemRegistry.register('cascader', markRaw(Cascader), {}, 'modelValue'); //指定传入主属性是 “modelValue” 126 | ``` 127 | 128 | 注册后即可使用: 129 | 130 | 传入的 additionalProps 就是这个组件的属性,如果你使用TypeScript,具体 arco 为我们写好了定义,也可以直接导入,这样就有有类型定义了。 131 | 132 | ```ts 133 | import { CascaderInstance } from "@arco-design/web-vue"; 134 | 135 | const formOptions = ref({ 136 | formItems: [ 137 | { 138 | type: 'cascader', label: '级联组件', name: 'test', 139 | additionalProps: { 140 | placeholder: '请选择发货地区' 141 | } as CascaderInstance['$props'] //这里导入了 CascaderInstance 为了有类型定义 142 | }, 143 | ] 144 | }); 145 | ``` 146 | -------------------------------------------------------------------------------- /docs/guide/custom-render.md: -------------------------------------------------------------------------------- 1 | # 自定义渲染 2 | 3 | 如果你需要在某个表单中插入高度自定义,不通用的内容,例如一个按钮,一个表格,一张图片等等,你可以使用动态表单提供的插槽 formCeil 进行某个条目的自定义渲染。 4 | 5 | ## 自定义渲染只读条目 6 | 7 | 你只需要将自定义条目类型设置为 `'custom'` 即可。 8 | 9 | 你需要实现 formCeil 插槽,根据 name (完整路径) 或者 item.name (名称) 判断当前是不是需要渲染的条目,如果是,则渲染自定内容。 10 | 11 | ```vue preview 12 | 36 | 37 | 55 | ``` 56 | 57 | ## 自定义渲染+双向绑定数据 58 | 59 | 同样,自定义渲染插槽也支持数据双向绑定。 60 | 61 | ```vue preview 62 | 84 | 85 | 99 | ``` 100 | -------------------------------------------------------------------------------- /docs/guide/form-funs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 表单方法 3 | --- 4 | 5 | # 表单方法 6 | 7 | 动态表单实例上提供了如下方法,方便你的使用。 8 | 9 | ## 获取表单组件实例引用 10 | 11 | 可以通过 `getFormRef` 实例函数获取到内置 Form 组件的实例引用。 12 | 13 | 注意,这个 Form 取决于你使用的 Form 组件, 例如,你使用了Arco Desgin的 Form 组件,那么获取的类型就是 Arco 的 FormInstance。 14 | 15 | 默认的内置 Form 组件类型是 `import { Form } from '@imengyu/vue-dynamic-form';`。 16 | 17 | ```vue 18 | 25 | 26 | 45 | ``` 46 | 47 | ## 获取表单项组件实例引用 48 | 49 | 某些情况下,你可能需要直接操作自己的表单自定义组件。例如,你写了一个动态加载数据的下拉框,表单外部有一些条件, 50 | 要求是表单某几项参数变化时,需要重新加载下拉框的数据。 51 | 52 | 你可以通过 `getFormItemControlRef` 实例函数获取到指定表单项组件的实例引用,获取引用后,你就可以自由进行调用了。 53 | 54 | 下方是一个例子。他的效果是当 type 更改时,调用 my-select 去加载数据 55 | 56 | ```vue 57 | 65 | 66 | 141 | ``` -------------------------------------------------------------------------------- /docs/guide/form-linkage.md: -------------------------------------------------------------------------------- 1 | # 表单联动 2 | 3 | 8 | 9 | 表单组件间的联动是开发中普遍的问题,希望能保持简洁易用的同时支持联动。 10 | 11 | IDynamicFormItem 表单项的以下属性支持传入回调,在回调中你可以自由判断,决定需要返回何值: 12 | 13 | |属性名|类型|说明| 14 | |--|--|--| 15 | |hidden|boolean|显是否隐藏当前表单项| 16 | |disabled|boolean|是否禁用当前表单项| 17 | |label|string|当前表单说明文字| 18 | |additionalProps|object|附加组件属性(只支持第一级传入回调)| 19 | 20 | 回调定义: 21 | 22 | |属性名|类型|说明| 23 | |--|--|--| 24 | |model|unknown|当前表单条目的值| 25 | |rawModel|unknown|整个 form 的值 (最常用,当两个关联组件距离较远时,可以从顶层的 rawModel 里获取)| 26 | |parentModel|unknown|父表单元素的值 (上一级的值,只在列表场景的使用,例如列表某个元素的父级就是整个 item)| 27 | |item|IDynamicFormItem|当前表单条目信息| 28 | |formRules|Record<string, Rule>|当前条目校验数据| 29 | 30 | ## 示例 31 | 32 | 根据表单的数据去控制表单项是否显示。 33 | 34 | ```ts 35 | const formModel = reactive({ 36 | stringProp: '', 37 | booleanProp: false, 38 | }); 39 | const formOptions = ref({ 40 | formItems: [ 41 | { 42 | type: 'base-check', label: '展示更多内容', name: 'booleanProp', 43 | additionalProps: { 44 | text: '显示', 45 | }, 46 | }, 47 | { 48 | hidden: { callback: (_, model) => (model as IDynamicFormObject).booleanProp == false }, 49 | type: 'base-text', label: '文本', name: 'stringProp', additionalProps: { placeholder: '请输入文本' }, 50 | }, 51 | ], 52 | }); 53 | ``` 54 | 55 | 效果: 56 | 57 | 58 | 59 | ## 复杂案例 60 | 61 | ```js 62 | const formModel = ref({ 63 | mobile: '', 64 | enterprise_name: '', 65 | password: '', 66 | password_confirm: '', 67 | authorization_code: '', 68 | isEnterprise: false, 69 | }); 70 | const formOptions : IDynamicFormOptions = { 71 | formRules: { 72 | enterprise_name: [{ required: true, message: '请输入企业全称' } ], 73 | enterprise_id: [{ required: true, message: '请输入企业全称' } ], 74 | mobile: [{ required: true, message: '请输入手机号' } ], 75 | password: [{ required: true, message: '请输入密码' } ], 76 | vcode: [{ required: true, message: '请输入验证码' } ], 77 | password_confirm: [ 78 | { required: true, message: '请再输入一次密码' }, 79 | { validator: ( value: unknown, callback: (error?: string) => void ) => { 80 | if (value !== formModel.value.password) 81 | callback('两次密码输入不一致,请重新输入'); 82 | else 83 | callback(); 84 | } } 85 | ], 86 | authorization_code: [{ required: true, message: '请输入授权码' } ], 87 | }, 88 | formLabelCol: { span: 6 }, 89 | formWrapperCol: { span: 18 }, 90 | formItems: [ 91 | { type: 'base-check', label: '企业用户', name: 'isEnterprise' }, 92 | { 93 | hidden: { callback: (_, rawModel) => (rawModel as IDynamicFormObject).isEnterprise === false }, 94 | type: 'base-text', label: '手机号', name: 'mobile', additionalProps: { placeholder: '请输入手机号' }, 95 | }, 96 | { 97 | hidden: { callback: (_, rawModel) => (rawModel as IDynamicFormObject).isEnterprise === true }, 98 | type: 'base-text', label: '企业全称', name: 'enterprise_name', additionalProps: { placeholder: '请输入企业全称' }, 99 | }, 100 | { 101 | hidden: { callback: (_, rawModel) => (rawModel as IDynamicFormObject).isEnterprise === true }, 102 | type: 'base-text', label: '统一社会信用代码', name: 'enterprise_id', additionalProps: { placeholder: '请输入企业统一社会信用代码' }, 103 | }, 104 | { type: 'base-text', label: '密码', name: 'password', additionalProps: { placeholder: '请输入密码', password: true }, 105 | { type: 'base-text', label: '确认密码', name: 'password_confirm', additionalProps: { placeholder: '请再输入一次密码', password: true }, 106 | { 107 | type: 'base-text', 108 | label: { callback: (_, rawModel) => (rawModel as IDynamicFormObject).isEnterprise === true ? '企业授权ID' : '授权密码' }, 109 | name: 'authorization_code', 110 | additionalProps: { 111 | placeholder: { callback: (_, rawModel) => (rawModel as IDynamicFormObject).isEnterprise === true ? '请输入企业授权ID,授权ID请咨询客服电话' : '请输入授权密码' }, 112 | } as IDynamicFormItemCallbackAdditionalProps 113 | }, 114 | ], 115 | }; 116 | ``` 117 | 118 | 完整源代码可以参考 [这里](https://github.com/imengyu/vue-dynamic-form/blob/master/src/examples/FormLinkage.vue)。 119 | 120 | 效果: 121 | 122 | 123 | 124 | ## 完全自定义 125 | 126 | 如果涉及到值的联动,或者极其复杂的表单展示联动,你也可以通过监听数据变化,然后执行你的自定义操作,达到联动的效果,例如, 127 | 下方是一个表单联动例子,他的效果是当 type 更改时,更改 item_id 的 options (也可以去后端加载数据)。 128 | 129 | ```ts 130 | 131 | import { BaseRadioProps, BaseSelectProps, DynamicForm, IDynamicFormOptions } from '@imengyu/vue-dynamic-form'; 132 | import { ref, reactive, watch } from 'vue' 133 | 134 | const formModel = ref({ 135 | type: 1, 136 | item_id: 0 as number|null, 137 | }); 138 | const formOptions = reactive({ 139 | formRules: {}, 140 | formLabelCol: { span: 6 }, 141 | formWrapperCol: { span: 18 }, 142 | formItems: [ 143 | { 144 | type: 'base-radio', label: '会员类型', name: 'type', 145 | additionalProps: { 146 | items: [ 147 | { label: '短期', value: 1 }, 148 | { label: '长期', value: 2 }, 149 | { label: '试用', value: 3 }, 150 | ] 151 | } as BaseRadioProps, 152 | watch: (oldv, newV) => { 153 | //也可以为条目设置watch 154 | //loadPackageSelect(newV); 155 | console.log('会员类型更改:', oldv, newV); 156 | }, 157 | }, 158 | { 159 | type: 'base-select', label: '选择套餐', name: 'item_id', 160 | additionalProps: { 161 | options: [ 162 | { text: '短期合作套餐', value: 0 }, 163 | { text: '短期高级套餐', value: 1 }, 164 | ], 165 | } as BaseSelectProps, 166 | }, 167 | ], 168 | }); 169 | 170 | function loadPackageSelect(newType: number) { 171 | //这里是写死手动判断了,实际在这里你可以去请求后端数据 172 | switch (newType) { 173 | case 1: 174 | (formOptions.formItems[1].additionalProps as BaseSelectProps).options = [ 175 | { text: '短期合作套餐', value: 0 }, 176 | { text: '短期高级套餐', value: 1 }, 177 | ]; 178 | break; 179 | case 2: 180 | (formOptions.formItems[1].additionalProps as BaseSelectProps).options = [ 181 | { text: '基础套餐', value: 0 }, 182 | { text: '商业套餐', value: 1 }, 183 | { text: '贵宾套餐', value: 2 }, 184 | ]; 185 | break; 186 | case 3: 187 | (formOptions.formItems[1].additionalProps as BaseSelectProps).options = [ 188 | { text: '试用套餐', value: 0 }, 189 | ]; 190 | break; 191 | } 192 | //清空之前的选择好让用户重新选择 193 | formModel.value.item_id = null; 194 | } 195 | 196 | //监听 type 属性的更改 197 | watch(() => formModel.value.type, (newType) => { 198 | //更改后重新加载数据 199 | loadPackageSelect(newType); 200 | }); 201 | ``` 202 | 203 | 效果: 204 | 205 | 206 | -------------------------------------------------------------------------------- /docs/guide/form-nest.md: -------------------------------------------------------------------------------- 1 | # 表单嵌套 2 | 3 | 7 | 8 | 在表单中经常会遇到对象与数组这种嵌套的结构,在之前的手动表单写法中,经常需要自己处理,当数据结构特别复杂的时候,这真的是一个非常麻烦的事情。所以,vue-dynamic-form 支持为你处理对象与数组的嵌套表单。 9 | 10 | ## 嵌套对象 11 | 12 | 假设现在有一个对象: 13 | 14 | ```js 15 | { 16 | singleObjectProp: { 17 | name: '', 18 | description: '说明文字文字文字文字文字', 19 | product: 0, 20 | }, 21 | //省略其他属性... 22 | } 23 | ``` 24 | 25 | 需要编辑其中的 singleObjectProp 对象,然后需要保证每个属性有单独的条目进行编辑,你可以 26 | 使用 'object' 条目类型,填写 children ,每个 children 都会自动按照对应的 name 属性名称编辑这个对象的属性。 27 | 28 | ```ts 29 | const formOptions : IDynamicFormOptions = { 30 | formLabelCol: { span: 6 }, 31 | formWrapperCol: { span: 18 }, 32 | formItems: [ 33 | { type: 'base-text', label: '正常条目', name: 'otherProp' }, 34 | { 35 | type: 'object', label: '单个对象条目', name: 'singleObjectProp', 36 | formProps: { 37 | center: false, 38 | }, 39 | children: [ 40 | { type: 'base-text', label: '名称', name: 'name', additionalProps: { placeholder: '请输入名称' } }, 41 | { type: 'base-text', label: '说明', name: 'description', additionalProps: { placeholder: '请输入说明' } }, 42 | { 43 | type: 'base-select', label: '商品', name: 'product', 44 | additionalProps: { 45 | options: [ 46 | { text: '全部', value: 0 }, 47 | { text: '苹果', value: 1 }, 48 | { text: '香蕉', value: 2 }, 49 | { text: '葡萄', value: 3 }, 50 | ] 51 | } as BaseSelectProps 52 | }, 53 | ] 54 | }, 55 | ] 56 | }; 57 | ``` 58 | 59 | 效果: 60 | 61 | 62 | 63 | ## 嵌套对象...对象 64 | 65 | 同理,你也可以在对象中再套对象,这可以无限嵌套(但不建议嵌套过多层,会让用户难以分辨), 66 | 每一级对象属性会按照你组织的结构自动对应到指定的表单项组件上。 67 | 68 | ```ts 69 | const formModel = ref({ 70 | nestedObjectProp: { 71 | name: '', 72 | description: '', 73 | product: { 74 | product_id: 0, 75 | enabled: false, 76 | }, 77 | }, 78 | }); 79 | const formOptions : IDynamicFormOptions = { 80 | formLabelCol: { span: 8 }, 81 | formWrapperCol: { span: 16 }, 82 | formItems: [ 83 | { 84 | type: 'object', label: '对象嵌套对象条目(这是一级对象)', name: 'nestedObjectProp', 85 | formProps: { 86 | center: false, 87 | }, 88 | children: [ 89 | { type: 'base-text', label: '名称', name: 'name', additionalProps: { placeholder: '请输入名称' } }, 90 | { type: 'base-text', label: '说明', name: 'description', additionalProps: { placeholder: '请输入说明' } }, 91 | { 92 | type: 'object', label: '商品信息(这是二级对象)', name: 'product', 93 | formProps: { 94 | center: false, 95 | }, 96 | childrenColProps: { 97 | span: 24 98 | }, 99 | children: [ 100 | { 101 | type: 'base-select', label: '商品信息-商品', name: 'product_id', 102 | additionalProps: { 103 | options: [ 104 | { text: '全部', value: 0 }, 105 | { text: '苹果', value: 1 }, 106 | { text: '香蕉', value: 2 }, 107 | { text: '葡萄', value: 3 }, 108 | ] 109 | } as BaseSelectProps 110 | }, 111 | { 112 | type: 'base-check', label: '商品信息-是否启用', name: 'enabled', 113 | additionalProps: { 114 | text: '是', 115 | } as BaseCheckProps, 116 | }, 117 | ] 118 | }, 119 | ] 120 | }, 121 | ], 122 | formRules: {}, 123 | }; 124 | ``` 125 | 126 | 效果: 127 | 128 | 129 | 130 | ## 嵌套基本数组 131 | 132 | 提交数组数据是表单中经常遇到的事情,因此你可以在表单中定义一个数组类型,只需要使用 `'array-single'` 类型,它会自动为你添加数组的功能,包括添加、删除、上移、下移。 133 | 134 | 注意,children 数组只能提供一个组件,多余的会被忽略。 135 | 136 | 数组类型必须提供一个 newChildrenObject 回调,用于添加按钮添加数据,如果不提供,则没有添加按钮。 137 | 138 | ```vue preview 139 | 148 | 149 | 181 | ``` 182 | 183 | 你可以指定 additionalProps 控制 添加、删除、上移/下移 按钮是否显示: 184 | 185 | ```ts 186 | import { FormArrayGroupProps } from '@imengyu/vue-dynamic-form'; 187 | 188 | { 189 | type: 'array-single', label: '数组单个元素条目', name: 'arrayProp', 190 | additionalProps: { 191 | showAddButton: true, //是否显示添加按钮 192 | showUpDownButton: true, //是否显示上移/下移按钮 193 | showDeleteButton: true, //是否显示删除按钮 194 | } as FormArrayGroupProps, 195 | //省略... 196 | }, 197 | ``` 198 | 199 | ## 嵌套对象数组 200 | 201 | 数组中除了基本类型,也可以编辑一个对象,只需要使用 `'array-object'` 类型。 202 | 203 | children 数组可以提供一个或者多个组件,每个 children 都会自动按照 name 编辑对应对象的属性。 204 | 205 | ```vue preview 206 | 215 | 216 | 263 | ``` 264 | 265 | ## 其他嵌套模式 266 | 267 | ### simple-flat 268 | 269 | 类似 object 但是它的子表单读取的是父级对象的属性而不是当前的,因此 flat 模式经常用于展平对象。 270 | 271 | 你还可以指定子表单的布局模式,把它当作一个容器使用,例如,下面的例子把两个属性横向显示: 272 | 273 | ```vue preview 274 | 283 | 284 | 337 | ``` 338 | 339 | ### group-object 340 | 341 | 同 object,但是会在外层添加一个外壳样式,可以用于分组表单,帮助用户阅读更清晰。 342 | 343 | ### group-flat 344 | 345 | 同 simple-flat,但是会在外层添加一个外壳样式,可以用于分组表单,帮助用户阅读更清晰。 346 | -------------------------------------------------------------------------------- /docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 开始使用 3 | --- 4 | 5 | # 开始使用 6 | 7 | ## 安装 8 | 9 | 安装 vue-dynamic-form 核心 10 | 11 | ```shell 12 | # npm 13 | npm i @imengyu/vue-dynamic-form 14 | 15 | # or yarn 16 | yarn add @imengyu/vue-dynamic-form 17 | ``` 18 | 19 | ## 绑定组件 20 | 21 | 因为 vue-dynamic-form,不与任何UI库耦合,内部只有几个基础组件(用于测试),不带复杂的表单组件, 22 | 所以若要搭建一个生产应用,推荐你使用现有的成熟的组件库,vue-dynamic-form 只作为动态生成器来使用,调用你的组件库。 23 | 24 | 您可以选择安装自己喜欢的UI框架或其他库。推荐: 25 | 26 | * [Ant Design Vue](https://www.antdv.com/docs/vue/getting-started-cn) 27 | * [arco.design Vue](https://arco.design/vue/docs/start) 28 | * [Element plus](https://element-plus.gitee.io/zh-CN/guide/installation.html) 29 | 30 | 或者你也可以调用你自己的组件库,只需要注册即可。 31 | 32 | 接下来你需要绑定表单组件,将其注册,请参考 [👉 绑定组件](./register-controls.md)。 33 | -------------------------------------------------------------------------------- /docs/guide/ingrate-ant-design.md: -------------------------------------------------------------------------------- 1 | # Ant Design Vue 案例 2 | 3 | --- 4 | 5 | 6 | 7 | ```ts-vue 8 | {{ codeAntDesginVue }} 9 | ``` 10 | 11 | 15 | -------------------------------------------------------------------------------- /docs/guide/ingrate-arco-design.md: -------------------------------------------------------------------------------- 1 | # Arco Design Vue 案例 2 | 3 | --- 4 | 5 | 6 | 7 | ```ts-vue 8 | {{ codeArcoDesginVue }} 9 | ``` 10 | 11 | 15 | -------------------------------------------------------------------------------- /docs/guide/ingrate-element.md: -------------------------------------------------------------------------------- 1 | # Element Plus 案例 2 | 3 | --- 4 | 5 | 6 | 7 | ```ts-vue 8 | {{ codeElementPlus }} 9 | ``` 10 | 11 | 15 | -------------------------------------------------------------------------------- /docs/guide/register-controls.md: -------------------------------------------------------------------------------- 1 | # 绑定组件 2 | 3 | vue-dynamic-form 不带复杂的表单组件,不与任何UI库耦合。因此您可以选择安装自己喜欢的UI框架或其他库, 4 | 使用UI库的表单组件,需要将其绑定后即可在动态表单中使用。 5 | 6 | 推荐: 7 | 8 | * [Ant Design Vue](https://www.antdv.com/docs/vue/getting-started-cn) 9 | * [arco.design Vue](https://arco.design/vue/docs/start) 10 | * [Element plus](https://element-plus.gitee.io/zh-CN/guide/installation.html) 11 | 12 | ## 步骤 13 | 14 | 这里以 arco.design Vue 为例,展示如何绑定组件。 15 | 16 | 首先参考 [arco.design Vue](https://arco.design/vue/docs/start) 文档将其安装。 17 | 18 | 然后在 main.js 中注册你需要的组件: 19 | 20 | ```js 21 | import { markRaw } from "vue"; 22 | import { Alert, Checkbox, DatePicker, Form, FormItem, Image, Input, InputNumber, Rate, Switch, Textarea, TimePicker } from "@arco-design/web-vue"; 23 | import { DynamicFormItemRegistry, type IDynamicFormOptions, configDefaultDynamicFormOptions } from "@imengyu/vue-dynamic-form"; 24 | 25 | createApp(App) 26 | .mount('#app') 27 | .$nextTick(() => { 28 | registerAllFormComponents(); 29 | }); 30 | 31 | //这个函数请保证全局只注册一次 32 | function registerAllFormComponents() { 33 | //1. 配置 Form 与 FormItem 34 | // 35 | //当你使用了自定义UI框架,别忘记替换默认的 Form 和 FormItem 为UI框架中的 Form 和 FormItem, 36 | //这样才能让表单工作正常(因为表单渲染、数据校验等等功能,这部分功能由UI框架提供)。 37 | configDefaultDynamicFormOptions({ 38 | internalWidgets: { 39 | Form: { 40 | component: markRaw(Form), 41 | propsMap: { 42 | //每个UI框架的属性名称有点不一样,你需要根据对应的文档重新映射对应的属性名称 43 | //右边是组件对应的属性名称 44 | rules: 'rules', 45 | wrapperCol: 'wrapper-col-props', 46 | labelCol: 'label-col-props', 47 | }, 48 | }, 49 | FormItem: { 50 | component: markRaw(FormItem), 51 | propsMap: { 52 | name: 'field', 53 | }, 54 | }, 55 | }, 56 | }); 57 | 58 | //2. 注册你需要使用的库组件。当然,这里也可以注册自己自定义的组件 59 | //第1个参数是:在表单定义中使用时的名称,请保证不重复。 60 | //第2个参数是:组件的类实例。 61 | //第3个参数是:组件的默认参数, 每一个组件创建时都会继承此默认参数。在表单定义中传入的 additionalProps 优先级比此高,会覆盖默认参数。 62 | //第4个参数是:组件的双向绑定 modelValue 字段名称,这个value用于双向绑定表单的值,当你的组件主 modelValue 名称不一致时, 63 | // 例如有些复选框双向绑定字段名称是checked,可以重新指定。 64 | // 65 | //注册自定义组件请参考下方章节 66 | DynamicFormItemRegistry 67 | .register('text', markRaw(Input), {}, 'modelValue') 68 | .register('password', markRaw(Inpu.tPassword), {}, {}, 'modelValue') 69 | .register('number', markRaw(InputNumber), {}, 'modelValue') 70 | .register('text-area', markRaw(Textarea), {}, 'modelValue') 71 | .register('switch', markRaw(Switch), {}, 'modelValue') 72 | .register('check-box', markRaw(Checkbox), {}, 'modelValue') 73 | .register('date', markRaw(DatePicker), {}, 'pickerValue') 74 | .register('time', markRaw(TimePicker), {}, 'modelValue') 75 | .register('date-time', markRaw(DatePicker), { showTime: true }, 'pickerValue'); 76 | } 77 | ``` 78 | 79 | 然后你就可以在表单中使用刚刚注册的组件了, 80 | 81 | ```vue preview 82 | 93 | 94 | 170 | ``` 171 | 172 | ## 案例 173 | 174 | 作者为你写了下面几个热门库的案例,你也可以复制到你自己的项目中使用, 175 | 对于其他组件库,如果有对应的表单组件,也可以按上方步骤自己注册使用。 176 | 177 | ::: warning 178 | 有些库可能不支持某些字段,例如Element plus不支持表单的内容宽度占比调整。 179 | ::: 180 | 181 | * [Ant Design Vue](./ingrate-ant-design.md) 182 | * [arco.design Vue](./ingrate-arco-design.md) 183 | * [Element plus](./ingrate-element.md) 184 | 185 | ## 注册自定义组件 186 | 187 | 注册自定义组件与注册UI库的组件一致,你只需使用 DynamicFormItemRegistry 注册即可使用: 188 | 189 | ```js 190 | import MyFormComponent from './MyFormComponent.vue'; 191 | 192 | DynamicFormItemRegistry.register('my-cmponent-name', markRaw(MyFormComponent), {}, 'modelValue'); 193 | ``` 194 | 195 | 注意,在写自定义组件时请处理与表单/动态表单的数据关系。具体请参考 [自定义组件](./custom-control.md)。 196 | -------------------------------------------------------------------------------- /docs/guide/tab.md: -------------------------------------------------------------------------------- 1 | # 表单标签页 2 | 3 | 表单支持使用Tab组件分页、分组,Tab分页适用于表单数据较多且有相似项目可以分组时。支持分组多个页面,方便用户操作。 4 | 5 | 要使用标签页组件,需要配置Tab组件和TabPage组件, 这两个组件没有内置,需要使用第三方的组件。 6 | 7 | 以 ArcoDesign 为例,注册其Tab组件。 8 | 9 | ```ts 10 | import { Tabs, TabPane } from "@arco-design/web-vue"; 11 | 12 | configDefaultDynamicFormOptions({ 13 | internalWidgets: { 14 | //...省略 15 | Tab: { 16 | component: markRaw(Tabs), 17 | propsMap: { 18 | activeKey: 'activeKey', 19 | defaultActiveKey: 'defaultActiveKey', 20 | }, 21 | }, 22 | TabPage: { 23 | component: markRaw(TabPane), 24 | propsMap: { 25 | title: 'title', 26 | disabled: 'disabled', 27 | }, 28 | }, 29 | }, 30 | formLabelCol: { span: 8 }, 31 | formWrapperCol: { span: 16 }, 32 | //...省略 33 | formItems: [], 34 | } as IDynamicFormOptions); 35 | ``` 36 | 37 | 注册后,即可在表单中使用tab组件,tab组件的对象属性嵌套类似于 [simple-flat](./form-nest.md#simple-flat),每个标签页访问同级对象。 38 | 39 | * 使用 `custom-tab` 声明一个标签组件。 40 | * 使用 `custom-tab-page` 声明一个标签页,标签页必须处于标签组件的子级。 41 | 42 | ```vue preview 43 | 52 | 53 | 136 | ``` -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: vue-dynamic-form 一个用数据驱动的 Vue3 动态表单组件。 3 | layout: home 4 | 5 | hero: 6 | name: vue-dynamic-form 7 | text: Vue 动态表单 8 | tagline: 一个用数据驱动的 Vue3 动态表单组件, 加快开发的一个小工具 9 | actions: 10 | - theme: brand 11 | text: 立即开始 12 | link: /guide/about 13 | - theme: alt 14 | text: 在 GitHub 上查看 15 | link: https://github.com/imengyu/vue-dynamic-form 16 | features: 17 | - icon: 💎 18 | title: 简单易用 19 | details: 用数据来写表单,就像搭积木一样简单 20 | - icon: 🛠️ 21 | title: 可拓展性强 22 | details: 可以与你喜欢的UI库搭配,AntDesgin、Element... 23 | - icon: ❤ 24 | title: 可视化表单生成器 25 | details: 加快开发速度,解放双手! 26 | --- 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /docs/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vueJsx from '@vitejs/plugin-vue-jsx' 3 | import { resolve } from 'path' 4 | export default defineConfig({ 5 | plugins: [ 6 | vueJsx() 7 | ], 8 | resolve: { 9 | alias: { 10 | '@imengyu/vue-dynamic-form': resolve(__dirname, '../library/main') 11 | }, 12 | }, 13 | }) -------------------------------------------------------------------------------- /library/DynamicForm.ts: -------------------------------------------------------------------------------- 1 | import { ColProps } from "./DynamicFormBasicControls/Layout/Col"; 2 | import { DynamicFormItemRegistryItem } from "./DynamicFormItemRenderer/DynamicFormItemRegistry"; 3 | import { Rule } from 'async-validator'; 4 | import { Form, FormItem, RowProps } from "./DynamicFormBasicControls"; 5 | import { Slot, VNode, h, markRaw } from "vue"; 6 | 7 | export type IDynamicFormObject = Record; 8 | /** 9 | * 表单动态属性定义 10 | */ 11 | export type IDynamicFormItemCallback = { 12 | /** 13 | * 预留,暂未使用 14 | */ 15 | type?: string, 16 | /** 17 | * @param model 当前表单条目的值 18 | * @param rawModel 整个 form 的值 (最常用,当两个关联组件距离较远时,可以从顶层的 rawModel 里获取) 19 | * @param parentModel 父表单元素的值 (上一级的值,只在列表场景的使用,例如列表某个元素的父级就是整个 item) 20 | * @param item 当前表单条目信息 21 | * @param formRules 当前条目校验数据 22 | */ 23 | callback: (model: unknown, rawModel: unknown, parentModel: unknown, item: IDynamicFormItem, formRules?: Record) => T; 24 | } 25 | 26 | export type IDynamicFormItemCallbackAdditionalProps = { [P in keyof T]?: T[P]|IDynamicFormItemCallback } 27 | 28 | export const MESSAGE_RELOAD = 'reload'; 29 | 30 | export interface IDynamicFormItem { 31 | /** 32 | * 当前表单类型 33 | */ 34 | type: string; 35 | /** 36 | * 是否隐藏当前表单项 37 | */ 38 | hidden?: boolean|IDynamicFormItemCallback; 39 | /** 40 | * 是否禁用当前表单项 41 | */ 42 | disabled?: boolean|IDynamicFormItemCallback; 43 | /** 44 | * 附加组件属性。支持动态回调(只支持第一级传入回调)。 45 | */ 46 | additionalProps?: Record>|unknown; 47 | /** 48 | * 附加组件插槽。 49 | * 50 | * 组件还内置了两个插槽: 51 | * * dynamicFormPrefix 表单项内容前缀 52 | * * dynamicFormSuffix 表单项内容后缀 53 | */ 54 | additionalSlot?: Record; 55 | /** 56 | * 附加组件事件绑定 57 | */ 58 | // eslint-disable-next-line @typescript-eslint/ban-types 59 | additionalEvents?: Record; 60 | /** 61 | * 附加组件属性。此属性直接应用到目标渲染组件上,没有联动回调。 62 | */ 63 | additionalDirectProps?: unknown; 64 | /** 65 | * 加载时的钩子函数 66 | * @param nowValue 当前组件的实例 67 | * @param getComponentRef 当前组件的实例 68 | * @returns 69 | */ 70 | mounted?: (nowValue: unknown, getComponentRef: () => unknown) => void; 71 | /** 72 | * 当前表单组件卸载之前的钩子函数 73 | * @param componentRef 当前组件的实例 74 | * @returns 75 | */ 76 | beforeUnmount?: (componentRef: () => unknown) => void; 77 | /** 78 | * 监听当前表单项数据更改 79 | * @param oldValue 旧数据 80 | * @param newValue 新数据 81 | * @param getComponentRef 当前组件的实例 82 | * @returns 83 | */ 84 | watch?: (oldValue: unknown, newValue: unknown, getComponentRef: () => unknown) => void, 85 | /** 86 | * 监听从外部或者其他表单发送过来的消息事件 87 | * @param messageName 消息名称 88 | * @param data 消息数据 89 | * @param getComponentRef 当前组件的实例 90 | * @returns 91 | */ 92 | message?: (messageName: string, data: unknown, getComponentRef: () => unknown) => void, 93 | /** 94 | * 附加 FormItem 组件属性 95 | */ 96 | formProps?: unknown; 97 | /** 98 | * 表单label栅格宽度。如果使用自定义Form,请同时设置属性名映射。 99 | */ 100 | formLabelCol?: { span: number, offset?: number }|string|number, 101 | /** 102 | * 表单组件栅格宽度。如果使用自定义Form,请同时设置属性名映射。 103 | */ 104 | formWrapperCol?: { span: number, offset?: number }|string|number, 105 | /** 106 | * 当前表单项名称。 107 | */ 108 | name: string; 109 | /** 110 | * 当前表单说明文字。支持动态回调。 111 | */ 112 | label?: string|IDynamicFormItemCallback; 113 | /** 114 | * 子条目。仅在 object 或者其他容器条目中有效。 115 | */ 116 | children?: IDynamicFormItem[]; 117 | /** 118 | * 当子对象为数组时,可设置这个自定义回调。用于添加按钮新建一个对象,如果这个函数为空,则没有添加按钮。 119 | */ 120 | newChildrenObject?: (arrayNow: unknown[]) => unknown; 121 | /** 122 | * 当子对象为数组时,可设置这个自定义回调。删除按钮回调,可选,不提供时默认操作为将 item 从 array 中移除。 123 | */ 124 | deleteChildrenCallback?: (arrayNow: unknown[], deleteObject: unknown) => unknown; 125 | /** 126 | * 子条目的 Col 配置属性(应用到当前条目的所有子条目上)。仅在 object 或者其他容器条目中有效。 127 | */ 128 | childrenColProps?: ColProps, 129 | /** 130 | * 当前条目的 Col 配置属性(应用到当前条目上)。仅在 object 或者其他容器条目中有效。 131 | */ 132 | colProps?: ColProps, 133 | /** 134 | * 当前条目的 Row 配置属性(应用到当前条目上)。仅在 object 或者其他容器条目中有效。 135 | */ 136 | rowProps?: RowProps, 137 | /** 138 | * 当显示嵌套的表单对象条目时是否在前部显示缩进。默认是 139 | */ 140 | nestObjectMargin?: boolean, 141 | } 142 | 143 | //DynamicForm 实例方法接口 144 | export interface IDynamicFormRef { 145 | /** 146 | * 获取表单组件的 Ref 147 | * @returns 148 | */ 149 | getFormRef: () => T; 150 | /** 151 | * 获取指定表单项组件的 Ref 152 | * @returns 153 | */ 154 | getFormItemControlRef: (key: string) => T; 155 | /** 156 | * 触发提交。同 getFormRef().submit() 。 157 | * @returns 158 | */ 159 | submit: () => void; 160 | /** 161 | * 外部修改指定单个 field 的数据 162 | * @param path 路径 163 | * @param value 值 164 | * @returns 165 | */ 166 | setValueByPath: (path: string, value: unknown) => void, 167 | /** 168 | * 外部获取指定单个 field 的数据 169 | * @param path 路径 170 | * @returns 171 | */ 172 | getValueByPath: (path: string) => unknown, 173 | /** 174 | * 向所有或者指定的子组件分发消息事件。 175 | * @param messageName 消息名称。 176 | * @param data 可选参数。 177 | * @param receiveFilter 可选名称筛选正则,此正则通过名称的子组件会接受事件,其他则不会。 178 | * @returns 179 | */ 180 | dispatchMessage: (messageName: string, data?: unknown, receiveFilter?: RegExp) => void; 181 | /** 182 | * 向所有子组件分发重新加载消息事件。 183 | * @returns 184 | */ 185 | dispatchReload: () => void; 186 | /** 187 | * 获取当前表单中可见的所有字段名 188 | */ 189 | getVisibleFormNames: () => string[]; 190 | } 191 | 192 | export const defaultDynamicFormInternalWidgets = { 193 | Form: { 194 | component: markRaw(Form), 195 | propsMap: {}, 196 | }, 197 | FormItem: { 198 | component: markRaw(FormItem), 199 | propsMap: {}, 200 | } 201 | } as IDynamicFormInternalWidgets 202 | 203 | export interface IDynamicFormInternalWidgets { 204 | Form?: { 205 | /** 206 | * 组件实例 207 | */ 208 | component: unknown, 209 | /** 210 | * 属性的名称修改 211 | */ 212 | propsMap: { 213 | rules?: string, 214 | model?: string, 215 | labelCol?: string, 216 | labelWidth?: string, 217 | wrapperCol?: string, 218 | onFinish?: string, 219 | onFinishFailed?: string, 220 | onSubmit?: string, 221 | }, 222 | }, 223 | FormItem?: { 224 | /** 225 | * 组件实例 226 | */ 227 | component: unknown, 228 | /** 229 | * 属性的名称修改 230 | */ 231 | propsMap: { 232 | name?: string, 233 | label?: string, 234 | labelCol?: string, 235 | wrapperCol?: string, 236 | }, 237 | }, 238 | Tab?: { 239 | /** 240 | * 组件实例 241 | */ 242 | component: unknown, 243 | /** 244 | * 属性的名称修改 245 | */ 246 | propsMap: { 247 | activeKey?: string, 248 | defaultActiveKey?: string, 249 | }, 250 | }, 251 | TabPage?: { 252 | /** 253 | * 组件实例 254 | */ 255 | component: unknown, 256 | /** 257 | * 属性的名称修改 258 | */ 259 | propsMap: { 260 | key?: string, 261 | title?: string, 262 | disabled?: string, 263 | }, 264 | }, 265 | } 266 | 267 | export interface IDynamicFormOptions { 268 | /** 269 | * 表单条目数据 270 | */ 271 | formItems: IDynamicFormItem[]; 272 | /** 273 | * 表单的校验规则 274 | */ 275 | formRules?: Record; 276 | /** 277 | * 表单label栅格宽度。如果使用自定义Form,请同时设置属性名映射。 278 | */ 279 | formLabelCol?: { span: number, offset?: number }|string|number, 280 | /** 281 | * 表单组件栅格宽度。如果使用自定义Form,请同时设置属性名映射。 282 | */ 283 | formWrapperCol?: { span: number, offset?: number }|string|number, 284 | /** 285 | * 表单label宽度。部分UI库的Form组件可能不支持这个属性。 286 | */ 287 | formLabelWidth?: number|string, 288 | /** 289 | * 表单组件附加属性 290 | */ 291 | formAdditionaProps?: Record; 292 | /** 293 | * 表单组件附加事件绑定 294 | */ 295 | // eslint-disable-next-line @typescript-eslint/ban-types 296 | formAdditionalEvents?: Record; 297 | 298 | /** 299 | * 自定义重写表单控件。你可以重写内置控件,在这个表单中会以此重写列表为先查找表单组件。 300 | */ 301 | widgets?: Record, 302 | /** 303 | * 自定义重写内置表单控件 Form FormItem。你可以使用其他组件库的组件例如 elemnent-ui 或者 ant-desgin-vue 304 | */ 305 | internalWidgets?: IDynamicFormInternalWidgets, 306 | /** 307 | * 表单是否禁用。默认否 308 | */ 309 | disabled?: boolean, 310 | /** 311 | * 当表单中无可用编辑条目时,显示的提示,为空则不显示提示。默认为空 312 | */ 313 | emptyText?: string, 314 | /** 315 | * 当显示嵌套的表单对象条目时是否在前部显示缩进。默认是 316 | */ 317 | nestObjectMargin?: boolean, 318 | } 319 | 320 | /** 321 | * 默认的动态表单属性 322 | */ 323 | export let defaultDynamicFormOptions = {} as IDynamicFormOptions; 324 | 325 | /** 326 | * 配置默认的动态表单属性,配置后将会对所有动态表单生效。 327 | * @param options 参数 328 | */ 329 | export function configDefaultDynamicFormOptions(options: IDynamicFormOptions) { 330 | defaultDynamicFormOptions = { 331 | ...defaultDynamicFormOptions, 332 | ...options, 333 | }; 334 | } 335 | 336 | export interface FormCustomLayoutProps { 337 | render: (item: IDynamicFormItem, defaultSlot: Slot) => VNode[]; 338 | } 339 | export interface IDynamicFormTabProps { 340 | defaultActiveKey?: string, 341 | tabProps?: any, 342 | renderTab?: (item: IDynamicFormItem, props: { 343 | activeKey: string, 344 | defaultActiveKey: string|undefined, 345 | 'onUpdate:activeKey': (newValue: string) => void, 346 | }, defaultSlot: Slot) => VNode[]; 347 | } 348 | export interface IDynamicFormTabPageProps { 349 | renderTabPage?: (item: IDynamicFormItem, props: { 350 | key: string, 351 | title: string, 352 | }, defaultSlot: Slot) => VNode[]; 353 | } 354 | 355 | export function renderTextDefaultSlot(text: string) { 356 | return { 357 | default: () => [ h('span', text) ], 358 | } 359 | } -------------------------------------------------------------------------------- /library/DynamicForm.vue: -------------------------------------------------------------------------------- 1 | 229 | 230 | -------------------------------------------------------------------------------- /library/DynamicFormBasicControls/Form.tsx: -------------------------------------------------------------------------------- 1 | import Schema, { Rules, Rule } from 'async-validator'; 2 | import { defineComponent, PropType, toRefs, provide, ref, onMounted, renderSlot } from 'vue'; 3 | import { FormContext, FormItemInternalContext, ValidTrigger } from './FormContext'; 4 | import ObjectUtils from './Utils/ObjectUtils'; 5 | import scrollIntoView from 'scroll-into-view-if-needed' 6 | 7 | /** 8 | * Form 实例接口 9 | */ 10 | export interface Form { 11 | clearValidate:(name?: string|string[]) => void; 12 | resetFields: (name?: string|string[]) => void; 13 | scrollToField: (name: string) => void; 14 | validate: (name?: string|string[]) => void; 15 | submit: (valid?: boolean) => void; 16 | } 17 | 18 | export interface FormProps { 19 | /** 20 | * 表单数据对象 21 | */ 22 | model?: Record; 23 | /** 24 | * 表单验证规则 25 | */ 26 | rules?: Record; 27 | /** 28 | * 配置 Form.Item 的 colon 的默认值 29 | */ 30 | colon?: boolean; 31 | /** 32 | * 隐藏所有表单项的必选标记 33 | */ 34 | hideRequiredMark?: boolean; 35 | /** 36 | * 统一设置左侧文本对齐 37 | */ 38 | labelAlign?: 'left'|'center'|'right'; 39 | /** 40 | * 统一设置左侧文本的flex占比,默认是2 41 | */ 42 | labelCol?: number; 43 | /** 44 | * 统一设置输入框的flex占比 45 | */ 46 | wrapperCol?: number; 47 | /** 48 | * 统一设置字段校验规则 49 | * * onBlur 文本框失去焦点时校验 50 | * * onValueChange 数值更改时校验 51 | * * onSubmit 提交时校验(默认) 52 | */ 53 | validateTrigger?: ValidTrigger; 54 | /** 55 | * 是否显左边标题,默认是 56 | */ 57 | showLabel?: boolean; 58 | /** 59 | * 表单的名称 60 | */ 61 | name?: string; 62 | } 63 | 64 | /** 65 | * 表单条目组件。 66 | */ 67 | export default defineComponent({ 68 | name: 'Form', 69 | props: { 70 | model: { 71 | type: Object, 72 | default: null, 73 | }, 74 | rules: { 75 | type: Object, 76 | default: null, 77 | }, 78 | hideRequiredMark: { 79 | type: Boolean, 80 | default: false, 81 | }, 82 | colon: { 83 | type: Boolean, 84 | default: true, 85 | }, 86 | labelAlign: { 87 | type: String as PropType<'start' | 'end' | 'left' | 'right' | 'center' | 'justify' | 'match-parent'>, 88 | default: "end", 89 | }, 90 | labelWidth: { 91 | type: [String,Number], 92 | default: undefined, 93 | }, 94 | labelCol: { 95 | type: Object, 96 | default: null 97 | }, 98 | wrapperCol: { 99 | type: Object, 100 | default: null 101 | }, 102 | validateTrigger: { 103 | type: String as PropType, 104 | default: 'onSubmit', 105 | }, 106 | validateOnRuleChange: { 107 | type: Boolean, 108 | default: false, 109 | }, 110 | showLabel: { 111 | type: Boolean, 112 | default: true, 113 | }, 114 | name: { 115 | type: String, 116 | default: '', 117 | }, 118 | }, 119 | emits: [ 120 | /** 121 | * 提交表单且数据验证成功后回调事件 122 | */ 123 | 'finish', 124 | /** 125 | * 提交表单且数据验证失败后回调事件 126 | */ 127 | 'finishFailed', 128 | /** 129 | * 数据验证成功后回调事件 130 | */ 131 | 'submit', 132 | ], 133 | setup(props, ctx) { 134 | const { slots } = ctx; 135 | const { 136 | labelCol, wrapperCol, colon, labelAlign, labelWidth, name, rules, 137 | validateTrigger, model, showLabel, hideRequiredMark, 138 | } = toRefs(props); 139 | 140 | const intitalModel = ref|null>(null); 141 | const formItems = ref(new Map()); 142 | 143 | function accessFormModel(keyName: string, isSet: boolean, setValue: unknown) : unknown { 144 | const keys = keyName.split('.'); 145 | let ret : unknown = undefined; 146 | let obj = model.value as Record; 147 | let keyIndex = 0; 148 | let key = keys[keyIndex]; 149 | while (obj) { 150 | const leftIndex = key.indexOf('['); 151 | if (leftIndex > 0 && key.endsWith(']')) { 152 | const arr = obj[key.substring(0, leftIndex)] as Record[]; 153 | const index = parseInt(key.substring(leftIndex + 1, key.length - 1)) 154 | obj = arr[index]; 155 | if (keyIndex >= keys.length - 1) { 156 | ret = obj; 157 | if (isSet) arr[index] = setValue as Record; 158 | } 159 | } else { 160 | const newObj = obj[key] as Record; 161 | if (keyIndex >= keys.length - 1) { 162 | ret = newObj; 163 | if (isSet) 164 | obj[key] = setValue as Record; 165 | } 166 | obj = newObj; 167 | } 168 | if (keyIndex < keys.length - 1) 169 | key = keys[++keyIndex]; 170 | else 171 | break; 172 | } 173 | return ret; 174 | } 175 | 176 | //Context 177 | const formContext = { 178 | onFieldBlur: (item: FormItemInternalContext) => { 179 | //validate 180 | if (item.getValidateTrigger() === 'blur') 181 | validate(item.getFieldName()); 182 | }, 183 | onFieldChange: (item: FormItemInternalContext, newValue: unknown) => { 184 | accessFormModel(item.getFieldName(), true, newValue) 185 | //validate 186 | if (item.getValidateTrigger() === 'change') 187 | validate(item.getFieldName()); 188 | }, 189 | addFormItemField: (item: FormItemInternalContext) => { 190 | formItems.value.set(item.getFieldName(), item); 191 | return formItems.value.size; 192 | }, 193 | removeFormItemField: (item: FormItemInternalContext) => { 194 | formItems.value.delete(item.getFieldName()); 195 | }, 196 | getItemValue: (item: FormItemInternalContext) => { 197 | return accessFormModel(item.getFieldName(), false, undefined); 198 | }, 199 | getItemRequieed: (item: FormItemInternalContext) => { 200 | return checkRuleRequired(item.getFieldName()); 201 | }, 202 | validateTrigger, 203 | hideRequiredMark, 204 | colon, 205 | labelAlign, 206 | labelCol, 207 | labelWidth, 208 | wrapperCol, 209 | showLabel, 210 | } as FormContext; 211 | 212 | function checkRuleRequired(name: string) { 213 | const rule = rules.value[name]; 214 | if (rule instanceof Array) 215 | return rule.find((r) => r.required === true) !== undefined; 216 | else if (rule) 217 | return rule.required === true; 218 | } 219 | 220 | onMounted(() => { 221 | //Save intital model 222 | intitalModel.value = model.value ? 223 | ObjectUtils.clone(model.value) as Record : {}; 224 | }); 225 | 226 | provide('formContext', formContext); 227 | 228 | //Clear valid error state 229 | function clearValidate(name?: string|string[]) { 230 | if (name instanceof Array) { 231 | name.forEach(k => resetFields(k)); 232 | return; 233 | } 234 | 235 | if (name) { 236 | const item = formItems.value.get(name); 237 | if (item) 238 | item.setErrorState(null); 239 | } else { 240 | for (const v of formItems.value) 241 | v[1].setErrorState(null); 242 | } 243 | } 244 | //Reset model to intital state 245 | function resetFields(name?: string|string[]) { 246 | if (name) { 247 | if (name instanceof Array) { 248 | name.forEach(k => resetFields(k)); 249 | return; 250 | } 251 | model.value[name] = intitalModel.value?.[name] || null; 252 | } else { 253 | for (const v of formItems.value) 254 | model.value[v[0]] = intitalModel.value?.[v[0]] || null; 255 | } 256 | } 257 | //Scroll to field 258 | function scrollToField(name: string) { 259 | const item = formItems.value.get(name); 260 | if (item) { 261 | const node = document.getElementById(item.getUniqueId()); 262 | if (node) 263 | scrollIntoView(node); 264 | } 265 | } 266 | //Valid 267 | function validate(name?: string|string[]) { 268 | const filteredRules = {} as Record; 269 | 270 | clearValidate(); 271 | 272 | //筛选需要验证的字段 273 | formItems.value.forEach((_, key) => { 274 | const rule = rules.value ? rules.value[key] : undefined; 275 | if (rule) { 276 | if (typeof name === 'string') { 277 | if (name === key) filteredRules[key] = rule; 278 | } else if (typeof name === 'object') { 279 | if (name.indexOf(key) >= 0) filteredRules[key] = rule; 280 | } else 281 | filteredRules[key] = rule; 282 | } 283 | }); 284 | 285 | //获取当前参数 286 | const nowValues = model.value; 287 | 288 | //开始验证 289 | return new Promise((resolve, reject) => { 290 | const validator = new Schema(filteredRules as Rules); 291 | validator.validate(nowValues, {}, (errors) => { 292 | if (errors) { 293 | //验证失败,把错误字段显示 294 | for (const key in errors) { 295 | if (Object.prototype.hasOwnProperty.call(errors, key)) { 296 | const err = errors[key]; 297 | const k = formItems.value.get(err.field as string); 298 | if (k) 299 | k.setErrorState(err.message || `Valid ${err.field} failed!`); 300 | } 301 | } 302 | reject(errors); 303 | } else { 304 | //验证成功,去除之前的验证错误信息 305 | clearValidate(); 306 | resolve(); 307 | } 308 | }); 309 | }); 310 | } 311 | //Submit form 312 | function submit(valid = true) { 313 | if (valid) { 314 | //验证 315 | validate().then(() => { 316 | ctx.emit('finish'); 317 | ctx.emit('submit', model.value); 318 | }).catch((e) => { 319 | console.warn('submit validate failed: ', e); 320 | ctx.emit('finishFailed', e); 321 | }); 322 | } else { 323 | //提交 324 | ctx.emit('submit', model.value); 325 | } 326 | } 327 | 328 | ctx.expose({ 329 | validate, 330 | scrollToField, 331 | resetFields, 332 | clearValidate, 333 | submit, 334 | }); 335 | 336 | return () => { 337 | return ( 338 |
{ 342 | e.preventDefault(); 343 | submit(); 344 | }} 345 | >{ renderSlot(slots, 'default') }
346 | ); 347 | }; 348 | }, 349 | }); -------------------------------------------------------------------------------- /library/DynamicFormBasicControls/FormContext.tsx: -------------------------------------------------------------------------------- 1 | import { InjectionKey, inject, provide, Ref } from "vue"; 2 | 3 | export type ValidTrigger = "blur" | "change" | "submit"; 4 | 5 | export type FormItemContext = { 6 | getFieldName: () => string, 7 | onFieldBlur: () => void; 8 | onFieldChange: (newValue: unknown) => void; 9 | clearValidate: () => void; 10 | }; 11 | export type FormItemInternalContext = { 12 | getFieldName: () => string, 13 | getValidateTrigger: () => ValidTrigger; 14 | getUniqueId: () => string, 15 | setErrorState: (errorMessage: string|null) => void; 16 | }; 17 | 18 | export type FormContext = { 19 | onFieldBlur: (item: FormItemInternalContext) => void; 20 | onFieldChange: (item: FormItemInternalContext, newValue: unknown) => void; 21 | clearValidate: (item: FormItemInternalContext) => void; 22 | addFormItemField: (item: FormItemInternalContext) => number; 23 | removeFormItemField: (item: FormItemInternalContext) => void; 24 | //form props 25 | validateTrigger: Ref; 26 | hideRequiredMark: Ref; 27 | colon: Ref; 28 | labelWidth: Ref; 29 | labelAlign: Ref; 30 | labelCol: Ref>; 31 | wrapperCol: Ref>; 32 | showLabel: Ref; 33 | name: Ref; 34 | getItemValue: (item: FormItemInternalContext) => unknown; 35 | getItemRequieed: (item: FormItemInternalContext) => boolean; 36 | }; 37 | 38 | export const FormItemContextContextKey: InjectionKey = Symbol('ContextProps'); 39 | 40 | export function useInjectFormItemContext() : FormItemContext { 41 | const context = inject(FormItemContextContextKey); 42 | provide(FormItemContextContextKey, {} as FormItemContext); 43 | return context as FormItemContext; 44 | } 45 | -------------------------------------------------------------------------------- /library/DynamicFormBasicControls/FormItem.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, PropType, ref, inject, toRefs, provide, onBeforeUnmount, renderSlot } from 'vue'; 2 | import { FormContext, FormItemContextContextKey, FormItemInternalContext, ValidTrigger } from './FormContext'; 3 | import Col from './Layout/Col'; 4 | import Row from './Layout/Row'; 5 | 6 | export interface FormItemProps { 7 | /** 8 | * 输入框左侧文本 9 | */ 10 | label?: string; 11 | /** 12 | * 名称,作为提交表单时的标识符 13 | */ 14 | name?: string; 15 | /** 16 | * 是否禁用输入框 17 | */ 18 | disabled?: boolean; 19 | /** 20 | * 是否内容垂直居中 21 | */ 22 | center?: boolean; 23 | /** 24 | * 是否在 label 后面添加冒号 25 | */ 26 | colon?: boolean; 27 | /** 28 | * 是否必填 29 | */ 30 | required?: boolean; 31 | /** 32 | * 是否显示表单必填星号 33 | */ 34 | showRequiredBadge?: boolean; 35 | /** 36 | * 左侧文本对齐 37 | */ 38 | labelAlign?: 'left'|'center'|'right'; 39 | /** 40 | * 左侧文本的flex占比,默认是2 41 | */ 42 | labelCol?: { span?: number, offset?: number }; 43 | /** 44 | * 输入框的flex占比 45 | */ 46 | wrapperCol?: { span?: number, offset?: number }; 47 | /** 48 | * 左侧文本的样式 49 | */ 50 | labelStyle?: unknown; 51 | /** 52 | * 左侧文本的颜色 53 | */ 54 | labelColor?: string; 55 | /** 56 | * 左侧文本的禁用颜色 57 | */ 58 | labelDisableColor?: string; 59 | /** 60 | * 设置字段校验的时机 61 | * * onBlur 文本框失去焦点时校验 62 | * * onValueChange 数值更改时校验 63 | * * onSubmit 提交时校验(默认) 64 | */ 65 | validateTrigger?: ValidTrigger; 66 | /** 67 | * 是否显左边标题,默认是 68 | */ 69 | showLabel?: boolean; 70 | /** 71 | * 是否去掉底部编辑,默认否 72 | */ 73 | noBottomMargin?: boolean; 74 | } 75 | 76 | /** 77 | * 表单条目组件。 78 | */ 79 | export default defineComponent({ 80 | name: 'FormItem', 81 | props: { 82 | label: { 83 | type: String, 84 | default: "", 85 | }, 86 | name: { 87 | type: String, 88 | default: "", 89 | }, 90 | disabled: { 91 | type: Boolean, 92 | default: false, 93 | }, 94 | center: { 95 | type: Boolean, 96 | default: false, 97 | }, 98 | colon: { 99 | type: Boolean, 100 | default: true, 101 | }, 102 | required: { 103 | type: Boolean, 104 | default: false, 105 | }, 106 | showRequiredBadge: { 107 | type: Boolean, 108 | default: true, 109 | }, 110 | labelAlign: { 111 | type: String as PropType<'start' | 'end' | 'left' | 'right' | 'center' | 'justify' | 'match-parent'>, 112 | default: "", 113 | }, 114 | labelWidth: { 115 | type: [String,Number], 116 | default: undefined, 117 | }, 118 | labelCol: { 119 | type: Object, 120 | default: null 121 | }, 122 | wrapperCol: { 123 | type: Object, 124 | default: null 125 | }, 126 | labelStyle: { 127 | type: Object, 128 | default: null 129 | }, 130 | labelColor: { 131 | type: String, 132 | default: "", 133 | }, 134 | labelDisableColor: { 135 | type: String, 136 | default: "", 137 | }, 138 | validateTrigger: { 139 | type: String as PropType, 140 | default: 'onSubmit', 141 | }, 142 | showLabel: { 143 | type: Boolean, 144 | default: true, 145 | }, 146 | noBottomMargin: { 147 | type: Boolean, 148 | default: false, 149 | }, 150 | }, 151 | components: { 152 | Col, 153 | Row, 154 | }, 155 | setup(props, ctx) { 156 | const formContextProps = inject('formContext'); 157 | const { 158 | name, 159 | showLabel, label, labelCol, wrapperCol, disabled, required, colon, center, 160 | labelDisableColor, labelColor, labelAlign, labelWidth, labelStyle, showRequiredBadge, 161 | validateTrigger, 162 | noBottomMargin, 163 | } = toRefs(props); 164 | 165 | const error = ref(null); 166 | 167 | //Context for parent 168 | const formItemInternalContext = { 169 | getValidateTrigger: () => validateTrigger.value || formContextProps?.validateTrigger.value || 'submit', 170 | getFieldName: () => name.value, 171 | setErrorState(errorMessage) { error.value = errorMessage; }, 172 | getUniqueId, 173 | } as FormItemInternalContext; 174 | 175 | //Context for custom children 176 | provide(FormItemContextContextKey, { 177 | getFieldName: () => name.value, 178 | onFieldBlur: () => { formContextProps?.onFieldBlur(formItemInternalContext); }, 179 | onFieldChange: (newValue: unknown) => { formContextProps?.onFieldChange(formItemInternalContext, newValue); }, 180 | clearValidate: () => { formContextProps?.clearValidate(formItemInternalContext); }, 181 | }); 182 | 183 | //Add ref in form 184 | const addNumber = formContextProps?.addFormItemField(formItemInternalContext); 185 | const uniqueId = (formContextProps?.name || 'form') + 'Item' + (name.value || `unknowProperity${addNumber}`); 186 | 187 | onBeforeUnmount(() => { 188 | formContextProps?.removeFormItemField(formItemInternalContext); 189 | }) 190 | 191 | ctx.expose({ 192 | error, 193 | }); 194 | 195 | function getUniqueId() { 196 | return uniqueId; 197 | } 198 | 199 | return () => { 200 | const labelColValue = labelCol.value?.span ?? formContextProps?.labelCol.value?.span; 201 | return ( 202 | 206 | { 207 | (labelColValue > 0 && showLabel.value && formContextProps?.showLabel.value && label) ? 208 | 214 | 229 | : '' 230 | } 231 | 236 |
237 | { renderSlot(ctx.slots, 'default') } 238 | { 239 | error.value ? 240 |
{ error.value }
241 | : '' 242 | } 243 |
244 | 245 |
246 | ); 247 | }; 248 | }, 249 | }); -------------------------------------------------------------------------------- /library/DynamicFormBasicControls/Layout/Col.tsx: -------------------------------------------------------------------------------- 1 | import { computed, defineComponent, inject, renderSlot, toRefs } from 'vue'; 2 | 3 | export interface ColProps { 4 | /** 5 | * 列元素偏移距离 6 | */ 7 | offset?: number; 8 | /** 9 | * 列元素宽度 10 | */ 11 | span?: number; 12 | 13 | style?: object; 14 | class?: string, 15 | } 16 | 17 | /** 18 | * 24列栅格列组件。 19 | * 20 | * 提供了 24列栅格,通过在 Col 上添加 span 属性设置列所占的宽度百分比。 21 | * 22 | * 此外,添加 offset 属性可以设置列的偏移宽度,计算方式与 span 相同。 23 | */ 24 | export default defineComponent({ 25 | name: 'Col', 26 | props: { 27 | /** 28 | * 列元素偏移距离 29 | */ 30 | offset: { 31 | type: Number, 32 | default: 0 33 | }, 34 | /** 35 | * 列元素宽度 36 | */ 37 | span: { 38 | type: Number, 39 | default: 0 40 | }, 41 | class: { 42 | type: String, 43 | default: undefined, 44 | }, 45 | style: { 46 | type: Object, 47 | default: undefined, 48 | } 49 | }, 50 | setup(props, ctx) { 51 | const { span, offset } = toRefs(props); 52 | 53 | const GRID_SIZE = inject('DynamicFormLayoyGridSize', 24); 54 | 55 | const pec = computed(() => { 56 | return ((span.value || 0) / GRID_SIZE) * 100; 57 | }); 58 | 59 | return () => ( 60 |
0 ? `${pec.value}%` : undefined, 64 | marginLeft: offset.value ? `${(offset.value / GRID_SIZE) * 100}%` : undefined, 65 | maxWidth: pec.value > 0 ? `${pec.value}%` : undefined, 66 | ...props.style, 67 | }} 68 | > 69 | { renderSlot(ctx.slots, 'default') } 70 |
71 | ) 72 | }, 73 | }); 74 | -------------------------------------------------------------------------------- /library/DynamicFormBasicControls/Layout/Row.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, toRefs, type VNode } from 'vue'; 2 | 3 | export interface RowProps { 4 | /** 5 | * 列元素之间的间距(单位为 px) 6 | */ 7 | gutter?: number; 8 | /** 9 | * 主轴对齐方式,可选值为 10 | */ 11 | justify?: 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'space-evenly' | undefined; 12 | /** 13 | * 交叉轴对齐方式 14 | */ 15 | align?: "center" | "flex-start" | "flex-end" | "stretch" | "baseline" | undefined; 16 | /** 17 | * 是否自动换行,默认 true 18 | */ 19 | wrap?: boolean; 20 | 21 | style?: object; 22 | class?: string, 23 | } 24 | 25 | /** 26 | * 24列栅格行组件。 27 | */ 28 | export default defineComponent({ 29 | name: 'Row', 30 | props: { 31 | /** 32 | * 列元素之间的间距(单位为px) 33 | */ 34 | gutter: { 35 | type: Number, 36 | default: 0 37 | }, 38 | /** 39 | * 主轴对齐方式,可选值为 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'space-evenly' 40 | */ 41 | justify: { 42 | type: String, 43 | default: undefined, 44 | }, 45 | /** 46 | * 交叉轴对齐方式,可选值为 "center" | "flex-start" | "flex-end" | "stretch" | "baseline" 47 | */ 48 | align: { 49 | type: String, 50 | default: undefined, 51 | }, 52 | /** 53 | * 是否自动换行,默认 true 54 | */ 55 | wrap: { 56 | type: Boolean, 57 | default: true 58 | }, 59 | 60 | class: { 61 | type: String, 62 | default: undefined, 63 | }, 64 | style: { 65 | type: Object, 66 | default: undefined, 67 | } 68 | }, 69 | setup(props, ctx) { 70 | const { gutter, wrap, align, justify } = toRefs(props); 71 | 72 | function setVnodeStyle(node: VNode, style: Record) { 73 | if (!node.props) node.props = {}; 74 | if (!node.props.style) node.props.style = {}; 75 | 76 | node.props.style = { 77 | ...node.props.style, 78 | ...style, 79 | } 80 | } 81 | 82 | return () => { 83 | 84 | const children = ctx.slots.default ? ctx.slots.default() : []; 85 | //处理子级元素,为其增加边距 86 | const count = children.length; 87 | if (count > 0 && gutter.value > 0) { 88 | if (count > 1) { 89 | children.forEach((k, index) => setVnodeStyle(children[0], { 90 | paddingLeft: index === 0 ? 0 : (index === count - 1 ? gutter.value : gutter.value / 2), 91 | paddingRight: index === count - 1 ? 0 : (index === 0 ? gutter.value : gutter.value / 2), 92 | })); 93 | } else { 94 | setVnodeStyle(children[0], { paddingLeft: gutter.value / 2, paddingRight: gutter.value / 2 }); 95 | } 96 | } 97 | 98 | return ( 99 |
108 | { children } 109 |
); 110 | }; 111 | }, 112 | }); 113 | -------------------------------------------------------------------------------- /library/DynamicFormBasicControls/Utils/ArrayUtils.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 交换数组两个元素 4 | * @param {Array} arr 数组 5 | * @param {Number} index1 索引1 6 | * @param {Number} index2 索引2 7 | */ 8 | function swapItems(arr: Array, index1: number, index2: number) { 9 | arr[index1] = arr.splice(index2, 1, arr[index1])[0] 10 | /* 11 | let x = arr[index1]; 12 | arr[index1] = arr[index2]; 13 | arr[index2] = x; 14 | */ 15 | return arr 16 | } 17 | /** 18 | * 指定数组索引位置元素向上移 19 | * @param {Array} arr 数组 20 | * @param {Number} index 索引 21 | */ 22 | function upData(arr: Array, index: number) { 23 | if (arr.length > 1 && index !== 0) 24 | return swapItems(arr, index, index - 1) 25 | } 26 | /** 27 | * 指定数组索引位置元素向下移 28 | * @param {Array} arr 数组 29 | * @param {Number} index 索引 30 | */ 31 | function downData(arr: Array, index: number) { 32 | if (arr.length > 1 && index !== (arr.length - 1)) 33 | return swapItems(arr, index, index + 1) 34 | } 35 | 36 | export default { 37 | swapItems, 38 | upData, 39 | downData, 40 | }; 41 | -------------------------------------------------------------------------------- /library/DynamicFormBasicControls/Utils/ObjectUtils.ts: -------------------------------------------------------------------------------- 1 | export type CKeyValueTypeAll = Record|unknown|undefined|string|bigint|number|boolean; 2 | 3 | export interface CKeyValue { 4 | [index: string]: CKeyValueTypeAll; 5 | } 6 | 7 | /** 8 | * 深克隆对象,数组 9 | * @param obj 要克隆的对象 10 | * @param deepArray 是否要深度克隆数组里的每个对象 11 | */ 12 | function clone(obj: CKeyValueTypeAll, deepArray = false): CKeyValue|Array|null { 13 | let temp: CKeyValue|Array|null = null; 14 | if (obj instanceof Array) { 15 | if (deepArray) { 16 | temp = (obj as CKeyValue[]).map((item) => clone(item, deepArray) as CKeyValue); 17 | } 18 | else { 19 | temp = obj.concat(); 20 | } 21 | } 22 | else if (typeof obj === 'object') { 23 | temp = {} as CKeyValue; 24 | for (const item in obj) { 25 | const val = (obj as CKeyValue)[item]; 26 | if (val === null) { temp[item] = null; } 27 | else { (temp as CKeyValue)[item] = clone(val, deepArray) as CKeyValueTypeAll; } 28 | } 29 | } else { 30 | temp = obj as CKeyValue; 31 | } 32 | return temp; 33 | } 34 | 35 | export default { 36 | clone, 37 | }; 38 | -------------------------------------------------------------------------------- /library/DynamicFormBasicControls/index.ts: -------------------------------------------------------------------------------- 1 | import Form from './Form' 2 | import FormItem from './FormItem' 3 | import Col from './Layout/Col' 4 | import Row from './Layout/Row' 5 | 6 | export { Form, FormItem, Col, Row } 7 | export * from './Form' 8 | export * from './FormItem' 9 | export * from './Layout/Col' 10 | export * from './Layout/Row' 11 | export * from './FormContext' 12 | -------------------------------------------------------------------------------- /library/DynamicFormInner.vue: -------------------------------------------------------------------------------- 1 | 2 | 36 | 37 | 57 | -------------------------------------------------------------------------------- /library/DynamicFormInternal.ts: -------------------------------------------------------------------------------- 1 | export type IDynamicFormMessageCenterCallback = (messageName: string, data: unknown) => void; 2 | 3 | export interface IDynamicFormMessageCenter { 4 | addInstance: (name: string, fn: IDynamicFormMessageCenterCallback) => void, 5 | removeInstance: (name: string) => void, 6 | } -------------------------------------------------------------------------------- /library/DynamicFormItem.vue: -------------------------------------------------------------------------------- 1 | 2 | 284 | 285 | -------------------------------------------------------------------------------- /library/DynamicFormItemControls/BaseCheck.ts: -------------------------------------------------------------------------------- 1 | export interface BaseCheckProps { 2 | text: string; 3 | } -------------------------------------------------------------------------------- /library/DynamicFormItemControls/BaseCheck.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /library/DynamicFormItemControls/BaseDivider.ts: -------------------------------------------------------------------------------- 1 | import { StyleHTMLAttributes } from "vue"; 2 | 3 | export interface BaseDividerProps { 4 | style?: StyleHTMLAttributes; 5 | } -------------------------------------------------------------------------------- /library/DynamicFormItemControls/BaseDivider.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /library/DynamicFormItemControls/BaseInput.ts: -------------------------------------------------------------------------------- 1 | export interface BaseInputProps { 2 | placeholder?: string; 3 | password?:boolean; 4 | } -------------------------------------------------------------------------------- /library/DynamicFormItemControls/BaseInput.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /library/DynamicFormItemControls/BaseRadio.ts: -------------------------------------------------------------------------------- 1 | export interface BaseRadioProps { 2 | /** 3 | * 是否禁用 4 | */ 5 | disabled: boolean; 6 | /** 7 | * 选项数据 8 | */ 9 | items: { 10 | label: string, 11 | value: string|number, 12 | }[]; 13 | /** 14 | * 选择值 15 | */ 16 | value: unknown; 17 | } -------------------------------------------------------------------------------- /library/DynamicFormItemControls/BaseRadio.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /library/DynamicFormItemControls/BaseSelect.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IDynamicFormItemSelectOption { 3 | text: string, 4 | value: string|number, 5 | } 6 | 7 | export interface BaseSelectProps { 8 | /** 9 | * 是否禁用 10 | */ 11 | disabled: boolean; 12 | /** 13 | * 选项数据 14 | */ 15 | options: IDynamicFormItemSelectOption[]; 16 | /** 17 | * 选择值 18 | */ 19 | value: unknown; 20 | } -------------------------------------------------------------------------------- /library/DynamicFormItemControls/BaseSelect.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /library/DynamicFormItemControls/BaseTextArea.ts: -------------------------------------------------------------------------------- 1 | export interface BaseTextAreaProps { 2 | cols?: number, 3 | rows?: number, 4 | maxlength?: number, 5 | } -------------------------------------------------------------------------------- /library/DynamicFormItemControls/BaseTextArea.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /library/DynamicFormItemControls/FormArrayGroup.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 动态表单类型是 arrag-object 时的 additionalProps 4 | */ 5 | export interface FormArrayGroupProps { 6 | /** 7 | * 是否显示添加按钮,默认是 8 | */ 9 | showAddButton?: boolean, 10 | /** 11 | * 是否显示删除按钮,默认是 12 | */ 13 | showDeleteButton?: boolean, 14 | /** 15 | * 是否显示上移下移按钮,默认是 16 | */ 17 | showUpDownButton?: boolean, 18 | /** 19 | * 删除按钮回调,可选,不提供时默认操作为将 item 从 array 中移除。 20 | */ 21 | deleteCallback?: (array: unknown[], item: unknown) => void, 22 | /** 23 | * 添加按钮回调,必填,否则用户无法添加数据 24 | */ 25 | addCallback: (array: unknown[]) => void, 26 | } -------------------------------------------------------------------------------- /library/DynamicFormItemControls/FormArrayGroup.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 156 | 157 | -------------------------------------------------------------------------------- /library/DynamicFormItemControls/FormArrayGroupItem.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | -------------------------------------------------------------------------------- /library/DynamicFormItemControls/FormCustomLayout.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /library/DynamicFormItemControls/FormGroup.ts: -------------------------------------------------------------------------------- 1 | export interface FormGroupProps { 2 | /** 3 | * 标题 4 | */ 5 | title: string 6 | /** 7 | * 栅格间隔,可以写成像素值或支持响应式的对象写法来设置水平间隔 { xs: 8, sm: 16, md: 24}。或者使用数组形式同时设置 [水平间距, 垂直间距] 8 | */ 9 | gutter: unknown, 10 | /** 11 | * flex 布局下的水平排列方式: 12 | */ 13 | justify: 'start'|'end'|'center'|'space-around'|'space-between'; 14 | } -------------------------------------------------------------------------------- /library/DynamicFormItemControls/FormGroup.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 41 | 42 | -------------------------------------------------------------------------------- /library/DynamicFormItemControls/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BaseRadio'; 2 | export * from './BaseSelect'; 3 | export * from './BaseCheck'; 4 | export * from './BaseTextArea'; 5 | export * from './BaseInput'; 6 | export * from './BaseDivider'; 7 | export * from './FormArrayGroup'; 8 | export * from './FormGroup'; -------------------------------------------------------------------------------- /library/DynamicFormItemNormal.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /library/DynamicFormItemRenderer/DynamicFormItemRegistry.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface DynamicFormItemRegistryItem { 3 | componentInstance: unknown; 4 | valueName: string; 5 | additionalProps: Record; 6 | } 7 | 8 | export function makeWidget(componentInstance: unknown, additionalProps?: Record, valueName?: string) : DynamicFormItemRegistryItem { 9 | return { componentInstance, additionalProps: additionalProps ?? {}, valueName: valueName || 'valueName' }; 10 | } 11 | 12 | const DynamicFormItemRegistryData = new Map(); 13 | 14 | /** 15 | * 动态表单组件注册器。 16 | * 17 | * 您可以在这里调用 register 注册自定义表单控件 18 | */ 19 | export const DynamicFormItemRegistry = { 20 | /** 21 | * 查找已注册的表单组件,如果未找到,则返回 null 22 | * @param type 唯一类型名称 23 | */ 24 | findDynamicFormItemByType(type: string) : DynamicFormItemRegistryItem|null { 25 | return DynamicFormItemRegistryData.get(type) || null; 26 | }, 27 | /** 28 | * 注册自定义表单控件 29 | * @param type 唯一类型名称 30 | * @param componentInstance 组件类 31 | * @param additionalProps 组件的附加属性,将会设置到渲染函数上 32 | * @param valueName 用于指定表单子组件的双向绑定值属性名称,默认是 value 33 | */ 34 | register(type: string, componentInstance: unknown, additionalProps: Record = {}, valueName = 'value') { 35 | if (DynamicFormItemRegistryData.has(type)) { 36 | console.warn('[DynamicFormItemRegistry] Type ' + type + ' already exists and cannot be registered twice.'); 37 | return this; 38 | } 39 | DynamicFormItemRegistryData.set(type, { componentInstance, additionalProps, valueName }); 40 | return this; 41 | }, 42 | /** 43 | * 取消注册自定义表单控件 44 | * @param type 唯一类型名称 45 | */ 46 | unregister(type: string) { 47 | if (!DynamicFormItemRegistryData.has(type)) { 48 | console.warn('[DynamicFormItemRegistry] Can not unregister nonexistent type ' + type + ' .'); 49 | return this; 50 | } 51 | DynamicFormItemRegistryData.delete(type); 52 | return this; 53 | }, 54 | /** 55 | * 清空所有已注册的自定义表单控件 56 | */ 57 | clearAll() { 58 | DynamicFormItemRegistryData.clear(); 59 | return this; 60 | } 61 | }; -------------------------------------------------------------------------------- /library/DynamicFormItemRenderer/DynamicFormItemRenderer.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /library/DynamicFormTab/DynamicFormTab.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /library/DynamicFormTab/DynamicFormTabPage.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /library/Scss/BaseControl.scss: -------------------------------------------------------------------------------- 1 | input[type="text"].dynamic-form-base-control, 2 | input[type="password"].dynamic-form-base-control, 3 | textarea.dynamic-form-base-control, 4 | select.dynamic-form-base-control { 5 | max-width: 100%; 6 | padding: 1px 11px; 7 | background-color: var(--dynamic-form-base-color); 8 | background-image: none; 9 | border-radius: 5px; 10 | border: 1px solid var(--dynamic-form-border-color); 11 | font-size: var(--dynamic-form-font-size); 12 | line-height: 30px; 13 | height: 30px; 14 | outline: none; 15 | 16 | &::-webkit-input-placeholder { 17 | color: var(--dynamic-form-placeholder-color); 18 | } 19 | 20 | &:disabled { 21 | opacity: 0.8; 22 | color: var(--dynamic-form-secondary-color); 23 | } 24 | 25 | &:hover, &:active, &:focus { 26 | border-color: var(--dynamic-form-primary-color); 27 | } 28 | } 29 | 30 | .dynamic-form-base-control.radio { 31 | display: flex; 32 | flex-direction: row; 33 | 34 | .item { 35 | display: flex; 36 | flex-direction: row; 37 | align-items: center; 38 | } 39 | 40 | input { 41 | position: relative; 42 | appearance: none; 43 | width: 22px; 44 | height: 22px; 45 | border-radius: 22px; 46 | background-color: var(--dynamic-form-base-color); 47 | border: 1px solid var(--dynamic-form-border-color); 48 | margin: 0; 49 | margin-right: 5px; 50 | 51 | &:after { 52 | position: absolute; 53 | display: inline-block; 54 | top: 3px; 55 | left: 3px; 56 | width: 14px; 57 | height: 14px; 58 | content: ""; 59 | border: none; 60 | text-align: center; 61 | border-radius: 14px; 62 | } 63 | 64 | &:checked { 65 | &:after { 66 | background-color: var(--dynamic-form-primary-color); 67 | } 68 | } 69 | 70 | } 71 | } 72 | 73 | textarea.dynamic-form-base-control { 74 | height: unset; 75 | } 76 | 77 | .dynamic-form-base-control.base-button { 78 | position: relative; 79 | display: inline-flex; 80 | justify-content: center; 81 | align-items: center; 82 | text-align: center; 83 | position: relative; 84 | flex-shrink: 0; 85 | user-select: none; 86 | width: auto; 87 | white-space: nowrap; 88 | transition: background-color .1s,transform .1s,color .35s cubic-bezier(.65,0,.25,1); 89 | border: none; 90 | cursor: pointer; 91 | border-radius: 5px; 92 | box-sizing: border-box; 93 | overflow: hidden; 94 | min-width: 60px; 95 | height: 30px; 96 | padding: 0 14px; 97 | font-size: var(--dynamic-form-font-size); 98 | 99 | background: #f0f0f0; 100 | color: #333; 101 | 102 | &:hover { 103 | background-color: #ebebeb; 104 | } 105 | &:active { 106 | background-color: #d8d8d8; 107 | transform: scale(0.9); 108 | } 109 | } 110 | .dynamic-form-base-control.base-checkbox { 111 | vertical-align: middle; 112 | display: inline-flex; 113 | align-items: center; 114 | justify-content: center; 115 | position: relative; 116 | background: transparent; 117 | appearance: none; 118 | height: 22px; 119 | width: 22px; 120 | overflow: hidden; 121 | background-color: var(--dynamic-form-base-color); 122 | background-image: none; 123 | border-radius: 5px; 124 | border: 1px solid var(--dynamic-form-border-color); 125 | color: var(--dynamic-form-primary-color); 126 | margin: 0; 127 | margin-right: 10px; 128 | 129 | &:after { 130 | display: inline-block; 131 | width: 22px; 132 | line-height: 22px; 133 | top: 0px; 134 | content: ""; 135 | color: var(--dynamic-form-primary-color); 136 | border: none; 137 | text-align: center; 138 | font-size: 12px; 139 | } 140 | 141 | &:checked:after { 142 | content: "✔️"; 143 | color: var(--dynamic-form-primary-color); 144 | } 145 | } 146 | 147 | .dynamic-form-row { 148 | display: flex; 149 | flex-direction: row; 150 | flex: 1; 151 | } 152 | .dynamic-form-col { 153 | display: flex; 154 | justify-content: flex-start; 155 | flex-direction: column; 156 | flex-grow: 0; 157 | flex-shrink: 0; 158 | } -------------------------------------------------------------------------------- /library/Scss/Color.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --dynamic-form-font-size: 14px; 3 | --dynamic-form-height: 32px; 4 | --dynamic-form-space: 18px; 5 | --dynamic-form-primary-color: #1a91f3; 6 | --dynamic-form-error-color: #f55; 7 | --dynamic-form-secondary-color: #666; 8 | --dynamic-form-placeholder-color: #bbbbbb; 9 | --dynamic-form-border-color: #dcdfe6; 10 | --dynamic-form-base-color: #fff; 11 | } -------------------------------------------------------------------------------- /library/Scss/Form.scss: -------------------------------------------------------------------------------- 1 | 2 | .dynamic-form-control { 3 | position: relative; 4 | } 5 | .dynamic-form-error-alert { 6 | color: var(--dynamic-form-error-color); 7 | } 8 | .dynamic-form-object-title { 9 | color: var(--dynamic-form-secondary-color); 10 | height: var(--dynamic-form-height); 11 | line-height: var(--dynamic-form-height); 12 | } 13 | .dynamic-form-control-item { 14 | padding: 2px 6px; 15 | margin-bottom: var(--dynamic-form-space); 16 | 17 | .dynamic-form-control-item { 18 | padding-left: 0; 19 | padding-right: 0; 20 | } 21 | 22 | .error-message { 23 | color: var(--dynamic-form-error-color); 24 | } 25 | 26 | &.no-bottom-margin { 27 | margin-bottom: 0; 28 | } 29 | 30 | label { 31 | color: var(--dynamic-form-secondary-color); 32 | height: var(--dynamic-form-height); 33 | line-height: var(--dynamic-form-height); 34 | padding: 0 12px 0 0; 35 | 36 | .colon { 37 | color: var(--dynamic-form-secondary-color); 38 | margin-left: 3px; 39 | margin-right: 5px; 40 | } 41 | .required { 42 | color: var(--dynamic-form-error-color); 43 | margin-right: 10px; 44 | } 45 | } 46 | .label-container { 47 | flex-shrink: 0; 48 | } 49 | .wrapper-container { 50 | flex-grow: 1; 51 | } 52 | } 53 | .dynamic-form-wrapper { 54 | position: relative; 55 | } 56 | .dynamic-form-item-wrapper { 57 | &.nest-with-margin > .dynamic-form-item-wrapper { 58 | margin-left: 20px; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /library/main.ts: -------------------------------------------------------------------------------- 1 | export * from './DynamicForm' 2 | export * from './DynamicFormBasicControls' 3 | export * from './DynamicFormItemControls' 4 | export * from './DynamicFormItemRenderer/DynamicFormItemRegistry' 5 | import DynamicForm from './DynamicForm.vue' 6 | 7 | export { DynamicForm }; -------------------------------------------------------------------------------- /library/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "env.d.ts", 4 | "./**/*", 5 | "./**/*.tsx", 6 | ], 7 | "exclude": ["./**/__tests__/*"], 8 | "compilerOptions": { 9 | "target": "esnext", 10 | "module": "esnext", 11 | "jsx": "preserve", 12 | "importHelpers": true, 13 | "strict": true, 14 | "moduleResolution": "node", 15 | "skipLibCheck": true, 16 | "esModuleInterop": true, 17 | "allowSyntheticDefaultImports": true, 18 | "sourceMap": true, 19 | "allowJs": false, 20 | "noImplicitAny": true, 21 | "noImplicitThis": true, 22 | "strictFunctionTypes": false, 23 | "composite": true, 24 | "baseUrl": ".", 25 | "isolatedModules": false, 26 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /library/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | import { defineConfig } from 'vite' 3 | import vue from '@vitejs/plugin-vue' 4 | import vueJsx from '@vitejs/plugin-vue-jsx' 5 | import dts from 'vite-plugin-dts' 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | vue(), 10 | vueJsx(), 11 | dts(), 12 | ], 13 | resolve: { 14 | alias: { 15 | '@': fileURLToPath(new URL('./', import.meta.url)) 16 | } 17 | }, 18 | build: { 19 | lib: { 20 | entry: 'main.ts', 21 | name: 'vue-dynamic-form', 22 | fileName: (format) => `vue-dynamic-form.${format}.js`, 23 | }, 24 | outDir: '../dist', 25 | sourcemap: true, 26 | minify: false, 27 | rollupOptions: { 28 | external: ['vue'], 29 | output: { 30 | globals: { 31 | vue: 'Vue', 32 | }, 33 | }, 34 | }, 35 | }, 36 | }) 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "deprecated": false, 3 | "license": "MIT", 4 | "main": "dist/vue-dynamic-form.umd.js", 5 | "types": "dist/main.d.ts", 6 | "name": "@imengyu/vue-dynamic-form", 7 | "version": "0.1.2", 8 | "description": "A data driven form component for vue3", 9 | "files": [ 10 | "dist" 11 | ], 12 | "publishConfig": { 13 | "access": "public", 14 | "registry": "https://registry.npmjs.org" 15 | }, 16 | "keywords": [ 17 | "vue", 18 | "form", 19 | "dynamic-form" 20 | ], 21 | "homepage": "https://github.com/imengyu/vue-dynamic-form", 22 | "author": "imengyu", 23 | "scripts": { 24 | "dev": "vite", 25 | "test:unit": "vitest --environment jsdom --root src/", 26 | "build": "vite build library", 27 | "docs:dev": "vitepress dev docs", 28 | "docs:build": "vitepress build docs", 29 | "docs:serve": "vitepress serve docs" 30 | }, 31 | "dependencies": { 32 | "async-validator": "^4.2.5", 33 | "scroll-into-view-if-needed": "^3.0.3", 34 | "vue": "^3.2.45" 35 | }, 36 | "devDependencies": { 37 | "@arco-design/web-vue": "^2.41.0", 38 | "@codemirror/lang-json": "^6.0.1", 39 | "@rushstack/eslint-patch": "^1.1.4", 40 | "@types/jsdom": "^20.0.1", 41 | "@types/node": "^18.11.12", 42 | "@vitejs/plugin-vue": "^4.0.0", 43 | "@vitejs/plugin-vue-jsx": "^3.0.0", 44 | "@vue/eslint-config-typescript": "^11.0.0", 45 | "@vue/test-utils": "^2.2.6", 46 | "@vue/tsconfig": "^0.1.3", 47 | "ant-design-vue": "^4.2.1", 48 | "codemirror": "^6.0.1", 49 | "element-plus": "^2.7.2", 50 | "eslint": "^8.22.0", 51 | "eslint-plugin-vue": "^9.3.0", 52 | "highlight.js": "^11.9.0", 53 | "jsdom": "^20.0.3", 54 | "npm-run-all": "^4.1.5", 55 | "sass": "^1.57.1", 56 | "typescript": "~4.7.4", 57 | "vite": "^4.0.0", 58 | "vite-plugin-dts": "^3.9.0", 59 | "vite-plugin-markdown-preview": "^1.1.1", 60 | "vitepress": "^1.1.4", 61 | "vitest": "^0.25.6", 62 | "vue-codemirror": "^6.1.1", 63 | "vue-router": "^4.1.6", 64 | "vue-tsc": "^1.0.12" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imengyu/vue-dynamic-form/67c458aaab05acf2e99d19512893f92b1d373148/public/favicon.ico -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "library/env.d.ts", 4 | "library/**/*", 5 | "library/**/*.tsx", 6 | "docs/**/*.vue", 7 | "docs/**/*.tsx", 8 | "docs/**/*" 9 | ], 10 | "exclude": ["library/**/__tests__/*"], 11 | "compilerOptions": { 12 | "target": "esnext", 13 | "module": "esnext", 14 | "jsx": "preserve", 15 | "importHelpers": true, 16 | "strict": true, 17 | "moduleResolution": "node", 18 | "skipLibCheck": true, 19 | "esModuleInterop": true, 20 | "allowSyntheticDefaultImports": true, 21 | "sourceMap": true, 22 | "allowJs": false, 23 | "noImplicitAny": true, 24 | "noImplicitThis": true, 25 | "strictFunctionTypes": false, 26 | "composite": true, 27 | "baseUrl": ".", 28 | "isolatedModules": false, 29 | "outDir": "./temp", 30 | "paths": { 31 | "@/*": ["./library/*"], 32 | "vue-dynamic-form": [ "./library/main" ], 33 | "@imengyu/vue-dynamic-form": [ "./library/main" ] 34 | }, 35 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 36 | } 37 | } 38 | --------------------------------------------------------------------------------