├── .browserslistrc ├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── 1-bug-report.md │ ├── 2-feature-request.md │ └── 3-docs-bug.md ├── PULL_REQUEST_TEMPLATE.md ├── preview.gif └── workflows │ └── ghpages.yml ├── .gitignore ├── .postcssrc.js ├── .prettierrc ├── .storybook ├── addons.js ├── config.js ├── presets.js └── webpack.config.js ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── README.zh-CN.md ├── babel.config.js ├── codecov.yml ├── commitlint.config.js ├── jest.config.js ├── package.json ├── rollup.config.js ├── src ├── ant-form-adaptor.js ├── components │ ├── FormBuilder.vue │ └── adaptors │ │ ├── AntFormAdaptor.vue │ │ ├── ElementFormAdaptor.vue │ │ └── ViewFormAdaptor.vue ├── el-form-adaptor.js ├── index.js └── view-form-adaptor.js ├── stories ├── antd │ ├── FormBuilder.md │ ├── FormBuilder.stories.js │ ├── components │ │ ├── ExampleActorComplex.vue │ │ ├── ExampleChannel.vue │ │ ├── ExampleName.vue │ │ ├── ExampleType.vue │ │ └── index.js │ └── style.scss ├── elementui │ ├── FormBuilder.md │ ├── FormBuilder.stories.js │ ├── components │ │ ├── ExampleActorComplex.vue │ │ ├── ExampleChannel.vue │ │ ├── ExampleName.vue │ │ ├── ExampleType.vue │ │ └── index.js │ └── style.scss └── viewui │ ├── FormBuilder.md │ ├── FormBuilder.stories.js │ ├── components │ ├── ExampleActorComplex.vue │ ├── ExampleChannel.vue │ ├── ExampleName.vue │ ├── ExampleType.vue │ └── index.js │ └── style.scss └── test ├── .eslintrc.js └── unit ├── setup.js └── specs ├── antd-form-adaptor.spec.js ├── element-form-adaptor.spec.js ├── formbuilder.spec.js ├── plugin.spec.js └── view-form-adaptor.spec.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | IE 10 -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | node: circleci/node@1.1.6 4 | codecov: codecov/codecov@1.0.2 5 | jobs: 6 | unit-test: 7 | executor: 8 | name: node/default 9 | steps: 10 | - checkout 11 | - node/with-cache: 12 | steps: 13 | - run: npm install 14 | - run: npm run test 15 | - codecov/upload: 16 | file: coverage/coverage-final.json 17 | conf: codecov.yml 18 | - store_artifacts: 19 | path: coverage 20 | 21 | workflows: 22 | test-flow: 23 | jobs: 24 | - unit-test: 25 | filters: 26 | branches: 27 | ignore: 28 | - gh-pages 29 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'plugin:vue/essential', 8 | '@vue/standard', 9 | 'plugin:prettier/recommended' 10 | ], 11 | plugins: ['prettier'], 12 | rules: { 13 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 14 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 15 | 16 | 'vue/no-parsing-error': [2, { 'x-invalid-end-tag': false }], 17 | 18 | 'prettier/prettier': 'error' 19 | }, 20 | parserOptions: { 21 | parser: 'babel-eslint' 22 | }, 23 | overrides: [ 24 | { 25 | files: [ 26 | '**/__tests__/*.{j,t}s?(x)', 27 | '**/tests/unit/**/*.spec.{j,t}s?(x)' 28 | ], 29 | env: { 30 | jest: true 31 | } 32 | } 33 | ] 34 | }; 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please help us process issues more efficiently by filing an 2 | issue using one of the following templates: 3 | 4 | 5 | 6 | Thank you! 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Bug Report' 3 | about: Report a bug in the Vue Form Builder 4 | --- 5 | 6 | ## Bug Report 7 | 8 | ### Affected Feature 9 | 10 | 11 | 12 | ### Is this a regression 13 | 14 | 15 | 16 | ### Description 17 | 18 | 19 | 20 | ## Minimal Reproduction 21 | 22 | 23 | 24 | ## Exception or Error 25 | 26 | ```plain 27 | 28 | ``` 29 | 30 | ## Your Environment 31 | 32 | **Vue Version:** 33 | 34 | ```plain 35 | 36 | ``` 37 | 38 | **Vue Composition API:** 39 | 40 | ```plain 41 | 42 | ``` 43 | 44 | **Anything else relevant?** 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Feature Request' 3 | about: Suggest a feature for Vue Form Builder 4 | --- 5 | 6 | ## Feature Request 7 | 8 | ### Relevant Package 9 | 10 | 11 | 12 | ### Description 13 | 14 | 15 | 16 | ### Describe the solution you'd like 17 | 18 | 19 | 20 | ### Describe alternatives you've considered 21 | 22 | 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-docs-bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Docs Issue Report' 3 | about: Report an issue in Vue Form Builder's documentation 4 | --- 5 | 6 | ## Docs Issue 7 | 8 | ### Description 9 | 10 | 11 | 12 | ## Minimal Reproduction 13 | 14 | ### What's the affected URL 15 | 16 | 17 | 18 | ### Reproduction Steps 19 | 20 | 21 | 22 | ### Expected vs Actual Behavior 23 | 24 | 25 | 26 | ## Screenshot 27 | 28 | 29 | 30 | ## Exception or Error 31 | 32 | ```plain 33 | 34 | ``` 35 | 36 | ## Your Environment 37 | 38 | ### Browser info 39 | 40 | 41 | 42 | ### Anything else relevant 43 | 44 | 45 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # PR Checklist 2 | 3 | Please check if your PR fulfills the following requirements: 4 | 5 | - [ ] The commit message follows our guidelines: 6 | - [ ] Tests for the changes have been added (for bug fixes / features) 7 | - [ ] Docs have been added / updated (for bug fixes / features) 8 | 9 | ## PR Type 10 | 11 | What kind of change does this PR introduce? 12 | 13 | 14 | 15 | - [ ] Bugfix 16 | - [ ] Feature 17 | - [ ] Code style update (formatting, local variables) 18 | - [ ] Refactoring (no functional changes, no api changes) 19 | - [ ] Build related changes 20 | - [ ] CI related changes 21 | - [ ] Documentation content changes 22 | - [ ] Other... Please describe: 23 | 24 | ## What is the current behavior? 25 | 26 | 27 | 28 | Issue Number: N/A 29 | 30 | ## What is the new behavior? 31 | 32 | ## Does this PR introduce a breaking change? 33 | 34 | - [ ] Yes 35 | - [ ] No 36 | 37 | 38 | 39 | ## Other information 40 | -------------------------------------------------------------------------------- /.github/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfext/vue-form-builder/209e7a6f88888fedaff21b4fc1253414ab99d449/.github/preview.gif -------------------------------------------------------------------------------- /.github/workflows/ghpages.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | build-and-deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | with: 13 | persist-credentials: false 14 | 15 | - name: Install and Build 16 | run: | 17 | npm install 18 | npm run build-storybook 19 | 20 | - name: Deploy 21 | uses: JamesIves/github-pages-deploy-action@releases/v3 22 | with: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | BRANCH: gh-pages 25 | FOLDER: storybook-static 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | lib 5 | storybook-static 6 | yarn.lock 7 | package-lock.json 8 | 9 | # Test 10 | coverage 11 | 12 | # Log files 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw* 25 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "eslintIntegration": true, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | import '@storybook/addon-notes/register'; 4 | import '@storybook/addon-knobs/register'; 5 | import '@storybook/addon-options/register'; 6 | import '@storybook/addon-storysource/register'; 7 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure, addParameters, addDecorator } from '@storybook/vue'; 2 | import { withKnobs } from '@storybook/addon-knobs'; 3 | import { themes } from '@storybook/theming'; 4 | 5 | addParameters({ 6 | options: { 7 | theme: themes.light 8 | } 9 | }); 10 | 11 | addDecorator( 12 | withKnobs({ 13 | escapeHTML: false 14 | }) 15 | ); 16 | 17 | // automatically import all files ending in *.stories.js 18 | configure( 19 | [require.context('../stories', true, /\.stories\.(js|mdx)$/)], 20 | module 21 | ); 22 | -------------------------------------------------------------------------------- /.storybook/presets.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | /* '@storybook/addon-docs/vue/preset' */ 3 | ]; 4 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = async ({ config, mode }) => { 4 | // alias 5 | config.resolve.alias['@'] = path.resolve(__dirname, '../'); 6 | 7 | // sass-loader 8 | config.module.rules.push({ 9 | test: /\.scss$/, 10 | use: ['style-loader', 'css-loader', 'sass-loader'], 11 | include: path.resolve(__dirname, '../') 12 | }); 13 | 14 | // source code 15 | config.module.rules.push({ 16 | test: /\.stories\.jsx?$/, 17 | loaders: [require.resolve('@storybook/source-loader')], 18 | enforce: 'pre' 19 | }); 20 | return config; 21 | }; 22 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [INSERT CONTACT METHOD]. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | Translations are available at 128 | 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Vue Form Builder 2 | 3 | Looking forward to your contribution to this project, but here are the guidelines we would like you to follow: 4 | 5 | - [Contributing to Vue Form Builder](#contributing-to-vue-form-builder) 6 | - [Code of Conduct](#code-of-conduct) 7 | 8 | ## Code of Conduct 9 | 10 | To keep this project open and inclusive. Please read and follow our [Code of Conduct][coc]. 11 | 12 | [coc]: https://github.com/openfext/vue-form-builder/blob/master/CODE_OF_CONDUCT.md 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Felix Yang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue Form Builder 2 | 3 | [![CircleCI](https://circleci.com/gh/openfext/vue-use.svg?style=svg)](https://circleci.com/gh/openfext/vue-use) 4 | [![codecov](https://codecov.io/gh/openfext/vue-form-builder/branch/develop/graph/badge.svg)](https://codecov.io/gh/openfext/vue-form-builder) 5 | [![License](https://img.shields.io/npm/l/@fext/vue-form-builder.svg)](https://www.npmjs.com/package/@fext/vue-form-builder) 6 | [![Version](https://img.shields.io/npm/v/@fext/vue-form-builder.svg)](https://www.npmjs.com/package/@fext/vue-form-builder) 7 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/openfext/vue-form-builder) 8 | 9 | Build powerful vue form with JSON schema and composition api. Any custom input components and popular ui frameworks such as [Element UI](https://element.eleme.cn/), [View UI](https://www.iviewui.com/), [Ant Design Vue](https://www.antdv.com/) are supported. 10 | 11 | :us: English | [:cn: 简体中文](README.zh-CN.md) 12 | 13 | ## Features 14 | 15 | - :tv: **Powerful** - use composition api to manage complex form state 16 | - :camera: **Flexible** - support any custom input components 17 | - :watch: **Adaptable** - different ui frameworks can be used out of the box through the adapters 18 | - :radio: **Reliable** - has been used in multiple applications in the production environment 19 | 20 | ## Docs 21 | 22 | ### 🇨🇳 Chinese 23 | 24 | - [Introduction](https://openfext.github.io/docs/vue-form-builder/intro.html) 25 | - [Guide](https://openfext.github.io/docs/vue-form-builder/guide/start.html) 26 | - [API Reference](https://openfext.github.io/docs/vue-form-builder/api/component.html) 27 | - [Config Reference](https://openfext.github.io/docs/vue-form-builder/config/schema.html) 28 | 29 | ### 🇺🇸 English 30 | 31 | WIP... 32 | 33 | ## Example 34 | 35 | [:zap: Live Preview](https://openfext.github.io/vue-admin-next/#/form/form-builder) | [:book: Element UI Storybook](https://openfext.github.io/vue-form-builder/?path=/story/formbuilder-element-ui--basic-usage) | [:book: View UI Storybook](https://openfext.github.io/vue-form-builder/?path=/story/formbuilder-view-ui--basic-usage) | [:book: Ant Design Storybook](https://openfext.github.io/vue-form-builder/?path=/story/formbuilder-ant-design-vue--basic-usage) 36 | 37 | ![Screen Capture](https://github.com/openfext/vue-form-builder/raw/develop/.github/preview.gif) 38 | 39 | ## Contacts 40 | 41 | Welcome to join the group chat to discuss project issues and front-end technologies 💬 42 | 43 | ### QQ 44 | 45 | ChatID: 667576147 46 | 47 | ![QQGroupQRCode](https://user-images.githubusercontent.com/2902215/84306575-3e785280-ab8e-11ea-8c53-af8620b5cc69.JPG) 48 | 49 | ### WeChat Group 50 | 51 | Add the author's WeChat to get into the group: 52 | 53 | ![WeChatQRCode](https://user-images.githubusercontent.com/2902215/84306570-3c15f880-ab8e-11ea-9041-8ea4ccbaa772.JPG) 54 | 55 | ## Built With 56 | 57 | - [Vue.js](https://github.com/vuejs/vue) 58 | - [Vue Use](https://github.com/openfext/vue-use) 59 | - [Vee Validate](https://github.com/logaretm/vee-validate) 60 | - [ElementUI](https://github.com/ElemeFE/element) 61 | - [View UI](https://github.com/view-design/ViewUI) 62 | - [Ant Design Vue](https://github.com/vueComponent/ant-design-vue) 63 | 64 | ## License 65 | 66 | [MIT](http://opensource.org/licenses/MIT) 67 | 68 | Copyright (c) 2018 - present, Felix Yang 69 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # Vue Form Builder 2 | 3 | [![CircleCI](https://circleci.com/gh/openfext/vue-use.svg?style=svg)](https://circleci.com/gh/openfext/vue-use) 4 | [![codecov](https://codecov.io/gh/openfext/vue-form-builder/branch/develop/graph/badge.svg)](https://codecov.io/gh/openfext/vue-form-builder) 5 | [![License](https://img.shields.io/npm/l/@fext/vue-form-builder.svg)](https://www.npmjs.com/package/@fext/vue-form-builder) 6 | [![Version](https://img.shields.io/npm/v/@fext/vue-form-builder.svg)](https://www.npmjs.com/package/@fext/vue-form-builder) 7 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/openfext/vue-form-builder) 8 | 9 | Vue Form Builder 是一个基于 JSON Schema 和 Vue Composition API 的动态表单构建器。任意自定义表单组件及主流 UI 框架例如 [Element UI](https://element.eleme.cn/),[View UI](https://www.iviewui.com/),[Ant Design Vue](https://www.antdv.com/) 等都能够轻松支持。 10 | 11 | :cn: 简体中文 | [:us: English](README.md) 12 | 13 | ## 主要特性 14 | 15 | - :tv: **强大** - 通过 Vue Composition API 管理表单状态,再也不用野路子了 16 | - :camera: **灵活** - 支持任意、任意、任意自定义表单组件,重要的事情说三遍 17 | - :watch: **兼容** - 多种主流 UI 框架开箱即配,就等你来 18 | - :radio: **可靠** - 多个生产环境的应用已在使用,还在等什么呢 19 | 20 | ## 参考文档 21 | 22 | - [介绍](https://openfext.github.io/docs/vue-form-builder/intro.html) 23 | - [指南](https://openfext.github.io/docs/vue-form-builder/guide/start.html) 24 | - [API Reference](https://openfext.github.io/docs/vue-form-builder/api/component.html) 25 | - [Config Reference](https://openfext.github.io/docs/vue-form-builder/config/schema.html) 26 | 27 | ## 在线示例 28 | 29 | [:zap: Live Preview](https://openfext.github.io/vue-admin-next/#/form/form-builder) | [:book: Element UI Storybook](https://openfext.github.io/vue-form-builder/?path=/story/formbuilder-element-ui--basic-usage) | [:book: View UI Storybook](https://openfext.github.io/vue-form-builder/?path=/story/formbuilder-view-ui--basic-usage) | [:book: Ant Design Storybook](https://openfext.github.io/vue-form-builder/?path=/story/formbuilder-ant-design-vue--basic-usage) 30 | 31 | ![Screen Capture](https://github.com/openfext/vue-form-builder/raw/develop/.github/preview.gif) 32 | 33 | ## 联系方式 34 | 35 | 欢迎加入群聊讨论项目问题和前端技术 💬 36 | 37 | ### QQ 38 | 39 | 群号:667576147 40 | 41 | ![QQGroupQRCode](https://user-images.githubusercontent.com/2902215/84306575-3e785280-ab8e-11ea-8c53-af8620b5cc69.JPG) 42 | 43 | ### 微信群 44 | 45 | 加作者微信进群: 46 | 47 | ![WeChatQRCode](https://user-images.githubusercontent.com/2902215/84306570-3c15f880-ab8e-11ea-9041-8ea4ccbaa772.JPG) 48 | 49 | ## 使用技术 50 | 51 | - [Vue.js](https://github.com/vuejs/vue) 52 | - [Vue Use](https://github.com/openfext/vue-use) 53 | - [Vee Validate](https://github.com/logaretm/vee-validate) 54 | - [ElementUI](https://github.com/ElemeFE/element) 55 | - [View UI](https://github.com/view-design/ViewUI) 56 | - [Ant Design Vue](https://github.com/vueComponent/ant-design-vue) 57 | 58 | ## License 59 | 60 | [MIT](http://opensource.org/licenses/MIT) 61 | 62 | Copyright (c) 2018 - present, Felix Yang 63 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | modules: false 7 | } 8 | ] 9 | ], 10 | plugins: [['@babel/plugin-transform-runtime', { corejs: 3 }]], 11 | env: { 12 | test: { 13 | presets: ['@babel/preset-env'] 14 | } 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | fixes: 2 | - '/home/circleci/project/::' 3 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'] 3 | }; 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | modulePaths: ['/'], 3 | moduleNameMapper: { 4 | '^vue$': '/node_modules/vue/dist/vue.js' 5 | }, 6 | moduleFileExtensions: ['js', 'json', 'vue'], 7 | transform: { 8 | '^.*\\.(vue)$': 'vue-jest', 9 | '^.+\\.[t|j]sx?$': 'babel-jest' 10 | }, 11 | setupFilesAfterEnv: ['/test/unit/setup.js'], 12 | collectCoverage: true, 13 | collectCoverageFrom: [ 14 | '**/src/**/*.{js,vue,jsx}', 15 | '!**/stories/**', 16 | '!**/test/**', 17 | '!**/node_modules/**' 18 | ], 19 | coverageDirectory: 'coverage' 20 | }; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fext/vue-form-builder", 3 | "version": "2.1.0", 4 | "description": "Build powerful vue form with JSON schema and composition api.", 5 | "main": "lib/index.js", 6 | "module": "lib/index.esm.js", 7 | "files": [ 8 | "src", 9 | "lib" 10 | ], 11 | "scripts": { 12 | "build": "rollup -c", 13 | "lint": "eslint --fix --ext .js,.vue src stories", 14 | "test": "jest", 15 | "storybook": "start-storybook -p 6006", 16 | "build-storybook": "build-storybook", 17 | "deploy-storybook": "storybook-to-ghpages" 18 | }, 19 | "husky": { 20 | "hooks": { 21 | "pre-commit": "npm run lint && lint-staged", 22 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 23 | } 24 | }, 25 | "lint-staged": { 26 | "*.{json,css,scss,md}": [ 27 | "prettier --write", 28 | "git add" 29 | ] 30 | }, 31 | "keywords": [ 32 | "vue", 33 | "use", 34 | "vue-use", 35 | "composition", 36 | "vue-composition", 37 | "vue-composition-api", 38 | "vue-form", 39 | "json-form", 40 | "formbuilder", 41 | "form-builder" 42 | ], 43 | "author": "felixpy.1993@gmail.com", 44 | "license": "MIT", 45 | "dependencies": { 46 | "@babel/runtime-corejs3": "^7.7.7", 47 | "deepmerge": "^4.2.2", 48 | "vue-runtime-helpers": "^1.1.2" 49 | }, 50 | "peerDependencies": { 51 | "@fext/vue-use": ">=0.1.0", 52 | "@vue/composition-api": ">=0.5.0", 53 | "vee-validate": ">=3.0.0", 54 | "vue": ">=2.6.0" 55 | }, 56 | "devDependencies": { 57 | "@babel/core": "^7.7.7", 58 | "@babel/plugin-transform-runtime": "^7.7.6", 59 | "@babel/preset-env": "^7.9.6", 60 | "@commitlint/cli": "^8.3.4", 61 | "@commitlint/config-conventional": "^8.3.4", 62 | "@fext/vue-use": "^0.1.1", 63 | "@rollup/plugin-alias": "^3.0.0", 64 | "@rollup/plugin-commonjs": "^11.0.1", 65 | "@rollup/plugin-node-resolve": "^6.1.0", 66 | "@storybook/addon-actions": "^5.2.8", 67 | "@storybook/addon-docs": "^5.2.8", 68 | "@storybook/addon-knobs": "^5.2.8", 69 | "@storybook/addon-links": "^5.2.8", 70 | "@storybook/addon-notes": "^5.2.8", 71 | "@storybook/addon-options": "^5.2.8", 72 | "@storybook/addon-storysource": "^5.2.8", 73 | "@storybook/addons": "^5.2.8", 74 | "@storybook/storybook-deployer": "^2.8.6", 75 | "@storybook/vue": "^5.2.8", 76 | "@vue/composition-api": "^0.5.0", 77 | "@vue/eslint-config-standard": "^5.0.1", 78 | "@vue/test-utils": "^1.0.2", 79 | "ant-design-vue": "^1.6.1", 80 | "babel-eslint": "^10.0.3", 81 | "babel-jest": "^26.0.1", 82 | "babel-loader": "^8.0.6", 83 | "babel-preset-vue": "^2.0.2", 84 | "element-ui": "^2.13.1", 85 | "eslint": "^6.8.0", 86 | "eslint-config-prettier": "^6.7.0", 87 | "eslint-plugin-import": "^2.19.1", 88 | "eslint-plugin-node": "^11.0.0", 89 | "eslint-plugin-prettier": "^3.1.2", 90 | "eslint-plugin-promise": "^4.2.1", 91 | "eslint-plugin-standard": "^4.0.1", 92 | "eslint-plugin-vue": "^6.1.2", 93 | "husky": "^3.1.0", 94 | "jest": "^26.0.1", 95 | "lint-staged": "^9.5.0", 96 | "prettier": "^1.19.1", 97 | "rollup": "^1.28.0", 98 | "rollup-plugin-babel": "^4.3.3", 99 | "rollup-plugin-vue": ">=5.0.0 <=5.1.1", 100 | "sass": "^1.24.2", 101 | "sass-loader": "^8.0.0", 102 | "vee-validate": "^3.3.0", 103 | "view-design": "^4.2.0", 104 | "vue": "^2.6.11", 105 | "vue-jest": "^3.0.5", 106 | "vue-loader": "^15.8.3" 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import RollupNodeResolve from '@rollup/plugin-node-resolve'; 2 | import RollupCommonJS from '@rollup/plugin-commonjs'; 3 | import RollupAlias from '@rollup/plugin-alias'; 4 | import RollupBabel from 'rollup-plugin-babel'; 5 | import RollupVue from 'rollup-plugin-vue'; 6 | import pkg from './package.json'; 7 | 8 | const commonPlugins = [ 9 | RollupVue({ 10 | needMap: false 11 | }), 12 | RollupBabel({ 13 | runtimeHelpers: true, 14 | extensions: ['.js', '.vue'], 15 | exclude: 'node_modules/**' 16 | }), 17 | RollupAlias({ 18 | entries: [] 19 | }), 20 | RollupNodeResolve(), 21 | RollupCommonJS() 22 | ]; 23 | 24 | const commonExternal = function(id) { 25 | return ( 26 | /^vue/i.test(id) || 27 | /^@vue\/composition-api/i.test(id) || 28 | /^@fext\/vue-use/i.test(id) || 29 | /^@babel\/runtime-corejs3/i.test(id) 30 | ); 31 | }; 32 | 33 | const config = [ 34 | // form-builder 35 | { 36 | input: 'src/index.js', 37 | output: [ 38 | { 39 | file: pkg.main, 40 | format: 'cjs', 41 | exports: 'named' 42 | } 43 | ], 44 | plugins: [...commonPlugins], 45 | external: commonExternal 46 | }, 47 | { 48 | input: 'src/index.js', 49 | output: [ 50 | { 51 | file: pkg.module, 52 | format: 'esm' 53 | } 54 | ], 55 | plugins: [...commonPlugins], 56 | external: commonExternal 57 | }, 58 | 59 | // element-ui-form-adaptor 60 | { 61 | input: 'src/el-form-adaptor.js', 62 | output: [ 63 | { 64 | file: 'lib/adaptor/element.js', 65 | format: 'cjs', 66 | exports: 'named' 67 | } 68 | ], 69 | plugins: [...commonPlugins], 70 | external: commonExternal 71 | }, 72 | { 73 | input: 'src/el-form-adaptor.js', 74 | output: [ 75 | { 76 | file: 'lib/adaptor/element.esm.js', 77 | format: 'esm' 78 | } 79 | ], 80 | plugins: [...commonPlugins], 81 | external: commonExternal 82 | }, 83 | 84 | // view-ui-form-adaptor 85 | { 86 | input: 'src/view-form-adaptor.js', 87 | output: [ 88 | { 89 | file: 'lib/adaptor/view.js', 90 | format: 'cjs', 91 | exports: 'named' 92 | } 93 | ], 94 | plugins: [...commonPlugins], 95 | external: commonExternal 96 | }, 97 | { 98 | input: 'src/view-form-adaptor.js', 99 | output: [ 100 | { 101 | file: 'lib/adaptor/view.esm.js', 102 | format: 'esm' 103 | } 104 | ], 105 | plugins: [...commonPlugins], 106 | external: commonExternal 107 | }, 108 | 109 | // ant-design-form-adaptor 110 | { 111 | input: 'src/ant-form-adaptor.js', 112 | output: [ 113 | { 114 | file: 'lib/adaptor/antd.js', 115 | format: 'cjs', 116 | exports: 'named' 117 | } 118 | ], 119 | plugins: [...commonPlugins], 120 | external: commonExternal 121 | }, 122 | { 123 | input: 'src/ant-form-adaptor.js', 124 | output: [ 125 | { 126 | file: 'lib/adaptor/antd.esm.js', 127 | format: 'esm' 128 | } 129 | ], 130 | plugins: [...commonPlugins], 131 | external: commonExternal 132 | } 133 | ]; 134 | 135 | export default config; 136 | -------------------------------------------------------------------------------- /src/ant-form-adaptor.js: -------------------------------------------------------------------------------- 1 | import AntFormAdaptor from './components/adaptors/AntFormAdaptor.vue'; 2 | 3 | const install = function installAntFormAdaptor(Vue) { 4 | Vue.component('ant-form-adaptor', AntFormAdaptor); 5 | }; 6 | 7 | export { AntFormAdaptor }; 8 | 9 | export default { 10 | install 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/FormBuilder.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 121 | -------------------------------------------------------------------------------- /src/components/adaptors/AntFormAdaptor.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 68 | 69 | 186 | -------------------------------------------------------------------------------- /src/components/adaptors/ElementFormAdaptor.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 70 | 71 | 178 | -------------------------------------------------------------------------------- /src/components/adaptors/ViewFormAdaptor.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 66 | 67 | 174 | -------------------------------------------------------------------------------- /src/el-form-adaptor.js: -------------------------------------------------------------------------------- 1 | import ElFormAdaptor from './components/adaptors/ElementFormAdaptor.vue'; 2 | 3 | const install = function installElFormAdaptor(Vue) { 4 | Vue.component('el-form-adaptor', ElFormAdaptor); 5 | }; 6 | 7 | export { ElFormAdaptor }; 8 | 9 | export default { 10 | install 11 | }; 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import FormBuilder from './components/FormBuilder.vue'; 2 | 3 | const install = function(Vue) { 4 | Vue.component('form-builder', FormBuilder); 5 | }; 6 | 7 | const createFormBuilder = function(options = {}) { 8 | return { 9 | ...FormBuilder, 10 | ...options 11 | }; 12 | }; 13 | 14 | export { FormBuilder, createFormBuilder }; 15 | 16 | export default { 17 | install 18 | }; 19 | -------------------------------------------------------------------------------- /src/view-form-adaptor.js: -------------------------------------------------------------------------------- 1 | import ViewFormAdaptor from './components/adaptors/ViewFormAdaptor.vue'; 2 | 3 | const install = function installViewFormAdaptor(Vue) { 4 | Vue.component('view-form-adaptor', ViewFormAdaptor); 5 | }; 6 | 7 | export { ViewFormAdaptor }; 8 | 9 | export default { 10 | install 11 | }; 12 | -------------------------------------------------------------------------------- /stories/antd/FormBuilder.md: -------------------------------------------------------------------------------- 1 | # Ant Design Form Builder 2 | 3 | Vue form builder with ant design vue. 4 | -------------------------------------------------------------------------------- /stories/antd/FormBuilder.stories.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueCompositionApi from '@vue/composition-api'; 3 | import { useForm, useLoading } from '@fext/vue-use'; 4 | import Antd from 'ant-design-vue'; 5 | import { 6 | ValidationProvider, 7 | ValidationObserver, 8 | localize 9 | } from 'vee-validate/dist/vee-validate.full'; 10 | 11 | import markdown from './FormBuilder.md'; 12 | import { createFormBuilder } from '@/src'; 13 | import { AntFormAdaptor } from '@/src/ant-form-adaptor'; 14 | import ExampleComponents from './components'; 15 | 16 | import 'ant-design-vue/dist/antd.css'; 17 | import './style.scss'; 18 | 19 | Vue.use(VueCompositionApi); 20 | Vue.use(Antd); 21 | 22 | localize({ 23 | zh: { 24 | name: 'zh', 25 | messages: { 26 | max: '该字段最大长度为 {length} 个字符', 27 | min: '该字段最小长度为 {length} 个字符', 28 | required: '该字段不能为空' 29 | } 30 | } 31 | }); 32 | 33 | localize('zh'); 34 | 35 | Vue.component('ValidationProvider', ValidationProvider); 36 | Vue.component('ValidationObserver', ValidationObserver); 37 | 38 | // =============== Start of Basic Usage =============== // 39 | 40 | export const BasicUsage = () => ({ 41 | template: ` 42 |
43 | 44 | 45 | 46 | 47 | 52 | 53 |
54 |
55 | 62 | 63 |
64 | 65 | 提交 66 | 67 |
68 |
69 |
70 |
71 | 72 | 73 | 74 | 75 | 76 |
77 |
78 | 79 |
{{ JSON.stringify(formValues, null, 4) }}
80 | 81 | 确 定 82 | 83 |
84 |
85 | `, 86 | 87 | components: { 88 | FormBuilder: createFormBuilder({ 89 | components: { 90 | AntFormAdaptor, 91 | 92 | ...ExampleComponents 93 | } 94 | }) 95 | }, 96 | 97 | setup() { 98 | const form = useForm(); 99 | const { formValues, setInitialFormValues, updateFormValues } = form; 100 | const { loading, withLoading } = useLoading(); 101 | 102 | return { 103 | // from form composition 104 | form, 105 | formValues, 106 | setInitialFormValues, 107 | updateFormValues, 108 | 109 | // from loading composition 110 | loading, 111 | withLoading 112 | }; 113 | }, 114 | 115 | data() { 116 | return { 117 | showResultModal: false, 118 | 119 | metadata: {}, 120 | 121 | formShares: { 122 | size: 'default', 123 | 124 | props: { 125 | allowClear: true 126 | } 127 | }, 128 | 129 | formConfig: [], 130 | 131 | formConfigJSON: JSON.stringify( 132 | [ 133 | { 134 | component: 'a-card', 135 | props: { 136 | title: '基础信息' 137 | }, 138 | fields: [ 139 | { 140 | name: 'channel', 141 | component: 'ExampleChannel', 142 | rules: { 143 | required: true 144 | } 145 | }, 146 | { 147 | name: 'name', 148 | component: 'ExampleName', 149 | rules: { 150 | required: true 151 | } 152 | }, 153 | { 154 | name: 'comment', 155 | component: 'AntFormAdaptor', 156 | label: '评语', 157 | tip: '一句话评价(使用 FormAdaptor 的自定义字段)', 158 | tooltip: '精彩点评', 159 | rules: { 160 | required: true, 161 | max: 50, 162 | min: 10 163 | }, 164 | props: { 165 | placeholder: '不超过 20 个字' 166 | } 167 | } 168 | ] 169 | }, 170 | { 171 | component: 'a-card', 172 | props: { 173 | title: '高级信息' 174 | }, 175 | fields: [ 176 | { 177 | name: 'type', 178 | component: 'ExampleType', 179 | defaultValue: 2 180 | }, 181 | { 182 | name: 'actor', 183 | component: 'ExampleActorComplex' 184 | }, 185 | { 186 | name: 'date', 187 | component: 'AntFormAdaptor', 188 | label: '发行日期', 189 | extend: { 190 | component: 'a-date-picker' 191 | }, 192 | props: { 193 | placeholder: '请通过日期选择器' 194 | } 195 | }, 196 | { 197 | name: 'description', 198 | component: 'AntFormAdaptor', 199 | label: '描述', 200 | tip: '剧情描述(使用 FormAdaptor 的自定义字段)', 201 | rules: { 202 | max: 180 203 | }, 204 | props: { 205 | type: 'textarea', 206 | rows: 5, 207 | placeholder: '不超过 180 个字' 208 | } 209 | } 210 | ] 211 | } 212 | ], 213 | null, 214 | 2 215 | ) 216 | }; 217 | }, 218 | 219 | watch: { 220 | formConfigJSON: { 221 | handler(value) { 222 | try { 223 | this.formConfig = JSON.parse(this.formConfigJSON); 224 | } catch (err) { 225 | // this.formConfig = this.formConfig; 226 | } 227 | }, 228 | immediate: true 229 | } 230 | }, 231 | 232 | created() { 233 | this.registerEvents(); 234 | this.renderForm(); 235 | }, 236 | 237 | methods: { 238 | delay(timeout = 1000) { 239 | return new Promise(resolve => { 240 | setTimeout(() => { 241 | resolve(); 242 | }, timeout); 243 | }); 244 | }, 245 | 246 | getInitialValues() { 247 | return {}; 248 | }, 249 | 250 | registerEvents() { 251 | this.$on('doSomething', options => { 252 | this.$message({ 253 | type: 'success', 254 | message: '触发表单事件成功!' 255 | }); 256 | }); 257 | }, 258 | 259 | handleCommand(cmd, ...args) { 260 | this.$emit(cmd, ...args); 261 | }, 262 | 263 | async renderForm() { 264 | return this.withLoading(() => { 265 | return Promise.all([this.getRenderMetadata(), this.getFormValues()]); 266 | }); 267 | }, 268 | 269 | async getRenderMetadata() { 270 | await this.delay(); 271 | 272 | return Promise.resolve({ 273 | channels: [ 274 | { 275 | id: 1, 276 | code: 'dy', 277 | name: '电影' 278 | }, 279 | { 280 | id: 2, 281 | code: 'dm', 282 | name: '动漫' 283 | } 284 | ] 285 | }).then(data => { 286 | this.metadata = data; 287 | }); 288 | }, 289 | 290 | async getFormValues() { 291 | await this.delay(); 292 | 293 | return Promise.resolve({ 294 | channel: 2, 295 | name: 'Form Builder', 296 | comment: 'A powerful form builder', 297 | actor: ['Yang', 'Zhang'], 298 | description: 'What a nice tool' 299 | }).then(data => { 300 | const { formValues } = this; 301 | 302 | this.updateFormValues({ 303 | ...formValues, 304 | ...data 305 | }); 306 | }); 307 | }, 308 | 309 | async save() { 310 | const valid = await this.$refs.observer.validate(); 311 | 312 | if (!valid) { 313 | this.$message({ 314 | type: 'error', 315 | message: '部分表单填写错误,请检查!' 316 | }); 317 | return; 318 | } 319 | 320 | this.showResultModal = true; 321 | } 322 | } 323 | }); 324 | 325 | BasicUsage.story = { 326 | name: 'Basic Usage' 327 | }; 328 | 329 | // =============== End of Basic Usage =============== // 330 | 331 | export default { 332 | title: 'FormBuilder|Ant Design Vue', 333 | parameters: { 334 | notes: { 335 | markdown 336 | } 337 | } 338 | }; 339 | -------------------------------------------------------------------------------- /stories/antd/components/ExampleActorComplex.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 43 | 44 | 142 | -------------------------------------------------------------------------------- /stories/antd/components/ExampleChannel.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | 34 | 117 | -------------------------------------------------------------------------------- /stories/antd/components/ExampleName.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 33 | 34 | 88 | -------------------------------------------------------------------------------- /stories/antd/components/ExampleType.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 88 | -------------------------------------------------------------------------------- /stories/antd/components/index.js: -------------------------------------------------------------------------------- 1 | import ExampleChannel from './ExampleChannel'; 2 | import ExampleName from './ExampleName'; 3 | import ExampleType from './ExampleType'; 4 | import ExampleActorComplex from './ExampleActorComplex'; 5 | 6 | export default { 7 | ExampleChannel, 8 | ExampleName, 9 | ExampleType, 10 | ExampleActorComplex 11 | }; 12 | -------------------------------------------------------------------------------- /stories/antd/style.scss: -------------------------------------------------------------------------------- 1 | .form-builder-example { 2 | padding: 10px; 3 | 4 | .ant-card { 5 | margin-bottom: 15px; 6 | 7 | .ant-card-head-title { 8 | padding: 10px 0px; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /stories/elementui/FormBuilder.md: -------------------------------------------------------------------------------- 1 | # Element UI Form Builder 2 | 3 | Vue form builder with element ui. 4 | -------------------------------------------------------------------------------- /stories/elementui/FormBuilder.stories.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueCompositionApi from '@vue/composition-api'; 3 | import { useForm, useLoading } from '@fext/vue-use'; 4 | import ElementUI from 'element-ui'; 5 | import { 6 | ValidationProvider, 7 | ValidationObserver, 8 | localize 9 | } from 'vee-validate/dist/vee-validate.full'; 10 | 11 | import markdown from './FormBuilder.md'; 12 | import { createFormBuilder } from '@/src'; 13 | import { ElFormAdaptor } from '@/src/el-form-adaptor'; 14 | import ExampleComponents from './components'; 15 | 16 | import 'element-ui/lib/theme-chalk/index.css'; 17 | import './style.scss'; 18 | 19 | Vue.use(VueCompositionApi); 20 | Vue.use(ElementUI); 21 | 22 | localize({ 23 | zh: { 24 | name: 'zh', 25 | messages: { 26 | max: '该字段最大长度为 {length} 个字符', 27 | min: '该字段最小长度为 {length} 个字符', 28 | required: '该字段不能为空' 29 | } 30 | } 31 | }); 32 | 33 | localize('zh'); 34 | 35 | Vue.component('ValidationProvider', ValidationProvider); 36 | Vue.component('ValidationObserver', ValidationObserver); 37 | 38 | // =============== Start of Basic Usage =============== // 39 | 40 | export const BasicUsage = () => ({ 41 | template: ` 42 |
43 | 44 | 45 | 46 | 50 | 51 |
52 |
53 | 60 | 61 |
62 | 63 | 提交 64 | 65 |
66 |
67 |
68 | 69 | 70 | 71 | 72 | 73 |
74 |
75 | 76 |
{{ JSON.stringify(formValues, null, 4) }}
77 | 78 | 确 定 79 | 80 |
81 |
82 | `, 83 | 84 | components: { 85 | FormBuilder: createFormBuilder({ 86 | components: { 87 | ElFormAdaptor, 88 | 89 | ...ExampleComponents 90 | } 91 | }) 92 | }, 93 | 94 | setup() { 95 | const form = useForm(); 96 | const { formValues, setInitialFormValues, updateFormValues } = form; 97 | const { loading, withLoading } = useLoading(); 98 | 99 | return { 100 | // from form composition 101 | form, 102 | formValues, 103 | setInitialFormValues, 104 | updateFormValues, 105 | 106 | // from loading composition 107 | loading, 108 | withLoading 109 | }; 110 | }, 111 | 112 | data() { 113 | return { 114 | showResultModal: false, 115 | 116 | metadata: {}, 117 | 118 | formShares: { 119 | size: 'medium', 120 | 121 | props: { 122 | clearable: true 123 | } 124 | }, 125 | 126 | formConfig: [], 127 | 128 | formConfigJSON: JSON.stringify( 129 | [ 130 | { 131 | component: 'el-card', 132 | props: { 133 | header: '基础信息' 134 | }, 135 | fields: [ 136 | { 137 | name: 'channel', 138 | component: 'ExampleChannel', 139 | rules: { 140 | required: true 141 | } 142 | }, 143 | { 144 | name: 'name', 145 | component: 'ExampleName', 146 | rules: { 147 | required: true 148 | } 149 | }, 150 | { 151 | name: 'comment', 152 | component: 'ElFormAdaptor', 153 | label: '评语', 154 | tip: '一句话评价(使用 FormAdaptor 的自定义字段)', 155 | tooltip: '精彩点评', 156 | rules: { 157 | required: true, 158 | max: 50, 159 | min: 10 160 | }, 161 | props: { 162 | placeholder: '不超过 20 个字' 163 | } 164 | } 165 | ] 166 | }, 167 | { 168 | component: 'el-card', 169 | props: { 170 | header: '高级信息' 171 | }, 172 | fields: [ 173 | { 174 | name: 'type', 175 | component: 'ExampleType', 176 | defaultValue: 2 177 | }, 178 | { 179 | name: 'actor', 180 | component: 'ExampleActorComplex' 181 | }, 182 | { 183 | name: 'date', 184 | component: 'ElFormAdaptor', 185 | label: '发行日期', 186 | extend: { 187 | component: 'el-date-picker' 188 | }, 189 | props: { 190 | placeholder: '请通过日期选择器' 191 | } 192 | }, 193 | { 194 | name: 'description', 195 | component: 'ElFormAdaptor', 196 | label: '描述', 197 | tip: '剧情描述(使用 FormAdaptor 的自定义字段)', 198 | rules: { 199 | max: 180 200 | }, 201 | props: { 202 | type: 'textarea', 203 | rows: 5, 204 | placeholder: '不超过 180 个字' 205 | } 206 | } 207 | ] 208 | } 209 | ], 210 | null, 211 | 2 212 | ) 213 | }; 214 | }, 215 | 216 | watch: { 217 | formConfigJSON: { 218 | handler(value) { 219 | try { 220 | this.formConfig = JSON.parse(this.formConfigJSON); 221 | } catch (err) { 222 | // this.formConfig = this.formConfig; 223 | } 224 | }, 225 | immediate: true 226 | } 227 | }, 228 | 229 | created() { 230 | this.registerEvents(); 231 | this.renderForm(); 232 | }, 233 | 234 | methods: { 235 | delay(timeout = 1000) { 236 | return new Promise(resolve => { 237 | setTimeout(() => { 238 | resolve(); 239 | }, timeout); 240 | }); 241 | }, 242 | 243 | getInitialValues() { 244 | return {}; 245 | }, 246 | 247 | registerEvents() { 248 | this.$on('doSomething', options => { 249 | this.$message({ 250 | type: 'success', 251 | message: '触发表单事件成功!' 252 | }); 253 | }); 254 | }, 255 | 256 | handleCommand(cmd, ...args) { 257 | this.$emit(cmd, ...args); 258 | }, 259 | 260 | async renderForm() { 261 | return this.withLoading(() => { 262 | return Promise.all([this.getRenderMetadata(), this.getFormValues()]); 263 | }); 264 | }, 265 | 266 | async getRenderMetadata() { 267 | await this.delay(); 268 | 269 | return Promise.resolve({ 270 | channels: [ 271 | { 272 | id: 1, 273 | code: 'dy', 274 | name: '电影' 275 | }, 276 | { 277 | id: 2, 278 | code: 'dm', 279 | name: '动漫' 280 | } 281 | ] 282 | }).then(data => { 283 | this.metadata = data; 284 | }); 285 | }, 286 | 287 | async getFormValues() { 288 | await this.delay(); 289 | 290 | return Promise.resolve({ 291 | channel: 2, 292 | name: 'Form Builder', 293 | comment: 'A powerful form builder', 294 | actor: ['Yang', 'Zhang'], 295 | description: 'What a nice tool' 296 | }).then(data => { 297 | const { formValues } = this; 298 | 299 | this.updateFormValues({ 300 | ...formValues, 301 | ...data 302 | }); 303 | }); 304 | }, 305 | 306 | async save() { 307 | const valid = await this.$refs.observer.validate(); 308 | 309 | if (!valid) { 310 | this.$message({ 311 | type: 'error', 312 | message: '部分表单填写错误,请检查!' 313 | }); 314 | return; 315 | } 316 | 317 | this.showResultModal = true; 318 | } 319 | } 320 | }); 321 | 322 | BasicUsage.story = { 323 | name: 'Basic Usage' 324 | }; 325 | 326 | // =============== End of Basic Usage =============== // 327 | 328 | export default { 329 | title: 'FormBuilder|Element UI', 330 | parameters: { 331 | notes: { 332 | markdown 333 | } 334 | } 335 | }; 336 | -------------------------------------------------------------------------------- /stories/elementui/components/ExampleActorComplex.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 37 | 38 | 136 | -------------------------------------------------------------------------------- /stories/elementui/components/ExampleChannel.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 32 | 33 | 116 | -------------------------------------------------------------------------------- /stories/elementui/components/ExampleName.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 30 | 31 | 85 | -------------------------------------------------------------------------------- /stories/elementui/components/ExampleType.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 87 | -------------------------------------------------------------------------------- /stories/elementui/components/index.js: -------------------------------------------------------------------------------- 1 | import ExampleChannel from './ExampleChannel'; 2 | import ExampleName from './ExampleName'; 3 | import ExampleType from './ExampleType'; 4 | import ExampleActorComplex from './ExampleActorComplex'; 5 | 6 | export default { 7 | ExampleChannel, 8 | ExampleName, 9 | ExampleType, 10 | ExampleActorComplex 11 | }; 12 | -------------------------------------------------------------------------------- /stories/elementui/style.scss: -------------------------------------------------------------------------------- 1 | .form-builder-example { 2 | padding: 10px; 3 | 4 | .el-card { 5 | margin-bottom: 15px; 6 | 7 | .el-card__header { 8 | padding: 10px 20px; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /stories/viewui/FormBuilder.md: -------------------------------------------------------------------------------- 1 | # View UI Form Builder 2 | 3 | Vue form builder with view ui. 4 | -------------------------------------------------------------------------------- /stories/viewui/FormBuilder.stories.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueCompositionApi from '@vue/composition-api'; 3 | import { useForm, useLoading } from '@fext/vue-use'; 4 | import ViewUI from 'view-design'; 5 | import { 6 | ValidationProvider, 7 | ValidationObserver, 8 | localize 9 | } from 'vee-validate/dist/vee-validate.full'; 10 | 11 | import markdown from './FormBuilder.md'; 12 | import { createFormBuilder } from '@/src'; 13 | import { ViewFormAdaptor } from '@/src/view-form-adaptor'; 14 | import ExampleComponents from './components'; 15 | 16 | import 'view-design/dist/styles/iview.css'; 17 | import './style.scss'; 18 | 19 | Vue.use(VueCompositionApi); 20 | Vue.use(ViewUI); 21 | 22 | localize({ 23 | zh: { 24 | name: 'zh', 25 | messages: { 26 | max: '该字段最大长度为 {length} 个字符', 27 | min: '该字段最小长度为 {length} 个字符', 28 | required: '该字段不能为空' 29 | } 30 | } 31 | }); 32 | 33 | localize('zh'); 34 | 35 | Vue.component('ValidationProvider', ValidationProvider); 36 | Vue.component('ValidationObserver', ValidationObserver); 37 | 38 | // =============== Start of Basic Usage =============== // 39 | 40 | export const BasicUsage = () => ({ 41 | template: ` 42 |
43 | 44 | 45 | 46 |
47 | 48 | 49 |
50 |
51 | 58 | 59 |
60 | 63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
72 |
73 | 74 |
{{ JSON.stringify(formValues, null, 4) }}
75 | 76 | 77 | 78 |
79 |
80 | `, 81 | 82 | components: { 83 | FormBuilder: createFormBuilder({ 84 | components: { 85 | ViewFormAdaptor, 86 | 87 | ...ExampleComponents 88 | } 89 | }) 90 | }, 91 | 92 | setup() { 93 | const form = useForm(); 94 | const { formValues, setInitialFormValues, updateFormValues } = form; 95 | const { loading, withLoading } = useLoading(); 96 | 97 | return { 98 | // from form composition 99 | form, 100 | formValues, 101 | setInitialFormValues, 102 | updateFormValues, 103 | 104 | // from loading composition 105 | loading, 106 | withLoading 107 | }; 108 | }, 109 | 110 | data() { 111 | return { 112 | showResultModal: false, 113 | 114 | metadata: {}, 115 | 116 | formShares: { 117 | size: 'default', 118 | 119 | props: { 120 | clearable: true 121 | } 122 | }, 123 | 124 | formConfig: [], 125 | 126 | formConfigJSON: JSON.stringify( 127 | [ 128 | { 129 | component: 'Card', 130 | props: { 131 | title: '基础信息' 132 | }, 133 | fields: [ 134 | { 135 | name: 'channel', 136 | component: 'ExampleChannel', 137 | rules: { 138 | required: true 139 | } 140 | }, 141 | { 142 | name: 'name', 143 | component: 'ExampleName', 144 | rules: { 145 | required: true 146 | } 147 | }, 148 | { 149 | name: 'comment', 150 | component: 'ViewFormAdaptor', 151 | label: '评语', 152 | tip: '一句话评价(使用 FormAdaptor 的自定义字段)', 153 | tooltip: '精彩点评', 154 | rules: { 155 | required: true, 156 | max: 50, 157 | min: 10 158 | }, 159 | props: { 160 | placeholder: '不超过 20 个字' 161 | } 162 | } 163 | ] 164 | }, 165 | { 166 | component: 'Card', 167 | props: { 168 | title: '高级信息' 169 | }, 170 | fields: [ 171 | { 172 | name: 'type', 173 | component: 'ExampleType', 174 | defaultValue: 2 175 | }, 176 | { 177 | name: 'actor', 178 | component: 'ExampleActorComplex' 179 | }, 180 | { 181 | name: 'date', 182 | component: 'ViewFormAdaptor', 183 | label: '发行日期', 184 | extend: { 185 | component: 'DatePicker' 186 | }, 187 | props: { 188 | placeholder: '请通过日期选择器' 189 | } 190 | }, 191 | { 192 | name: 'description', 193 | component: 'ViewFormAdaptor', 194 | label: '描述', 195 | tip: '剧情描述(使用 FormAdaptor 的自定义字段)', 196 | rules: { 197 | max: 180 198 | }, 199 | props: { 200 | type: 'textarea', 201 | rows: 5, 202 | placeholder: '不超过 180 个字' 203 | } 204 | } 205 | ] 206 | } 207 | ], 208 | null, 209 | 2 210 | ) 211 | }; 212 | }, 213 | 214 | watch: { 215 | formConfigJSON: { 216 | handler(value) { 217 | try { 218 | this.formConfig = JSON.parse(this.formConfigJSON); 219 | } catch (err) { 220 | // this.formConfig = this.formConfig; 221 | } 222 | }, 223 | immediate: true 224 | } 225 | }, 226 | 227 | created() { 228 | this.registerEvents(); 229 | this.renderForm(); 230 | }, 231 | 232 | methods: { 233 | delay(timeout = 1000) { 234 | return new Promise(resolve => { 235 | setTimeout(() => { 236 | resolve(); 237 | }, timeout); 238 | }); 239 | }, 240 | 241 | getInitialValues() { 242 | return {}; 243 | }, 244 | 245 | registerEvents() { 246 | this.$on('doSomething', options => { 247 | this.$message({ 248 | type: 'success', 249 | message: '触发表单事件成功!' 250 | }); 251 | }); 252 | }, 253 | 254 | handleCommand(cmd, ...args) { 255 | this.$emit(cmd, ...args); 256 | }, 257 | 258 | async renderForm() { 259 | return this.withLoading(() => { 260 | return Promise.all([this.getRenderMetadata(), this.getFormValues()]); 261 | }); 262 | }, 263 | 264 | async getRenderMetadata() { 265 | await this.delay(); 266 | 267 | return Promise.resolve({ 268 | channels: [ 269 | { 270 | id: 1, 271 | code: 'dy', 272 | name: '电影' 273 | }, 274 | { 275 | id: 2, 276 | code: 'dm', 277 | name: '动漫' 278 | } 279 | ] 280 | }).then(data => { 281 | this.metadata = data; 282 | }); 283 | }, 284 | 285 | async getFormValues() { 286 | await this.delay(); 287 | 288 | return Promise.resolve({ 289 | channel: 2, 290 | name: 'Form Builder', 291 | comment: 'A powerful form builder', 292 | actor: ['Yang', 'Zhang'], 293 | description: 'What a nice tool' 294 | }).then(data => { 295 | const { formValues } = this; 296 | 297 | this.updateFormValues({ 298 | ...formValues, 299 | ...data 300 | }); 301 | }); 302 | }, 303 | 304 | async save() { 305 | const valid = await this.$refs.observer.validate(); 306 | 307 | if (!valid) { 308 | this.$message({ 309 | type: 'error', 310 | message: '部分表单填写错误,请检查!' 311 | }); 312 | return; 313 | } 314 | 315 | this.showResultModal = true; 316 | } 317 | } 318 | }); 319 | 320 | BasicUsage.story = { 321 | name: 'Basic Usage' 322 | }; 323 | 324 | // =============== End of Basic Usage =============== // 325 | 326 | export default { 327 | title: 'FormBuilder|View UI', 328 | parameters: { 329 | notes: { 330 | markdown 331 | } 332 | } 333 | }; 334 | -------------------------------------------------------------------------------- /stories/viewui/components/ExampleActorComplex.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 32 | 33 | 131 | -------------------------------------------------------------------------------- /stories/viewui/components/ExampleChannel.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 31 | 32 | 115 | -------------------------------------------------------------------------------- /stories/viewui/components/ExampleName.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | 29 | 83 | -------------------------------------------------------------------------------- /stories/viewui/components/ExampleType.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 87 | -------------------------------------------------------------------------------- /stories/viewui/components/index.js: -------------------------------------------------------------------------------- 1 | import ExampleChannel from './ExampleChannel'; 2 | import ExampleName from './ExampleName'; 3 | import ExampleType from './ExampleType'; 4 | import ExampleActorComplex from './ExampleActorComplex'; 5 | 6 | export default { 7 | ExampleChannel, 8 | ExampleName, 9 | ExampleType, 10 | ExampleActorComplex 11 | }; 12 | -------------------------------------------------------------------------------- /stories/viewui/style.scss: -------------------------------------------------------------------------------- 1 | .form-builder-example { 2 | padding: 10px; 3 | 4 | .ivu-form { 5 | position: relative; 6 | } 7 | 8 | .ivu-card { 9 | margin-bottom: 15px; 10 | 11 | .ivu-card-header { 12 | padding: 10px 20px; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | 'jest/globals': true 4 | }, 5 | plugins: ['jest'] 6 | }; 7 | -------------------------------------------------------------------------------- /test/unit/setup.js: -------------------------------------------------------------------------------- 1 | global.console = { 2 | log: console.log, 3 | error: console.error, 4 | warn: jest.fn(), 5 | info: console.info, 6 | debug: console.debug 7 | }; 8 | -------------------------------------------------------------------------------- /test/unit/specs/antd-form-adaptor.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueCompositionAPI from '@vue/composition-api'; 3 | import { mount } from '@vue/test-utils'; 4 | import { useForm, useFormElement } from '@fext/vue-use'; 5 | import Antd from 'ant-design-vue'; 6 | import { 7 | ValidationProvider, 8 | ValidationObserver 9 | } from 'vee-validate/dist/vee-validate.full'; 10 | import { createFormBuilder } from 'src'; 11 | import { AntFormAdaptor } from 'src/ant-form-adaptor'; 12 | 13 | const TestComponent = { 14 | template: ` 15 | 21 | `, 22 | 23 | components: { 24 | FormBuilder: createFormBuilder({ 25 | components: { 26 | AntFormAdaptor 27 | } 28 | }) 29 | }, 30 | 31 | setup() { 32 | const form = useForm(); 33 | const { formValues, updateFormValues } = form; 34 | 35 | return { 36 | form, 37 | formValues, 38 | updateFormValues 39 | }; 40 | }, 41 | 42 | data() { 43 | return { 44 | showResultModal: false, 45 | 46 | metadata: {}, 47 | 48 | formShares: { 49 | size: 'medium' 50 | }, 51 | 52 | formConfig: [ 53 | { 54 | component: 'div', 55 | fields: [ 56 | { 57 | name: 'comment', 58 | component: 'AntFormAdaptor', 59 | label: 'comment-label', 60 | tip: 'comment-tip', 61 | rules: { 62 | required: true, 63 | max: 50, 64 | min: 10 65 | }, 66 | props: { 67 | placeholder: 'comment-placeholder' 68 | } 69 | }, 70 | { 71 | name: 'type', 72 | component: 'AntFormAdaptor', 73 | label: 'type-label', 74 | tip: 'type-tip', 75 | items: [ 76 | { text: 'Type-1', value: '1' }, 77 | { text: 'Type-2', value: '2' }, 78 | { text: 'Type-3', value: '3' } 79 | ], 80 | extend: { 81 | component: 'a-checkbox-group' 82 | } 83 | } 84 | ] 85 | } 86 | ] 87 | }; 88 | } 89 | }; 90 | 91 | let wrapper = null; 92 | let vm = null; 93 | beforeEach(() => { 94 | wrapper = mount(TestComponent); 95 | vm = wrapper.vm; 96 | }); 97 | 98 | afterEach(() => { 99 | wrapper.destroy(); 100 | }); 101 | 102 | beforeAll(() => { 103 | Vue.use(VueCompositionAPI); 104 | Vue.use(Antd); 105 | 106 | Vue.component('ValidationProvider', ValidationProvider); 107 | Vue.component('ValidationObserver', ValidationObserver); 108 | }); 109 | 110 | describe('ant design vue form adaptor', () => { 111 | test('render adaptors', () => { 112 | const adaptorWrappers = wrapper.findAllComponents(AntFormAdaptor); 113 | 114 | expect(adaptorWrappers.length).toBe(2); 115 | 116 | expect(wrapper.findAll('.ant-input').length).toBe(1); 117 | expect(wrapper.findAll('.ant-checkbox').length).toBe(3); 118 | }); 119 | 120 | test('update value', async () => { 121 | const inputWrapper = wrapper.findAllComponents(AntFormAdaptor).at(0); 122 | 123 | inputWrapper.vm.updateAntLocalValue({ 124 | target: { 125 | value: 'ant' 126 | } 127 | }); 128 | 129 | await Vue.nextTick(); 130 | 131 | expect(vm.formValues.comment).toBe('ant'); 132 | 133 | inputWrapper.vm.updateAntLocalValue('foo'); 134 | 135 | await Vue.nextTick(); 136 | 137 | expect(vm.formValues.comment).toBe('foo'); 138 | 139 | vm.formValues.comment = 'bar'; 140 | 141 | await Vue.nextTick(); 142 | 143 | expect(inputWrapper.vm.localValue).toBe('bar'); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /test/unit/specs/element-form-adaptor.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueCompositionAPI from '@vue/composition-api'; 3 | import { mount } from '@vue/test-utils'; 4 | import { useForm, useFormElement } from '@fext/vue-use'; 5 | import ElementUI from 'element-ui'; 6 | import { 7 | ValidationProvider, 8 | ValidationObserver 9 | } from 'vee-validate/dist/vee-validate.full'; 10 | import { createFormBuilder } from 'src'; 11 | import { ElFormAdaptor } from 'src/el-form-adaptor'; 12 | 13 | const TestComponent = { 14 | template: ` 15 | 21 | `, 22 | 23 | components: { 24 | FormBuilder: createFormBuilder({ 25 | components: { 26 | ElFormAdaptor 27 | } 28 | }) 29 | }, 30 | 31 | setup() { 32 | const form = useForm(); 33 | const { formValues, updateFormValues } = form; 34 | 35 | return { 36 | form, 37 | formValues, 38 | updateFormValues 39 | }; 40 | }, 41 | 42 | data() { 43 | return { 44 | showResultModal: false, 45 | 46 | metadata: {}, 47 | 48 | formShares: { 49 | size: 'medium' 50 | }, 51 | 52 | formConfig: [ 53 | { 54 | component: 'div', 55 | fields: [ 56 | { 57 | name: 'comment', 58 | component: 'ElFormAdaptor', 59 | label: 'comment-label', 60 | tip: 'comment-tip', 61 | rules: { 62 | required: true, 63 | max: 50, 64 | min: 10 65 | }, 66 | props: { 67 | placeholder: 'comment-placeholder' 68 | } 69 | }, 70 | { 71 | name: 'type', 72 | component: 'ElFormAdaptor', 73 | label: 'type-label', 74 | tip: 'type-tip', 75 | items: [ 76 | { text: 'Type-1', value: '1' }, 77 | { text: 'Type-2', value: '2' }, 78 | { text: 'Type-3', value: '3' } 79 | ], 80 | extend: { 81 | component: 'el-checkbox-group' 82 | }, 83 | props: { 84 | min: 1 85 | } 86 | } 87 | ] 88 | } 89 | ] 90 | }; 91 | } 92 | }; 93 | 94 | let wrapper = null; 95 | let vm = null; 96 | beforeEach(() => { 97 | wrapper = mount(TestComponent); 98 | vm = wrapper.vm; 99 | }); 100 | 101 | afterEach(() => { 102 | wrapper.destroy(); 103 | }); 104 | 105 | beforeAll(() => { 106 | Vue.use(VueCompositionAPI); 107 | Vue.use(ElementUI); 108 | 109 | Vue.component('ValidationProvider', ValidationProvider); 110 | Vue.component('ValidationObserver', ValidationObserver); 111 | }); 112 | 113 | describe('element ui form adaptor', () => { 114 | test('render adaptors', () => { 115 | const adaptorWrappers = wrapper.findAllComponents(ElFormAdaptor); 116 | 117 | expect(adaptorWrappers.length).toBe(2); 118 | 119 | expect(wrapper.findAll('.el-input').length).toBe(1); 120 | expect(wrapper.findAll('.el-checkbox').length).toBe(3); 121 | }); 122 | 123 | test('update value', async () => { 124 | const inputWrapper = wrapper.findAllComponents(ElFormAdaptor).at(0); 125 | 126 | inputWrapper.vm.updateLocalValue('foo'); 127 | 128 | await Vue.nextTick(); 129 | 130 | expect(vm.formValues.comment).toBe('foo'); 131 | 132 | vm.formValues.comment = 'bar'; 133 | 134 | await Vue.nextTick(); 135 | 136 | expect(inputWrapper.vm.localValue).toBe('bar'); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /test/unit/specs/formbuilder.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueCompositionAPI from '@vue/composition-api'; 3 | import { mount } from '@vue/test-utils'; 4 | import { useForm, useFormElement } from '@fext/vue-use'; 5 | import { createFormBuilder } from 'src'; 6 | import { ElFormAdaptor } from 'src/el-form-adaptor'; 7 | 8 | const ExampleName = { 9 | template: ` 10 |
11 | 12 | 13 |
14 | `, 15 | 16 | props: { 17 | name: String, 18 | tip: String, 19 | tooltip: String, 20 | label: String, 21 | hide: Boolean, 22 | rules: { 23 | type: [String, Object] 24 | }, 25 | value: { 26 | required: false 27 | }, 28 | defaultValue: { 29 | required: false 30 | }, 31 | items: Array, 32 | extend: Object, 33 | metadata: Object, 34 | formValues: { 35 | type: Object, 36 | required: false 37 | } 38 | }, 39 | 40 | setup(props, context) { 41 | const { localValue, updateLocalValue } = useFormElement(props, context); 42 | 43 | return { 44 | localValue, 45 | updateLocalValue 46 | }; 47 | }, 48 | 49 | methods: { 50 | doSomething(evt) { 51 | this.$emit('command', 'doSomething', { id: 123456 }); 52 | } 53 | } 54 | }; 55 | 56 | const TestComponent = { 57 | template: `
58 | 65 |
`, 66 | 67 | components: { 68 | FormBuilder: createFormBuilder({ 69 | components: { 70 | ExampleName, 71 | ElFormAdaptor 72 | } 73 | }) 74 | }, 75 | 76 | setup() { 77 | const form = useForm(); 78 | const { formValues, updateFormValues } = form; 79 | 80 | return { 81 | form, 82 | formValues, 83 | updateFormValues 84 | }; 85 | }, 86 | 87 | data() { 88 | return { 89 | showResultModal: false, 90 | 91 | metadata: {}, 92 | 93 | formShares: { 94 | size: 'medium' 95 | }, 96 | 97 | formConfig: [ 98 | { 99 | component: 'div', 100 | fields: [ 101 | { 102 | name: 'name', 103 | component: 'ExampleName', 104 | label: 'Name', 105 | defaultValue: 'example', 106 | rules: { 107 | required: true 108 | } 109 | } 110 | ] 111 | } 112 | ] 113 | }; 114 | }, 115 | 116 | methods: { 117 | handleCommand(cmd, ...args) { 118 | mockCommand(cmd, ...args); 119 | } 120 | } 121 | }; 122 | 123 | let mockCommand = null; 124 | let wrapper = null; 125 | let vm = null; 126 | beforeEach(() => { 127 | wrapper = mount(TestComponent); 128 | vm = wrapper.vm; 129 | mockCommand = jest.fn(); 130 | }); 131 | 132 | afterEach(() => { 133 | wrapper.destroy(); 134 | }); 135 | 136 | beforeAll(() => { 137 | Vue.use(VueCompositionAPI); 138 | }); 139 | 140 | describe('form builder', () => { 141 | test('receive props', () => { 142 | const nameWrapper = wrapper.findComponent(ExampleName); 143 | const nameVM = nameWrapper.vm; 144 | 145 | expect(nameVM.name).toBe('name'); 146 | }); 147 | 148 | test('default value', () => { 149 | const nameWrapper = wrapper.findComponent(ExampleName); 150 | const nameVM = nameWrapper.vm; 151 | 152 | expect(nameVM.value).toBe('example'); 153 | }); 154 | 155 | test('handle command', () => { 156 | const nameWrapper = wrapper.findComponent(ExampleName); 157 | const nameVM = nameWrapper.vm; 158 | const button = nameWrapper.find('button'); 159 | 160 | button.trigger('click'); 161 | button.trigger('click'); 162 | 163 | expect(mockCommand.mock.calls.length).toBe(2); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /test/unit/specs/plugin.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueCompositionAPI from '@vue/composition-api'; 3 | import { mount } from '@vue/test-utils'; 4 | import { useForm, useFormElement } from '@fext/vue-use'; 5 | import FormBuilder from 'src'; 6 | import ElFormAdatpor from 'src/el-form-adaptor'; 7 | import ViewFormAdatpor from 'src/view-form-adaptor'; 8 | import AntFormAdatpor from 'src/ant-form-adaptor'; 9 | 10 | const TestComponent = { 11 | template: `
12 | 16 |
`, 17 | 18 | setup() { 19 | const form = useForm(); 20 | 21 | return { 22 | form 23 | }; 24 | } 25 | }; 26 | 27 | let wrapper = null; 28 | let vm = null; 29 | beforeEach(() => { 30 | wrapper = mount(TestComponent); 31 | vm = wrapper.vm; 32 | }); 33 | 34 | afterEach(() => { 35 | wrapper.destroy(); 36 | }); 37 | 38 | beforeAll(() => { 39 | Vue.use(VueCompositionAPI); 40 | Vue.use(FormBuilder); 41 | Vue.use(ElFormAdatpor); 42 | Vue.use(ViewFormAdatpor); 43 | Vue.use(AntFormAdatpor); 44 | }); 45 | 46 | describe('vue plugin', () => { 47 | test('use form builder', () => { 48 | const component = Vue.component('form-builder'); 49 | 50 | expect(component).toBeDefined(); 51 | }); 52 | 53 | test('use element form adaptor', () => { 54 | const component = Vue.component('el-form-adaptor'); 55 | 56 | expect(component).toBeDefined(); 57 | }); 58 | 59 | test('use view form adaptor', () => { 60 | const component = Vue.component('view-form-adaptor'); 61 | 62 | expect(component).toBeDefined(); 63 | }); 64 | 65 | test('use ant design adaptor', () => { 66 | const component = Vue.component('ant-form-adaptor'); 67 | 68 | expect(component).toBeDefined(); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/unit/specs/view-form-adaptor.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueCompositionAPI from '@vue/composition-api'; 3 | import { mount } from '@vue/test-utils'; 4 | import { useForm, useFormElement } from '@fext/vue-use'; 5 | import ViewUI from 'view-design'; 6 | import { 7 | ValidationProvider, 8 | ValidationObserver 9 | } from 'vee-validate/dist/vee-validate.full'; 10 | import { createFormBuilder } from 'src'; 11 | import { ViewFormAdaptor } from 'src/view-form-adaptor'; 12 | 13 | const TestComponent = { 14 | template: `
15 | 21 |
`, 22 | 23 | components: { 24 | FormBuilder: createFormBuilder({ 25 | components: { 26 | ViewFormAdaptor 27 | } 28 | }) 29 | }, 30 | 31 | setup() { 32 | const form = useForm(); 33 | const { formValues, updateFormValues } = form; 34 | 35 | return { 36 | form, 37 | formValues, 38 | updateFormValues 39 | }; 40 | }, 41 | 42 | data() { 43 | return { 44 | showResultModal: false, 45 | 46 | metadata: {}, 47 | 48 | formShares: { 49 | size: 'medium' 50 | }, 51 | 52 | formConfig: [ 53 | { 54 | component: 'div', 55 | fields: [ 56 | { 57 | name: 'comment', 58 | component: 'ViewFormAdaptor', 59 | label: 'comment-label', 60 | tip: 'comment-tip', 61 | rules: { 62 | required: true, 63 | max: 50, 64 | min: 10 65 | }, 66 | props: { 67 | placeholder: 'comment-placeholder' 68 | } 69 | }, 70 | { 71 | name: 'type', 72 | component: 'ViewFormAdaptor', 73 | label: 'type-label', 74 | tip: 'type-tip', 75 | items: [ 76 | { text: 'Type-1', value: '1' }, 77 | { text: 'Type-2', value: '2' }, 78 | { text: 'Type-3', value: '3' } 79 | ], 80 | extend: { 81 | component: 'CheckboxGroup' 82 | } 83 | } 84 | ] 85 | } 86 | ] 87 | }; 88 | } 89 | }; 90 | 91 | let wrapper = null; 92 | let vm = null; 93 | beforeEach(() => { 94 | wrapper = mount(TestComponent); 95 | vm = wrapper.vm; 96 | }); 97 | 98 | afterEach(() => { 99 | wrapper.destroy(); 100 | }); 101 | 102 | beforeAll(() => { 103 | Vue.use(VueCompositionAPI); 104 | Vue.use(ViewUI); 105 | 106 | Vue.component('ValidationProvider', ValidationProvider); 107 | Vue.component('ValidationObserver', ValidationObserver); 108 | }); 109 | 110 | describe('view ui form adaptor', () => { 111 | test('render adaptors', () => { 112 | const adaptorWrappers = wrapper.findAllComponents(ViewFormAdaptor); 113 | 114 | expect(adaptorWrappers.length).toBe(2); 115 | 116 | expect(wrapper.findAll('.ivu-input').length).toBe(1); 117 | expect(wrapper.findAll('.ivu-checkbox').length).toBe(3); 118 | }); 119 | 120 | test('update value', async () => { 121 | const inputWrapper = wrapper.findAllComponents(ViewFormAdaptor).at(0); 122 | 123 | inputWrapper.vm.updateLocalValue('foo'); 124 | 125 | await Vue.nextTick(); 126 | 127 | expect(vm.formValues.comment).toBe('foo'); 128 | 129 | vm.formValues.comment = 'bar'; 130 | 131 | await Vue.nextTick(); 132 | 133 | expect(inputWrapper.vm.localValue).toBe('bar'); 134 | }); 135 | }); 136 | --------------------------------------------------------------------------------