├── docs ├── guide │ ├── form.md │ ├── meta.md │ ├── README.md │ ├── custom-validator.md │ ├── terms.md │ └── layout.md ├── components │ ├── array.md │ ├── rate.md │ ├── tag.md │ ├── text.md │ ├── README.md │ ├── boolean.md │ ├── date.md │ ├── object.md │ ├── radio.md │ ├── select.md │ ├── string.md │ ├── time.md │ ├── cascader.md │ ├── checkbox.md │ ├── custom-components.md │ ├── number.md │ ├── slider.md │ ├── textarea.md │ └── autocomplete.md ├── .vuepress │ ├── styles │ │ ├── palette.styl │ │ └── index.styl │ ├── public │ │ ├── hero.png │ │ ├── logo.png │ │ ├── icons │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── mstile-150x150.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon-60x60.png │ │ │ ├── apple-touch-icon-76x76.png │ │ │ ├── apple-touch-icon-120x120.png │ │ │ ├── apple-touch-icon-152x152.png │ │ │ ├── apple-touch-icon-180x180.png │ │ │ └── msapplication-icon-144x144.png │ │ └── manifest.json │ ├── enhanceApp.js │ ├── components │ │ └── BaseForm.vue │ └── config.js ├── zh │ ├── README.md │ ├── guide │ │ ├── custom-validator.md │ │ ├── terms.md │ │ ├── README.md │ │ └── form.md │ └── components │ │ ├── object.md │ │ ├── number.md │ │ ├── tag.md │ │ ├── date.md │ │ ├── checkbox.md │ │ ├── README.md │ │ ├── text.md │ │ ├── rate.md │ │ ├── boolean.md │ │ ├── radio.md │ │ └── textarea.md └── README.md ├── public ├── qq.jpg ├── qq.png ├── favicon.ico └── index.html ├── src ├── assets │ └── logo.png ├── examples │ ├── layout │ │ ├── EmptyLayout.vue │ │ ├── components │ │ │ ├── redirect.vue │ │ │ └── Menu │ │ │ │ ├── SideMenu.vue │ │ │ │ └── menu.js │ │ └── BasicLayout.vue │ ├── views │ │ ├── ObjectView.vue │ │ ├── LayoutView.vue │ │ ├── PasswordView.vue │ │ ├── HomeView.vue │ │ ├── VisibleIfView.vue │ │ ├── ChkInputView.vue │ │ ├── NumberView.vue │ │ ├── TagView.vue │ │ ├── CustomValidatorView.vue │ │ ├── DateView.vue │ │ ├── CheckboxView.vue │ │ ├── SubmitButtonView.vue │ │ ├── BooleanView.vue │ │ ├── RateView.vue │ │ ├── TextareaView.vue │ │ ├── TextView.vue │ │ ├── RadioView.vue │ │ ├── ArrayView.vue │ │ └── AutoCompleteView.vue │ ├── components │ │ ├── password │ │ │ ├── password.meta.js │ │ │ └── Password.vue │ │ └── chk-input │ │ │ ├── ChkInput.vue │ │ │ └── chk-input.meta.js │ ├── utils │ │ └── index.js │ └── ant-design-vue.js ├── utils │ ├── consts.js │ ├── context.js │ ├── register.factory.js │ ├── event-bus.js │ ├── utils.js │ ├── global.js │ └── validate.factory.js ├── mixin │ ├── component.mixin.js │ ├── visible-if.mixin.js │ └── slots.mixin.js ├── App.vue ├── meta │ ├── boolean.meta.js │ ├── string.meta.js │ ├── number.meta.js │ ├── object.meta.js │ ├── base.meta.js │ └── array.meta.js ├── main.js ├── formly.js ├── components │ ├── Boolean.vue │ ├── Rate.vue │ ├── Radio.vue │ ├── Textarea.vue │ ├── AutoComplete.vue │ ├── Text.vue │ ├── String.vue │ ├── Checkbox.vue │ ├── Object.vue │ ├── Tag.vue │ ├── Cascader.vue │ ├── Slider.vue │ ├── Time.vue │ ├── Number.vue │ ├── Wrapper.vue │ ├── Date.vue │ └── Select.vue └── FormlyItem.vue ├── vue.config.js ├── .gitignore ├── jsconfig.json ├── .npmignore ├── babel.config.js ├── .eslintrc.js ├── gh-deploy.sh ├── rollup.config.js ├── README.md └── package.json /docs/guide/form.md: -------------------------------------------------------------------------------- 1 | # 表单 -------------------------------------------------------------------------------- /docs/components/array.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/guide/meta.md: -------------------------------------------------------------------------------- 1 | # meta -------------------------------------------------------------------------------- /docs/components/rate.md: -------------------------------------------------------------------------------- 1 | # Rate 评分 -------------------------------------------------------------------------------- /docs/components/tag.md: -------------------------------------------------------------------------------- 1 | # Tag 标签 -------------------------------------------------------------------------------- /docs/components/text.md: -------------------------------------------------------------------------------- 1 | # Text 文本 -------------------------------------------------------------------------------- /docs/components/README.md: -------------------------------------------------------------------------------- 1 | # Components -------------------------------------------------------------------------------- /docs/components/boolean.md: -------------------------------------------------------------------------------- 1 | # boolean -------------------------------------------------------------------------------- /docs/components/date.md: -------------------------------------------------------------------------------- 1 | # Date 日期选择框 -------------------------------------------------------------------------------- /docs/components/object.md: -------------------------------------------------------------------------------- 1 | # Object 对象 -------------------------------------------------------------------------------- /docs/components/radio.md: -------------------------------------------------------------------------------- 1 | # Radio 单选框 -------------------------------------------------------------------------------- /docs/components/select.md: -------------------------------------------------------------------------------- 1 | # Select 选择器 -------------------------------------------------------------------------------- /docs/components/string.md: -------------------------------------------------------------------------------- 1 | # String 文本框 -------------------------------------------------------------------------------- /docs/components/time.md: -------------------------------------------------------------------------------- 1 | # Time 时间选择器 -------------------------------------------------------------------------------- /docs/components/cascader.md: -------------------------------------------------------------------------------- 1 | # Cascader 级联选择 -------------------------------------------------------------------------------- /docs/components/checkbox.md: -------------------------------------------------------------------------------- 1 | # Checkbox 多选框 -------------------------------------------------------------------------------- /docs/components/custom-components.md: -------------------------------------------------------------------------------- 1 | # 自定义组件 -------------------------------------------------------------------------------- /docs/components/number.md: -------------------------------------------------------------------------------- 1 | # Number 数字输入框 -------------------------------------------------------------------------------- /docs/components/slider.md: -------------------------------------------------------------------------------- 1 | # Slider 滑动输入条 -------------------------------------------------------------------------------- /docs/components/textarea.md: -------------------------------------------------------------------------------- 1 | # Textarea 多行文本框 -------------------------------------------------------------------------------- /docs/guide/README.md: -------------------------------------------------------------------------------- 1 | echo '# Hello VuePress' -------------------------------------------------------------------------------- /docs/components/autocomplete.md: -------------------------------------------------------------------------------- 1 | # autocomplete -------------------------------------------------------------------------------- /docs/guide/custom-validator.md: -------------------------------------------------------------------------------- 1 | # custom-validator -------------------------------------------------------------------------------- /public/qq.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinZhang19870314/v-formly/HEAD/public/qq.jpg -------------------------------------------------------------------------------- /public/qq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinZhang19870314/v-formly/HEAD/public/qq.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinZhang19870314/v-formly/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinZhang19870314/v-formly/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/examples/layout/EmptyLayout.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /docs/.vuepress/styles/palette.styl: -------------------------------------------------------------------------------- 1 | // placeholder for test, dont't remove it. 2 | 3 | //$accentColor = #f00 4 | -------------------------------------------------------------------------------- /docs/.vuepress/public/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinZhang19870314/v-formly/HEAD/docs/.vuepress/public/hero.png -------------------------------------------------------------------------------- /docs/.vuepress/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinZhang19870314/v-formly/HEAD/docs/.vuepress/public/logo.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinZhang19870314/v-formly/HEAD/docs/.vuepress/public/icons/favicon-16x16.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinZhang19870314/v-formly/HEAD/docs/.vuepress/public/icons/favicon-32x32.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinZhang19870314/v-formly/HEAD/docs/.vuepress/public/icons/mstile-150x150.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinZhang19870314/v-formly/HEAD/docs/.vuepress/public/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinZhang19870314/v-formly/HEAD/docs/.vuepress/public/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinZhang19870314/v-formly/HEAD/docs/.vuepress/public/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinZhang19870314/v-formly/HEAD/docs/.vuepress/public/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinZhang19870314/v-formly/HEAD/docs/.vuepress/public/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinZhang19870314/v-formly/HEAD/docs/.vuepress/public/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinZhang19870314/v-formly/HEAD/docs/.vuepress/public/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinZhang19870314/v-formly/HEAD/docs/.vuepress/public/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /src/utils/consts.js: -------------------------------------------------------------------------------- 1 | const FORM_VALUE_CHANGE = 'form-value-change'; 2 | const FORM_ERROR_CHANGE = 'form-error-change'; 3 | 4 | export { FORM_VALUE_CHANGE, FORM_ERROR_CHANGE }; -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinZhang19870314/v-formly/HEAD/docs/.vuepress/public/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /src/examples/views/ObjectView.vue: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devServer: { 3 | proxy: { 4 | '^/api': { 5 | target: 'https://randomuser.me', 6 | ws: true, 7 | changeOrigin: true, 8 | secure: true, 9 | logLevel: 'debug' 10 | } 11 | } 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/mixin/component.mixin.js: -------------------------------------------------------------------------------- 1 | export const componentMixin = { 2 | inject: ["state"], 3 | props: { 4 | id: String, 5 | meta: { 6 | type: Object, 7 | default: () => {}, 8 | }, 9 | }, 10 | computed: { 11 | ui() { 12 | return this.context.ui; 13 | }, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /src/examples/layout/components/redirect.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/examples/components/password/password.meta.js: -------------------------------------------------------------------------------- 1 | import { BaseMeta } from "@/formly.js"; 2 | class PasswordMeta extends BaseMeta { 3 | constructor(state, id, meta) { 4 | super(state, id, meta); 5 | } 6 | 7 | setValue(val) { 8 | this._value = (val && val.trim()) || undefined; 9 | } 10 | } 11 | 12 | export { PasswordMeta }; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | docs/.vuepress/dist 5 | 6 | 7 | # local env files 8 | .env.local 9 | .env.*.local 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | pnpm-debug.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "baseUrl": "./", 6 | "moduleResolution": "node", 7 | "paths": { 8 | "@/*": [ 9 | "src/*" 10 | ] 11 | }, 12 | "lib": [ 13 | "esnext", 14 | "dom", 15 | "dom.iterable", 16 | "scripthost" 17 | ] 18 | } 19 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | docs/ 4 | public/ 5 | src/ 6 | vuepress/ 7 | .vscode/ 8 | dist/ 9 | 10 | # local env files 11 | .env.local 12 | .env.*.local 13 | 14 | # Log files 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | pnpm-debug.log* 19 | 20 | # Editor directories and files 21 | .idea 22 | .vscode 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw? -------------------------------------------------------------------------------- /src/examples/utils/index.js: -------------------------------------------------------------------------------- 1 | export function deepClone(obj) { 2 | if (obj === null) return null; 3 | let clone = Object.assign({}, obj); 4 | Object.keys(clone).forEach( 5 | (key) => 6 | (clone[key] = 7 | typeof obj[key] === "object" ? deepClone(obj[key]) : obj[key]) 8 | ); 9 | if (Array.isArray(obj)) { 10 | clone.length = obj.length; 11 | return Array.from(clone); 12 | } 13 | return clone; 14 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ], 5 | // presets: [ 6 | // [ 7 | // "@vue/cli-plugin-babel/preset", 8 | // { 9 | // useBuiltIns: false, 10 | // }, 11 | // ], 12 | // ], 13 | plugins: [ 14 | [ 15 | "import", 16 | { libraryName: "ant-design-vue", libraryDirectory: "es", style: true }, 17 | ], 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: ["plugin:vue/essential", "eslint:recommended"], 7 | parserOptions: { 8 | parser: "babel-eslint", 9 | }, 10 | rules: { 11 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 12 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", 13 | "no-unused-vars": "warn", 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/context.js: -------------------------------------------------------------------------------- 1 | class FormItemContext { 2 | constructor() { 3 | this._map = new Map(); 4 | } 5 | 6 | addContext(id, instance) { 7 | this._map.set(id, instance); 8 | } 9 | 10 | removeContext(id) { 11 | this._map.delete(id); 12 | } 13 | 14 | getContext(id) { 15 | return this._map.get(id); 16 | } 17 | 18 | getContexts() { 19 | return this._map; 20 | } 21 | } 22 | 23 | export { FormItemContext }; 24 | -------------------------------------------------------------------------------- /docs/.vuepress/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "VuePress", 3 | "short_name": "VuePress", 4 | "icons": [ 5 | { 6 | "src": "/icons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/icons/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "/index.html", 17 | "display": "standalone", 18 | "background_color": "#fff", 19 | "theme_color": "#3eaf7c" 20 | } 21 | -------------------------------------------------------------------------------- /gh-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 确保脚本抛出遇到的错误 4 | set -e 5 | 6 | # 生成静态文件 7 | npm run docs:build 8 | 9 | # 进入生成的文件夹 10 | cd docs/.vuepress/dist 11 | 12 | # 如果是发布到自定义域名 13 | # echo 'www.example.com' > CNAME 14 | 15 | git init 16 | git add -A 17 | git commit -m 'deploy' 18 | 19 | # 如果发布到 https://.github.io 20 | # git push -f git@github.com:/.github.io.git main 21 | 22 | # 如果发布到 https://.github.io/ 23 | git push -f git@github.com:kevinzhang19870314/v-formly.git master:gh-pages 24 | 25 | cd - -------------------------------------------------------------------------------- /src/meta/boolean.meta.js: -------------------------------------------------------------------------------- 1 | import { BaseMeta } from "./base.meta"; 2 | class BooleanMeta extends BaseMeta { 3 | constructor(state, id, meta) { 4 | super(state, id, meta); 5 | } 6 | 7 | initValue() { 8 | if (typeof this._initMetaValue !== "undefined") { 9 | this.value = this._initMetaValue; 10 | } else if (typeof this.meta.default === "boolean") { 11 | this.value = this.meta.default; 12 | } 13 | } 14 | 15 | setValue(val) { 16 | this._value = val || false; 17 | } 18 | } 19 | 20 | export { BooleanMeta }; 21 | -------------------------------------------------------------------------------- /src/meta/string.meta.js: -------------------------------------------------------------------------------- 1 | import { BaseMeta } from "./base.meta"; 2 | class StringMeta extends BaseMeta { 3 | constructor(state, id, meta) { 4 | super(state, id, meta); 5 | 6 | if (this.meta) { 7 | this.open = (this.meta.ui && this.meta.ui.open) || false; 8 | } 9 | } 10 | 11 | initValue() { 12 | if (this._initMetaValue) { 13 | this.value = this._initMetaValue; 14 | } else if (this.meta.default) { 15 | this.value = this.meta.default; 16 | } 17 | } 18 | 19 | setValue(val) { 20 | this._value = val || undefined; 21 | } 22 | } 23 | 24 | export { StringMeta }; 25 | -------------------------------------------------------------------------------- /docs/zh/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroImage: /hero.png 4 | actionText: 快速上手 → 5 | actionLink: /zh/guide/ 6 | footer: MIT Licensed | Copyright © 2022-present v-formly 7 | --- 8 | 9 |
10 |
11 |

简洁至上

12 |

通过标准JSON Schema & Ajv Validator生成复杂的动态表单及校验,快速、简洁、高效。

13 |
14 |
15 |

可复用性

16 |

通过JSON的形式生成表单模板,一份表单简单修改即可多处复用!使您能够快速开发表单页面,相比编写传统的html form表单,使用JSON形式定义表单能够极大的提高了开发效率。

17 |
18 |
19 |

Vue 驱动

20 |

目前支持Vue 2.x & Ant Design of Vue v1,Vue 2.x和Vue 3.x的其他UI库(AntDv v3,ElementUI等)支持正在开发中。。。

21 |
22 |
23 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/.vuepress/enhanceApp.js: -------------------------------------------------------------------------------- 1 | import Antd from 'ant-design-vue'; 2 | import 'ant-design-vue/dist/antd.css'; 3 | import "@/style/index.less"; 4 | 5 | import VFormly from "@/formly.js"; 6 | import VPassword from "@/examples/components/password/Password.vue"; 7 | import VChkInput from "@/examples/components/chk-input/ChkInput.vue"; 8 | import { registerFormComponent } from "@/formly.js"; 9 | import _ from 'lodash'; 10 | 11 | registerFormComponent("v-password", VPassword); 12 | registerFormComponent("v-chkinput", VChkInput); 13 | 14 | export default ({ 15 | Vue, // VuePress 正在使用的 Vue 构造函数 16 | options, // 附加到根实例的一些选项 17 | router, // 当前应用的路由实例 18 | }) => { 19 | Vue.use(Antd); 20 | Vue.use(VFormly); 21 | } -------------------------------------------------------------------------------- /src/meta/number.meta.js: -------------------------------------------------------------------------------- 1 | import { BaseMeta } from "./base.meta"; 2 | class NumberMeta extends BaseMeta { 3 | constructor(state, id, meta) { 4 | super(state, id, meta); 5 | } 6 | 7 | initValue() { 8 | if (this._initMetaValue) { 9 | this.value = this._initMetaValue; 10 | } else if (this.meta.default) { 11 | this.value = this.meta.default; 12 | } 13 | } 14 | 15 | setValue(val) { 16 | switch (this.type) { 17 | case "slider": 18 | this._value = 19 | Array.isArray(val) || typeof val == "number" ? val : undefined; 20 | break; 21 | default: 22 | this._value = val || undefined; 23 | break; 24 | } 25 | } 26 | } 27 | 28 | export { NumberMeta }; 29 | -------------------------------------------------------------------------------- /src/utils/register.factory.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | 3 | /** 4 | * 注册组件 5 | * 6 | * @param {String} id 注册组件的id 7 | * @param {*} component 需要注册的组件 8 | */ 9 | function registerFormComponent(id, component) { 10 | Vue.component(id, component); 11 | } 12 | 13 | /** 14 | * 过滤出antd组件需要的props 15 | * 16 | * @param {*} props antd组件全量的props 17 | * @param {*} ui 组件对应meta的ui对象 18 | */ 19 | function getBindings(props, ui) { 20 | const bindings = {} 21 | const uiKeys = Object.keys(ui); 22 | uiKeys.forEach((key) => { 23 | if (props.indexOf(key) > -1) { 24 | bindings[key] = ui[key] 25 | } 26 | }); 27 | 28 | return bindings; 29 | } 30 | 31 | 32 | export { registerFormComponent, getBindings }; -------------------------------------------------------------------------------- /docs/.vuepress/styles/index.styl: -------------------------------------------------------------------------------- 1 | // placeholder for test, dont't remove it. 2 | 3 | //.content { 4 | // font-size 30px; 5 | //} 6 | 7 | .theme-default-content 8 | max-width: 876px !important 9 | 10 | .btns 11 | display: flex; 12 | justify-content: flex-end; 13 | & .ant-btn 14 | margin-right: 8px 15 | 16 | pre.vue-container 17 | border-left-width: .5rem; 18 | border-left-style: solid; 19 | border-color: #42b983; 20 | border-radius: 0px; 21 | & > code 22 | font-size: 14px !important; 23 | & > p 24 | margin: -5px 0 -20px 0; 25 | code 26 | background-color: #42b983 !important; 27 | padding: 3px 5px; 28 | border-radius: 3px; 29 | color #000 30 | em 31 | color #808080 32 | font-weight light 33 | 34 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./examples/router"; 4 | import "@/examples/ant-design-vue.js"; 5 | import "@/style/index.less"; 6 | import VFormly from "@/formly.js"; 7 | import VPassword from "@/examples/components/password/Password.vue"; 8 | import VChkInput from "@/examples/components/chk-input/ChkInput.vue"; 9 | import { registerFormComponent } from "@/formly.js"; 10 | 11 | Vue.config.productionTip = false; 12 | 13 | Vue.use(VFormly, { 14 | ui: { 15 | errors: { 16 | required: "必填项", 17 | }, 18 | }, 19 | }); 20 | 21 | registerFormComponent("v-password", VPassword); 22 | registerFormComponent("v-chkinput", VChkInput); 23 | 24 | new Vue({ 25 | router, 26 | render: (h) => h(App), 27 | }).$mount("#app"); 28 | -------------------------------------------------------------------------------- /docs/guide/terms.md: -------------------------------------------------------------------------------- 1 | # 名词解释 2 | 3 | ## meta 4 | 5 | `meta`在 v-formly 中代表的是标准的 JSON Schema 和嵌套的`ui`结合的结构。例如如下结构: 6 | 7 | ```json 8 | { 9 | "meta": { 10 | "type": "object", 11 | "properties": { 12 | "name": { 13 | "title": "姓名", 14 | "type": "string", 15 | "default": "kevin", 16 | "ui": { 17 | "showRequired": true 18 | } 19 | }, 20 | "desc": { 21 | "title": "描述", 22 | "type": "string", 23 | "default": "Base on technical, but not limited on it!", 24 | "ui": {} 25 | }, 26 | "enable": { 27 | "title": "启用", 28 | "type": "boolean" 29 | } 30 | }, 31 | "required": ["name"] 32 | }, 33 | "data": { 34 | "enable": true 35 | } 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /src/utils/event-bus.js: -------------------------------------------------------------------------------- 1 | function EventBus(Vue) { 2 | const bus = new Vue() 3 | 4 | Object.defineProperties(bus, { 5 | on: { 6 | get() { 7 | return this.$on.bind(this) 8 | } 9 | }, 10 | once: { 11 | get() { 12 | return this.$once.bind(this) 13 | } 14 | }, 15 | off: { 16 | get() { 17 | return this.$off.bind(this) 18 | } 19 | }, 20 | emit: { 21 | get() { 22 | return this.$emit.bind(this) 23 | } 24 | } 25 | }) 26 | 27 | Object.defineProperty(Vue, 'bus', { 28 | get() { 29 | return bus 30 | } 31 | }) 32 | 33 | Object.defineProperty(Vue.prototype, '$bus', { 34 | get() { 35 | return bus 36 | } 37 | }) 38 | } 39 | 40 | if (typeof window !== 'undefined' && window.Vue) { 41 | window.Vue.use(EventBus) 42 | } 43 | 44 | export default EventBus -------------------------------------------------------------------------------- /src/mixin/visible-if.mixin.js: -------------------------------------------------------------------------------- 1 | export const visibleIfMixin = { 2 | methods: { 3 | /** 4 | * 根据visibleIf函数判断是否需要隐藏被触发方组件 5 | * @param {Object} context 全局context 6 | * @param {Object} meta 被触发方组件meta 7 | * @param {Boolean} visible 被触发方组件当前是否可见 8 | * @param {Object} change 触发方组件的change事件数据 { id: xxx, value: xxx } 9 | * @returns 是否隐藏 10 | */ 11 | visibleIf(context, meta, visible, change) { 12 | if (!meta.ui || !meta.ui.visibleIf) { 13 | return visible; 14 | } 15 | 16 | const changeId = Object.keys(meta.ui.visibleIf)[0]; 17 | const changeFunc = Object.values(meta.ui.visibleIf)[0]; 18 | change = change || {}; 19 | 20 | if (typeof changeFunc !== "function" || changeId !== change.id) { 21 | return visible; 22 | } 23 | 24 | return changeFunc.call(this, context, change.id, change.value); 25 | }, 26 | }, 27 | } -------------------------------------------------------------------------------- /src/formly.js: -------------------------------------------------------------------------------- 1 | import EventBus from "@/utils/event-bus.js"; 2 | import VFormly from "./Formly.vue"; 3 | import VWrapper from "@/components/Wrapper.vue"; 4 | import { componentMixin } from "@/mixin/component.mixin.js"; 5 | import { BaseMeta } from "@/meta/base.meta.js"; 6 | import { registerFormComponent } from "@/utils/register.factory.js"; 7 | import { FORM_VALUE_CHANGE } from "@/utils/consts.js"; 8 | 9 | const components = [VFormly, VWrapper]; 10 | 11 | const install = function (Vue, options) { 12 | Vue.use(EventBus); 13 | components.forEach((component) => { 14 | Vue.component(component.name, component); 15 | }); 16 | 17 | // 传入自定义options 18 | Vue.prototype.$VFORMLY_OPTIONS = options; 19 | }; 20 | 21 | /* istanbul ignore if */ 22 | if (typeof window !== "undefined" && window.Vue) { 23 | install(window.Vue); 24 | } 25 | 26 | export { 27 | install, 28 | BaseMeta, 29 | componentMixin, 30 | registerFormComponent, 31 | FORM_VALUE_CHANGE, 32 | }; 33 | 34 | export default { 35 | install, 36 | }; 37 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroImage: /hero.png 4 | actionText: Get Started → 5 | actionLink: /guide/getting-started.html 6 | footer: MIT Licensed | Copyright © 2022-present v-formly 7 | --- 8 | 9 |
10 |
11 |

Simplicity First

12 |

Generate complex dynamic forms and validations through standard JSON Schema & Ajv Validator, fast, concise and efficient.

13 |
14 |
15 |

Reusability

16 |

The form template is generated in the form of JSON, which can be reused in multiple places by simply modifying it! It enables you to quickly develop form pages. Compared with writing traditional html forms, using JSON defined forms can greatly improve development efficiency.

17 |
18 |
19 |

Vue-Powered

20 |

Currently support for Vue 2.x & Ant Design of Vue v1, other UI libraries (AntDv v3, ElementUI, etc.) support for Vue 2.x and Vue 3.x are under development. . .

21 |
22 |
23 | -------------------------------------------------------------------------------- /src/examples/ant-design-vue.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import { 3 | Card, 4 | Button, 5 | FormModel, 6 | Tooltip, 7 | Icon, 8 | Input, 9 | Row, 10 | Col, 11 | Switch, 12 | AutoComplete, 13 | Select, 14 | Checkbox, 15 | DatePicker, 16 | InputNumber, 17 | Menu, 18 | Layout, 19 | Breadcrumb, 20 | TimePicker, 21 | Radio, 22 | Rate, 23 | Tag, 24 | Slider, 25 | Space, 26 | Divider, 27 | Spin, 28 | Upload, 29 | Cascader, 30 | } from "ant-design-vue"; 31 | 32 | Vue.use(Card); 33 | Vue.use(Button); 34 | Vue.use(FormModel); 35 | Vue.use(Tooltip); 36 | Vue.use(Icon); 37 | Vue.use(Input); 38 | Vue.use(Row); 39 | Vue.use(Col); 40 | Vue.use(Switch); 41 | Vue.use(AutoComplete); 42 | Vue.use(Select); 43 | Vue.use(Checkbox); 44 | Vue.use(DatePicker); 45 | Vue.use(InputNumber); 46 | Vue.use(Menu); 47 | Vue.use(Layout); 48 | Vue.use(Breadcrumb); 49 | Vue.use(TimePicker); 50 | Vue.use(Radio); 51 | Vue.use(Slider); 52 | Vue.use(Rate); 53 | Vue.use(Tag); 54 | Vue.use(Spin); 55 | Vue.use(Space); 56 | Vue.use(Divider); 57 | Vue.use(Upload); 58 | Vue.use(Cascader); 59 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 3 | import vue from "rollup-plugin-vue"; 4 | import babel from "@rollup/plugin-babel"; 5 | import commonjs from "@rollup/plugin-commonjs"; 6 | import alias from "@rollup/plugin-alias"; 7 | import postcss from "rollup-plugin-postcss"; 8 | import { uglify } from "rollup-plugin-uglify"; 9 | 10 | const config = { 11 | input: "./src/formly.js", 12 | output: { 13 | file: "./lib/v-formly.umd.js", 14 | exports: "named", 15 | format: "umd", 16 | name: "v-formly", 17 | globals: { 18 | vue: "Vue", 19 | }, 20 | }, 21 | external: ["ajv", "vue", "core-js", "ant-design-vue", /@babel\/runtime/], 22 | plugins: [ 23 | nodeResolve(), 24 | postcss(), 25 | vue(), 26 | alias({ 27 | entries: { 28 | ["@"]: path.resolve(__dirname, "src"), 29 | }, 30 | }), 31 | babel({ 32 | exclude: "**/node_modules/**", 33 | babelHelpers: "runtime", 34 | }), 35 | commonjs(), 36 | uglify(), 37 | ], 38 | }; 39 | 40 | export default config; 41 | -------------------------------------------------------------------------------- /src/meta/object.meta.js: -------------------------------------------------------------------------------- 1 | class ObjectMeta { 2 | constructor(state, id, meta) { 3 | this.state = state; 4 | this.id = id; 5 | this.meta = meta; 6 | this.childMetaPairs = this.buildChildMetaPairs(id, meta); 7 | 8 | state.context.addContext(id, this); 9 | } 10 | 11 | set value(val) { 12 | this.childMetaPairs.forEach(({ key, propertyName }) => { 13 | const ctx = this.state.context.getContext(key); 14 | ctx.value = (val || {})[propertyName]; 15 | }); 16 | } 17 | 18 | /** 19 | * 构造结构数据给Object循环使用 20 | * @param {String} id 每个组件实例的唯一id,构造成json-schema中的`instancePath + '/' + params.missingProperty` 21 | * @param {Object} meta json-schema的某个层级的schema 22 | * @returns 返回构造后的数据给Object使用 23 | */ 24 | buildChildMetaPairs(id, meta) { 25 | let results = []; 26 | for (let [key, value] of Object.entries(meta.properties || {})) { 27 | let keyVal = id === "/" ? `/${key}` : `${id}/${key}`; 28 | results.push({ key: keyVal, propertyName: key, meta: value }); 29 | } 30 | 31 | return results; 32 | } 33 | } 34 | 35 | export { ObjectMeta }; 36 | -------------------------------------------------------------------------------- /src/examples/components/chk-input/ChkInput.vue: -------------------------------------------------------------------------------- 1 | 18 | 45 | 46 | -------------------------------------------------------------------------------- /src/mixin/slots.mixin.js: -------------------------------------------------------------------------------- 1 | export const slotsMixin = { 2 | data() { 3 | return { 4 | slotsName: [], 5 | slotNameStartWith: "slotName", 6 | }; 7 | }, 8 | created() { 9 | if (this.meta && !this.meta.type) { 10 | this.meta.type = "object"; 11 | } 12 | this.getSlotsNameFromMeta(this.meta); 13 | }, 14 | methods: { 15 | getSlotsNameFromMeta(meta) { 16 | switch (meta.type) { 17 | case "object": 18 | Object.keys(meta.properties).forEach((key) => { 19 | const curMeta = meta.properties[key]; 20 | this.getSlotsNameFromMeta(curMeta); 21 | }); 22 | 23 | break; 24 | case "array": 25 | Object.keys(meta.items.properties).forEach((key) => { 26 | const curMeta = meta.items.properties[key]; 27 | this.getSlotsNameFromMeta(curMeta); 28 | }); 29 | 30 | break; 31 | default: 32 | if (meta.ui) { 33 | const keys = Object.keys(meta.ui).filter((f) => 34 | f.startsWith(this.slotNameStartWith) 35 | ); 36 | if (keys && keys.length > 0) { 37 | keys.forEach((key) => { 38 | this.slotsName.push(meta.ui[key]); 39 | }); 40 | } 41 | } 42 | break; 43 | } 44 | }, 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /src/examples/components/chk-input/chk-input.meta.js: -------------------------------------------------------------------------------- 1 | import { BaseMeta } from "@/formly.js"; 2 | class ChkInputMeta extends BaseMeta { 3 | constructor(state, id, meta) { 4 | super(state, id, meta); 5 | 6 | this._optionsValue = []; 7 | this._othersValue = undefined; 8 | 9 | this.initValue(); 10 | } 11 | 12 | initValue() { 13 | const val = this._initMetaValue || this.meta.default || {}; 14 | this._applyToValue(val.options, val.others); 15 | } 16 | 17 | setValue(val) { 18 | this._value = val || undefined; 19 | 20 | this._optionsValue = (val && val.options) || []; 21 | this._othersValue = (val && val.others) || undefined; 22 | } 23 | 24 | get optionsValue() { 25 | return this._optionsValue; 26 | } 27 | 28 | set optionsValue(val) { 29 | this._optionsValue = val; 30 | this._applyToValue(this._optionsValue, this._othersValue); 31 | } 32 | 33 | get othersValue() { 34 | return this._othersValue; 35 | } 36 | 37 | set othersValue(val) { 38 | this._othersValue = val; 39 | this._applyToValue(this._optionsValue, this._othersValue); 40 | } 41 | 42 | _applyToValue(options, others) { 43 | if ((!options || options.length === 0) && !others) { 44 | this.value = undefined; 45 | return; 46 | } 47 | 48 | this.value = { options, others }; 49 | } 50 | } 51 | 52 | export { ChkInputMeta }; 53 | -------------------------------------------------------------------------------- /src/examples/layout/components/Menu/SideMenu.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 64 | 65 | -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * len 长度 3 | * radix 基数(进制数) 4 | */ 5 | export function UUID(len = 8, radix = 10) { 6 | const chars = 7 | "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split(""); 8 | const _radix = radix || chars.length; 9 | const uuid = []; 10 | 11 | if (len) { 12 | // Compact form 13 | for (let i = 0; i < len; i++) { 14 | uuid[i] = chars[0 | (Math.random() * _radix)]; 15 | } 16 | } else { 17 | // rfc4122, version 4 form 18 | let r; 19 | 20 | // rfc4122 requires these characters 21 | uuid[8] = uuid[13] = uuid[18] = uuid[23] = "-"; 22 | uuid[14] = "4"; 23 | 24 | // Fill in random data. At i==19 set the high bits of clock sequence as 25 | // per rfc4122, sec. 4.1.5 26 | for (let i = 0; i < 36; i++) { 27 | if (!uuid[i]) { 28 | r = 0 | (Math.random() * 16); 29 | uuid[i] = chars[i == 19 ? (r & 0x3) | 0x8 : r]; 30 | } 31 | } 32 | } 33 | 34 | return uuid.join(""); 35 | } 36 | 37 | export function deepClone(obj) { 38 | if (obj === undefined) return undefined; 39 | if (obj === null) return null; 40 | let clone = Object.assign({}, obj); 41 | Object.keys(clone).forEach( 42 | (key) => 43 | (clone[key] = 44 | typeof obj[key] === "object" ? deepClone(obj[key]) : obj[key]) 45 | ); 46 | if (Array.isArray(obj)) { 47 | clone.length = obj.length; 48 | return Array.from(clone); 49 | } 50 | return clone; 51 | } 52 | -------------------------------------------------------------------------------- /src/examples/components/password/Password.vue: -------------------------------------------------------------------------------- 1 | 21 | 57 | 58 | -------------------------------------------------------------------------------- /src/examples/views/LayoutView.vue: -------------------------------------------------------------------------------- 1 | 14 | 56 | 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # v-formly 2 | 3 | [v-formly](https://kevinzhang19870314.github.io/v-formly/zh/) 是 vue 的动态(JSON 驱动)表单库。 4 | 5 | **Vue 3版本的请移步这里[v-formly-v3](https://github.com/KevinZhang19870314/v-formly-v3)** 6 | 7 |
8 |
9 |

简洁至上

10 |

通过标准JSON Schema & Ajv Validator生成复杂的动态表单及校验,快速、简洁、高效。

11 |
12 |
13 |

可复用性

14 |

通过JSON的形式生成表单模板,一份表单简单修改即可多处复用!使您能够快速开发表单页面,相比编写传统的html form表单,使用JSON形式定义表单能够极大的提高了开发效率。

15 |
16 |
17 |

Vue 驱动

18 |

目前支持Vue 2.x & Ant Design of Vue v1,Vue 2.x和Vue 3.x的其他UI库(AntDv v3,ElementUI等)支持正在开发中。。。

19 |
20 |
21 | 22 | ## 快速开始 23 | 24 | ### 文档 & Demo 25 | 26 | [文档](https://kevinzhang19870314.github.io/v-formly/zh/) 27 | 28 | [Stackblitz](https://stackblitz.com/edit/github-gr9ozc?file=src%2FApp.vue&terminal=serve) 29 | 30 | [CodeSandbox](https://codesandbox.io/s/blazing-sun-gtvwwz) 31 | 32 | ### 安装 33 | 34 | 使用`yarn`安装`v-formly`: 35 | 36 | ```sh 37 | yarn add v-formly 38 | ``` 39 | 40 | 或者使用`npm`安装它: 41 | 42 | ```sh 43 | npm i v-formly --save 44 | ``` 45 | 46 | ### 使用 47 | 48 | ```js 49 | // 别忘了引入Ant Design of Vue 1.7.8,v-formly当前只支持这个版本。 50 | 51 | import VFormly from "v-formly"; 52 | 53 | // ... 54 | Vue.use(VFormly); 55 | // ... 56 | ``` 57 | 58 | ## 其他 59 | 60 | 不论是学习还是使用 v-formly,有任何问题可以添加 QQ 群:610930944,我们为你解答关于使用 v-formly 过程中的的任何疑难杂症! 61 | 62 | 暗号:v-formly 63 | 64 | 65 | 66 | MIT Licensed | Copyright © 2022-present v-formly 67 | -------------------------------------------------------------------------------- /src/examples/views/PasswordView.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 62 | 63 | -------------------------------------------------------------------------------- /src/components/Boolean.vue: -------------------------------------------------------------------------------- 1 | 19 | 56 | -------------------------------------------------------------------------------- /docs/.vuepress/components/BaseForm.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 60 | -------------------------------------------------------------------------------- /docs/guide/layout.md: -------------------------------------------------------------------------------- 1 | # 布局 2 | 3 | v-formly 表单支持三种布局,水平`horizontal`,垂直`vertical`,行内`inline`。 4 | 5 | ## 代码演示 6 | 7 | ::: demo 8 | 9 | ```vue 10 | 24 | 66 | 67 | ``` 68 | 69 | ::: 70 | -------------------------------------------------------------------------------- /src/examples/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 66 | 67 | -------------------------------------------------------------------------------- /src/components/Rate.vue: -------------------------------------------------------------------------------- 1 | 18 | 61 | 62 | -------------------------------------------------------------------------------- /src/examples/views/VisibleIfView.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 65 | 66 | -------------------------------------------------------------------------------- /src/components/Radio.vue: -------------------------------------------------------------------------------- 1 | 35 | 67 | 68 | -------------------------------------------------------------------------------- /src/examples/views/ChkInputView.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 69 | 70 | -------------------------------------------------------------------------------- /src/components/Textarea.vue: -------------------------------------------------------------------------------- 1 | 17 | 68 | 69 | -------------------------------------------------------------------------------- /src/components/AutoComplete.vue: -------------------------------------------------------------------------------- 1 | 21 | 68 | -------------------------------------------------------------------------------- /src/components/Text.vue: -------------------------------------------------------------------------------- 1 | 27 | 57 | 62 | -------------------------------------------------------------------------------- /src/meta/base.meta.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import { FORM_VALUE_CHANGE } from "@/utils/consts.js"; 3 | 4 | class BaseMeta { 5 | constructor(state, id, meta) { 6 | if (this.constructor == BaseMeta) { 7 | throw new Error("Abstract classes can't be instantiated."); 8 | } 9 | 10 | this.id = id; 11 | this.state = state; 12 | this.meta = meta; 13 | this.type = (this.meta.ui && this.meta.ui.component) || this.meta.type; 14 | this.ui = Object.assign({}, this.state.ui, this.meta.ui); 15 | 16 | // TODO:可能需要一个getter/setter,setter需要判断是否有错误,有错误才设置上去 17 | this.error = undefined; 18 | state.context.addContext(id, this); 19 | this._value = undefined; 20 | 21 | this._initMetaValue = this.getInitMetaValue(); 22 | this.initValue(); 23 | } 24 | 25 | initValue() { 26 | if (this._initMetaValue) { 27 | this.value = this._initMetaValue; 28 | } else if (this.meta.default) { 29 | this.value = this.meta.default; 30 | } 31 | } 32 | 33 | setValue(val) { 34 | this._value = val || undefined; 35 | } 36 | 37 | get value() { 38 | return this._value; 39 | } 40 | 41 | set value(val) { 42 | if (this._value === val) return; 43 | 44 | this.setValue(val); 45 | 46 | Vue.bus.emit(`${FORM_VALUE_CHANGE}-${this.state._formId}`, { 47 | id: this.id, 48 | value: this._value, 49 | }); 50 | 51 | this.state.updateObjProp(this.state.formData, this.id, this._value); 52 | this.state.validate.runValidationFormItem(this); 53 | } 54 | 55 | /** 56 | * v-formly 中通过v-model传入的组件初始值 57 | * @returns 组件初始值 58 | */ 59 | getInitMetaValue() { 60 | const props = this.id.split("/").filter((f) => f); 61 | let curVal = ""; 62 | props.reduce((acc, key, idx) => { 63 | if (idx === props.length - 1) { 64 | curVal = acc[key]; 65 | } 66 | 67 | return acc[key] || {}; 68 | }, this.state.formData); 69 | 70 | return curVal; 71 | } 72 | } 73 | 74 | // 注意:此类为基类,不能直接实例化 75 | export { BaseMeta }; 76 | -------------------------------------------------------------------------------- /src/components/String.vue: -------------------------------------------------------------------------------- 1 | 29 | 66 | 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v-formly", 3 | "version": "1.0.2", 4 | "private": false, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build:examples": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "build": "rollup -c", 10 | "docs:dev": "vuepress dev docs", 11 | "docs:build": "vuepress build docs" 12 | }, 13 | "author": "KevinZhang19870314", 14 | "license": "MIT", 15 | "main": "./lib/v-formly.umd.js", 16 | "dependencies": { 17 | "ajv": "^8.11.0", 18 | "ant-design-vue": "1.7.8", 19 | "core-js": "^3.6.5", 20 | "vue": "^2.6.11" 21 | }, 22 | "devDependencies": { 23 | "@babel/core": "^7.19.3", 24 | "@babel/runtime": "^7.19.4", 25 | "@rollup/plugin-alias": "^4.0.0", 26 | "@rollup/plugin-babel": "^6.0.0", 27 | "@rollup/plugin-commonjs": "^23.0.0", 28 | "@rollup/plugin-node-resolve": "^15.0.0", 29 | "@vue/cli-plugin-babel": "~4.5.18", 30 | "@vue/cli-plugin-eslint": "~4.5.18", 31 | "@vue/cli-plugin-router": "~4.5.18", 32 | "@vue/cli-service": "~4.5.18", 33 | "@vue/compiler-sfc": "^3.2.40", 34 | "@vuepress/plugin-back-to-top": "^1.9.7", 35 | "@vuepress/plugin-medium-zoom": "^1.9.7", 36 | "@vuepress/plugin-pwa": "^1.9.7", 37 | "babel-eslint": "^10.1.0", 38 | "babel-plugin-import": "^1.13.5", 39 | "eslint": "^6.7.2", 40 | "eslint-plugin-vue": "^6.2.2", 41 | "gh-pages": "^4.0.0", 42 | "less": "^2.7.3", 43 | "less-loader": "^4.1.0", 44 | "lodash": "^4.17.21", 45 | "postcss": "^8.4.18", 46 | "rollup": "~2.78.0", 47 | "rollup-plugin-postcss": "^4.0.2", 48 | "rollup-plugin-terser": "^7.0.2", 49 | "rollup-plugin-uglify": "^6.0.4", 50 | "rollup-plugin-vue": "5.1.9", 51 | "vue-router": "^3.5.1", 52 | "vue-template-compiler": "^2.7.12", 53 | "vuepress": "^1.9.7", 54 | "vuepress-plugin-container": "^2.1.5", 55 | "vuepress-plugin-demo-container": "^0.2.0" 56 | }, 57 | "browserslist": [ 58 | "> 1%", 59 | "last 2 versions", 60 | "not dead" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /docs/zh/guide/custom-validator.md: -------------------------------------------------------------------------------- 1 | # 自定义校验 2 | 3 | v-formly 除了支持标准的[JSON Schema](https://js-schema.org/)和[Ajv](https://ajv.js.org/)校验以外,还支持自定义校验规则及错误文本内容。 4 | 5 | ::: tip 注意 6 | 不管采用哪种方式来构建错误文本,都必须通过`keyword`来区分错误类型。 7 | ::: 8 | 9 | ## 自定义错误文本内容 10 | 11 | 支持全局定义`errors`和局部更改`ui.errors`覆盖默认的错误文本。 12 | 13 | ### 全局修改 14 | 15 | ```js {4-8} 16 | import VFormly from "@/formly.js"; 17 | 18 | Vue.use(VFormly, { 19 | ui: { 20 | errors: { 21 | required: "必填项", 22 | }, 23 | }, 24 | }); 25 | ``` 26 | 27 | ### 局部更改 28 | 29 | 局部更改即在编写某个表单的 meta 时候覆盖某个属性的`ui.errors`。 30 | 31 | ```js {8,10-13} 32 | meta: { 33 | type: "object", 34 | properties: { 35 | name: { 36 | title: "姓名", 37 | type: "string", 38 | default: "kevin", 39 | ui: { 40 | showRequired: true, 41 | errors: { 42 | "required": "请输入姓名" 43 | } 44 | }, 45 | }, 46 | }, 47 | required: ["name"], 48 | } 49 | ``` 50 | 51 | ## 自定义校验 52 | 53 | 标准校验有时候并不一定满足业务需求,这里就需要写自定义校验。 54 | 55 | ### 同步校验 56 | 57 | ```js {8,10-11} 58 | meta: { 59 | type: "object", 60 | properties: { 61 | name: { 62 | title: "姓名", 63 | type: "string", 64 | default: "kevin", 65 | ui: { 66 | showRequired: true, 67 | validator: (val) => !val ? [{ keyword: "required", message: "Required name" }] : [], 68 | }, 69 | }, 70 | }, 71 | required: ["name"], 72 | }, 73 | ``` 74 | 75 | ### 异步校验 76 | 77 | ```js {7,9-17} 78 | meta: { 79 | type: "object", 80 | properties: { 81 | asyncError: { 82 | title: "异步错误(2秒)", 83 | type: "string", 84 | ui: { 85 | showRequired: true, 86 | validatorAsync: (val) => { 87 | return new Promise((resolve) => { 88 | setTimeout(() => { 89 | resolve( 90 | !val ? [{ keyword: "required", message: "Required asyncError",}] : [] 91 | ); 92 | }, 2000); 93 | }); 94 | }, 95 | }, 96 | }, 97 | }, 98 | required: ["asyncError"], 99 | }, 100 | ``` 101 | -------------------------------------------------------------------------------- /src/examples/views/NumberView.vue: -------------------------------------------------------------------------------- 1 | 11 | 77 | 78 | -------------------------------------------------------------------------------- /src/components/Checkbox.vue: -------------------------------------------------------------------------------- 1 | 39 | 76 | 77 | -------------------------------------------------------------------------------- /docs/zh/components/object.md: -------------------------------------------------------------------------------- 1 | # Object 对象 2 | 3 | 创建对象,`type="object"`。 4 | 5 | ## 代码演示 6 | 7 | ::: demo 8 | 9 | ```vue 10 | 19 | 20 | 79 | 80 | ``` 81 | 82 | ::: 83 | 84 | ## API 85 | 86 | **我们只列出属性中不一致的或新添加的,一致的地方请参考 AntDv 文档** 87 | 88 | 对象组件是一个包裹组件,包裹着诸如`string`,`number`等类型的组件,所以API请参考具体包裹组件即可。 89 | -------------------------------------------------------------------------------- /src/components/Object.vue: -------------------------------------------------------------------------------- 1 | 28 | 76 | 77 | -------------------------------------------------------------------------------- /src/examples/views/TagView.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 80 | 81 | -------------------------------------------------------------------------------- /src/examples/views/CustomValidatorView.vue: -------------------------------------------------------------------------------- 1 | 10 | 79 | 80 | -------------------------------------------------------------------------------- /src/examples/views/DateView.vue: -------------------------------------------------------------------------------- 1 | 14 | 86 | 87 | -------------------------------------------------------------------------------- /src/utils/global.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import { UUID } from "@/utils/utils"; 3 | 4 | class Global { 5 | constructor() { 6 | this._formId = UUID(4); 7 | this._context = null; 8 | this._layout = "horizontal"; 9 | this._ajvOptions = { 10 | allErrors: true, 11 | strict: false, 12 | loopEnum: 50, 13 | }; 14 | this._formData = null; 15 | this._meta = {}; 16 | this._ui = { 17 | ingoreKeywords: ["type", "enum"], 18 | spanLabel: 5, 19 | spanControl: 19, 20 | grid: { 21 | gutter: 36, 22 | span: 24, 23 | }, 24 | errors: { 25 | required: "必填项", 26 | }, 27 | }; 28 | this._validate = null; 29 | this._ignoreErrorIds = []; 30 | 31 | this._applyUseOptions(); 32 | } 33 | 34 | get context() { 35 | return this._context; 36 | } 37 | 38 | set context(val) { 39 | this._context = val; 40 | } 41 | 42 | get layout() { 43 | return this._layout; 44 | } 45 | 46 | set layout(val) { 47 | this._layout = val; 48 | } 49 | 50 | get ajvOptions() { 51 | return this._ajvOptions; 52 | } 53 | 54 | set ajvOptions(val) { 55 | this._ajvOptions = val; 56 | } 57 | 58 | get formData() { 59 | return this._formData; 60 | } 61 | 62 | set formData(val) { 63 | this._formData = val; 64 | } 65 | 66 | get meta() { 67 | return this._meta; 68 | } 69 | 70 | set meta(val) { 71 | this._meta = val; 72 | } 73 | 74 | get ui() { 75 | return this._ui; 76 | } 77 | 78 | set ui(val) { 79 | this._ui = Object.assign({}, this._ui, val); 80 | } 81 | 82 | get validate() { 83 | return this._validate; 84 | } 85 | 86 | set validate(val) { 87 | this._validate = val; 88 | } 89 | 90 | updateObjProp(obj, propPath, value) { 91 | const [head, ...rest] = propPath.split("/").filter((f) => f); 92 | 93 | if (rest.length) { 94 | this.updateObjProp(obj[head], rest.join("/"), value); 95 | } else { 96 | if (obj) { 97 | obj[head] = value; 98 | } 99 | } 100 | } 101 | 102 | _applyUseOptions() { 103 | const options = Vue.prototype.$VFORMLY_OPTIONS; 104 | if (!options || typeof options !== "object") return; 105 | 106 | this.ui = options.ui; 107 | } 108 | } 109 | 110 | export { Global }; 111 | -------------------------------------------------------------------------------- /src/examples/views/CheckboxView.vue: -------------------------------------------------------------------------------- 1 | 11 | 87 | 88 | -------------------------------------------------------------------------------- /docs/zh/guide/terms.md: -------------------------------------------------------------------------------- 1 | # 名词解释 2 | 3 | ## meta 4 | 5 | `meta`在 v-formly 中代表的是标准的 JSON Schema 和嵌套的`ui`结合的结构。例如如下结构: 6 | 7 | ```json {9-11,19-21} 8 | { 9 | "type": "object", 10 | "properties": { 11 | "name": { 12 | "title": "姓名", 13 | "type": "string", 14 | "default": "kevin", 15 | "ui": { 16 | "showRequired": true 17 | } 18 | }, 19 | "obj": { 20 | "type": "object", 21 | "properties": { 22 | "subObj": { 23 | "title": "test", 24 | "type": "string", 25 | "ui": { 26 | "showRequired": true 27 | } 28 | } 29 | }, 30 | "required": ["subObj"] 31 | }, 32 | "enable": { 33 | "title": "启用", 34 | "type": "boolean" 35 | } 36 | }, 37 | "required": ["name"] 38 | } 39 | ``` 40 | 41 | 这是一个标准的`meta`格式的数据结构,除了高亮的`ui`部分,它其实是一个标准的 JSON Schema 结构,我们把它传入 v-formly 中即可渲染出表单如下。 42 | 43 | ::: demo 44 | 45 | ```vue 46 | 49 | 50 | 90 | ``` 91 | 92 | ::: 93 | 94 | 我们称此 JSON 结构为`meta`,当然此结构中也嵌套了许多子`meta`,比如`name`,`obj`,`subObj`,`enable`对应的值都称之为`meta`。 95 | 96 | ## 类 meta (js class) 97 | 98 | 当我们把`meta`传入 v-formly 中,接收它的是我们的**类 meta**,**类 meta**是一个 JavaScript 的 class 类,它负责保存及处理表单中每个类型的表单项的处理逻辑及数据。 99 | 100 | ## context 101 | 102 | **类 meta**实例化之后就是`context`,每一个表单项都有一个对应的`context`。 103 | -------------------------------------------------------------------------------- /src/components/Tag.vue: -------------------------------------------------------------------------------- 1 | 2 | 16 | 77 | 82 | -------------------------------------------------------------------------------- /src/components/Cascader.vue: -------------------------------------------------------------------------------- 1 | 29 | 79 | 80 | -------------------------------------------------------------------------------- /src/components/Slider.vue: -------------------------------------------------------------------------------- 1 | 19 | 84 | 85 | -------------------------------------------------------------------------------- /src/FormlyItem.vue: -------------------------------------------------------------------------------- 1 | 15 | 90 | 91 | -------------------------------------------------------------------------------- /src/examples/views/SubmitButtonView.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 97 | 98 | -------------------------------------------------------------------------------- /src/examples/views/BooleanView.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 103 | 104 | -------------------------------------------------------------------------------- /src/components/Time.vue: -------------------------------------------------------------------------------- 1 | 48 | 92 | 93 | -------------------------------------------------------------------------------- /src/components/Number.vue: -------------------------------------------------------------------------------- 1 | 19 | 99 | 100 | -------------------------------------------------------------------------------- /docs/zh/components/number.md: -------------------------------------------------------------------------------- 1 | # Number 数字输入框 2 | 3 | 通过鼠标或键盘,输入范围内的数值。 4 | 5 | ## 代码演示 6 | 7 | ::: demo 8 | 9 | ```vue 10 | 20 | 78 | 79 | ``` 80 | 81 | ::: 82 | 83 | ## API 84 | 85 | **我们只列出属性中不一致的或新添加的,一致的地方请参考 AntDv 文档** 86 | 87 | ### meta 属性 88 | 89 | | 成员 | 说明 | 类型 | 默认值 | 90 | | -------------------- | ------------------------------------------------------- | --------- | ------ | 91 | | `[minimum]` | 最小值 | `number` | - | 92 | | `[exclusiveMinimum]` | 约束是否包括 `minimum` 值,`true` 表示排除 `minimum` 值 | `boolean` | - | 93 | | `[maximum]` | 最大值 | `number` | - | 94 | | `[exclusiveMaximum]` | 约束是否包括 `maximum` 值,`true` 表示排除 `maximum` 值 | `boolean` | - | 95 | | `[multipleOf]` | 倍数 | `number` | `1` | 96 | 97 | ### meta.ui 属性 98 | 99 | | 成员 | 说明 | 类型 | 默认值 | 100 | | --------- | -------- | ----------------------- | -------- | --- | 101 | | `@change` | 变化回调 | `Function(value: number | string)` | - | 102 | -------------------------------------------------------------------------------- /src/components/Wrapper.vue: -------------------------------------------------------------------------------- 1 | 37 | 105 | 106 | -------------------------------------------------------------------------------- /src/examples/views/RateView.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 109 | 110 | -------------------------------------------------------------------------------- /docs/zh/components/tag.md: -------------------------------------------------------------------------------- 1 | # Tag 标签 2 | 3 | 进行标记和分类的小标签,注: 只支持 checkable 标签模式。 4 | 5 | ## 代码演示 6 | 7 | ::: demo 8 | 9 | ```vue 10 | 20 | 21 | 89 | ``` 90 | 91 | ::: 92 | 93 | ## API 94 | 95 | **我们只列出属性中不一致的或新添加的,一致的地方请参考 AntDv 文档** 96 | 97 | ### meta 属性 98 | 99 | | 成员 | 说明 | 类型 | 默认值 | 100 | | ----------- | -------- | --------- | --------------------- | --- | 101 | | `:enum` | 数据源 | `any[] | array<{value, label>` | - | 102 | | `:readOnly` | 禁用状态 | `boolean` | - | 103 | 104 | ### meta.ui 属性 105 | 106 | | 成员 | 说明 | 类型 | 默认值 | 107 | | ---------------- | ------------------------ | ------------------- | ------ | 108 | | `@change` | 点击标签时触发的回调 | `function(value)` | - | 109 | | `@checkedChange` | 设置标签的选中状态的回调 | `function(checked)` | - | 110 | -------------------------------------------------------------------------------- /src/components/Date.vue: -------------------------------------------------------------------------------- 1 | 57 | 107 | 108 | -------------------------------------------------------------------------------- /docs/zh/components/date.md: -------------------------------------------------------------------------------- 1 | # Date 日期选择框 2 | 3 | 输入或选择日期的控件。 4 | 5 | ## 代码演示 6 | 7 | ::: demo 8 | 9 | ```vue 10 | 23 | 95 | 96 | ``` 97 | 98 | ::: 99 | 100 | ## API 101 | 102 | **我们只列出属性中不一致的或新添加的,一致的地方请参考 AntDv 文档** 103 | 104 | ### meta 属性 105 | 106 | | 成员 | 说明 | 类型 | 默认值 | 107 | | ----------- | -------- | --------- | ------ | 108 | | `:readOnly` | 禁用状态 | `boolean` | - | 109 | 110 | ### meta.ui 属性 111 | 112 | | 成员 | 说明 | 类型 | 默认值 | 113 | | ----------------------- | --------------------------------- | ----------------------------------------- | ------ | 114 | | `:slotNameOfSuffixIcon` | 自定义的选择框后缀图标,slot 名称 | `string` | - | 115 | | `@change` | 输入框内容变化时的回调 | `Function(checked:Boolean, event: Event)` | - | 116 | -------------------------------------------------------------------------------- /src/examples/views/TextareaView.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 100 | 101 | -------------------------------------------------------------------------------- /docs/zh/components/checkbox.md: -------------------------------------------------------------------------------- 1 | # Checkbox 多选框 2 | 3 | 多选框 4 | 5 | ## 代码演示 6 | 7 | ::: demo 8 | 9 | ```vue 10 | 20 | 88 | 89 | ``` 90 | 91 | ::: 92 | 93 | ## API 94 | 95 | **我们只列出属性中不一致的或新添加的,一致的地方请参考 AntDv 文档** 96 | 97 | ### meta 属性 98 | 99 | | 成员 | 说明 | 类型 | 默认值 | 100 | | ----------- | ------------------------------------ | --------- | -------------------------------------------------------------------------------------------------------- | --- | 101 | | `:enum` | 数据源,当数据源存在于表示一组多选框 | `string[] | Array<{ label: string value: string disabled?: boolean, indeterminate?: boolean, onChange?: function }>` | - | 102 | | `:readOnly` | 禁用状态 | `boolean` | - | 103 | 104 | ### meta.ui 属性 105 | 106 | **参考 Checkbox Group 的属性** 107 | 108 | | 成员 | 说明 | 类型 | 默认值 | 109 | | --------- | -------------- | ------------------------ | ------ | 110 | | `@change` | 变化时回调函数 | `Function(checkedValue)` | - | 111 | -------------------------------------------------------------------------------- /src/examples/views/TextView.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 114 | 115 | -------------------------------------------------------------------------------- /src/examples/views/RadioView.vue: -------------------------------------------------------------------------------- 1 | 12 | 113 | 114 | -------------------------------------------------------------------------------- /docs/zh/guide/README.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 3 | v-formly 是 vue 的动态(JSON 驱动)表单库。它通过[JSON Schema](https://json-schema.org/)和[Ajv Validator](https://ajv.js.org/)结合生成复杂的动态表单及校验,快速、简洁、高效。通过使用 v-formly 及对应的组件库即可快速构造一个 Form 表单,目前支持 Vue 2.x & [Ant Design of Vue v1](https://1x.antdv.com/docs/vue/introduce-cn/),Vue 2.x 和 Vue 3.x 的其他 UI 库(AntDv v3,ElementUI 等)支持正在开发中。 4 | 5 | v-formly 内置封装了所有的在 Ant Design of Vue 中的`Data Entry`下面的组件,同时 v-formly 也支持[自定义封装组件](/zh/components/custom-components.html),从而可以让你轻松构建复杂的动态表单。 6 | 7 | ## 一个简单示例 8 | 9 | ::: demo 一个简单的 v-formly 示例,请打开控制台查看表单提交结果 10 | 11 | ```vue 12 | 21 | 22 | 71 | ``` 72 | 73 | ::: 74 | 75 | **针对上述示例我们做以下几点解释**: 76 | 77 | 1. v-formly 支持 v-model 双向绑定,可通过修改 data 来随时改变 form 表单数据; 78 | 79 | 2. 传入的 schema 是`JSON-Schema`结构 + 嵌套`ui`的组合,v-formly 使用 schema 来解析并渲染表单页面; 80 | 81 | 3. 以上表单包括两个`string`类型和一个`boolean`类型的内置`component`. 82 | 83 | a. 其中`name`为必填项(`required: ["name"]`体现出来),且默认内容为“kevin”,其中`ui.showRequired`为 true 会添加 label 前面的红色星号; 84 | 85 | b. `desc`非必填,默认内容为“Base on technical, but not limited on it!”,且提供了 change 事件,当输入改变时触发; 86 | 87 | c. `enable`为一个简单的 AntDv 的`Switch`组件。 88 | 89 | 通过上述简单的表单示例,我们大概了解了如何开始使用 v-formly,更多内容请查看[组件](/zh/components/)。 90 | 91 | ## 快速开始 92 | 93 | ### 安装 94 | 95 |
96 | 97 | 使用`yarn`安装`v-formly`: 98 | 99 | ```sh 100 | yarn add v-formly 101 | ``` 102 | 103 | 或者使用`npm`安装它: 104 | 105 | ```sh 106 | npm i v-formly --save 107 | ``` 108 | 109 | ### 使用 110 | 111 |
112 | 113 | ```js 114 | // 别忘了引入Ant Design of Vue 1.7.8,v-formly当前只支持这个版本。 115 | 116 | import VFormly from "v-formly"; 117 | 118 | // ... 119 | Vue.use(VFormly); 120 | // ... 121 | ``` 122 | 123 | ## 参考 124 | 125 | **v-formly 参考了以下文章及三方库**: 126 | 127 | 1. [Create a Vue.js component library](https://itnext.io/create-a-vue-js-component-library-as-a-module-part-1-a1116e632751); 128 | 129 | 2. [基于 Angular 的动态表单库@delon/form](https://ng-alain.com/form/getting-started/zh); 130 | 131 | 3. [Ant Design of Vue 1.7.8](https://1x.antdv.com/docs/vue/introduce-cn/); 132 | -------------------------------------------------------------------------------- /src/examples/layout/BasicLayout.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 92 | 93 | 136 | -------------------------------------------------------------------------------- /docs/zh/components/README.md: -------------------------------------------------------------------------------- 1 | # 组件介绍 2 | 3 | v-formly 内置了一些基础组件,包括常用的输入框、选择框、单选、多选、下拉框、上传等组件,覆盖了大多数业务所需要的组件需求,如果这些内置组件不足以满足你的业务,你也可以[自定义组件](/zh/components/custom-components.html)。 4 | 5 | ## 内置组件属性 6 | 7 | v-formly 中的内置组件为了保证与原生 AntDv 组件的功能保持一致性,尽量不破坏原有属性名称和定义,所以在下述具体组件介绍中,**我们只列出属性中不一致的或新添加的,一致的地方请参考 AntDv 文档**,我们列出了组件对应关系如下,请自行查阅: 8 | 9 | ### 内置组件与 AntDv 组件一一对应关系 10 | 11 | ::: details 内置组件与 AntDv 组件一一对应关系 => 12 | 13 | [AutoComplete 自动完成](/zh/components/autocomplete.html) -> [AutoComplete 自动完成](https://1x.antdv.com/components/auto-complete-cn/) 14 | 15 | [Boolean 开关](/zh/components/boolean.html) -> [Switch 开关](https://1x.antdv.com/components/switch-cn/) 16 | 17 | [Cascader 级联选择](/zh/components/cascader.html) -> [Cascader 级联选择](https://1x.antdv.com/components/cascader-cn/) 18 | 19 | [Checkbox 多选框](/zh/components/checkbox.html) -> [Checkbox 多选框](https://1x.antdv.com/components/checkbox-cn/) 20 | 21 | [Date 日期选择框](/zh/components/date.html) -> [DatePicker 日期选择框](https://1x.antdv.com/components/date-picker-cn/) 22 | 23 | [Number 数字输入框](/zh/components/number.html) -> [InputNumber 数字输入框](https://1x.antdv.com/components/input-number-cn/) 24 | 25 | [Radio 单选框](/zh/components/radio.html) -> [Radio 单选框](https://1x.antdv.com/components/radio-cn/) 26 | 27 | [Rate 评分](/zh/components/rate.html) -> [Rate 评分](https://1x.antdv.com/components/rate-cn/) 28 | 29 | [Select 选择器](/zh/components/select.html) -> [Select 选择器](https://1x.antdv.com/components/select-cn/) 30 | 31 | [Slider 滑动输入条](/zh/components/slider.html) -> [Slider 滑动输入条](https://1x.antdv.com/components/slider-cn/) 32 | 33 | [String 文本框](/zh/components/string.html) -> [Input 输入框](https://1x.antdv.com/components/input-cn/) 34 | 35 | [Tag 标签](/zh/components/tag.html) -> [Tag 标签](https://1x.antdv.com/components/tag-cn/) 36 | 37 | [Textarea 多行文本框](/zh/components/textarea.html) -> [Input 输入框](https://1x.antdv.com/components/input-cn/#components-input-demo-textarea) 38 | 39 | [Time 时间选择器](/zh/components/time.html) -> [TimePicker 时间选择框](https://1x.antdv.com/components/time-picker-cn/) 40 | ::: 41 | 42 | ## 关于`slot`的传递 43 | 44 | v-formly 中的内置组件封装的是 AntDv 的表单组件,这样就带来一个问题,一些类型为`slot`的属性我们无法透传,比如`Input 输入框`的`addonBefore`属性,它即可以直接传字符串,这个没问题,但是`slot`我们没法传进去,因为我们对`a-input`做了封装,在使用 v-formly 的时候我们无法直接接触`a-input`组件,所以这里我让模板``一级一级的从父组件传递到子组件,再传递到孙子组件,直到传递到 AntDv 原生组件为止。下面我们举例说明一下: 45 | 46 | ::: demo 47 | 48 | ```vue 49 | 64 | 65 | 89 | 90 | ``` 91 | 92 | ::: 93 | 94 | 从上面的代码中,我们可以看到,我们这里的前缀使用了`slot`来传值,我们只需要在`meta`里面定义`slotNameOfPrefix`来标明它的`slot name`,然后在`v-formly`模板标签里面定义我们需要传递的模板内容,从而让表单知道我需要把这个`slot`模板传递到 AntDv 的输入框,这样就达到了`slot`的传递。 95 | 96 | ::: tip 97 | **v-formly 的每个表单项的属性中如果有需要传递`slot`的,必须保证`slot name`的唯一(比如这里的`slotNameOfPrefix`和`slotNameOfSuffix`的值必须不一样),否则无法识别或者识别错误。** 98 | ::: 99 | -------------------------------------------------------------------------------- /docs/zh/components/text.md: -------------------------------------------------------------------------------- 1 | # Text 文本 2 | 3 | 一般用于直接显示文本。 4 | 5 | ## 代码演示 6 | 7 | ::: demo 8 | 9 | ```vue 10 | 22 | 23 | 123 | ``` 124 | 125 | ::: 126 | 127 | ## API 128 | 129 | **我们只列出属性中不一致的或新添加的,一致的地方请参考 AntDv 文档** 130 | 131 | ### meta.ui 属性 132 | 133 | | 成员 | 说明 | 类型 | 默认值 | 134 | | -------------------- | ------------- | -------- | ------------- | --- | 135 | | `:text` | 指定的 `text` | `string | () => string` | | 136 | | `:html` | 指定的 `html` | `HTML` | - | 137 | | `:slotNameOfDefault` | 指定的 `slot` | `string` | - | 138 | 139 | ::: tip 注意 140 | 文本显示的优先级:slot > html > text 141 | ::: 142 | -------------------------------------------------------------------------------- /src/components/Select.vue: -------------------------------------------------------------------------------- 1 | 45 | 132 | 133 | -------------------------------------------------------------------------------- /docs/zh/components/rate.md: -------------------------------------------------------------------------------- 1 | # Rate 评分 2 | 3 | 对评价进行展示,对事物进行快速的评级操作。 4 | 5 | ## 代码演示 6 | 7 | ::: demo 8 | 9 | ```vue 10 | 22 | 23 | 118 | ``` 119 | 120 | ::: 121 | 122 | ## API 123 | 124 | **我们只列出属性中不一致的或新添加的,一致的地方请参考 AntDv 文档** 125 | 126 | ### meta 属性 127 | 128 | | 成员 | 说明 | 类型 | 默认值 | 129 | | ----------- | -------- | --------- | ------ | 130 | | `:maximum` | 总星数 | `number` | 5 | 131 | | `:readOnly` | 禁用状态 | `boolean` | - | 132 | 133 | ### meta.ui 属性 134 | 135 | | 成员 | 说明 | 类型 | 默认值 | 136 | | ---------------------- | ------------------------ | ------------------------- | ------- | 137 | | `:character` | 自定义字符 | `string` | - | 138 | | `:slotNameOfCharacter` | 自定义字符 slot | `string` | - | 139 | | `@change` | 选择时的回调 | `function(value: number)` | - | 140 | | `@hoverChange` | 鼠标经过时数值变化的回调 | `function(value: number)` | - | 141 | -------------------------------------------------------------------------------- /src/examples/views/ArrayView.vue: -------------------------------------------------------------------------------- 1 | 14 | 133 | 134 | -------------------------------------------------------------------------------- /docs/zh/components/boolean.md: -------------------------------------------------------------------------------- 1 | # Boolean 开关 2 | 3 | 开关选择器。 4 | 5 | ## 代码演示 6 | 7 | ::: demo 8 | 9 | ```vue 10 | 27 | 28 | 112 | 113 | ``` 114 | 115 | ::: 116 | 117 | ## API 118 | 119 | **我们只列出属性中不一致的或新添加的,一致的地方请参考 AntDv 文档** 120 | 121 | ### meta 属性 122 | 123 | | 成员 | 说明 | 类型 | 默认值 | 124 | | ------------ | -------- | --------- | ------ | 125 | | `:readOnly` | 禁用状态 | `boolean` | - | 126 | 127 | ### meta.ui 属性 128 | 129 | | 成员 | 说明 | 类型 | 默认值 | 130 | | ------------------------------- | ------------------------------- | ----------------------------------------- | --------- | 131 | | `:checkedChildren` | 选中时的内容 | `string` | - | 132 | | `:slotNameOfCheckedChildren` | 选中时的内容,slot 名称 | `string` | - | 133 | | `:unCheckedChildren` | 非选中时的内容 | `string` | - | 134 | | `:slotNameOfUnCheckedChildren` | 非选中时的内容,slot 名称 | `string` | - | 135 | | `@change` | 输入框内容变化时的回调 | `Function(checked:Boolean, event: Event)` | - | 136 | -------------------------------------------------------------------------------- /docs/zh/components/radio.md: -------------------------------------------------------------------------------- 1 | # Radio 单选框 2 | 3 | 单选框。 4 | 5 | ## 代码演示 6 | 7 | ::: demo 8 | 9 | ```vue 10 | 21 | 122 | 123 | ``` 124 | 125 | ::: 126 | 127 | ## API 128 | 129 | **我们只列出属性中不一致的或新添加的,一致的地方请参考 AntDv 文档** 130 | 131 | ### meta 属性 132 | 133 | | 成员 | 说明 | 类型 | 默认值 | 134 | | ----------- | -------- | --------- | ------ | 135 | | `:readOnly` | 禁用状态 | `boolean` | - | 136 | 137 | ### meta.ui 属性 138 | 139 | 参考 RadioGroup 组件属性 140 | 141 | | 成员 | 说明 | 类型 | 默认值 | 142 | | --------------- | -------------------------------------------------- | ---------------------- | ------- | 143 | | `:component` | 指定组件为'radio' | `string` | `radio` | 144 | | `:direction` | 单选框展示方向,默认横向展示,`vertical`则竖向展示 | `string` | - | 145 | | `:showRequired` | 是否显示标签前的红色\*号 | `boolean` | false | 146 | | `@change` | 选项变化时的回调函数 | `Function(value: any)` | - | 147 | -------------------------------------------------------------------------------- /docs/zh/components/textarea.md: -------------------------------------------------------------------------------- 1 | # Textarea 多行文本框 2 | 3 | 一般用于多行字符串。 4 | 5 | ## 代码演示 6 | 7 | ::: demo 8 | 9 | ```vue 10 | 19 | 20 | 109 | ``` 110 | 111 | ::: 112 | 113 | ## API 114 | 115 | **我们只列出属性中不一致的或新添加的,一致的地方请参考 AntDv 文档** 116 | 117 | ### meta 属性 118 | 119 | | 成员 | 说明 | 类型 | 默认值 | 120 | | ------------ | -------- | --------- | ------ | 121 | | `:maxLength` | 最大长度 | `number` | - | 122 | | `:readOnly` | 禁用状态 | `boolean` | - | 123 | 124 | ### meta.ui 属性 125 | 126 | | 成员 | 说明 | 类型 | 默认值 | 127 | | -------------- | ----------------------------- | ---------------------------------------- | -------- | ------- | ---- | 128 | | `:autosize` | 自适应内容高度,可设置为`true | false`或对象:{ minRows: 2, maxRows: 6 } | `boolean | object` | true | 129 | | `:placeholder` | 默认文字 | `string` | - | 130 | | `:allowClear` | 可以点击清除图标删除内容 | `boolean` | - | 131 | | `@change` | 内容变更事件 | `function(value)` | - | 132 | | `@focus` | 焦点事件 | `function(e)` | - | 133 | | `@blur` | 失焦事件 | `function(e)` | - | 134 | | `@pressEnter` | 按下回车事件 | `function(e)` | - | 135 | -------------------------------------------------------------------------------- /src/utils/validate.factory.js: -------------------------------------------------------------------------------- 1 | const Ajv = require("ajv"); 2 | import Vue from "vue"; 3 | import { FORM_ERROR_CHANGE } from "@/utils/consts.js"; 4 | 5 | class ValidateFactory { 6 | constructor(state) { 7 | this.state = state; 8 | this._ajv = new Ajv(state.ajvOptions); 9 | this._validate = null; 10 | } 11 | 12 | // TODO 提交表单时,界面应该抛出所有错误 13 | async runValidateForm() { 14 | const { valid, errors } = this._isAjvValid(); 15 | const contexts = this.state.context.getContexts(); 16 | const instances = contexts.values(); 17 | let isValid = true; 18 | for (const instance of instances) { 19 | isValid = (await this._validation(instance, valid, errors)) && isValid; 20 | } 21 | 22 | return isValid; 23 | } 24 | 25 | async runValidationFormItem(context) { 26 | const { valid, errors } = this._isAjvValid(); 27 | let isValid = await this._validation(context, valid, errors); 28 | return isValid; 29 | } 30 | 31 | _ajvValidate(meta) { 32 | if (this._validate) return this._validate; 33 | 34 | this._validate = this._ajv.compile(meta); 35 | return this._validate; 36 | } 37 | 38 | _getAjvError(id, errors) { 39 | let _error = undefined; 40 | for (let i = 0; i < errors.length; i++) { 41 | const error = errors[i]; 42 | const _id = this._getId(error); 43 | if (id === _id) { 44 | _error = error; 45 | break; 46 | } 47 | } 48 | 49 | return _error; 50 | } 51 | 52 | _isAjvValid() { 53 | const validate = this._ajvValidate(this.state.meta); 54 | const valid = validate(this.state.formData || {}); 55 | 56 | return { valid, errors: validate.errors }; 57 | } 58 | 59 | async _validation(context, valid, errs) { 60 | let errors = []; 61 | const ERROR_CHANGE = `${FORM_ERROR_CHANGE}-${this.state._formId}`; 62 | if (!valid) { 63 | const customErrors = this._getCustomError(context); 64 | const customAsyncErrors = await this._getCustomAsyncError(context); 65 | const cusErrors = [...customErrors, ...customAsyncErrors]; 66 | this._replaceWithDefaultErrors(context, errs); 67 | this._replaceWithCustomErrors(context.id, errs, cusErrors); 68 | const ingoreKeywords = this.state.ui.ingoreKeywords || []; 69 | errors = errs.filter((f) => ingoreKeywords.indexOf(f.keyword) === -1); 70 | errors = this._removeIgnoreErrors(errors); 71 | const error = this._getAjvError(context.id, errors); 72 | Vue.bus.emit(ERROR_CHANGE, { 73 | id: context.id, 74 | error: error, 75 | }); 76 | 77 | return error ? false : true; 78 | } else { 79 | Vue.bus.emit(ERROR_CHANGE, { 80 | id: context.id, 81 | error: undefined, 82 | }); 83 | 84 | return true; 85 | } 86 | } 87 | 88 | _getCustomError(context) { 89 | const validator = context.meta.ui && context.meta.ui.validator; 90 | if (!validator) return []; 91 | 92 | return validator(context.value); 93 | } 94 | 95 | async _getCustomAsyncError(context) { 96 | const validatorAsync = context.meta.ui && context.meta.ui.validatorAsync; 97 | if (!validatorAsync) return []; 98 | 99 | return await validatorAsync(context.value); 100 | } 101 | 102 | _replaceWithDefaultErrors(context, errors) { 103 | if (!errors || errors.length === 0) return; 104 | 105 | const localErrors = (context.meta.ui && context.meta.ui.errors) || {}; 106 | const globalErrors = Object.assign({}, this.state.ui.errors, localErrors); 107 | const keywords = Object.keys(globalErrors); 108 | if (keywords && keywords.length > 0) { 109 | errors.forEach((error) => { 110 | if (keywords.indexOf(error.keyword) > -1) { 111 | error.message = globalErrors[error.keyword]; 112 | } 113 | }); 114 | } 115 | } 116 | 117 | _replaceWithCustomErrors(id, errors, customErrors) { 118 | customErrors.forEach((err) => { 119 | let cur = errors.find( 120 | (f) => f.keyword === err.keyword && this._getId(f) === id 121 | ); 122 | if (cur) { 123 | cur.message = err.message; 124 | } 125 | }); 126 | } 127 | 128 | _removeIgnoreErrors(errors) { 129 | const ids = this.state._ignoreErrorIds; 130 | if (!ids || ids.length === 0) return errors; 131 | 132 | const errs = errors.filter((f) => ids.indexOf(this._getId(f)) === -1); 133 | 134 | return errs; 135 | } 136 | 137 | _getId(error) { 138 | const hasMissingProperty = error.params && error.params.missingProperty; 139 | if (hasMissingProperty) { 140 | return `${error.instancePath}/${error.params.missingProperty}`; 141 | } 142 | 143 | return `${error.instancePath}`; 144 | } 145 | } 146 | 147 | export { ValidateFactory }; 148 | -------------------------------------------------------------------------------- /docs/zh/guide/form.md: -------------------------------------------------------------------------------- 1 | # 表单 2 | 3 | ## 提交表单 4 | 5 | v-formly 提交表单的三种方式。 6 | 7 | 1. 使用默认的提交按钮,通过设置 `button='default'`。 8 | 2. 使用 slot 暴露出来的 `submit function`,通过设置 `button='custom'` 并传入 `name='button'` 的 slot。 9 | 3. 使用 ref 获取 form 实例,直接调用实例的 `validate function`,eg: `this.$refs.form.validate()`。 10 | 11 | ### 代码演示 12 | 13 | ::: demo 14 | 15 | ```vue 16 | 53 | 116 | ``` 117 | 118 | ::: 119 | 120 | ### API 121 | 122 | | 参数 | 说明 | 类型 | 默认值 | 123 | | ----------------- | ---------------------------- | ---------------------------------------- | ------------------------------------ | 124 | | `:layout` | 表单布局 | `'horizontal' | 'vertical' | 'inline'` | `'horizontal'` | 125 | | `:button` | 提交按钮 | `'default' | 'custom'` | 默认不显示提交按钮 | 126 | | `:meta` | JSON Schema + UI Schema | - | - | 127 | | `:value(v-model)` | 表单绑定数据 | `object` | - | 128 | | `button` | 自定义提交按钮 | `slot` | `{ loading, clearForm, submitForm }` | 129 | | `@form-reset` | 重置数据后回调事件 | `function(data)` | - | 130 | | `@form-submit` | 数据验证成功或失败后回调事件 | `function({ valid, data })` | - | 131 | 132 | #### 组件方法 133 | 134 | | 名称 | 描述 | 135 | | ------------ | ------------------------------------------- | 136 | | `validate` | 校验表单 | 137 | | `clearForm` | 重置表单,触发 form-reset 事件 | 138 | | `submitForm` | 校验表单,在校验完成后触发 form-submit 事件 | 139 | -------------------------------------------------------------------------------- /src/meta/array.meta.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import { UUID, deepClone } from "@/utils/utils"; 3 | import { BaseMeta } from "./base.meta"; 4 | 5 | class ArrayMeta extends BaseMeta { 6 | constructor(state, id, meta) { 7 | super(state, id, meta); 8 | 9 | this.ids = []; 10 | this.initValue(); 11 | } 12 | 13 | initValue() { 14 | if (!this.ids) return; 15 | if (this._initMetaValue && this._initMetaValue.length > 0) { 16 | this.value = this._initMetaValue; 17 | } else if (this.meta.default && this.meta.default.length > 0) { 18 | const data = deepClone(this.meta.default); 19 | data.forEach(() => this.add(false)); 20 | setTimeout(() => { 21 | data.forEach((item, index) => { 22 | const ctx = this.state.context.getContext(`${this.id}/${index}`); 23 | ctx.value = item; 24 | }); 25 | }); 26 | this.validate(); 27 | } 28 | } 29 | 30 | get value() { 31 | return this.getPathValue(this.state.formData, this.id) || []; 32 | } 33 | 34 | set value(val) { 35 | // 避免重复校验 36 | if (this.value.length === 0 && val && val.length === 0) return; 37 | // 只允许设置 null / undefined / Array 类型的值 38 | if (!Array.isArray(val) && val != null) return; 39 | const newVal = deepClone(val); 40 | const len = this.ids.length; 41 | // 从后往前删除 42 | for (let i = 0; i < len; i++) { 43 | this.remove(len - i - 1, false); 44 | } 45 | if (newVal && newVal.length) { 46 | newVal.forEach(() => this.add(false)); 47 | 48 | Vue.nextTick(() => { 49 | newVal.forEach((item, index) => { 50 | const ctx = this.state.context.getContext(`${this.id}/${index}`); 51 | ctx.value = item; 52 | }); 53 | }); 54 | } 55 | 56 | this.validate(); 57 | } 58 | 59 | validate() { 60 | return this.state.validate.runValidationFormItem(this); 61 | } 62 | 63 | getPathValue(sourceData, path) { 64 | const props = path.split("/").filter((f) => f); 65 | let res = deepClone(sourceData); 66 | for (let i = 0; i < props.length; i++) { 67 | const key = props[i]; 68 | res = res[key]; 69 | } 70 | return res; 71 | } 72 | 73 | // 根据 itmes properties 生成空对象 74 | getEmptyData() { 75 | const obj = {}; 76 | const properties = this.meta.items.properties; 77 | Object.keys(properties).forEach((key) => { 78 | const meta = properties[key]; 79 | switch (meta.type) { 80 | case "object": 81 | obj[key] = {}; 82 | break; 83 | case "array": 84 | obj[key] = []; 85 | break; 86 | default: 87 | obj[key] = undefined; 88 | break; 89 | } 90 | }); 91 | return obj; 92 | } 93 | 94 | // 在数组尾添加一个空对象 95 | add(validate = true) { 96 | const id = `${this.id}/${this.ids.length}`; 97 | const value = this.getEmptyData(); 98 | this.state.updateObjProp(this.state.formData, id, value); 99 | this.ids.push({ key: UUID() }); 100 | 101 | validate && this.validate(); 102 | return id; 103 | } 104 | 105 | remove(index, validate = true) { 106 | // update ids 107 | this.ids.splice(index, 1); 108 | // update formData 109 | const props = this.id.split("/").filter((f) => !!f); 110 | props.reduce((obj, key, idx) => { 111 | if (idx === props.length - 1) { 112 | obj[key].splice(index, 1); 113 | } 114 | return obj[key]; 115 | }, this.state.formData); 116 | // update context 117 | const keys = []; 118 | for (const key of this.state.context._map.keys()) { 119 | if (new RegExp(`^/?${this.id}/`).test(key)) { 120 | keys.push(key); 121 | } 122 | } 123 | for (let i = index + 1; i < this.ids.length + 1; i++) { 124 | const regex = new RegExp(`^(/?${this.id}/)${i}`); 125 | keys 126 | .filter((key) => regex.test(key)) 127 | .forEach((key) => { 128 | const ctx = this.state.context.getContext(key); 129 | const newKey = key.replace(regex, (match, p1) => { 130 | return `${p1}${i - 1}`; 131 | }); 132 | ctx.id = newKey; 133 | // object 的 id 比较特殊,需要重新计算 134 | if (ctx.childMetaPairs) { 135 | const newChildMetaPairs = ctx.buildChildMetaPairs(newKey, ctx.meta); 136 | ctx.childMetaPairs.forEach((item, idx) => { 137 | item.key = newChildMetaPairs[idx].key; 138 | }); 139 | } 140 | this.state.context.addContext(newKey, ctx); 141 | }); 142 | } 143 | const regex = new RegExp(`^/?${this.id}/${this.ids.length}`); 144 | keys 145 | .filter((key) => regex.test(key)) 146 | .forEach((key) => { 147 | this.state.context.removeContext(key); 148 | }); 149 | 150 | validate && this.validate(); 151 | } 152 | } 153 | 154 | export { ArrayMeta }; 155 | -------------------------------------------------------------------------------- /src/examples/views/AutoCompleteView.vue: -------------------------------------------------------------------------------- 1 | 26 | 150 | 151 | -------------------------------------------------------------------------------- /src/examples/layout/components/Menu/menu.js: -------------------------------------------------------------------------------- 1 | import Menu from "ant-design-vue/es/menu"; 2 | import Icon from "ant-design-vue/es/icon"; 3 | 4 | export default { 5 | name: "SMenu", 6 | props: { 7 | menu: { 8 | type: Array, 9 | required: true, 10 | }, 11 | theme: { 12 | type: String, 13 | required: false, 14 | default: "light", 15 | }, 16 | mode: { 17 | type: String, 18 | required: false, 19 | default: "inline", 20 | }, 21 | collapsed: { 22 | type: Boolean, 23 | required: false, 24 | default: false, 25 | }, 26 | }, 27 | data() { 28 | return { 29 | openKeys: [], 30 | selectedKeys: [], 31 | cachedOpenKeys: [], 32 | }; 33 | }, 34 | computed: { 35 | rootSubmenuKeys: (vm) => { 36 | const keys = []; 37 | vm.menu.forEach((item) => keys.push(item.path)); 38 | return keys; 39 | }, 40 | }, 41 | mounted() { 42 | this.updateMenu(); 43 | }, 44 | watch: { 45 | collapsed(val) { 46 | if (val) { 47 | this.cachedOpenKeys = this.openKeys.concat(); 48 | this.openKeys = []; 49 | } else { 50 | this.openKeys = this.cachedOpenKeys; 51 | } 52 | }, 53 | $route: function () { 54 | this.updateMenu(); 55 | }, 56 | }, 57 | methods: { 58 | // select menu item 59 | onOpenChange(openKeys) { 60 | // 在水平模式下时执行,并且不再执行后续 61 | if (this.mode === "horizontal") { 62 | this.openKeys = openKeys; 63 | return; 64 | } 65 | // 非水平模式时 66 | const latestOpenKey = openKeys.find( 67 | (key) => !this.openKeys.includes(key) 68 | ); 69 | if (!this.rootSubmenuKeys.includes(latestOpenKey)) { 70 | this.openKeys = openKeys; 71 | } else { 72 | this.openKeys = latestOpenKey ? [latestOpenKey] : []; 73 | } 74 | }, 75 | onSelect({ item, key, selectedKeys }) { 76 | this.selectedKeys = selectedKeys; 77 | this.$emit("select", { item, key, selectedKeys }); 78 | }, 79 | updateMenu() { 80 | const routes = this.$route.matched.concat(); 81 | const { hidden } = this.$route.meta; 82 | if (routes.length >= 3 && hidden) { 83 | routes.pop(); 84 | this.selectedKeys = [routes[routes.length - 1].path]; 85 | } else { 86 | this.selectedKeys = [routes.pop().path]; 87 | } 88 | const openKeys = []; 89 | if (this.mode === "inline") { 90 | routes.forEach((item) => { 91 | openKeys.push(item.path); 92 | }); 93 | } 94 | 95 | this.collapsed 96 | ? (this.cachedOpenKeys = openKeys) 97 | : (this.openKeys = openKeys); 98 | }, 99 | 100 | // render 101 | renderItem(menu) { 102 | if (!menu.hidden) { 103 | return menu.children && !menu.hideChildrenInMenu 104 | ? this.renderSubMenu(menu) 105 | : this.renderMenuItem(menu); 106 | } 107 | return null; 108 | }, 109 | renderMenuItem(menu) { 110 | const target = menu.meta.target || null; 111 | const CustomTag = (target && "a") || "router-link"; 112 | const props = { to: { name: menu.name } }; 113 | const attrs = { href: menu.path, target: menu.meta.target }; 114 | 115 | if (menu.children && menu.hideChildrenInMenu) { 116 | // 把有子菜单的 并且 父菜单是要隐藏子菜单的 117 | // 都给子菜单增加一个 hidden 属性 118 | // 用来给刷新页面时, selectedKeys 做控制用 119 | menu.children.forEach((item) => { 120 | item.meta = Object.assign(item.meta, { hidden: true }); 121 | }); 122 | } 123 | 124 | return ( 125 | 126 | 127 | {this.renderIcon(menu.meta.icon)} 128 | {menu.meta.title} 129 | 130 | 131 | ); 132 | }, 133 | renderSubMenu(menu) { 134 | const itemArr = []; 135 | if (!menu.hideChildrenInMenu) { 136 | menu.children.forEach((item) => itemArr.push(this.renderItem(item))); 137 | } 138 | return ( 139 | 140 | 141 | {this.renderIcon(menu.meta.icon)} 142 | {menu.meta.title} 143 | 144 | {itemArr} 145 | 146 | ); 147 | }, 148 | renderIcon(icon) { 149 | if (icon === "none" || icon === undefined) { 150 | return null; 151 | } 152 | const props = {}; 153 | typeof icon === "object" ? (props.component = icon) : (props.type = icon); 154 | return ; 155 | }, 156 | }, 157 | 158 | render() { 159 | const dynamicProps = { 160 | props: { 161 | mode: this.mode, 162 | theme: this.theme, 163 | openKeys: this.openKeys, 164 | selectedKeys: this.selectedKeys, 165 | }, 166 | on: { 167 | openChange: this.onOpenChange, 168 | select: this.onSelect, 169 | }, 170 | }; 171 | 172 | const menuTree = this.menu.map((item) => { 173 | if (item.hidden) { 174 | return null; 175 | } 176 | return this.renderItem(item); 177 | }); 178 | 179 | return {menuTree}; 180 | }, 181 | }; 182 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = (ctx) => ({ 4 | base: "/v-formly/", 5 | locales: { 6 | "/": { 7 | lang: "en-US", 8 | title: "v-formly", 9 | description: "v-formly is a dynamic (JSON powered) form library for vue.", 10 | }, 11 | "/zh/": { 12 | lang: "zh-CN", 13 | title: "v-formly", 14 | description: "v-formly是vue的动态(JSON驱动)表单库。", 15 | }, 16 | }, 17 | head: [ 18 | ["link", { rel: "icon", href: `/logo.png` }], 19 | ["link", { rel: "manifest", href: "/manifest.json" }], 20 | ["meta", { name: "theme-color", content: "#3eaf7c" }], 21 | ["meta", { name: "apple-mobile-web-app-capable", content: "yes" }], 22 | [ 23 | "meta", 24 | { name: "apple-mobile-web-app-status-bar-style", content: "black" }, 25 | ], 26 | [ 27 | "link", 28 | { rel: "apple-touch-icon", href: `/icons/apple-touch-icon-152x152.png` }, 29 | ], 30 | [ 31 | "link", 32 | { 33 | rel: "mask-icon", 34 | href: "/icons/safari-pinned-tab.svg", 35 | color: "#3eaf7c", 36 | }, 37 | ], 38 | [ 39 | "meta", 40 | { 41 | name: "msapplication-TileImage", 42 | content: "/icons/msapplication-icon-144x144.png", 43 | }, 44 | ], 45 | ["meta", { name: "msapplication-TileColor", content: "#000000" }], 46 | ], 47 | // theme: '@vuepress/vue', 48 | themeConfig: { 49 | repo: "KevinZhang19870314/v-formly", 50 | editLinks: true, 51 | docsDir: "docs", 52 | // #697 Provided by the official algolia team. 53 | // algolia: ctx.isProd ? ({ 54 | // apiKey: '3a539aab83105f01761a137c61004d85', 55 | // indexName: 'vuepress', 56 | // algoliaOptions: { 57 | // facetFilters: ['tags:v1'] 58 | // } 59 | // }) : null, 60 | smoothScroll: true, 61 | locales: { 62 | "/": { 63 | label: "English", 64 | selectText: "Languages", 65 | ariaLabel: "Select language", 66 | editLinkText: "Edit this page on GitHub", 67 | lastUpdated: "Last Updated", 68 | nav: [ 69 | { 70 | text: "Guide", 71 | link: "/guide/", 72 | }, 73 | { 74 | text: "Config Reference", 75 | link: "/components/", 76 | }, 77 | ], 78 | sidebar: { 79 | "/guide/": getGuideSidebar("Guide", "Advanced"), 80 | "/components/": getComponentsSidebar("Components", "Advanced"), 81 | }, 82 | }, 83 | "/zh/": { 84 | label: "简体中文", 85 | selectText: "选择语言", 86 | ariaLabel: "选择语言", 87 | editLinkText: "在 GitHub 上编辑此页", 88 | lastUpdated: "上次更新", 89 | nav: [ 90 | { 91 | text: "指南", 92 | link: "/zh/guide/", 93 | }, 94 | { 95 | text: "组件", 96 | link: "/zh/components/", 97 | }, 98 | ], 99 | sidebar: { 100 | "/zh/guide/": getGuideSidebar("指南", "深入"), 101 | "/zh/components/": getComponentsSidebar("组件", "高级"), 102 | }, 103 | }, 104 | }, 105 | }, 106 | plugins: [ 107 | ["@vuepress/back-to-top", true], 108 | [ 109 | "@vuepress/pwa", 110 | { 111 | serviceWorker: true, 112 | updatePopup: true, 113 | }, 114 | ], 115 | ["@vuepress/medium-zoom", true], 116 | [ 117 | "container", 118 | { 119 | type: "vue", 120 | before: '
',
121 |         after: "
", 122 | }, 123 | ], 124 | [ 125 | "container", 126 | { 127 | type: "upgrade", 128 | before: (info) => ``, 129 | after: "", 130 | }, 131 | ], 132 | ["demo-container"], 133 | ], 134 | chainWebpack: (config) => { 135 | config.resolve.alias.set("core-js/library/fn", "core-js/features"); 136 | }, 137 | configureWebpack: { 138 | resolve: { 139 | alias: { 140 | "@": path.resolve(__dirname, "../../src/"), 141 | }, 142 | }, 143 | }, 144 | }); 145 | 146 | function getGuideSidebar(groupA, groupB) { 147 | return [ 148 | { 149 | title: groupA, 150 | collapsable: false, 151 | sidebarDepth: 2, 152 | children: ["", "meta", "terms"], 153 | }, 154 | { 155 | title: groupB, 156 | collapsable: false, 157 | sidebarDepth: 2, 158 | children: ["custom-validator", "layout", "form"], 159 | }, 160 | ]; 161 | } 162 | 163 | function getComponentsSidebar(groupA, groupB) { 164 | return [ 165 | { 166 | title: groupA, 167 | collapsable: false, 168 | sidebarDepth: 0, 169 | children: [ 170 | "", 171 | "array", 172 | "autocomplete", 173 | "boolean", 174 | "cascader", 175 | "checkbox", 176 | "date", 177 | "number", 178 | "object", 179 | "radio", 180 | "rate", 181 | "select", 182 | "slider", 183 | "string", 184 | "tag", 185 | "text", 186 | "textarea", 187 | "time", 188 | ], 189 | }, 190 | { 191 | title: groupB, 192 | collapsable: false, 193 | sidebarDepth: 2, 194 | children: ["custom-components"], 195 | }, 196 | ]; 197 | } 198 | --------------------------------------------------------------------------------