├── .env.docs ├── .browserslistrc ├── babel.config.js ├── tests └── unit │ ├── .eslintrc.js │ ├── __mocks__ │ └── popper.js.js │ ├── Button.spec.js │ ├── FormBuilder.spec.js │ ├── Radio.spec.js │ ├── data │ └── formSchema.js │ ├── Checkbox.spec.js │ ├── Input.spec.js │ └── Select.spec.js ├── postcss.config.js ├── src ├── assets │ └── fonts │ │ ├── icomoon.ttf │ │ └── icomoon.woff ├── utils │ ├── index.js │ └── directives.js ├── scss │ ├── _fonts.scss │ ├── _variables.scss │ └── _mixins.scss └── components │ ├── checkbox │ ├── CheckboxGroup.vue │ └── Checkbox.vue │ ├── index.js │ ├── form │ ├── FormItem.vue │ └── Form.vue │ ├── select │ ├── Option.vue │ └── Select.vue │ ├── button │ └── Button.vue │ ├── radio │ └── Radio.vue │ ├── from-builder │ └── FormBuilder.vue │ ├── input │ └── Input.vue │ └── popper │ └── Popper.vue ├── dist ├── demo.html └── vfc.css ├── example ├── main.js ├── App.vue ├── assets │ ├── variables.scss │ ├── logo.svg │ ├── main.scss │ └── normalize.scss ├── router.js ├── components │ ├── Tabs │ │ ├── TabsItem.vue │ │ └── Tabs.vue │ └── CarbonAd.vue ├── navigation.js ├── views │ ├── Home.vue │ └── Page.vue ├── index.html └── util.js ├── dev ├── main.js ├── App.vue └── index.html ├── .travis.yml ├── .gitignore ├── public └── docs │ ├── CHANGELOG.md │ ├── install.md │ ├── button.md │ ├── radio.md │ ├── checkbox.md │ ├── input.md │ ├── select.md │ ├── form.md │ └── form-builder.md ├── .eslintrc.js ├── jest.config.js ├── vue.config.js ├── LICENSE ├── README.md └── package.json /.env.docs: -------------------------------------------------------------------------------- 1 | NODE_ENV=docs 2 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonreshetov/vue-form-components/HEAD/src/assets/fonts/icomoon.ttf -------------------------------------------------------------------------------- /src/assets/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonreshetov/vue-form-components/HEAD/src/assets/fonts/icomoon.woff -------------------------------------------------------------------------------- /dist/demo.html: -------------------------------------------------------------------------------- 1 | 2 | vfc demo 3 | 4 | 5 | 6 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import router from './router' 3 | import App from './App.vue' 4 | 5 | Vue.config.productionTip = false 6 | 7 | new Vue({ 8 | render: h => h(App), 9 | router 10 | }).$mount('#app') 11 | -------------------------------------------------------------------------------- /dev/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import VFC from '../src/components' 4 | 5 | Vue.config.productionTip = false 6 | Vue.use(VFC) 7 | 8 | new Vue({ 9 | render: h => h(App) 10 | }).$mount('#app') 11 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export function cloneShallow (obj) { 2 | return Object.keys(obj).reduce((acc, prop) => { 3 | if (Array.isArray(obj[prop])) { 4 | acc[prop] = obj[prop].slice(0) 5 | } else { 6 | acc[prop] = obj[prop] 7 | } 8 | return acc 9 | }, {}) 10 | } 11 | -------------------------------------------------------------------------------- /tests/unit/__mocks__/popper.js.js: -------------------------------------------------------------------------------- 1 | import PopperJs from 'popper.js' 2 | 3 | export default class Popper { 4 | static placements = PopperJs.placements 5 | 6 | constructor () { 7 | return { 8 | destroy: () => {}, 9 | scheduleUpdate: () => {} 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | languange: node_js 2 | node_js: 3 | - stable 4 | script: 5 | - npm install 6 | - npm run build:docs 7 | deploy: 8 | provider: pages 9 | skip-cleanup: true 10 | github-token: $GITHUB_TOKEN 11 | keep-history: true 12 | local_dir: docs 13 | on: 14 | branch: dev 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /docs 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /public/docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## `2.0.0` 4 | 5 | ### BREAKING 6 | - Drop async validation 7 | - Add VeeValidate for form validation 8 | 9 | ### ADD 10 | - Form Builder 11 | 12 | ## `1.1.0` 13 | 14 | ### ADD 15 | - Collapse tags props in Select 16 | 17 | ## `1.0.0` 18 | 19 | First release 20 | -------------------------------------------------------------------------------- /example/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | 24 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/recommended', 8 | '@vue/standard' 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 13 | }, 14 | parserOptions: { 15 | parser: 'babel-eslint' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /dev/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /example/assets/variables.scss: -------------------------------------------------------------------------------- 1 | /*------------------------------------*/ 2 | /* COLORS */ 3 | /*------------------------------------*/ 4 | $color-primary: #498aed; 5 | $color-success: #68cc68; 6 | $color-warning: #efc669; 7 | $color-danger: #e4474e; 8 | $color-text-regular: #303133; 9 | $color-text-faded: lighten($color-text-regular, 45%); 10 | $color-text-faded-low: lighten($color-text-regular, 30%); 11 | $color-grey: #dddddd; 12 | $color-grey-light: #fafafa; -------------------------------------------------------------------------------- /example/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Home from './views/Home.vue' 4 | import Page from './views/Page.vue' 5 | 6 | Vue.use(Router) 7 | 8 | export default new Router({ 9 | linkActiveClass: 'active', 10 | routes: [ 11 | { 12 | path: '/', 13 | name: 'home', 14 | component: Home 15 | }, 16 | { 17 | path: '/components/:component', 18 | component: Page, 19 | meta: 'docs' 20 | }, 21 | { 22 | path: '/changelog', 23 | component: Page 24 | } 25 | ] 26 | }) 27 | -------------------------------------------------------------------------------- /example/components/Tabs/TabsItem.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 27 | 28 | 31 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: [ 3 | 'js', 4 | 'jsx', 5 | 'json', 6 | 'vue' 7 | ], 8 | transform: { 9 | '^.+\\.vue$': 'vue-jest', 10 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', 11 | '^.+\\.jsx?$': 'babel-jest' 12 | }, 13 | transformIgnorePatterns: [ 14 | '/node_modules/' 15 | ], 16 | moduleNameMapper: { 17 | '^@/(.*)$': '/src/$1' 18 | }, 19 | snapshotSerializers: [ 20 | 'jest-serializer-vue' 21 | ], 22 | testMatch: [ 23 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 24 | ], 25 | testURL: 'http://localhost/' 26 | } 27 | -------------------------------------------------------------------------------- /example/navigation.js: -------------------------------------------------------------------------------- 1 | export default { 2 | development: [ 3 | { 4 | title: 'Install', 5 | path: '/components/install' 6 | } 7 | ], 8 | components: [ 9 | { 10 | title: 'Input', 11 | path: '/components/input' 12 | }, 13 | { 14 | title: 'Select', 15 | path: '/components/select' 16 | }, 17 | { 18 | title: 'Checkbox', 19 | path: '/components/checkbox' 20 | }, 21 | { 22 | title: 'Radio', 23 | path: '/components/radio' 24 | }, 25 | { 26 | title: 'Button', 27 | path: '/components/button' 28 | }, 29 | { 30 | title: 'Form', 31 | path: '/components/form' 32 | }, 33 | { 34 | title: 'Form Builder', 35 | path: '/components/form-builder' 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const isDev = process.env.NODE_ENV === 'development' 2 | const isDocs = process.env.APP_TARGET === 'docs' 3 | 4 | module.exports = { 5 | publicPath: isDocs ? '/vue-form-components/' : '/', 6 | 7 | chainWebpack: config => { 8 | config.entryPoints.delete('app') 9 | 10 | if (isDocs) { 11 | config 12 | .entry('docs') 13 | .add('./example/main.js') 14 | .end() 15 | .plugin('html') 16 | .tap(args => { 17 | args[0].template = './example/index.html' 18 | return args 19 | }) 20 | } 21 | if (isDev) { 22 | config 23 | .entry('dev') 24 | .add('./dev/main.js') 25 | .end() 26 | .plugin('html') 27 | .tap(args => { 28 | args[0].template = './dev/index.html' 29 | return args 30 | }) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /dev/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | Vue Form Component - Development 12 | 13 | 14 | 15 | 18 |
19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/scss/_fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'icomoon'; 3 | src: url('~@/assets/fonts/icomoon.ttf') format('truetype'), 4 | url('~@/assets/fonts/icomoon.woff') format('woff'); 5 | font-weight: normal; 6 | font-style: normal; 7 | } 8 | 9 | [class^='icon-'], 10 | [class*=' icon-'] { 11 | /* use !important to prevent issues with browser extensions that change fonts */ 12 | font-family: 'icomoon' !important; 13 | font-style: normal; 14 | font-weight: normal; 15 | font-variant: normal; 16 | text-transform: none; 17 | line-height: 1; 18 | 19 | /* Better Font Rendering =========== */ 20 | -webkit-font-smoothing: antialiased; 21 | -moz-osx-font-smoothing: grayscale; 22 | } 23 | 24 | .icon-check:before { 25 | content: '\e900'; 26 | } 27 | .icon-chevron-down:before { 28 | content: '\e901'; 29 | } 30 | .icon-close:before { 31 | content: '\e902'; 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/directives.js: -------------------------------------------------------------------------------- 1 | export const clickOutside = { 2 | bind (el, binding, vNode) { 3 | if (typeof binding.value !== 'function') { 4 | const compName = vNode.context.name 5 | 6 | let warn = `provided expression '${ 7 | binding.expression 8 | }' is not a function, but has to be` 9 | 10 | if (compName) warn += `Found in component '${compName}'` 11 | 12 | console.warn('[v-click-outside]', warn) 13 | } 14 | 15 | const bubble = binding.modifiers.bubble 16 | const handler = e => { 17 | if (bubble || (!el.contains(e.target) && el !== e.target)) { 18 | binding.value(e) 19 | } 20 | } 21 | el.$_vfcClickOutside_ = handler 22 | 23 | document.addEventListener('click', handler) 24 | }, 25 | 26 | unbind (el, binding) { 27 | document.removeEventListener('click', el.$_vfcClickOutside_) 28 | el.$_vfcClickOutside_ = null 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | /*------------------------------------*/ 2 | /* COLORS */ 3 | /*------------------------------------*/ 4 | $color-primary: #498aed; 5 | $color-success: #68cc68; 6 | $color-warning: #efc669; 7 | $color-danger: #e4474e; 8 | $color-text-regular: #303133; 9 | $color-text-faded: lighten($color-text-regular, 45%); 10 | $color-text-faded-low: lighten($color-text-regular, 30%); 11 | $color-grey: #dddddd; 12 | $color-grey-dark: #aaa; 13 | $color-grey-light: #f6f6f6; 14 | $color-border: $color-grey; 15 | /*------------------------------------*/ 16 | /* FONTS */ 17 | /*------------------------------------*/ 18 | $font: Helvetica, Arial, sans-serif; 19 | /*------------------------------------*/ 20 | /* FROM */ 21 | /*------------------------------------*/ 22 | $input-font-size: 14px; 23 | $input-height: 40px; 24 | $input-inner-padding: 0 15px; 25 | $input-border-radius: 4px; 26 | $input-border: 1px solid $color-border; -------------------------------------------------------------------------------- /src/components/checkbox/CheckboxGroup.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 59 | -------------------------------------------------------------------------------- /public/docs/install.md: -------------------------------------------------------------------------------- 1 | # Install 2 | 3 | ## NPM 4 | 5 | Installing with npm is recommended and it works seamlessly with webpack. 6 | 7 | ```js 8 | npm i vfc 9 | ``` 10 | 11 | ## Quick start 12 | 13 | ### Global 14 | 15 | To use in your project, just import vfc and install into Vue. 16 | 17 | ```js 18 | import Vue from 'vue' 19 | import App from './App.vue' 20 | import VFC from 'vfc' 21 | import 'vfc/dist/vfc.css' 22 | 23 | Vue.use(VFC) 24 | 25 | new Vue({ 26 | render: h => h(App) 27 | }).$mount('#app') 28 | ``` 29 | 30 | ### On demand 31 | 32 | ```html 33 | 36 | 37 | 47 | ``` 48 | 49 | ```js 50 | import { 51 | Input, 52 | Button, 53 | Checkbox, 54 | CheckboxGroup, 55 | Radio, 56 | Select, 57 | Option, 58 | Form, 59 | FormItem, 60 | FormBuilder 61 | } from 'vfc' 62 | ``` 63 | 64 | ## License 65 | 66 | MIT © 2018-present [Anton Reshetov](http://antonreshetov.com) 67 | -------------------------------------------------------------------------------- /example/views/Home.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 25 | 26 | 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-present Anton Reshetov 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. -------------------------------------------------------------------------------- /example/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group Copy 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import Input from './input/Input' 2 | import Button from './button/Button' 3 | import Checkbox from './checkbox/Checkbox.vue' 4 | import CheckboxGroup from './checkbox/CheckboxGroup.vue' 5 | import Radio from './radio/Radio.vue' 6 | import Select from './select/Select.vue' 7 | import Option from './select/Option.vue' 8 | import Form from './form/Form.vue' 9 | import FormItem from './form/FormItem.vue' 10 | import VeeValidate from 'vee-validate' 11 | import FormBuilder from './from-builder/FormBuilder.vue' 12 | 13 | const components = [ 14 | Input, 15 | Button, 16 | Checkbox, 17 | CheckboxGroup, 18 | Radio, 19 | Select, 20 | Option, 21 | Form, 22 | FormItem, 23 | FormBuilder 24 | ] 25 | 26 | export default { 27 | install (Vue, options = {}) { 28 | let veeValidateOptions = { 29 | events: 'change|input|blur' 30 | } 31 | 32 | if (options.veeValidate) { 33 | veeValidateOptions = Object.assign(veeValidateOptions, options.veeValidate) 34 | } 35 | 36 | Vue.use(VeeValidate, veeValidateOptions) 37 | 38 | components.forEach(component => { 39 | Vue.component(component.name, component) 40 | }) 41 | } 42 | } 43 | 44 | export { 45 | Input, 46 | Button, 47 | Checkbox, 48 | CheckboxGroup, 49 | Radio, 50 | Select, 51 | Option, 52 | Form, 53 | FormItem, 54 | FormBuilder 55 | } 56 | -------------------------------------------------------------------------------- /src/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | /*------------------------------------*/ 2 | /* FORM */ 3 | /*------------------------------------*/ 4 | @mixin form-input-default() { 5 | box-sizing: border-box; 6 | -webkit-appearance: none; 7 | border-radius: $input-border-radius; 8 | outline: none; 9 | border: $input-border; 10 | line-height: $input-height; 11 | padding: $input-inner-padding; 12 | height: $input-height; 13 | background-color: transparent; 14 | font-size: 14px; 15 | color: $color-text-regular; 16 | transition: border-color 0.3s; 17 | &:focus { 18 | border-color: $color-primary; 19 | transition: border-color 0.3s; 20 | } 21 | &::-webkit-input-placeholder { 22 | color: $color-text-faded; 23 | } 24 | } 25 | /*------------------------------------*/ 26 | /* BUTTON */ 27 | /*------------------------------------*/ 28 | @mixin button-hover-active($bg-color, $text-color, $lighten-amount, $darken-amount) { 29 | background-color: $bg-color; 30 | border-color: $bg-color; 31 | color: $text-color; 32 | &:hover { 33 | background-color: lighten($bg-color, $lighten-amount); 34 | } 35 | &:active { 36 | background-color: darken($bg-color, $darken-amount); 37 | } 38 | } 39 | @mixin button-disabled($main-color, $lighten-amount) { 40 | background-color: lighten($main-color, $lighten-amount); 41 | border-color: lighten($main-color, $lighten-amount); 42 | cursor: no-drop; 43 | } 44 | -------------------------------------------------------------------------------- /tests/unit/Button.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import Button from '../../src/components/button/Button.vue' 3 | 4 | describe('Button.vue', () => { 5 | it('is rendered', () => { 6 | const wrapper = shallowMount(Button, { 7 | propsData: { 8 | type: 'primary', 9 | disabled: false 10 | } 11 | }) 12 | expect(wrapper.exists()).toBe(true) 13 | expect(wrapper.props().type).toBe('primary') 14 | expect(wrapper.props().disabled).toBe(false) 15 | }) 16 | it('all types is rendered', () => { 17 | const wrapper = shallowMount(Button) 18 | const types = ['primary', 'success', 'warning', 'danger'] 19 | types.forEach(type => { 20 | wrapper.setProps({ type }) 21 | expect(wrapper.find(`.vue-button--${type}`)) 22 | }) 23 | }) 24 | it('is disabled', () => { 25 | const wrapper = shallowMount(Button, { 26 | propsData: { 27 | disabled: true 28 | } 29 | }) 30 | expect(wrapper.attributes().disabled).toBe('disabled') 31 | }) 32 | it('default slot is rendered', () => { 33 | const wrapper = shallowMount(Button, { 34 | slots: { 35 | default: 'text' 36 | } 37 | }) 38 | expect(wrapper.text()).toBe('text') 39 | }) 40 | it('emitted "click"', () => { 41 | const wrapper = shallowMount(Button) 42 | wrapper.trigger('click') 43 | expect(wrapper.emitted().click).toBeTruthy() 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /example/assets/main.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto:100,300,400,500'); 2 | @import url('https://fonts.googleapis.com/css?family=Fira+Mono'); 3 | @import './normalize'; 4 | @import './variables'; 5 | 6 | html, 7 | body { 8 | font-family: 'Helvetica Neue', 'Roboto'; 9 | font-weight: 400; 10 | font-size: 14px; 11 | color: $color-text-regular; 12 | } 13 | 14 | pre { 15 | padding: 0; 16 | border: none; 17 | font-family: 'Fira Mono', monospace; 18 | } 19 | 20 | code { 21 | border-radius: 3px; 22 | font-family: 'Fira Mono', monospace; 23 | background-color: #eee; 24 | padding: 0 5px; 25 | } 26 | 27 | h1 { 28 | font-size: 36px; 29 | } 30 | 31 | h2 { 32 | font-size: 26px; 33 | margin-top: 40px; 34 | } 35 | 36 | h1, 37 | h2, 38 | h3 { 39 | font-weight: 400 !important; 40 | > a { 41 | color: inherit; 42 | text-decoration: none; 43 | &:hover { 44 | text-decoration: none; 45 | color: inherit; 46 | } 47 | } 48 | } 49 | 50 | h2 { 51 | .anchor { 52 | position: relative; 53 | cursor: pointer; 54 | &::before { 55 | content: '#'; 56 | position: absolute; 57 | left: -25px; 58 | color: #ccc; 59 | transition: all 0.3s; 60 | } 61 | &:hover { 62 | &::before { 63 | color: $color-primary; 64 | } 65 | } 66 | } 67 | } 68 | 69 | p { 70 | -webkit-font-smoothing: antialiased; 71 | } 72 | 73 | .app { 74 | max-width: 1140px; 75 | margin: 0 auto; 76 | } 77 | -------------------------------------------------------------------------------- /example/components/CarbonAd.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 67 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | Vue Form Component 12 | 13 | 14 | 15 | 16 | 19 |
20 | 21 | <% if (process.env.NODE_ENV === 'production') { %> 22 | 33 | <% } %> 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/components/form/FormItem.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 56 | 57 | 70 | -------------------------------------------------------------------------------- /tests/unit/FormBuilder.spec.js: -------------------------------------------------------------------------------- 1 | import { mount, createLocalVue } from '@vue/test-utils' 2 | import flushPromises from 'flush-promises' 3 | import FormBuilder from '../../src/components/from-builder/FormBuilder.vue' 4 | import schema from './data/formSchema' 5 | import VeeValidate from 'vee-validate' 6 | 7 | const localVue = createLocalVue() 8 | localVue.use(VeeValidate) 9 | 10 | describe('FormBuilder', () => { 11 | const wrapper = mount(FormBuilder, { 12 | localVue, 13 | propsData: { 14 | model: { 15 | id: 1, 16 | name: 'John Doe', 17 | password: '123', 18 | passwordConfirm: '123', 19 | skills: [1], 20 | email: 'john.do@gmail.com', 21 | status: true, 22 | addons: [1, 3], 23 | delivery: 1, 24 | comment: 'some text' 25 | }, 26 | schema 27 | } 28 | }) 29 | it('rendered all fields from schema', () => { 30 | expect(wrapper.exists()).toBe(true) 31 | schema.fields.map(f => { 32 | if (f.type !== 'actions') { 33 | expect(wrapper.find(`[name="${f.name}"]`).exists()).toBe(true) 34 | } else { 35 | f.buttons.map((b, i) => { 36 | expect(wrapper.findAll('.vue-button').at(i).exists()).toBe(true) 37 | }) 38 | } 39 | }) 40 | }) 41 | it('check validation', async () => { 42 | const input = wrapper.find('[name="delivery"]') 43 | expect(wrapper.vm.errors.count()).toBe(0) 44 | wrapper.setData({ clonedModel: { ...wrapper.vm.clonedModel, delivery: null } }) 45 | input.trigger('input') 46 | await flushPromises() 47 | expect(wrapper.vm.errors.count()).toBe(1) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

Vue Form Components

5 |

6 | 7 | 8 | 9 |

10 | 11 | ## Documentation 12 | 13 | [https://antonreshetov.github.io/vue-form-components](https://antonreshetov.github.io/vue-form-components/) 14 | 15 | ## Install 16 | 17 | ### NPM 18 | 19 | Installing with npm is recommended and it works seamlessly with webpack. 20 | 21 | ```js 22 | npm i vfc 23 | ``` 24 | 25 | ### Download 26 | 27 | You can download latest version from the Github: Download 28 | 29 | ## Quick start 30 | 31 | ### Global 32 | 33 | To use in your project, just import vfc and install into Vue. 34 | 35 | ```js 36 | import Vue from 'vue' 37 | import App from './App.vue' 38 | import VFC from 'vfc' 39 | import 'vfc/dist/vfc.css' 40 | 41 | Vue.use(VFC) 42 | 43 | new Vue({ 44 | render: h => h(App) 45 | }).$mount('#app') 46 | ``` 47 | 48 | ### On demand 49 | 50 | ```html 51 | 54 | 55 | 65 | ``` 66 | 67 | Full component list: 68 | 69 | ```js 70 | import { 71 | Input, 72 | Button, 73 | Checkbox, 74 | CheckboxGroup, 75 | Radio, 76 | Select, 77 | Option, 78 | Form, 79 | FormItem, 80 | FormBuilder 81 | } from 'vfc' 82 | ``` 83 | 84 | ## License 85 | 86 | MIT © 2018-present [Anton Reshetov](http://antonreshetov.com) 87 | -------------------------------------------------------------------------------- /src/components/form/Form.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 41 | 42 | 91 | -------------------------------------------------------------------------------- /src/components/select/Option.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 65 | 66 | 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vfc", 3 | "description": "Vue form components with validation", 4 | "version": "1.1.2", 5 | "private": false, 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/antonreshetov/vue-form-components.git" 9 | }, 10 | "files": [ 11 | "dist", 12 | "src" 13 | ], 14 | "keywords": [ 15 | "vue", 16 | "vuejs", 17 | "vue-component", 18 | "vue-form", 19 | "components", 20 | "form", 21 | "form-validation" 22 | ], 23 | "main": "dist/vfc.common.js", 24 | "style": "dist/vfc.css", 25 | "license": "MIT", 26 | "scripts": { 27 | "serve": "vue-cli-service serve", 28 | "serve:docs": "APP_TARGET=docs vue-cli-service serve --mode docs", 29 | "build:docs": "APP_TARGET=docs vue-cli-service build --dest docs", 30 | "build": "vue-cli-service build --target lib --name vfc ./src/components/index.js", 31 | "lint": "vue-cli-service lint", 32 | "test:unit": "vue-cli-service test:unit" 33 | }, 34 | "dependencies": { 35 | "popper.js": "^1.14.4", 36 | "vee-validate": "^2.2.0", 37 | "vue": "^2.6.0", 38 | "vue-router": "^3.0.1" 39 | }, 40 | "devDependencies": { 41 | "@vue/cli-plugin-babel": "^3.5.0", 42 | "@vue/cli-plugin-eslint": "^3.5.0", 43 | "@vue/cli-plugin-unit-jest": "^3.5.0", 44 | "@vue/cli-service": "^3.5.0", 45 | "@vue/eslint-config-standard": "^4.0.0", 46 | "@vue/test-utils": "^1.0.0-beta.20", 47 | "axios": "^0.18.0", 48 | "babel-core": "^7.0.0-bridge.0", 49 | "babel-eslint": "^10.0.1", 50 | "babel-jest": "^23.0.1", 51 | "eslint": "^5.16.0", 52 | "eslint-plugin-vue": "^5.2.2", 53 | "flush-promises": "^1.0.2", 54 | "highlight.js": "^9.12.0", 55 | "lint-staged": "^8.1.5", 56 | "marked": "^0.5.1", 57 | "node-sass": "^4.13.1", 58 | "sass-loader": "^7.1.0", 59 | "vue-svg-loader": "^0.10.0", 60 | "vue-template-compiler": "^2.5.17" 61 | }, 62 | "gitHooks": { 63 | "pre-commit": "lint-staged" 64 | }, 65 | "lint-staged": { 66 | "*.js": [ 67 | "vue-cli-service lint", 68 | "git add" 69 | ], 70 | "*.vue": [ 71 | "vue-cli-service lint", 72 | "git add" 73 | ] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /example/components/Tabs/Tabs.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 69 | 70 | 105 | -------------------------------------------------------------------------------- /public/docs/button.md: -------------------------------------------------------------------------------- 1 | # Button 2 | 3 | Simple button component 4 | 5 | ## Basic usage 6 | 7 | ```example 8 | 15 | 22 | ``` 23 | 24 | ## Disabled 25 | 26 | ```example 27 | 53 | 60 | ``` 61 | 62 | ## With icon 63 | 64 | ```example 65 | 76 | 83 | ``` 84 | 85 | ## Attributes 86 | 87 | | Attributes | Description | Type | Accepted values | Default | 88 | | ---------- | ------------------ | --------- | ----------------------------------------- | ------- | 89 | | `type` | Type of button | `String` | `primary`, `success`, `warning`, `danger` | - | 90 | | `disabled` | Disable the button | `Boolean` | - | `false` | 91 | 92 | ## Events 93 | 94 | | Name | Description | Payload | 95 | | ------- | ---------------------------- | ------- | 96 | | `click` | Triggers when button clicked | - | 97 | -------------------------------------------------------------------------------- /src/components/button/Button.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 33 | 34 | 97 | -------------------------------------------------------------------------------- /public/docs/radio.md: -------------------------------------------------------------------------------- 1 | # Radio 2 | 3 | ## Basic usage 4 | 5 | ```example 6 | 18 | 27 | ``` 28 | 29 | ## Disabled 30 | 31 | ```example 32 | 46 | 55 | ``` 56 | 57 | ## Bordered 58 | 59 | ```example 60 | 81 | 90 | ``` 91 | 92 | ## Attributes 93 | 94 | | Attributes | Description | Type | Accepted values | Default | 95 | | ---------- | ------------------------------ | ------------------ | --------------- | ------- | 96 | | `value` | Value of radio | `String`, `Number` | - | - | 97 | | `type` | Type of radio | `String` | `border` | - | 98 | | `disabled` | Disable the radio | `Boolean` | - | `false` | 99 | | `label` | Label of the radio | `String` | - | - | 100 | | `name` | Same as `name` in native radio | `String` | - | - | 101 | 102 | ## Events 103 | 104 | | Name | Description | Payload | 105 | | -------- | -------------------------------- | ------- | 106 | | `change` | Triggers when radio change value | `value` | 107 | -------------------------------------------------------------------------------- /tests/unit/Radio.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount, mount } from '@vue/test-utils' 2 | import Radio from '../../src/components/radio/Radio.vue' 3 | 4 | describe('Radio.vue', () => { 5 | it('is rendered', () => { 6 | const wrapper = shallowMount(Radio, { 7 | propsData: { 8 | value: 1, 9 | label: 'label', 10 | type: 'border', 11 | name: 'name', 12 | disabled: false 13 | } 14 | }) 15 | expect(wrapper.exists()).toBe(true) 16 | expect(wrapper.props().value).toBe(1) 17 | expect(wrapper.props().label).toBe('label') 18 | expect(wrapper.props().type).toBe('border') 19 | expect(wrapper.props().name).toBe('name') 20 | expect(wrapper.props().disabled).toBe(false) 21 | }) 22 | it('label is rendered (label & slot)', () => { 23 | const wrapper = shallowMount(Radio, { 24 | slots: { 25 | default: '
' 26 | } 27 | }) 28 | expect(wrapper.find('.vue-radio__label').contains('div')).toBe(true) 29 | wrapper.setProps({ label: 'label' }) 30 | expect(wrapper.find('.vue-radio__label span').text()).toBe('label') 31 | }) 32 | it('is disabled', () => { 33 | const wrapper = shallowMount(Radio, { 34 | propsData: { 35 | disabled: true 36 | } 37 | }) 38 | expect(wrapper.find('.vue-radio--disabled').exists()).toBe(true) 39 | }) 40 | it('attributes is rendered', () => { 41 | const wrapper = shallowMount(Radio) 42 | wrapper.setProps({ name: 'text', disabled: true }) 43 | const attrs = wrapper.find('input').attributes() 44 | expect(attrs.name).toContain('text') 45 | expect(attrs.disabled).toContain('disabled') 46 | }) 47 | it('is checked', () => { 48 | const wrapper = mount({ 49 | template: '', 50 | components: { 51 | [Radio.name]: Radio 52 | }, 53 | data () { 54 | return { 55 | radio: '1' 56 | } 57 | } 58 | }) 59 | expect(wrapper.find('.vue-radio--checked').exists()).toBe(true) 60 | }) 61 | it('type is border', () => { 62 | const wrapper = shallowMount(Radio, { 63 | propsData: { 64 | type: 'border' 65 | } 66 | }) 67 | expect(wrapper.find('.vue-radio--bordered').exists()).toBe(true) 68 | }) 69 | it('change event (v-model)', () => { 70 | const wrapper = mount({ 71 | template: ` 72 |
73 | 74 | 75 |
76 | `, 77 | components: { 78 | [Radio.name]: Radio 79 | }, 80 | data () { 81 | return { 82 | radio: '2' 83 | } 84 | } 85 | }) 86 | const radioInput = wrapper.find('.vue-radio input') 87 | radioInput.trigger('change') 88 | expect(wrapper.vm.radio).toBe('1') 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /example/util.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue' 2 | import VFC from '../src/components' 3 | import Tabs from './components/Tabs/Tabs.vue' 4 | import TabsItem from './components/Tabs/TabsItem.vue' 5 | import { escape } from 'he' 6 | import marked from 'marked' 7 | 8 | Vue.use(VFC) 9 | Vue.component('app-tabs', Tabs) 10 | Vue.component('app-tabs-item', TabsItem) 11 | 12 | function sluggify (text) { 13 | return text 14 | .toLowerCase() 15 | .trim() 16 | .replace(/:.*:/g, '') 17 | .replace(/ +$/g, '') 18 | .replace(/(&| & )/g, '-and-') 19 | .replace(/&(.+?);/g, '') 20 | .replace(/[\s\W-]+/g, '-') 21 | } 22 | 23 | export function parse (markdown, cb) { 24 | const renderer = new marked.Renderer({ langPrefix: 'lang-' }) 25 | const base = new marked.Renderer({ langPrefix: 'lang-' }) 26 | const vms = [] 27 | let vm 28 | 29 | const example = code => { 30 | let template = code.match(/