├── .gitignore
├── .travis.yml
├── .jsbeautifyrc
├── .babelrc
├── .editorconfig
├── src
├── providers.js
├── main.js
├── components
│ ├── field.js
│ ├── messages.js
│ ├── vue-form.js
│ └── validate.js
├── config.js
├── validators.js
├── util.js
└── directives
│ └── vue-form-validator.js
├── rollup.config.js
├── LICENSE
├── package.json
├── karma.conf.js
├── example
├── bootstrap.html
└── component.html
├── README.md
├── dist
├── vue-form.min.js
└── vue-form.js
└── test
└── specs
└── vue-form.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist/vue-form.js.map
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | sudo: false
3 | before_script: npm run dist
4 | node_js:
5 | - "12"
6 |
--------------------------------------------------------------------------------
/.jsbeautifyrc:
--------------------------------------------------------------------------------
1 | {
2 | "indent_size": 2,
3 | "indent_char": " ",
4 | "brace_style": "collapse-preserve-inline",
5 | "max_preserve_newlines": 4,
6 | "preserve_newlines": true
7 | }
8 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "es2015",
5 | {
6 | "modules": false
7 | }
8 | ]
9 | ],
10 | "plugins": [
11 | "external-helpers"
12 | ]
13 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 2
9 | end_of_line = crlf
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
--------------------------------------------------------------------------------
/src/providers.js:
--------------------------------------------------------------------------------
1 | import { randomId } from './util';
2 |
3 | export const vueFormConfig = `VueFormProviderConfig${randomId()}`;
4 | export const vueFormState = `VueFormProviderState${randomId()}`;
5 | export const vueFormParentForm = `VueFormProviderParentForm${randomId()}`;
6 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 | import { argv } from 'yargs';
3 | import resolve from 'rollup-plugin-node-resolve';
4 | import commonjs from 'rollup-plugin-commonjs';
5 |
6 | export default {
7 | entry: 'src/main.js',
8 | dest: 'dist/vue-form.js',
9 | format: 'umd',
10 | moduleName: 'VueForm',
11 | sourceMap: argv.w,
12 | plugins: [
13 | resolve({ jsnext: true, main: true }),
14 | commonjs(),
15 | babel({
16 | exclude: 'node_modules/**'
17 | })
18 | ]
19 | };
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 fergaldoyle
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 |
23 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import vueForm from './components/vue-form';
2 | import messages from './components/messages';
3 | import validate from './components/validate';
4 | import field from './components/field';
5 | import vueFormValidator from './directives/vue-form-validator';
6 | import extend from 'extend';
7 | import { config } from './config';
8 | import { vueFormConfig } from './providers';
9 |
10 | function VueFormBase (options) {
11 | const c = extend(true, {}, config, options);
12 | this.provide = () => ({
13 | [vueFormConfig]: c
14 | })
15 | this.components = {
16 | [c.formComponent]: vueForm,
17 | [c.messagesComponent]: messages,
18 | [c.validateComponent]: validate,
19 | [c.fieldComponent]: field,
20 | };
21 | this.directives = { vueFormValidator };
22 | }
23 |
24 | export default class VueForm extends VueFormBase {
25 | static install(Vue, options) {
26 | Vue.mixin(new this(options));
27 | }
28 | static get installed() {
29 | return !!this.install.done;
30 | }
31 | static set installed(val) {
32 | this.install.done = val;
33 | }
34 | }
35 |
36 | VueFormBase.call(VueForm);
37 | // temp fix for vue 2.3.0
38 | VueForm.options = new VueForm();
39 |
--------------------------------------------------------------------------------
/src/components/field.js:
--------------------------------------------------------------------------------
1 | import { getVModelAndLabel, randomId } from '../util';
2 | import { vueFormConfig } from '../providers';
3 |
4 | export default {
5 | inject: {vueFormConfig},
6 | render(h) {
7 | let foundVnodes = getVModelAndLabel(this.$slots.default, this.vueFormConfig);
8 | const vModelnodes = foundVnodes.vModel;
9 | const attrs = {
10 | for: null
11 | };
12 | if (vModelnodes.length) {
13 | if(this.autoLabel) {
14 | const id = (vModelnodes[0].data.attrs && vModelnodes[0].data.attrs.id) || 'vf' + randomId();
15 | vModelnodes[0].data.attrs.id = id;
16 | if(foundVnodes.label) {
17 | foundVnodes.label.data = foundVnodes.label.data || {};
18 | foundVnodes.label.data.attrs = foundVnodes.label.data.attrs = {};
19 | foundVnodes.label.data.attrs.for = id;
20 | } else if (this.tag === 'label') {
21 | attrs.for = id;
22 | }
23 | }
24 | }
25 | return h(this.tag || this.vueFormConfig.fieldTag , { attrs }, this.$slots.default);
26 | },
27 | props: {
28 | tag: {
29 | type: String
30 | },
31 | autoLabel: {
32 | type: Boolean,
33 | default: true
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-form",
3 | "version": "4.10.3",
4 | "description": "Form validation for Vue.js",
5 | "repository": "github:fergaldoyle/vue-form",
6 | "main": "dist/vue-form.js",
7 | "scripts": {
8 | "dev": "rollup -w -c",
9 | "test": "karma start",
10 | "dist": "rollup -c && uglifyjs -c -m -- dist/vue-form.js > dist/vue-form.min.js"
11 | },
12 | "author": "Fergal Doyle",
13 | "license": "MIT",
14 | "devDependencies": {
15 | "babel-core": "^6.14.0",
16 | "babel-plugin-external-helpers": "^6.8.0",
17 | "babel-preset-es2015": "^6.14.0",
18 | "jasmine-core": "^2.5.2",
19 | "karma": "^1.5.0",
20 | "karma-babel-preprocessor": "^6.0.1",
21 | "karma-chrome-launcher": "^2.0.0",
22 | "karma-firefox-launcher": "^0.1.7",
23 | "karma-jasmine": "^1.0.2",
24 | "karma-phantomjs-launcher": "^1.0.2",
25 | "phantomjs": "^2.1.7",
26 | "rollup": "^0.41.4",
27 | "rollup-plugin-babel": "^2.7.1",
28 | "rollup-plugin-commonjs": "^8.0.2",
29 | "rollup-plugin-node-resolve": "^3.0.0",
30 | "rollup-watch": "^3.2.2",
31 | "uglify-js": "^2.7.3",
32 | "vue": "2.4.1",
33 | "yargs": "^7.0.2"
34 | },
35 | "dependencies": {
36 | "extend": "3.0.2",
37 | "scope-eval": "1.0.0"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | import { validators } from './validators';
2 |
3 | export const config = {
4 | validators,
5 | formComponent: 'VueForm',
6 | formTag: 'form',
7 | messagesComponent: 'FieldMessages',
8 | messagesTag: 'div',
9 | showMessages: '',
10 | validateComponent: 'Validate',
11 | validateTag: 'div',
12 | fieldComponent: 'Field',
13 | fieldTag: 'div',
14 | formClasses: {
15 | dirty: 'vf-form-dirty',
16 | pristine: 'vf-form-pristine',
17 | valid: 'vf-form-valid',
18 | invalid: 'vf-form-invalid',
19 | touched: 'vf-form-touched',
20 | untouched: 'vf-form-untouched',
21 | focused: 'vf-form-focused',
22 | submitted: 'vf-form-submitted',
23 | pending: 'vf-form-pending'
24 | },
25 | validateClasses: {
26 | dirty: 'vf-field-dirty',
27 | pristine: 'vf-field-pristine',
28 | valid: 'vf-field-valid',
29 | invalid: 'vf-field-invalid',
30 | touched: 'vf-field-touched',
31 | untouched: 'vf-field-untouched',
32 | focused: 'vf-field-focused',
33 | submitted: 'vf-field-submitted',
34 | pending: 'vf-field-pending'
35 | },
36 | inputClasses: {
37 | dirty: 'vf-dirty',
38 | pristine: 'vf-pristine',
39 | valid: 'vf-valid',
40 | invalid: 'vf-invalid',
41 | touched: 'vf-touched',
42 | untouched: 'vf-untouched',
43 | focused: 'vf-focused',
44 | submitted: 'vf-submitted',
45 | pending: 'vf-pending'
46 | },
47 | Promise: typeof Promise === 'function' ? Promise : null
48 | };
49 |
--------------------------------------------------------------------------------
/src/validators.js:
--------------------------------------------------------------------------------
1 | const emailRegExp = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; // from angular
2 | const urlRegExp = /^(http\:\/\/|https\:\/\/)(.{4,})$/;
3 |
4 | const email = function (value, attrValue, vnode) {
5 | return emailRegExp.test(value);
6 | }
7 | email._allowNulls = true;
8 |
9 | const number = function (value, attrValue, vnode) {
10 | return !isNaN(value);
11 | }
12 | number._allowNulls = true;
13 |
14 | const url = function (value, attrValue, vnode) {
15 | return urlRegExp.test(value);
16 | }
17 | url._allowNulls = true;
18 |
19 | export const validators = {
20 | email,
21 | number,
22 | url,
23 | required(value, attrValue, vnode) {
24 | if (attrValue === false) {
25 | return true;
26 | }
27 |
28 | if (value === 0) {
29 | return true;
30 | }
31 |
32 | if ((vnode.data.attrs && typeof vnode.data.attrs.bool !== 'undefined') || (vnode.componentOptions && vnode.componentOptions.propsData && typeof vnode.componentOptions.propsData.bool !== 'undefined')) {
33 | // bool attribute is present, allow false pass validation
34 | if (value === false) {
35 | return true;
36 | }
37 | }
38 |
39 | if (Array.isArray(value)) {
40 | return !!value.length;
41 | }
42 | return !!value;
43 | },
44 | minlength(value, length) {
45 | return value.length >= length;
46 | },
47 | maxlength(value, length) {
48 | return length >= value.length;
49 | },
50 | pattern(value, pattern) {
51 | const patternRegExp = new RegExp('^' + pattern + '$');
52 | return patternRegExp.test(value);
53 | },
54 | min(value, min, vnode) {
55 | if ((vnode.data.attrs.type || '').toLowerCase() == 'number') {
56 | return +value >= +min;
57 | }
58 | return value >= min;
59 | },
60 | max(value, max, vnode) {
61 | if ((vnode.data.attrs.type || '').toLowerCase() == 'number') {
62 | return +max >= +value;
63 | }
64 | return max >= value;
65 | }
66 | };
67 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | // Generated on Mon Oct 19 2015 20:15:39 GMT+0100 (GMT Daylight Time)
3 |
4 | module.exports = function(config) {
5 | config.set({
6 |
7 | // base path that will be used to resolve all patterns (eg. files, exclude)
8 | basePath: '',
9 |
10 |
11 | // frameworks to use
12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
13 | frameworks: ['jasmine'],
14 |
15 |
16 | // list of files / patterns to load in the browser
17 | files: [
18 | 'https://www.promisejs.org/polyfills/promise-6.1.0.js',
19 |
20 | //'https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.js',
21 | //'https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.1/vue.js',
22 | 'node_modules/vue/dist/vue.js', // 2.3.4
23 | 'dist/vue-form.js',
24 | 'test/specs/*.js'
25 | ],
26 |
27 |
28 | // list of files to exclude
29 | exclude: [],
30 |
31 |
32 | // preprocess matching files before serving them to the browser
33 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
34 | preprocessors: {
35 | 'test/specs/*.js': ['babel']
36 | },
37 |
38 | babel: {
39 | options: {
40 | sourceMap: 'inline'
41 | },
42 | filename: function (file) {
43 | return file.originalPath.replace(/\.js$/, '.es5.js');
44 | },
45 | sourceFileName: function (file) {
46 | return file.originalPath;
47 | }
48 | },
49 |
50 | // test results reporter to use
51 | // possible values: 'dots', 'progress'
52 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
53 | reporters: ['progress'],
54 |
55 |
56 | // web server port
57 | port: 9876,
58 |
59 |
60 | // enable / disable colors in the output (reporters and logs)
61 | colors: true,
62 |
63 |
64 | // level of logging
65 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
66 | logLevel: config.LOG_DEBUG,
67 |
68 | client: {
69 | captureConsole: true
70 | },
71 |
72 |
73 | // enable / disable watching file and executing tests whenever any file changes
74 | autoWatch: true,
75 |
76 |
77 | // start these browsers
78 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
79 | browsers: ['PhantomJS'],
80 |
81 |
82 | // Continuous Integration mode
83 | // if true, Karma captures browsers, runs the tests and exits
84 | singleRun: true
85 | })
86 | }
87 |
--------------------------------------------------------------------------------
/src/components/messages.js:
--------------------------------------------------------------------------------
1 | import { vueFormConfig, vueFormState, vueFormParentForm } from '../providers';
2 | import scopeEval from 'scope-eval';
3 |
4 | function findLabel (nodes) {
5 | if(!nodes) {
6 | return;
7 | }
8 | for (let i = 0; i < nodes.length; i++) {
9 | let vnode = nodes[i];
10 | if(vnode.tag === 'label') {
11 | return nodes[i];
12 | } else if (nodes[i].children) {
13 | return findLabel(nodes[i].children);
14 | }
15 | }
16 | }
17 |
18 | export default {
19 | inject: {vueFormConfig, vueFormState, vueFormParentForm},
20 | render(h) {
21 | const children = [];
22 | const field = this.formstate[this.fieldname];
23 | if (field && field.$error && this.isShown) {
24 | Object.keys(field.$error).forEach((key) => {
25 | if(this.$slots[key] || this.$scopedSlots[key]) {
26 | const out = this.$slots[key] || this.$scopedSlots[key](field);
27 | if(this.autoLabel) {
28 | const label = findLabel(out);
29 | if(label) {
30 | label.data = label.data || {};
31 | label.data.attrs = label.data.attrs || {};
32 | label.data.attrs.for = field._id;
33 | }
34 | }
35 | children.push(out);
36 | }
37 | });
38 | if(!children.length && field.$valid) {
39 | if(this.$slots.default || this.$scopedSlots.default) {
40 | const out = this.$slots.default || this.$scopedSlots.default(field);
41 | if(this.autoLabel) {
42 | const label = findLabel(out);
43 | if(label) {
44 | label.data = label.data || {};
45 | label.data.attrs = label.data.attrs || {};
46 | label.data.attrs.for = field._id;
47 | }
48 | }
49 | children.push(out);
50 | }
51 | }
52 | }
53 | return h(this.tag || this.vueFormConfig.messagesTag, children);
54 | },
55 | props: {
56 | state: Object,
57 | name: String,
58 | show: {
59 | type: String,
60 | default: ''
61 | },
62 | tag: {
63 | type: String
64 | },
65 | autoLabel: Boolean,
66 | },
67 | data () {
68 | return {
69 | formstate: null,
70 | fieldname: ''
71 | };
72 | },
73 | created () {
74 | this.fieldname = this.name;
75 | this.formstate = this.state || this.vueFormState;
76 | },
77 | computed: {
78 | isShown() {
79 | const field = this.formstate[this.fieldname];
80 | const show = this.show || this.vueFormParentForm.showMessages || this.vueFormConfig.showMessages;
81 |
82 | if (!show || !field) {
83 | return true;
84 | }
85 |
86 | return scopeEval(show,field);
87 | }
88 | }
89 | };
90 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | export function getClasses(classConfig, state) {
2 | return {
3 | [classConfig.dirty]: state.$dirty,
4 | [classConfig.pristine]: state.$pristine,
5 | [classConfig.valid]: state.$valid,
6 | [classConfig.invalid]: state.$invalid,
7 | [classConfig.touched]: state.$touched,
8 | [classConfig.untouched]: state.$untouched,
9 | [classConfig.focused]: state.$focused,
10 | [classConfig.pending]: state.$pending,
11 | [classConfig.submitted]: state.$submitted,
12 | };
13 | }
14 |
15 | export function addClass(el, className) {
16 | if (el.classList) {
17 | el.classList.add(className);
18 | } else {
19 | el.className += ' ' + className;
20 | }
21 | }
22 |
23 | export function removeClass(el, className) {
24 | if (el.classList) {
25 | el.classList.remove(className);
26 | } else {
27 | el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
28 | }
29 | }
30 |
31 | export function vModelValue(data) {
32 | if (data.model) {
33 | return data.model.value;
34 | }
35 | return data.directives.filter(v => v.name === 'model')[0].value;
36 | }
37 |
38 | export function getVModelAndLabel(nodes, config) {
39 | const foundVnodes = {
40 | vModel: [],
41 | label: null,
42 | messages: null
43 | };
44 |
45 | if(!nodes) {
46 | return foundVnodes;
47 | }
48 |
49 | function traverse(nodes) {
50 | for (let i = 0; i < nodes.length; i++) {
51 | let node = nodes[i];
52 |
53 | if(node.componentOptions) {
54 | if(node.componentOptions.tag === hyphenate(config.messagesComponent)) {
55 | foundVnodes.messages = node;
56 | }
57 | }
58 |
59 | if(node.tag === 'label' && !foundVnodes.label) {
60 | foundVnodes.label = node;
61 | }
62 |
63 | if (node.data) {
64 | if (node.data.model) {
65 | // model check has to come first. If a component has
66 | // a directive and v-model, the directive will be in .directives
67 | // and v-modelstored in .model
68 | foundVnodes.vModel.push(node);
69 | } else if (node.data.directives) {
70 | const match = node.data.directives.filter(v => v.name === 'model');
71 | if (match.length) {
72 | foundVnodes.vModel.push(node);
73 | }
74 | }
75 | }
76 | if (node.children) {
77 | traverse(node.children);
78 | } else if (node.componentOptions && node.componentOptions.children) {
79 | traverse(node.componentOptions.children);
80 | }
81 | }
82 | }
83 |
84 | traverse(nodes);
85 |
86 | return foundVnodes;
87 | }
88 |
89 | export function getName(vnode) {
90 | if(vnode.data && vnode.data.attrs && vnode.data.attrs.name) {
91 | return vnode.data.attrs.name;
92 | } else if (vnode.componentOptions && vnode.componentOptions.propsData && vnode.componentOptions.propsData.name) {
93 | return vnode.componentOptions.propsData.name;
94 | }
95 | }
96 |
97 | const hyphenateRE = /([^-])([A-Z])/g;
98 | export function hyphenate (str) {
99 | return str
100 | .replace(hyphenateRE, '$1-$2')
101 | .replace(hyphenateRE, '$1-$2')
102 | .toLowerCase()
103 | }
104 |
105 | export function randomId() {
106 | return Math.random().toString(36).substr(2, 10);
107 | }
108 |
109 | // https://davidwalsh.name/javascript-debounce-function
110 | export function debounce(func, wait, immediate) {
111 | var timeout;
112 | return function() {
113 | var context = this, args = arguments;
114 | var later = function() {
115 | timeout = null;
116 | if (!immediate) func.apply(context, args);
117 | };
118 | var callNow = immediate && !timeout;
119 | clearTimeout(timeout);
120 | timeout = setTimeout(later, wait);
121 | if (callNow) func.apply(context, args);
122 | };
123 | };
124 |
125 | export function isShallowObjectDifferent(a, b) {
126 | let aValue = '';
127 | let bValue = '';
128 | Object.keys(a).sort().filter(v => typeof a[v] !== 'function').forEach(v => aValue += a[v]);
129 | Object.keys(b).sort().filter(v => typeof a[v] !== 'function').forEach(v => bValue += b[v]);
130 | return aValue !== bValue;
131 | }
132 |
--------------------------------------------------------------------------------
/src/directives/vue-form-validator.js:
--------------------------------------------------------------------------------
1 | import { config } from '../config';
2 | import { vModelValue, getName, debounce } from '../util';
3 | import extend from 'extend';
4 |
5 | const debouncedValidators = {};
6 |
7 | function addValidators(attrs, validators, fieldValidators) {
8 | Object.keys(attrs).forEach(attr => {
9 | const prop = (attr === 'type') ? attrs[attr].toLowerCase() : attr.toLowerCase();
10 |
11 | if (validators[prop] && !fieldValidators[prop]) {
12 | fieldValidators[prop] = validators[prop];
13 | }
14 | });
15 | }
16 |
17 | export function compareChanges(vnode, oldvnode, validators) {
18 |
19 | let hasChanged = false;
20 | const attrs = vnode.data.attrs || {};
21 | const oldAttrs = oldvnode.data.attrs || {};
22 | const out = {};
23 |
24 | if (vModelValue(vnode.data) !== vModelValue(oldvnode.data)) {
25 | out.vModel = true;
26 | hasChanged = true;
27 | }
28 |
29 | Object.keys(validators).forEach((validator) => {
30 | if (attrs[validator] !== oldAttrs[validator]) {
31 | out[validator] = true;
32 | hasChanged = true;
33 | }
34 | });
35 |
36 | // if is a component
37 | if (vnode.componentOptions && vnode.componentOptions.propsData) {
38 | const attrs = vnode.componentOptions.propsData;
39 | const oldAttrs = oldvnode.componentOptions.propsData;
40 | Object.keys(validators).forEach((validator) => {
41 | if (attrs[validator] !== oldAttrs[validator]) {
42 | out[validator] = true;
43 | hasChanged = true;
44 | }
45 | });
46 | }
47 |
48 | if (hasChanged) {
49 | return out;
50 | }
51 | }
52 |
53 | export default {
54 | name: 'vue-form-validator',
55 | bind(el, binding, vnode) {
56 | const { fieldstate } = binding.value;
57 | const { validators } = binding.value.config;
58 | const attrs = (vnode.data.attrs || {});
59 | const inputName = getName(vnode);
60 |
61 | if (!inputName) {
62 | console.warn('vue-form: name attribute missing');
63 | return;
64 | }
65 |
66 | if(attrs.debounce) {
67 | debouncedValidators[fieldstate._id] = debounce(function(fieldstate, vnode) {
68 | if (fieldstate._hasFocused) {
69 | fieldstate._setDirty();
70 | }
71 | fieldstate._validate(vnode);
72 | }, attrs.debounce);
73 | }
74 |
75 | // add validators
76 | addValidators(attrs, validators, fieldstate._validators);
77 |
78 | // if is a component, a validator attribute could be a prop this component uses
79 | if (vnode.componentOptions && vnode.componentOptions.propsData) {
80 | addValidators(vnode.componentOptions.propsData, validators, fieldstate._validators);
81 | }
82 |
83 | fieldstate._validate(vnode);
84 |
85 | // native listeners
86 | el.addEventListener('blur', () => {
87 | fieldstate._setFocused(false);
88 | }, false);
89 | el.addEventListener('focus', () => {
90 | fieldstate._setFocused(true);
91 | }, false);
92 |
93 | // component listeners
94 | const vm = vnode.componentInstance;
95 | if (vm) {
96 | vm.$on('blur', () => {
97 | fieldstate._setFocused(false);
98 | });
99 | vm.$on('focus', () => {
100 | fieldstate._setFocused(true);
101 | });
102 |
103 | vm.$once('vf:addFocusListeners', () => {
104 | el.addEventListener('focusout', () => {
105 | fieldstate._setFocused(false);
106 | }, false);
107 | el.addEventListener('focusin', () => {
108 | fieldstate._setFocused(true);
109 | }, false);
110 | });
111 |
112 | vm.$on('vf:validate', data => {
113 | if(!vm._vfValidationData_) {
114 | addValidators(data, validators, fieldstate._validators);
115 | }
116 | vm._vfValidationData_ = data;
117 | fieldstate._validate(vm.$vnode);
118 | });
119 | }
120 | },
121 |
122 | update(el, binding, vnode, oldVNode) {
123 | const { validators } = binding.value.config;
124 | const changes = compareChanges(vnode, oldVNode, validators);
125 | const { fieldstate } = binding.value;
126 |
127 | let attrs = vnode.data.attrs || {};
128 | const vm = vnode.componentInstance;
129 | if(vm && vm._vfValidationData_) {
130 | attrs = extend({}, attrs, vm[vm._vfValidationData_]);
131 | }
132 |
133 | if(vnode.elm.className.indexOf(fieldstate._className[0]) === -1) {
134 | vnode.elm.className = vnode.elm.className + ' ' + fieldstate._className.join(' ');
135 | }
136 |
137 | if (!changes) {
138 | return;
139 | }
140 |
141 | if (changes.vModel) {
142 | // re-validate all
143 | if(attrs.debounce) {
144 | fieldstate.$pending = true;
145 | debouncedValidators[fieldstate._id](fieldstate, vnode);
146 | } else {
147 | if (fieldstate._hasFocused) {
148 | fieldstate._setDirty();
149 | }
150 | fieldstate._validate(vnode);
151 | }
152 | } else {
153 | // attributes have changed
154 | // to do: loop through them and re-validate changed ones
155 | //for(let prop in changes) {
156 | // fieldstate._validate(vnode, validator);
157 | //}
158 | // for now
159 | fieldstate._validate(vnode);
160 | }
161 |
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/components/vue-form.js:
--------------------------------------------------------------------------------
1 | import { config } from '../config';
2 | import { getClasses } from '../util';
3 | import { vueFormConfig, vueFormState, vueFormParentForm } from '../providers';
4 | import extend from 'extend';
5 |
6 | export default {
7 | render(h) {
8 | const attrs = {};
9 |
10 | if (typeof window !== 'undefined') {
11 | attrs.novalidate = '';
12 | }
13 |
14 | return h(
15 | this.tag || this.vueFormConfig.formTag, {
16 | on: {
17 | submit: (event) => {
18 | if(this.state.$pending) {
19 | event.preventDefault();
20 | this.vueFormConfig.Promise.all(this.promises).then(() => {
21 | this.state._submit();
22 | this.$emit('submit', event);
23 | this.promises = [];
24 | });
25 | } else {
26 | this.state._submit();
27 | this.$emit('submit', event);
28 | }
29 | },
30 | reset: (event) => {
31 | this.state._reset();
32 | this.$emit('reset', event);
33 | }
34 | },
35 | class: this.className,
36 | attrs
37 | }, [this.$slots.default]
38 | );
39 | },
40 | props: {
41 | state: {
42 | type: Object,
43 | required: true
44 | },
45 | tag: String,
46 | showMessages: String
47 | },
48 | inject: { vueFormConfig },
49 | provide() {
50 | return {
51 | [vueFormState]: this.state,
52 | [vueFormParentForm]: this
53 | };
54 | },
55 | data:() => ({
56 | promises: []
57 | }),
58 | created() {
59 | if(!this.state) { return }
60 | const controls = {};
61 | const state = this.state;
62 | const formstate = {
63 | $dirty: false,
64 | $pristine: true,
65 | $valid: true,
66 | $invalid: false,
67 | $submitted: false,
68 | $touched: false,
69 | $untouched: true,
70 | $focused: false,
71 | $pending: false,
72 | $error: {},
73 | $submittedState: {},
74 | _id: '',
75 | _submit: () => {
76 | this.state.$submitted = true;
77 | this.state._cloneState();
78 | },
79 | _cloneState: () => {
80 | const cloned = JSON.parse(JSON.stringify(state));
81 | delete cloned.$submittedState;
82 | Object.keys(cloned).forEach((key) => {
83 | this.$set(this.state.$submittedState, key, cloned[key]);
84 | });
85 | },
86 | _addControl: (ctrl) => {
87 | controls[ctrl.$name] = ctrl;
88 | this.$set(state, ctrl.$name, ctrl);
89 | },
90 | _removeControl: (ctrl) => {
91 | delete controls[ctrl.$name];
92 | this.$delete(this.state, ctrl.$name);
93 | this.$delete(this.state.$error, ctrl.$name);
94 | },
95 | _validate: () => {
96 | Object.keys(controls).forEach((key) => {
97 | controls[key]._validate();
98 | });
99 | },
100 | _reset: () => {
101 | state.$submitted = false;
102 | state.$pending = false;
103 | state.$submittedState = {};
104 | Object.keys(controls).forEach((key) => {
105 | const control = controls[key];
106 | control._hasFocused = false;
107 | control._setUntouched();
108 | control._setPristine();
109 | control.$submitted = false;
110 | control.$pending = false;
111 | });
112 | }
113 | }
114 |
115 | Object.keys(formstate).forEach((key) => {
116 | this.$set(this.state, key, formstate[key]);
117 | });
118 |
119 | this.$watch('state', () => {
120 | let isDirty = false;
121 | let isValid = true;
122 | let isTouched = false;
123 | let isFocused = false;
124 | let isPending = false;
125 | Object.keys(controls).forEach((key) => {
126 | const control = controls[key];
127 |
128 | control.$submitted = state.$submitted;
129 |
130 | if (control.$dirty) {
131 | isDirty = true;
132 | }
133 | if (control.$touched) {
134 | isTouched = true;
135 | }
136 | if (control.$focused) {
137 | isFocused = true;
138 | }
139 | if (control.$pending) {
140 | isPending = true;
141 | }
142 | if (!control.$valid) {
143 | isValid = false;
144 | // add control to errors
145 | this.$set(state.$error, control.$name, control);
146 | } else {
147 | this.$delete(state.$error, control.$name);
148 | }
149 | });
150 |
151 | state.$dirty = isDirty;
152 | state.$pristine = !isDirty;
153 | state.$touched = isTouched;
154 | state.$untouched = !isTouched;
155 | state.$focused = isFocused;
156 | state.$valid = isValid;
157 | state.$invalid = !isValid;
158 | state.$pending = isPending;
159 |
160 | }, {
161 | deep: true,
162 | immediate: true
163 | });
164 |
165 | /* watch pristine? if set to true, set all children to pristine
166 | Object.keys(controls).forEach((ctrl) => {
167 | controls[ctrl].setPristine();
168 | });*/
169 |
170 | },
171 | computed: {
172 | className() {
173 | const classes = getClasses(this.vueFormConfig.formClasses, this.state);
174 | return classes;
175 | }
176 | },
177 | methods: {
178 | reset() {
179 | this.state._reset();
180 | },
181 | validate() {
182 | this.state._validate();
183 | }
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/example/bootstrap.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | vue-form example
6 |
7 |
8 |
17 |
18 |
19 |
20 |
21 |
22 |
Example showing vue-form usage with Bootstrap styles, validation messages are shown on field touch or form submission
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | Success!
32 | Name is a required field
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | Success!
43 | Email is a required field
44 | Email is invalid
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | Success!
55 | Phone number is a required field
56 | Phone number is invalid
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
70 |
71 |
72 | Success!
73 | Department is a required field
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | Enter no more than 50 characters.
82 |
83 | Success!
84 | Comments must be less than 50 characters
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
{{formstate}}
99 |
100 |
101 |
102 |
103 |
104 |
145 |
146 |
147 |
148 |
149 |
--------------------------------------------------------------------------------
/example/component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | vue-form component examples
6 |
7 |
9 |
10 |
11 |
12 |
13 |
14 |
Example showing vue-form usage with custom components
15 |
16 |
17 |
18 |
19 |
20 |
21 | Component with validation attributes set on the component element
22 |
23 | Success!
24 | Field is required
25 | Pattern does not match, format example: 123-123
26 |
27 |
28 |
29 |
30 |
31 |
32 | Component where validation attributes are props, along with v-bind
33 |
34 | Success!
35 | Minimum allowed characters for this field is {{formstate.bbb.$attrs.minlength}}
36 | Maximum allowed characters for this field is {{formstate.bbb.$attrs.maxlength}}
37 | Field is required
38 |
39 |
40 |
41 |
42 |
43 |
44 | Component where validation attributes are emitted within the component using the vf:validate event
45 |
46 | Success!
47 | Number is too large
48 | Number is too small
49 | Field is required
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
{{formstate}}
59 |
60 |
61 |
62 |
63 |
64 |
205 |
206 |
207 |
208 |
209 |
--------------------------------------------------------------------------------
/src/components/validate.js:
--------------------------------------------------------------------------------
1 | import { getVModelAndLabel, vModelValue, addClass, removeClass, getName, hyphenate, randomId, getClasses, isShallowObjectDifferent } from '../util';
2 | import { vueFormConfig, vueFormState, vueFormParentForm } from '../providers';
3 | import { validators } from '../validators';
4 | import extend from 'extend';
5 |
6 | export default {
7 | render(h) {
8 | let foundVnodes = getVModelAndLabel(this.$slots.default, this.vueFormConfig);
9 | const vModelnodes = foundVnodes.vModel;
10 | const attrs = {
11 | for: null
12 | };
13 | if (vModelnodes.length) {
14 | this.name = getName(vModelnodes[0]);
15 |
16 | if (foundVnodes.messages) {
17 | this.$nextTick(() => {
18 | const messagesVm = foundVnodes.messages.componentInstance
19 | if (messagesVm) {
20 | messagesVm.fieldname = messagesVm.fieldname || this.name;
21 | }
22 | });
23 | }
24 |
25 | if (this.autoLabel) {
26 | const id = vModelnodes[0].data.attrs.id || this.fieldstate._id;
27 | this.fieldstate._id = id;
28 | vModelnodes[0].data.attrs.id = id;
29 | if (foundVnodes.label) {
30 | foundVnodes.label.data = foundVnodes.label.data || {};
31 | foundVnodes.label.data.attrs = foundVnodes.label.data.attrs || {};
32 | foundVnodes.label.data.attrs.for = id;
33 | } else if (this.tag === 'label') {
34 | attrs.for = id;
35 | }
36 | }
37 | vModelnodes.forEach((vnode) => {
38 | if (!vnode.data.directives) {
39 | vnode.data.directives = [];
40 | }
41 | vnode.data.directives.push({ name: 'vue-form-validator', value: { fieldstate: this.fieldstate, config: this.vueFormConfig } });
42 | vnode.data.attrs['vue-form-validator'] = '';
43 | vnode.data.attrs['debounce'] = this.debounce;
44 | });
45 | } else {
46 | //console.warn('Element with v-model not found');
47 | }
48 | return h(this.tag || this.vueFormConfig.validateTag, { 'class': this.className, attrs }, this.$slots.default);
49 | },
50 | props: {
51 | state: Object,
52 | custom: null,
53 | autoLabel: Boolean,
54 | tag: {
55 | type: String
56 | },
57 | debounce: Number
58 | },
59 | inject: { vueFormConfig, vueFormState, vueFormParentForm },
60 | data() {
61 | return {
62 | name: '',
63 | formstate: null,
64 | fieldstate: {}
65 | };
66 | },
67 | methods: {
68 | getClasses(classConfig) {
69 | var s = this.fieldstate;
70 | return Object.keys(s.$error).reduce((classes, error) => {
71 | classes[classConfig.invalid + '-' + hyphenate(error)] = true;
72 | return classes;
73 | }, getClasses(classConfig, s));
74 | }
75 | },
76 | computed: {
77 | className() {
78 | return this.getClasses(this.vueFormConfig.validateClasses);
79 | },
80 | inputClassName() {
81 | return this.getClasses(this.vueFormConfig.inputClasses);
82 | }
83 | },
84 | mounted() {
85 | this.fieldstate.$name = this.name;
86 | this.formstate._addControl(this.fieldstate);
87 |
88 | const vModelEls = this.$el.querySelectorAll('[vue-form-validator]');
89 |
90 | // add classes to the input element
91 | this.$watch('inputClassName', (value, oldValue) => {
92 | let out;
93 | for (let i = 0, el; el = vModelEls[i++];) {
94 | if (oldValue) {
95 | Object.keys(oldValue).filter(k => oldValue[k]).forEach(k => removeClass(el, k));
96 | }
97 | out = [];
98 | Object.keys(value).filter(k => value[k]).forEach((k) => {
99 | out.push(k);
100 | addClass(el, k)
101 | });
102 | }
103 | this.fieldstate._className = out;
104 | }, {
105 | deep: true,
106 | immediate: true
107 | });
108 |
109 | this.$watch('name', (value, oldValue) => {
110 | this.formstate._removeControl(this.fieldstate);
111 | this.fieldstate.$name = value;
112 | this.formstate._addControl(this.fieldstate);
113 | });
114 |
115 | },
116 | created() {
117 | this.formstate = this.state || this.vueFormState;
118 | const vm = this;
119 | let pendingValidators = [];
120 | let _val;
121 | let prevVnode;
122 | this.fieldstate = {
123 | $name: '',
124 | $dirty: false,
125 | $pristine: true,
126 | $valid: true,
127 | $invalid: false,
128 | $touched: false,
129 | $untouched: true,
130 | $focused: false,
131 | $pending: false,
132 | $submitted: false,
133 | $error: {},
134 | $attrs: {},
135 | _className: null,
136 | _id: 'vf' + randomId(),
137 | _setValidatorValidity(validator, isValid) {
138 | if (isValid) {
139 | vm.$delete(this.$error, validator);
140 | } else {
141 | vm.$set(this.$error, validator, true);
142 | }
143 | },
144 | _setValidity(isValid) {
145 | this.$valid = isValid;
146 | this.$invalid = !isValid;
147 | },
148 | _setDirty() {
149 | this.$dirty = true;
150 | this.$pristine = false;
151 | },
152 | _setPristine() {
153 | this.$dirty = false;
154 | this.$pristine = true;
155 | },
156 | _setTouched() {
157 | this.$touched = true;
158 | this.$untouched = false;
159 | },
160 | _setUntouched() {
161 | this.$touched = false;
162 | this.$untouched = true;
163 | },
164 | _setFocused(value) {
165 | this.$focused = typeof value === 'boolean' ? value : false;
166 | if (this.$focused) {
167 | this._setHasFocused();
168 | } else {
169 | this._setTouched();
170 | }
171 | },
172 | _setHasFocused() {
173 | this._hasFocused = true;
174 | },
175 | _hasFocused: false,
176 | _validators: {},
177 | _validate(vnode) {
178 | if(!vnode) {
179 | vnode = prevVnode;
180 | } else {
181 | prevVnode = vnode;
182 | }
183 | this.$pending = true;
184 | let isValid = true;
185 | let emptyAndRequired = false;
186 | const value = vModelValue(vnode.data);
187 | _val = value;
188 |
189 | const pending = {
190 | promises: [],
191 | names: []
192 | };
193 |
194 | pendingValidators.push(pending);
195 |
196 | let attrs = vnode.data.attrs || {};
197 | const childvm = vnode.componentInstance;
198 | if(childvm && childvm._vfValidationData_) {
199 | attrs = extend({}, attrs, childvm._vfValidationData_);
200 | }
201 |
202 | const propsData = (vnode.componentOptions && vnode.componentOptions.propsData ? vnode.componentOptions.propsData : {});
203 |
204 | Object.keys(this._validators).forEach((validator) => {
205 | // when value is empty and current validator is not the required validator, the field is valid
206 | if ((value === '' || value === undefined || value === null) && validator !== 'required') {
207 | this._setValidatorValidity(validator, true);
208 | emptyAndRequired = true;
209 | // return early, required validator will
210 | // fall through if it is present
211 | return;
212 | }
213 |
214 | const attrValue = typeof attrs[validator] !== 'undefined' ? attrs[validator] : propsData[validator];
215 | const isFunction = typeof this._validators[validator] === 'function';
216 |
217 | // match vue behaviour, ignore if attribute is null or undefined. But for type=email|url|number and custom validators, the value will be null, so allow with _allowNulls
218 | if (isFunction && (attrValue === null || typeof attrValue === 'undefined') && !this._validators[validator]._allowNulls) {
219 | return;
220 | }
221 |
222 | if(attrValue) {
223 | this.$attrs[validator] = attrValue;
224 | }
225 |
226 | const result = isFunction ? this._validators[validator](value, attrValue, vnode) : vm.custom[validator];
227 |
228 | if (typeof result === 'boolean') {
229 | if (result) {
230 | this._setValidatorValidity(validator, true);
231 | } else {
232 | isValid = false;
233 | this._setValidatorValidity(validator, false);
234 | }
235 | } else {
236 | pending.promises.push(result);
237 | pending.names.push(validator);
238 | vm.vueFormParentForm.promises.push(result);
239 | }
240 | });
241 |
242 | if (pending.promises.length) {
243 | vm.vueFormConfig.Promise.all(pending.promises).then((results) => {
244 |
245 | // only concerned with the last promise results, in case
246 | // async responses return out of order
247 | if (pending !== pendingValidators[pendingValidators.length - 1]) {
248 | //console.log('ignoring old promise', pending.promises);
249 | return;
250 | }
251 |
252 | pendingValidators = [];
253 |
254 | results.forEach((result, i) => {
255 | const name = pending.names[i];
256 | if (result) {
257 | this._setValidatorValidity(name, true);
258 | } else {
259 | isValid = false;
260 | this._setValidatorValidity(name, false);
261 | }
262 | });
263 | this._setValidity(isValid);
264 | this.$pending = false;
265 | });
266 | } else {
267 | this._setValidity(isValid);
268 | this.$pending = false;
269 | }
270 | }
271 | }
272 |
273 | // add custom validators
274 | if (this.custom) {
275 | Object.keys(this.custom).forEach((prop) => {
276 | if (typeof this.custom[prop] === 'function') {
277 | this.custom[prop]._allowNulls = true;
278 | this.fieldstate._validators[prop] = this.custom[prop];
279 | } else {
280 | this.fieldstate._validators[prop] = this.custom[prop];
281 | }
282 | });
283 | }
284 |
285 | this.$watch('custom', (v, oldV) => {
286 | if(!oldV || !v) { return }
287 | if(isShallowObjectDifferent(v, oldV)){
288 | this.fieldstate._validate();
289 | }
290 | }, {
291 | deep: true
292 | });
293 |
294 | },
295 | destroyed() {
296 | this.formstate._removeControl(this.fieldstate);
297 | }
298 | };
299 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vue-form
2 |
3 | [](https://travis-ci.org/fergaldoyle/vue-form)
4 |
5 | Form validation for Vue.js 2.2+
6 |
7 | ### Install
8 |
9 | Available through npm as `vue-form`.
10 |
11 | ``` js
12 | import VueForm from 'vue-form';
13 | // or var VueForm = require('vue-form') or window.VueForm if you are linking directly to the dist file
14 |
15 | // install globally
16 | Vue.use(VueForm);
17 | Vue.use(VueForm, options);
18 |
19 | // or use the mixin
20 | ...
21 | mixins: [VueForm]
22 | ...
23 | mixins: [new VueForm(options)]
24 | ...
25 | ```
26 |
27 | ### Usage
28 |
29 | Once installed you have access to four components (`vue-form`, `validate`, `field`, `field-messages`) for managing form state, validating form fields and displaying validation messages.
30 |
31 | Live examples
32 | * Configured to work with Bootstrap styles: https://jsfiddle.net/fergal_doyle/zfqt4yhq/
33 | * Matching passwords and password strength: https://jsfiddle.net/fergal_doyle/9rn5kLkw/
34 | * Field error messages based on submitted state (labels tied to inputs with `auto-label`): https://jsfiddle.net/fergal_doyle/bqys2p5y/
35 |
36 | Example
37 |
38 | ```html
39 |
40 |
41 |
42 |
43 | Name *
44 |
45 |
46 |
47 | Success!
48 | Name is a required field
49 |
50 |
51 |
52 |
53 | Email
54 |
55 |
56 |
57 | Email is a required field
58 | Email is not valid
59 |
60 |
61 |
62 |
63 |
64 |
{{ formstate }}
65 |
66 | ```
67 |
68 | ```js
69 | Vue.use(VueForm);
70 |
71 | new Vue({
72 | el: '#app',
73 | data: {
74 | formstate: {},
75 | model: {
76 | name: '',
77 | email: 'invalid-email'
78 | }
79 | },
80 | methods: {
81 | onSubmit: function () {
82 | if(this.formstate.$invalid) {
83 | // alert user and exit early
84 | return;
85 | }
86 | // otherwise submit form
87 | }
88 | }
89 | });
90 | ```
91 |
92 | The output of `formstate` will be:
93 | ```js
94 | {
95 | "$dirty": false,
96 | "$pristine": true,
97 | "$valid": false,
98 | "$invalid": true,
99 | "$submitted": false,
100 | "$touched": false,
101 | "$untouched": true,
102 | "$focused": false,
103 | "$pending": false,
104 | "$error": {
105 | // fields with errors are copied into this object
106 | },
107 | "$submittedState": {
108 | // each form sumbit, state is cloned into this object
109 | },
110 | "name": {
111 | "$name": "name",
112 | "$dirty": false,
113 | "$pristine": true,
114 | "$valid": false,
115 | "$invalid": true,
116 | "$touched": false,
117 | "$untouched": true,
118 | "$focused": false,
119 | "$pending": false,
120 | "$error": {
121 | "required": true
122 | }
123 | },
124 | "email": {
125 | "$name": "email",
126 | "$dirty": false,
127 | "$pristine": true,
128 | "$valid": false,
129 | "$invalid": true,
130 | "$touched": false,
131 | "$untouched": true,
132 | "$focused": false,
133 | "$pending": false,
134 | "$error": {
135 | "email": true
136 | }
137 | }
138 | }
139 | ```
140 |
141 | ### Displaying messages
142 | Display validation errors or success messages with `field-messages`.
143 |
144 | The `show` prop supports simple expressions which specifiy when messages should be displayed based on the current state of the field, e.g: `$dirty`, `$dirty && $touched`, `$dirty || $touched`, `$touched || $submitted`, `$focused && ($dirty || $submitted)`
145 |
146 | ```html
147 |
148 | Error message A
149 | Error message B
150 |
151 | ```
152 |
153 | Or use scoped templates:
154 | ```html
155 |
156 | Success
157 |
158 | Name is a required field
159 |
160 |
161 | Error message B
162 |
163 |
164 | ```
165 |
166 | ### Validators
167 |
168 | ```
169 | type="email"
170 | type="url"
171 | type="number"
172 | required
173 | minlength
174 | maxlength
175 | pattern
176 | min (for type="number")
177 | max (for type="number")
178 |
179 | ```
180 |
181 | You can use static validation attributes or bindings. If it is a binding, the input will be re-validated every binding update meaning you can have inputs which are conditionally required etc.
182 | ```html
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 | ```
199 |
200 | #### Custom validators
201 | You can register global and local custom validators.
202 |
203 | Global custom validator
204 | ```js
205 |
206 | var options = {
207 | validators: {
208 | 'my-custom-validator': function (value, attrValue, vnode) {
209 | // return true to set input as $valid, false to set as $invalid
210 | return value === 'custom';
211 | }
212 | }
213 | }
214 |
215 | Vue.use(VueForm, options);
216 | // or
217 | // mixins: [new VueForm(options)]
218 | ```
219 |
220 | ```html
221 |
222 |
223 |
226 |
227 | ```
228 |
229 | Local custom validator
230 | ```js
231 | // ...
232 | methods: {
233 | customValidator: function (value) {
234 | // return true to set input as $valid, false to set as $invalid
235 | return value === 'custom';
236 | }
237 | },
238 | // local custom validator can also be a data or computed property
239 | computed: {
240 | isEmailAvailable: function () {
241 | // return true to set input as $valid, false to set as $invalid
242 | }
243 | }
244 | // ...
245 | ```
246 |
247 | ```html
248 |
249 |
250 |
253 |
254 | ```
255 |
256 | #### Async validators:
257 |
258 | Async validators are custom validators which return a Promise. `resolve()` `true` or `false` to set field validity.
259 | ```js
260 | // ...
261 | methods: {
262 | customValidator (value) {
263 | return new Promise((resolve, reject) => {
264 | setTimeout(() => {
265 | resolve(value === 'ajax');
266 | }, 100);
267 | });
268 | }
269 | }
270 | // ...
271 | ```
272 |
273 | Async validator with debounce (example uses lodash debounce)
274 | ```js
275 | methods: {
276 | debounced: _.debounce(function (value, resolve, reject) {
277 | fetch('https://httpbin.org/get').then(function(response){
278 | resolve(response.isValid);
279 | });
280 | }, 500),
281 | customValidator (value) {
282 | return new Promise((resolve, reject) => {
283 | this.debounced(value, resolve, reject);
284 | });
285 | }
286 | }
287 | ```
288 |
289 | ### Reset state
290 | ```
291 |
292 |
293 | resetState: function () {
294 | this.formstate._reset();
295 | // or
296 | this.$refs.form.reset();
297 | }
298 | ```
299 |
300 | ### State classes
301 | As form and input validation states change, state classes are added and removed
302 |
303 | Possible form classes:
304 | ```
305 | vf-form-dirty, vf-form-pristine, vf-form-valid, vf-form-invalid, vf-form-submitted, vf-form-focused- vf-form-pending
306 | ```
307 |
308 | Possible input classes:
309 | ```
310 | vf-dirty, vf-pristine, vf-valid, vf-invalid, vf-focused, vf-pending
311 |
312 | // also for every validation error, a class will be added, e.g.
313 | vf-invalid-required, vf-invalid-minlength, vf-invalid-max, etc
314 | ```
315 |
316 | Input wrappers (e.g. the tag the `validate` component renders) will also get state classes, but with the `field` prefix, e.g.
317 | ```
318 | vf-field-dirty, vf-field-pristine, vf-field-valid, vf-field-invalid, vf-field-focused, vf-field-pending
319 | ```
320 |
321 | ### Custom components
322 |
323 | When writing custom form field components, e.g. `` you should trigger the `focus` and `blur` events after user interaction either by triggering native dom events on the root node of your component, or emitting Vue events (`this.$emit('focus)`) so the `validate` component can detect and set the `$dirty` and `$touched` states on the field.
324 |
325 | ### Component props
326 |
327 | #### vue-form
328 | * `state` Object on which form state is set
329 | * `tag` String, defaults to `form`
330 | * `show-messages` String, applies to all child `field-messages`, show errors dependant on form field state e.g. `$touched`, `$dirty || $touched`, '$touched || $submitted'
331 |
332 | #### validate
333 | * `state` Optional way of passing in the form state. If omitted form state will be found in the $parent
334 | * `custom` Object containing one or many custom validators. `{validatorName: validatorFunction}`
335 | * `tag` String which specifies what element tag should be rendered by the `validate` component, defaults to `span`
336 | * `auto-label`: Boolean, defaults to false. Automatically set `for` and `id` attributes of label and input elements found inside the `validate` component
337 | * `debounce` Number, defaults to none, which specifies the delay (milliseconds) before validation takes place.
338 |
339 | #### field-messages
340 | * `state` Optional way of passing in the form state. If omitted form state will be found in the $parent
341 | * `name` String which specifies the related field name
342 | * `tag` String, defaults to `div`
343 | * `show` String, show error dependant on form field state e.g. `$touched`, `$dirty || $touched`, '$touched || $submitted'
344 | * `auto-label` Boolean, defaults to false. Automatically set the `for` attribute of labels found inside the `field-messages` component
345 |
346 | #### field
347 | * `tag` String, defaults to `div`
348 | * `auto-label` Boolean, defaults to true. Automatically set `for` and `id` attributes of label and input elements found inside the `validate` component
349 |
350 | ### Config
351 | Set config options when using `Vue.use(VueForm, options)`, or when using a mixin `mixins: [new VueForm(options)]` defaults:
352 |
353 | ```js
354 | {
355 | validators: {},
356 | formComponent: 'vueForm',
357 | formTag: 'form',
358 | messagesComponent: 'fieldMessages',
359 | messagesTag: 'div',
360 | showMessages: '',
361 | validateComponent: 'validate',
362 | validateTag: 'div',
363 | fieldComponent: 'field',
364 | fieldTag: 'div',
365 | formClasses: {
366 | dirty: 'vf-form-dirty',
367 | pristine: 'vf-form-pristine',
368 | valid: 'vf-form-valid',
369 | invalid: 'vf-form-invalid',
370 | touched: 'vf-form-touched',
371 | untouched: 'vf-form-untouched',
372 | focused: 'vf-form-focused',
373 | submitted: 'vf-form-submitted',
374 | pending: 'vf-form-pending'
375 | },
376 | validateClasses: {
377 | dirty: 'vf-field-dirty',
378 | pristine: 'vf-field-pristine',
379 | valid: 'vf-field-valid',
380 | invalid: 'vf-field-invalid',
381 | touched: 'vf-field-touched',
382 | untouched: 'vf-field-untouched',
383 | focused: 'vf-field-focused',
384 | submitted: 'vf-field-submitted',
385 | pending: 'vf-field-pending'
386 | },
387 | inputClasses: {
388 | dirty: 'vf-dirty',
389 | pristine: 'vf-pristine',
390 | valid: 'vf-valid',
391 | invalid: 'vf-invalid',
392 | touched: 'vf-touched',
393 | untouched: 'vf-untouched',
394 | focused: 'vf-focused',
395 | submitted: 'vf-submitted',
396 | pending: 'vf-pending'
397 | },
398 | Promise: typeof Promise === 'function' ? Promise : null
399 | }
400 | ```
401 |
--------------------------------------------------------------------------------
/dist/vue-form.min.js:
--------------------------------------------------------------------------------
1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.VueForm=e()}(this,function(){"use strict";function t(t,e){var n;return n={},F(n,t.dirty,e.$dirty),F(n,t.pristine,e.$pristine),F(n,t.valid,e.$valid),F(n,t.invalid,e.$invalid),F(n,t.touched,e.$touched),F(n,t.untouched,e.$untouched),F(n,t.focused,e.$focused),F(n,t.pending,e.$pending),F(n,t.submitted,e.$submitted),n}function e(t,e){t.classList?t.classList.add(e):t.className+=" "+e}function n(t,e){t.classList?t.classList.remove(e):t.className=t.className.replace(new RegExp("(^|\\b)"+e.split(" ").join("|")+"(\\b|$)","gi")," ")}function a(t){return t.model?t.model.value:t.directives.filter(function(t){return"model"===t.name})[0].value}function i(t,e){function n(t){for(var i=0;i=e},maxlength:function(t,e){return e>=t.length},pattern:function(t,e){return new RegExp("^"+e+"$").test(t)},min:function(t,e,n){return"number"==(n.data.attrs.type||"").toLowerCase()?+t>=+e:t>=e},max:function(t,e,n){return"number"==(n.data.attrs.type||"").toLowerCase()?+e>=+t:e>=t}},_={validators:g,formComponent:"VueForm",formTag:"form",messagesComponent:"FieldMessages",messagesTag:"div",showMessages:"",validateComponent:"Validate",validateTag:"div",fieldComponent:"Field",fieldTag:"div",formClasses:{dirty:"vf-form-dirty",pristine:"vf-form-pristine",valid:"vf-form-valid",invalid:"vf-form-invalid",touched:"vf-form-touched",untouched:"vf-form-untouched",focused:"vf-form-focused",submitted:"vf-form-submitted",pending:"vf-form-pending"},validateClasses:{dirty:"vf-field-dirty",pristine:"vf-field-pristine",valid:"vf-field-valid",invalid:"vf-field-invalid",touched:"vf-field-touched",untouched:"vf-field-untouched",focused:"vf-field-focused",submitted:"vf-field-submitted",pending:"vf-field-pending"},inputClasses:{dirty:"vf-dirty",pristine:"vf-pristine",valid:"vf-valid",invalid:"vf-invalid",touched:"vf-touched",untouched:"vf-untouched",focused:"vf-focused",submitted:"vf-submitted",pending:"vf-pending"},Promise:"function"==typeof Promise?Promise:null},y=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")},O=function(){function t(t,e){for(var n=0;n
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | Field is OK
33 | required error
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | required error
43 | minlength error
44 |
45 |
46 |
47 |
48 |
49 |
50 | Field is OK
51 | required error
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | Sample
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 | Sample
136 |
137 |
138 |
139 |
140 |
141 |
142 | `,
143 | data: {
144 | hasSubmitted: false,
145 | formstate: {},
146 | isRequired: true,
147 | minlength: 5,
148 | isCEnabled: true,
149 | asyncEnabled: false,
150 | model: {
151 | a: 'aaa',
152 | b: '',
153 | c: null,
154 | d: '',
155 | email: 'joe.doe@foo.com',
156 | number: 1,
157 | url: 'https://foo.bar.com',
158 | length: '12345',
159 | minmax: 5,
160 | pattern: '1234',
161 | custom: 'custom',
162 | custom2: 'custom2',
163 | multicheck: [],
164 | sample: ''
165 | }
166 | },
167 | methods: {
168 | onSubmit() {
169 | this.hasSubmitted = true;
170 | },
171 | customValidator(value) {
172 | return value === 'custom';
173 | },
174 | customValidatorAsync(value) {
175 | return new Promise((resolve, reject) => {
176 | setTimeout(() => {
177 | resolve(value === 'custom2');
178 | }, 100);
179 | });
180 | }
181 | }
182 | });
183 | Vue.nextTick(done);
184 | });
185 |
186 | afterEach(function(done) {
187 | vm.$destroy();
188 | Vue.nextTick(done);
189 | });
190 |
191 | it('should create a form tag and listen to submit event', () => {
192 | expect(vm.$el.tagName).toBe('FORM');
193 | vm.$el.querySelector('button[type=submit]').click();
194 | expect(vm.hasSubmitted).toBe(true);
195 | });
196 |
197 | it('should populate formstate', () => {
198 | expect(vm.formstate.$valid).toBeDefined();
199 | });
200 |
201 | it('should automatically find parent formstate and also work by passing state as a prop', () => {
202 | expect(vm.formstate.a).toBeDefined(true);
203 | expect(vm.formstate.b).toBeDefined(true);
204 | });
205 |
206 | it('should validate required fields', (done) => {
207 | expect(vm.formstate.a.$valid).toBe(true);
208 | expect(vm.formstate.a.$error.required).toBeUndefined();
209 | vm.model.a = '';
210 | vm.$nextTick(() => {
211 | expect(vm.formstate.a.$valid).toBe(false);
212 | expect(vm.formstate.a.$error.required).toBe(true);
213 | done();
214 | });
215 | });
216 |
217 | it('should not show other validators if required validator fails', (done) => {
218 | expect(vm.formstate.b.$error.required).toBe(true);
219 | expect(vm.formstate.b.$error.minlength).toBeUndefined();
220 | vm.model.b = 'acb';
221 | vm.$nextTick(() => {
222 | expect(vm.formstate.b.$error.required).toBeUndefined();
223 | expect(vm.formstate.b.$error.minlength).toBe(true);
224 | done();
225 | });
226 | });
227 |
228 | it('should react to bound validators', (done) => {
229 | expect(vm.formstate.c.$error.required).toBe(true);
230 | vm.isRequired = false;
231 | vm.$nextTick(() => {
232 | expect(vm.formstate.c.$error.required).toBeUndefined();
233 | vm.model.c = '1234';
234 | vm.$nextTick(() => {
235 | expect(vm.formstate.c.$error.minlength).toBe(true);
236 | vm.minlength = 2;
237 | vm.$nextTick(() => {
238 | expect(vm.formstate.c.$error.minlength).toBeUndefined();
239 | done();
240 | });
241 | });
242 | });
243 | });
244 |
245 | it('should validate [type=email]', (done) => {
246 | expect(vm.formstate.email.$valid).toBe(true);
247 | expect(vm.formstate.email.$error.email).toBeUndefined();
248 | vm.model.email = 'not a real email';
249 | vm.$nextTick(() => {
250 | expect(vm.formstate.email.$valid).toBe(false);
251 | expect(vm.formstate.email.$error.email).toBe(true);
252 | done();
253 | });
254 | });
255 |
256 | it('should validate [type=number]', (done) => {
257 | expect(vm.formstate.number.$valid).toBe(true);
258 | expect(vm.formstate.number.$error.number).toBeUndefined();
259 | vm.model.number = 'a string';
260 | vm.$nextTick(() => {
261 | expect(vm.formstate.number.$valid).toBe(false);
262 | expect(vm.formstate.number.$error.number).toBe(true);
263 | done();
264 | });
265 | });
266 |
267 | it('should validate required [type=number] === 0', (done) => {
268 | vm.model.number = 0;
269 | vm.$nextTick(() => {
270 | expect(vm.formstate.number.$valid).toBe(true);
271 | expect(vm.formstate.number.$error.number).toBeUndefined();
272 | expect(vm.formstate.number.$error.required).toBeUndefined();
273 | done();
274 | });
275 | });
276 |
277 | it('should validate [type=url]', (done) => {
278 | expect(vm.formstate.url.$valid).toBe(true);
279 | expect(vm.formstate.url.$error.url).toBeUndefined();
280 | vm.model.url = 'not a real url';
281 | vm.$nextTick(() => {
282 | expect(vm.formstate.url.$valid).toBe(false);
283 | expect(vm.formstate.url.$error.url).toBe(true);
284 | done();
285 | });
286 | });
287 |
288 | it('should validate [minlength]', (done) => {
289 | expect(vm.formstate.length.$valid).toBe(true);
290 | expect(vm.formstate.length.$error.minlength).toBeUndefined();
291 | vm.model.length = '1';
292 | vm.$nextTick(() => {
293 | expect(vm.formstate.length.$valid).toBe(false);
294 | expect(vm.formstate.length.$error.minlength).toBe(true);
295 | done();
296 | });
297 | });
298 |
299 | it('should validate [maxlength]', (done) => {
300 | expect(vm.formstate.length.$valid).toBe(true);
301 | expect(vm.formstate.length.$error.maxlength).toBeUndefined();
302 | vm.model.length = '1234567890';
303 | vm.$nextTick(() => {
304 | expect(vm.formstate.length.$valid).toBe(false);
305 | expect(vm.formstate.length.$error.maxlength).toBe(true);
306 | done();
307 | });
308 | });
309 |
310 | it('should validate [number][min]', (done) => {
311 | expect(vm.formstate.minmax.$valid).toBe(true);
312 | expect(vm.formstate.minmax.$error.min).toBeUndefined();
313 | vm.model.minmax = 1;
314 | vm.$nextTick(() => {
315 | expect(vm.formstate.minmax.$valid).toBe(false);
316 | expect(vm.formstate.minmax.$error.min).toBe(true);
317 | done();
318 | });
319 | });
320 |
321 | it('should validate [number][max]', (done) => {
322 | expect(vm.formstate.minmax.$valid).toBe(true);
323 | expect(vm.formstate.minmax.$error.max).toBeUndefined();
324 | vm.model.minmax = 100;
325 | vm.$nextTick(() => {
326 | expect(vm.formstate.minmax.$valid).toBe(false);
327 | expect(vm.formstate.minmax.$error.max).toBe(true);
328 | done();
329 | });
330 | });
331 |
332 | it('should validate [pattern]', (done) => {
333 | expect(vm.formstate.pattern.$valid).toBe(true);
334 | expect(vm.formstate.pattern.$error.pattern).toBeUndefined();
335 | vm.model.pattern = 'not four numbers';
336 | vm.$nextTick(() => {
337 | expect(vm.formstate.pattern.$valid).toBe(false);
338 | expect(vm.formstate.pattern.$error.pattern).toBe(true);
339 | done();
340 | });
341 | });
342 |
343 | it('should validate custom validators', function(done) {
344 | expect(vm.formstate.custom.$valid).toBe(true);
345 | expect(vm.formstate.custom.$error.customKey).toBeUndefined();
346 | vm.model.custom = 'custom invalid value';
347 | vm.$nextTick(function() {
348 | expect(vm.formstate.custom.$valid).toBe(false);
349 | expect(vm.formstate.custom.$error.customKey).toBe(true);
350 | done();
351 | });
352 | });
353 |
354 | it('should validate async validators', function(done) {
355 | vm.asyncEnabled = true;
356 | vm.$nextTick(() => {
357 | expect(vm.formstate.custom2.$pending).toBe(true);
358 | expect(vm.formstate.custom2.$valid).toBe(true);
359 | setTimeout(() => {
360 | expect(vm.formstate.custom2.$pending).toBe(false);
361 | expect(vm.formstate.custom2.$valid).toBe(true);
362 | vm.model.custom2 = 'foo';
363 | setTimeout(() => {
364 | expect(vm.formstate.custom2.$pending).toBe(false);
365 | expect(vm.formstate.custom2.$valid).toBe(false);
366 | done();
367 | }, 150);
368 | }, 150);
369 | });
370 | });
371 |
372 | it('should hyphenate camelcase validator names', function(done) {
373 | vm.model.custom = 'custom invalid value';
374 | vm.$nextTick(function() {
375 | expect(vm.$el.querySelector('[name="custom"]').classList.contains('vf-invalid-custom-key')).toBe(true);
376 | done();
377 | });
378 | });
379 |
380 | it('should validate checkbox array', (done) => {
381 | expect(vm.formstate.multicheck.$valid).toBe(false);
382 | expect(vm.formstate.multicheck.$error.required).toBe(true);
383 | vm.$el.querySelector('[name=multicheck]').click();
384 | vm.$nextTick(() => {
385 | expect(vm.formstate.multicheck.$valid).toBe(true);
386 | expect(vm.formstate.multicheck.$error.required).toBeUndefined();
387 | done();
388 | });
389 | });
390 |
391 | it('should set $dirty when model changed by user', (done) => {
392 | expect(vm.formstate.a.$dirty).toBe(false);
393 | // non user change
394 | vm.model.a = 'abc';
395 |
396 | vm.$nextTick(() => {
397 | expect(vm.formstate.a.$dirty).toBe(false);
398 |
399 | // user interacted with field then changed text
400 | vm.$el.querySelector('[name=a]').focus();
401 | vm.model.a = 'abcc';
402 |
403 | vm.$nextTick(() => {
404 | expect(vm.formstate.a._hasFocused).toBe(true);
405 | expect(vm.formstate.a.$focused).toBe(true);
406 | expect(vm.formstate.a.$dirty).toBe(true);
407 | done();
408 | });
409 |
410 | });
411 |
412 | });
413 |
414 | it('should set $touched on blur', (done) => {
415 | expect(vm.formstate.a.$touched).toBe(false);
416 | vm.$el.querySelector('[name=a]').focus();
417 | vm.$el.querySelector('[name=a]').blur();
418 | vm.$nextTick(() => {
419 | expect(vm.formstate.a.$touched).toBe(true);
420 | done();
421 | });
422 | });
423 |
424 | it('should set $focused to false on blur', (done) => {
425 | expect(vm.formstate.a.$focused).toBe(false);
426 | expect(vm.formstate.a._hasFocused).toBe(false);
427 | vm.$el.querySelector('[name=a]').focus();
428 | expect(vm.formstate.a.$focused).toBe(true);
429 | expect(vm.formstate.a._hasFocused).toBe(true);
430 | vm.$el.querySelector('[name=a]').blur();
431 | vm.$nextTick(() => {
432 | expect(vm.formstate.a.$focused).toBe(false);
433 | expect(vm.formstate.a._hasFocused).toBe(true);
434 | done();
435 | });
436 | });
437 |
438 | it('should set form properties when child properties change', (done) => {
439 | // starts off invalid
440 | expect(vm.formstate.$valid).toBe(false);
441 | expect(vm.formstate.$invalid).toBe(true);
442 | expect(vm.formstate.$dirty).toBe(false);
443 | expect(vm.formstate.$pristine).toBe(true);
444 | expect(vm.formstate.$touched).toBe(false);
445 | expect(vm.formstate.$untouched).toBe(true);
446 | expect(vm.formstate.$focused).toBe(false)
447 | expect(Object.keys(vm.formstate.$error).length).toBe(5);
448 |
449 | // emulate user interaction
450 | vm.$el.querySelector('[name=b]').focus();
451 |
452 | vm.$nextTick(() => {
453 | expect(vm.formstate.$focused).toBe(true)
454 |
455 | vm.$el.querySelector('[name=b]').blur();
456 |
457 | setValid();
458 |
459 | vm.$nextTick(() => {
460 | expect(vm.formstate.$valid).toBe(true);
461 | expect(vm.formstate.$invalid).toBe(false);
462 | expect(vm.formstate.$dirty).toBe(true);
463 | expect(vm.formstate.$pristine).toBe(false);
464 | expect(vm.formstate.$touched).toBe(true);
465 | expect(vm.formstate.$untouched).toBe(false);
466 | expect(vm.formstate.$focused).toBe(false);
467 | expect(Object.keys(vm.formstate.$error).length).toBe(0);
468 | done();
469 | });
470 | });
471 | });
472 |
473 | it('should add and remove state classes', (done) => {
474 |
475 | // starts off invalid, pristine and untouched
476 | expect(vm.$el.classList.contains('vf-form-pristine')).toBe(true);
477 | expect(vm.$el.classList.contains('vf-form-invalid')).toBe(true);
478 | expect(vm.$el.classList.contains('vf-form-untouched')).toBe(true);
479 | expect(vm.$el.classList.contains('vf-form-focused')).toBe(false);
480 |
481 | const input = vm.$el.querySelector('[name=b]');
482 |
483 | expect(input.classList.contains('vf-pristine')).toBe(true);
484 | expect(input.classList.contains('vf-invalid')).toBe(true);
485 | expect(input.classList.contains('vf-untouched')).toBe(true);
486 | expect(input.classList.contains('vf-focused')).toBe(false);
487 | expect(input.classList.contains('vf-invalid-required')).toBe(true);
488 |
489 | // set valid and interacted
490 | input.focus();
491 |
492 | vm.$nextTick(() => {
493 | expect(vm.$el.classList.contains('vf-form-focused')).toBe(true);
494 | expect(input.classList.contains('vf-focused')).toBe(true);
495 |
496 | input.blur();
497 | setValid();
498 |
499 | vm.$nextTick(() => {
500 | expect(vm.$el.classList.contains('vf-form-dirty')).toBe(true);
501 | expect(vm.$el.classList.contains('vf-form-valid')).toBe(true);
502 | expect(vm.$el.classList.contains('vf-form-touched')).toBe(true);
503 | expect(vm.$el.classList.contains('vf-form-focused')).toBe(false);
504 | expect(input.classList.contains('vf-dirty')).toBe(true);
505 | expect(input.classList.contains('vf-valid')).toBe(true);
506 | expect(input.classList.contains('vf-touched')).toBe(true);
507 | expect(input.classList.contains('vf-focused')).toBe(false);
508 | expect(input.classList.contains('vf-invalid-required')).toBe(false);
509 | done();
510 | });
511 | });
512 | });
513 |
514 | it('should add and remove a field from overall state when inside v-if', (done) => {
515 | setValid();
516 | vm.model.c = '';
517 |
518 | vm.$nextTick(() => {
519 | expect(vm.formstate.c).toBeDefined();
520 | expect(vm.formstate.c.$invalid).toBe(true);
521 | expect(vm.formstate.$invalid).toBe(true);
522 |
523 | vm.isCEnabled = false;
524 |
525 | vm.$nextTick(() => {
526 | expect(vm.formstate.c).toBeUndefined();
527 | expect(vm.formstate.$valid).toBe(true);
528 | done();
529 | });
530 | });
531 | });
532 |
533 | it('should set tie labels and inputs together with auto-label', (done) => {
534 | vm.$nextTick(() => {
535 | expect(vm.$el.querySelector('#auto-label-a > label').getAttribute('for')).toBeDefined();
536 | expect(vm.$el.querySelector('#auto-label-b > label').getAttribute('for')).toBeDefined();
537 | expect(vm.$el.querySelector('#auto-label-c > label').getAttribute('for')).toBeDefined();
538 | expect(vm.$el.querySelector('#auto-label-c > label').getAttribute('for')).toBe('existing');
539 | expect(vm.$el.querySelector('#auto-label-d').getAttribute('for')).toBeDefined();
540 |
541 | expect(vm.$el.querySelector('#field-auto-label-a > label').getAttribute('for')).toBeDefined();
542 | expect(vm.$el.querySelector('#field-auto-label-b > label').getAttribute('for')).toBeDefined();
543 | expect(vm.$el.querySelector('#field-auto-label-c > label').getAttribute('for')).toBeDefined();
544 | expect(vm.$el.querySelector('#field-auto-label-c > label').getAttribute('for')).toBe('existing');
545 | expect(vm.$el.querySelector('#field-auto-label-d').getAttribute('for')).toBeDefined();
546 |
547 | expect(vm.$el.querySelector('#field-messages-auto-label-a').getAttribute('for')).toBeDefined();
548 | expect(vm.$el.querySelector('#field-messages-auto-label-b > label').getAttribute('for')).toBeDefined();
549 | done();
550 |
551 | });
552 | });
553 |
554 | it('should show the correct field messages', (done) => {
555 | vm.$nextTick(() => {
556 |
557 | // field b
558 | expect(vm.$el.querySelector('#message-b')).toBeNull();
559 | expect(vm.$el.querySelector('#message-b-ok')).toBeNull();
560 | vm.model.b = '123';
561 |
562 | // field c
563 | expect(vm.$el.querySelector('#message')).not.toBeNull();
564 | expect(vm.$el.querySelector('#minlength-message')).toBeNull();
565 | vm.model.c = '123';
566 |
567 | vm.$nextTick(() => {
568 | // field b sould still be null
569 | expect(vm.$el.querySelector('#message-b')).toBeNull();
570 | vm.$el.querySelector('[name=b]').focus();
571 | vm.$el.querySelector('[name=b]').blur();
572 | vm.model.b = '123456';
573 |
574 | // field c
575 | expect(vm.$el.querySelector('#message')).toBeNull();
576 | expect(vm.$el.querySelector('#minlength-message')).not.toBeNull();
577 | vm.model.c = '123456';
578 |
579 | vm.$nextTick(() => {
580 |
581 | // field b
582 | expect(vm.$el.querySelector('#message-b-ok')).not.toBeNull();
583 | expect(vm.$el.querySelector('#message-b')).toBeNull();
584 | vm.model.b = '';
585 |
586 | // field c
587 | expect(vm.$el.querySelector('#message')).toBeNull();
588 | expect(vm.$el.querySelector('#minlength-message')).toBeNull();
589 |
590 | vm.$nextTick(()=>{
591 | // field b
592 | expect(vm.$el.querySelector('#message-b-ok')).toBeNull();
593 | expect(vm.$el.querySelector('#message-b')).not.toBeNull();
594 | done();
595 | });
596 |
597 | });
598 | });
599 | });
600 | });
601 |
602 | it('should work with v-for', function(done) {
603 |
604 | vm.$destroy();
605 |
606 | const div = document.createElement('div');
607 | document.body.appendChild(div);
608 |
609 | new Vue({
610 | mixins: [VueForm],
611 | el: div,
612 | template: `
613 |
614 |
615 | {{input.label}}
616 |
617 |
618 |
619 | `,
620 | data: {
621 | inputs: [{
622 | label: 'Input A',
623 | name: 'a',
624 | model: '',
625 | required: true
626 | }, {
627 | label: 'Input B',
628 | name: 'b',
629 | model: '',
630 | required: false
631 | }, {
632 | label: 'Input C',
633 | name: 'c',
634 | model: 'abc',
635 | required: true
636 | }],
637 | formstate: {}
638 | },
639 | mounted: function() {
640 | this.$nextTick(() => {
641 | expect(this.formstate.a.$valid).toBe(false);
642 | expect(this.formstate.b.$valid).toBe(true);
643 | expect(this.formstate.c.$valid).toBe(true);
644 | done();
645 | });
646 | }
647 | });
648 |
649 | });
650 |
651 | it('should work with components, even if some validation attributes are also component props', function(done) {
652 |
653 | vm.$destroy();
654 |
655 | const div = document.createElement('div');
656 | document.body.appendChild(div);
657 |
658 | new Vue({
659 | mixins: [VueForm],
660 | el: div,
661 | components: {
662 | test: {
663 | props: ['value'],
664 | template: ''
665 | },
666 | test2: {
667 | props: ['value', 'required', 'name'],
668 | template: '',
669 | mounted () {
670 | this.$el.querySelector('input').addEventListener('focus', this.$emit('focus'));
671 | this.$el.querySelector('input').addEventListener('blur', this.$emit('blur'));
672 | }
673 | }
674 | },
675 | template: `
676 |
677 |
678 |
679 |
680 |
681 |
682 |
683 |
684 | `,
685 | data: {
686 | model: {
687 | test: '',
688 | test2: ''
689 | },
690 | formstate: {}
691 | },
692 | mounted: function() {
693 | expect(this.formstate.test).toBeDefined();
694 | expect(this.formstate.test.$error.required).toBe(true);
695 | expect(this.formstate.test2).toBeDefined();
696 | expect(this.formstate.test2.$error.required).toBe(true);
697 | expect(this.formstate.test2.$dirty).toBe(false);
698 | this.$el.querySelector('#input').focus();
699 | this.$el.querySelector('#input').blur();
700 |
701 | this.model.test = 'xxx';
702 | this.model.test2 = 'xxx';
703 |
704 | this.$nextTick(() => {
705 | expect(this.formstate.test.$error.required).toBeUndefined();
706 | expect(this.formstate.test.$dirty).toBe(false);
707 | expect(this.formstate.test2.$dirty).toBe(true);
708 | expect(this.formstate.test2.$touched).toBe(true);
709 | expect(this.formstate.test2.$focused).toBe(false);
710 | expect(this.formstate.test2._hasFocused).toBe(true);
711 | done();
712 | });
713 | }
714 | });
715 |
716 | });
717 |
718 | it('should be configurable', function(done) {
719 |
720 | vm.$destroy();
721 |
722 | const div = document.createElement('div');
723 | document.body.appendChild(div);
724 |
725 | var optionsA = {
726 | formTag: 'article',
727 | validateTag: 'section',
728 | messagesTag: 'ul',
729 | inputClasses: {
730 | invalid: 'form-control-danger',
731 | valid: 'form-control-success',
732 | focused: 'input-focused-class'
733 | },
734 | formClasses: {
735 | invalid: 'foo',
736 | valid: 'bar',
737 | focused: 'form-focused-class'
738 | },
739 | validateClasses: {
740 | invalid: 'baz',
741 | valid: 'jaz',
742 | focused: 'validate-focused-class'
743 | },
744 | validators: {
745 | 'foo-validator' () { return false },
746 | 'bar-validator' () { return false }
747 | }
748 | }
749 |
750 | new Vue({
751 | mixins: [new VueForm(optionsA)],
752 | el: div,
753 | template: `
754 |
755 |
756 |
757 |
758 |
759 |
760 |
761 |
762 |
763 |
764 |
765 | `,
766 | data: {
767 | name: '',
768 | formstate: {}
769 | },
770 | mounted: function() {
771 | this.name = 'abc';
772 | const form = this.$el;
773 |
774 | this.$nextTick(() => {
775 | expect(form.querySelector('#foo-message')).not.toBeNull();
776 | expect(form.querySelector('#bar-message')).not.toBeNull();
777 | expect(form.tagName).toBe('ARTICLE');
778 | expect(form.querySelector('section')).not.toBeNull();
779 | expect(form.querySelector('.messages').tagName).toBe('UL');
780 | expect(form.querySelector('.form-control-danger')).not.toBeNull();
781 | expect(form.className.indexOf('foo')).not.toBe(-1);
782 | expect(form.querySelector('section.baz')).not.toBeNull();
783 | expect(form.classList.contains('form-focused-class')).toBe(false);
784 | expect(form.querySelector('section.validate-focused-class')).toBeNull();
785 | expect(form.querySelector('.input-focused-class')).toBeNull();
786 |
787 | form.querySelector('input').focus();
788 |
789 | this.$nextTick(() => {
790 | expect(form.classList.contains('form-focused-class')).toBe(true);
791 | expect(form.querySelector('section.validate-focused-class')).not.toBeNull();
792 | expect(form.querySelector('.input-focused-class')).not.toBeNull();
793 |
794 | done();
795 | });
796 | });
797 | }
798 | });
799 |
800 | });
801 |
802 | });
803 |
--------------------------------------------------------------------------------
/dist/vue-form.js:
--------------------------------------------------------------------------------
1 | (function (global, factory) {
2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3 | typeof define === 'function' && define.amd ? define(factory) :
4 | (global.VueForm = factory());
5 | }(this, (function () { 'use strict';
6 |
7 | var emailRegExp = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; // from angular
8 | var urlRegExp = /^(http\:\/\/|https\:\/\/)(.{4,})$/;
9 |
10 | var email = function email(value, attrValue, vnode) {
11 | return emailRegExp.test(value);
12 | };
13 | email._allowNulls = true;
14 |
15 | var number = function number(value, attrValue, vnode) {
16 | return !isNaN(value);
17 | };
18 | number._allowNulls = true;
19 |
20 | var url = function url(value, attrValue, vnode) {
21 | return urlRegExp.test(value);
22 | };
23 | url._allowNulls = true;
24 |
25 | var validators = {
26 | email: email,
27 | number: number,
28 | url: url,
29 | required: function required(value, attrValue, vnode) {
30 | if (attrValue === false) {
31 | return true;
32 | }
33 |
34 | if (value === 0) {
35 | return true;
36 | }
37 |
38 | if (vnode.data.attrs && typeof vnode.data.attrs.bool !== 'undefined' || vnode.componentOptions && vnode.componentOptions.propsData && typeof vnode.componentOptions.propsData.bool !== 'undefined') {
39 | // bool attribute is present, allow false pass validation
40 | if (value === false) {
41 | return true;
42 | }
43 | }
44 |
45 | if (Array.isArray(value)) {
46 | return !!value.length;
47 | }
48 | return !!value;
49 | },
50 | minlength: function minlength(value, length) {
51 | return value.length >= length;
52 | },
53 | maxlength: function maxlength(value, length) {
54 | return length >= value.length;
55 | },
56 | pattern: function pattern(value, _pattern) {
57 | var patternRegExp = new RegExp('^' + _pattern + '$');
58 | return patternRegExp.test(value);
59 | },
60 | min: function min(value, _min, vnode) {
61 | if ((vnode.data.attrs.type || '').toLowerCase() == 'number') {
62 | return +value >= +_min;
63 | }
64 | return value >= _min;
65 | },
66 | max: function max(value, _max, vnode) {
67 | if ((vnode.data.attrs.type || '').toLowerCase() == 'number') {
68 | return +_max >= +value;
69 | }
70 | return _max >= value;
71 | }
72 | };
73 |
74 | var config = {
75 | validators: validators,
76 | formComponent: 'VueForm',
77 | formTag: 'form',
78 | messagesComponent: 'FieldMessages',
79 | messagesTag: 'div',
80 | showMessages: '',
81 | validateComponent: 'Validate',
82 | validateTag: 'div',
83 | fieldComponent: 'Field',
84 | fieldTag: 'div',
85 | formClasses: {
86 | dirty: 'vf-form-dirty',
87 | pristine: 'vf-form-pristine',
88 | valid: 'vf-form-valid',
89 | invalid: 'vf-form-invalid',
90 | touched: 'vf-form-touched',
91 | untouched: 'vf-form-untouched',
92 | focused: 'vf-form-focused',
93 | submitted: 'vf-form-submitted',
94 | pending: 'vf-form-pending'
95 | },
96 | validateClasses: {
97 | dirty: 'vf-field-dirty',
98 | pristine: 'vf-field-pristine',
99 | valid: 'vf-field-valid',
100 | invalid: 'vf-field-invalid',
101 | touched: 'vf-field-touched',
102 | untouched: 'vf-field-untouched',
103 | focused: 'vf-field-focused',
104 | submitted: 'vf-field-submitted',
105 | pending: 'vf-field-pending'
106 | },
107 | inputClasses: {
108 | dirty: 'vf-dirty',
109 | pristine: 'vf-pristine',
110 | valid: 'vf-valid',
111 | invalid: 'vf-invalid',
112 | touched: 'vf-touched',
113 | untouched: 'vf-untouched',
114 | focused: 'vf-focused',
115 | submitted: 'vf-submitted',
116 | pending: 'vf-pending'
117 | },
118 | Promise: typeof Promise === 'function' ? Promise : null
119 | };
120 |
121 | var classCallCheck = function (instance, Constructor) {
122 | if (!(instance instanceof Constructor)) {
123 | throw new TypeError("Cannot call a class as a function");
124 | }
125 | };
126 |
127 | var createClass = function () {
128 | function defineProperties(target, props) {
129 | for (var i = 0; i < props.length; i++) {
130 | var descriptor = props[i];
131 | descriptor.enumerable = descriptor.enumerable || false;
132 | descriptor.configurable = true;
133 | if ("value" in descriptor) descriptor.writable = true;
134 | Object.defineProperty(target, descriptor.key, descriptor);
135 | }
136 | }
137 |
138 | return function (Constructor, protoProps, staticProps) {
139 | if (protoProps) defineProperties(Constructor.prototype, protoProps);
140 | if (staticProps) defineProperties(Constructor, staticProps);
141 | return Constructor;
142 | };
143 | }();
144 |
145 |
146 |
147 |
148 |
149 | var defineProperty = function (obj, key, value) {
150 | if (key in obj) {
151 | Object.defineProperty(obj, key, {
152 | value: value,
153 | enumerable: true,
154 | configurable: true,
155 | writable: true
156 | });
157 | } else {
158 | obj[key] = value;
159 | }
160 |
161 | return obj;
162 | };
163 |
164 |
165 |
166 | var inherits = function (subClass, superClass) {
167 | if (typeof superClass !== "function" && superClass !== null) {
168 | throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
169 | }
170 |
171 | subClass.prototype = Object.create(superClass && superClass.prototype, {
172 | constructor: {
173 | value: subClass,
174 | enumerable: false,
175 | writable: true,
176 | configurable: true
177 | }
178 | });
179 | if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
180 | };
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 | var possibleConstructorReturn = function (self, call) {
193 | if (!self) {
194 | throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
195 | }
196 |
197 | return call && (typeof call === "object" || typeof call === "function") ? call : self;
198 | };
199 |
200 | function getClasses(classConfig, state) {
201 | var _ref;
202 |
203 | return _ref = {}, defineProperty(_ref, classConfig.dirty, state.$dirty), defineProperty(_ref, classConfig.pristine, state.$pristine), defineProperty(_ref, classConfig.valid, state.$valid), defineProperty(_ref, classConfig.invalid, state.$invalid), defineProperty(_ref, classConfig.touched, state.$touched), defineProperty(_ref, classConfig.untouched, state.$untouched), defineProperty(_ref, classConfig.focused, state.$focused), defineProperty(_ref, classConfig.pending, state.$pending), defineProperty(_ref, classConfig.submitted, state.$submitted), _ref;
204 | }
205 |
206 | function addClass(el, className) {
207 | if (el.classList) {
208 | el.classList.add(className);
209 | } else {
210 | el.className += ' ' + className;
211 | }
212 | }
213 |
214 | function removeClass(el, className) {
215 | if (el.classList) {
216 | el.classList.remove(className);
217 | } else {
218 | el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
219 | }
220 | }
221 |
222 | function vModelValue(data) {
223 | if (data.model) {
224 | return data.model.value;
225 | }
226 | return data.directives.filter(function (v) {
227 | return v.name === 'model';
228 | })[0].value;
229 | }
230 |
231 | function getVModelAndLabel(nodes, config) {
232 | var foundVnodes = {
233 | vModel: [],
234 | label: null,
235 | messages: null
236 | };
237 |
238 | if (!nodes) {
239 | return foundVnodes;
240 | }
241 |
242 | function traverse(nodes) {
243 | for (var i = 0; i < nodes.length; i++) {
244 | var node = nodes[i];
245 |
246 | if (node.componentOptions) {
247 | if (node.componentOptions.tag === hyphenate(config.messagesComponent)) {
248 | foundVnodes.messages = node;
249 | }
250 | }
251 |
252 | if (node.tag === 'label' && !foundVnodes.label) {
253 | foundVnodes.label = node;
254 | }
255 |
256 | if (node.data) {
257 | if (node.data.model) {
258 | // model check has to come first. If a component has
259 | // a directive and v-model, the directive will be in .directives
260 | // and v-modelstored in .model
261 | foundVnodes.vModel.push(node);
262 | } else if (node.data.directives) {
263 | var match = node.data.directives.filter(function (v) {
264 | return v.name === 'model';
265 | });
266 | if (match.length) {
267 | foundVnodes.vModel.push(node);
268 | }
269 | }
270 | }
271 | if (node.children) {
272 | traverse(node.children);
273 | } else if (node.componentOptions && node.componentOptions.children) {
274 | traverse(node.componentOptions.children);
275 | }
276 | }
277 | }
278 |
279 | traverse(nodes);
280 |
281 | return foundVnodes;
282 | }
283 |
284 | function getName(vnode) {
285 | if (vnode.data && vnode.data.attrs && vnode.data.attrs.name) {
286 | return vnode.data.attrs.name;
287 | } else if (vnode.componentOptions && vnode.componentOptions.propsData && vnode.componentOptions.propsData.name) {
288 | return vnode.componentOptions.propsData.name;
289 | }
290 | }
291 |
292 | var hyphenateRE = /([^-])([A-Z])/g;
293 | function hyphenate(str) {
294 | return str.replace(hyphenateRE, '$1-$2').replace(hyphenateRE, '$1-$2').toLowerCase();
295 | }
296 |
297 | function randomId() {
298 | return Math.random().toString(36).substr(2, 10);
299 | }
300 |
301 | // https://davidwalsh.name/javascript-debounce-function
302 | function debounce(func, wait, immediate) {
303 | var timeout;
304 | return function () {
305 | var context = this,
306 | args = arguments;
307 | var later = function later() {
308 | timeout = null;
309 | if (!immediate) func.apply(context, args);
310 | };
311 | var callNow = immediate && !timeout;
312 | clearTimeout(timeout);
313 | timeout = setTimeout(later, wait);
314 | if (callNow) func.apply(context, args);
315 | };
316 | }
317 |
318 | function isShallowObjectDifferent(a, b) {
319 | var aValue = '';
320 | var bValue = '';
321 | Object.keys(a).sort().filter(function (v) {
322 | return typeof a[v] !== 'function';
323 | }).forEach(function (v) {
324 | return aValue += a[v];
325 | });
326 | Object.keys(b).sort().filter(function (v) {
327 | return typeof a[v] !== 'function';
328 | }).forEach(function (v) {
329 | return bValue += b[v];
330 | });
331 | return aValue !== bValue;
332 | }
333 |
334 | var vueFormConfig = 'VueFormProviderConfig' + randomId();
335 | var vueFormState = 'VueFormProviderState' + randomId();
336 | var vueFormParentForm = 'VueFormProviderParentForm' + randomId();
337 |
338 | var hasOwn = Object.prototype.hasOwnProperty;
339 | var toStr = Object.prototype.toString;
340 | var defineProperty$1 = Object.defineProperty;
341 | var gOPD = Object.getOwnPropertyDescriptor;
342 |
343 | var isArray = function isArray(arr) {
344 | if (typeof Array.isArray === 'function') {
345 | return Array.isArray(arr);
346 | }
347 |
348 | return toStr.call(arr) === '[object Array]';
349 | };
350 |
351 | var isPlainObject = function isPlainObject(obj) {
352 | if (!obj || toStr.call(obj) !== '[object Object]') {
353 | return false;
354 | }
355 |
356 | var hasOwnConstructor = hasOwn.call(obj, 'constructor');
357 | var hasIsPrototypeOf = obj.constructor && obj.constructor.prototype && hasOwn.call(obj.constructor.prototype, 'isPrototypeOf');
358 | // Not own constructor property must be Object
359 | if (obj.constructor && !hasOwnConstructor && !hasIsPrototypeOf) {
360 | return false;
361 | }
362 |
363 | // Own properties are enumerated firstly, so to speed up,
364 | // if last one is own, then all properties are own.
365 | var key;
366 | for (key in obj) { /**/ }
367 |
368 | return typeof key === 'undefined' || hasOwn.call(obj, key);
369 | };
370 |
371 | // If name is '__proto__', and Object.defineProperty is available, define __proto__ as an own property on target
372 | var setProperty = function setProperty(target, options) {
373 | if (defineProperty$1 && options.name === '__proto__') {
374 | defineProperty$1(target, options.name, {
375 | enumerable: true,
376 | configurable: true,
377 | value: options.newValue,
378 | writable: true
379 | });
380 | } else {
381 | target[options.name] = options.newValue;
382 | }
383 | };
384 |
385 | // Return undefined instead of __proto__ if '__proto__' is not an own property
386 | var getProperty = function getProperty(obj, name) {
387 | if (name === '__proto__') {
388 | if (!hasOwn.call(obj, name)) {
389 | return void 0;
390 | } else if (gOPD) {
391 | // In early versions of node, obj['__proto__'] is buggy when obj has
392 | // __proto__ as an own property. Object.getOwnPropertyDescriptor() works.
393 | return gOPD(obj, name).value;
394 | }
395 | }
396 |
397 | return obj[name];
398 | };
399 |
400 | var extend = function extend() {
401 | var options, name, src, copy, copyIsArray, clone;
402 | var target = arguments[0];
403 | var i = 1;
404 | var length = arguments.length;
405 | var deep = false;
406 |
407 | // Handle a deep copy situation
408 | if (typeof target === 'boolean') {
409 | deep = target;
410 | target = arguments[1] || {};
411 | // skip the boolean and the target
412 | i = 2;
413 | }
414 | if (target == null || (typeof target !== 'object' && typeof target !== 'function')) {
415 | target = {};
416 | }
417 |
418 | for (; i < length; ++i) {
419 | options = arguments[i];
420 | // Only deal with non-null/undefined values
421 | if (options != null) {
422 | // Extend the base object
423 | for (name in options) {
424 | src = getProperty(target, name);
425 | copy = getProperty(options, name);
426 |
427 | // Prevent never-ending loop
428 | if (target !== copy) {
429 | // Recurse if we're merging plain objects or arrays
430 | if (deep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) {
431 | if (copyIsArray) {
432 | copyIsArray = false;
433 | clone = src && isArray(src) ? src : [];
434 | } else {
435 | clone = src && isPlainObject(src) ? src : {};
436 | }
437 |
438 | // Never move original objects, clone them
439 | setProperty(target, { name: name, newValue: extend(deep, clone, copy) });
440 |
441 | // Don't bring in undefined values
442 | } else if (typeof copy !== 'undefined') {
443 | setProperty(target, { name: name, newValue: copy });
444 | }
445 | }
446 | }
447 | }
448 | }
449 |
450 | // Return the modified object
451 | return target;
452 | };
453 |
454 | var vueForm = {
455 | render: function render(h) {
456 | var _this = this;
457 |
458 | var attrs = {};
459 |
460 | if (typeof window !== 'undefined') {
461 | attrs.novalidate = '';
462 | }
463 |
464 | return h(this.tag || this.vueFormConfig.formTag, {
465 | on: {
466 | submit: function submit(event) {
467 | if (_this.state.$pending) {
468 | event.preventDefault();
469 | _this.vueFormConfig.Promise.all(_this.promises).then(function () {
470 | _this.state._submit();
471 | _this.$emit('submit', event);
472 | _this.promises = [];
473 | });
474 | } else {
475 | _this.state._submit();
476 | _this.$emit('submit', event);
477 | }
478 | },
479 | reset: function reset(event) {
480 | _this.state._reset();
481 | _this.$emit('reset', event);
482 | }
483 | },
484 | class: this.className,
485 | attrs: attrs
486 | }, [this.$slots.default]);
487 | },
488 |
489 | props: {
490 | state: {
491 | type: Object,
492 | required: true
493 | },
494 | tag: String,
495 | showMessages: String
496 | },
497 | inject: { vueFormConfig: vueFormConfig },
498 | provide: function provide() {
499 | var _ref;
500 |
501 | return _ref = {}, defineProperty(_ref, vueFormState, this.state), defineProperty(_ref, vueFormParentForm, this), _ref;
502 | },
503 |
504 | data: function data() {
505 | return {
506 | promises: []
507 | };
508 | },
509 | created: function created() {
510 | var _this2 = this;
511 |
512 | if (!this.state) {
513 | return;
514 | }
515 | var controls = {};
516 | var state = this.state;
517 | var formstate = {
518 | $dirty: false,
519 | $pristine: true,
520 | $valid: true,
521 | $invalid: false,
522 | $submitted: false,
523 | $touched: false,
524 | $untouched: true,
525 | $focused: false,
526 | $pending: false,
527 | $error: {},
528 | $submittedState: {},
529 | _id: '',
530 | _submit: function _submit() {
531 | _this2.state.$submitted = true;
532 | _this2.state._cloneState();
533 | },
534 | _cloneState: function _cloneState() {
535 | var cloned = JSON.parse(JSON.stringify(state));
536 | delete cloned.$submittedState;
537 | Object.keys(cloned).forEach(function (key) {
538 | _this2.$set(_this2.state.$submittedState, key, cloned[key]);
539 | });
540 | },
541 | _addControl: function _addControl(ctrl) {
542 | controls[ctrl.$name] = ctrl;
543 | _this2.$set(state, ctrl.$name, ctrl);
544 | },
545 | _removeControl: function _removeControl(ctrl) {
546 | delete controls[ctrl.$name];
547 | _this2.$delete(_this2.state, ctrl.$name);
548 | _this2.$delete(_this2.state.$error, ctrl.$name);
549 | },
550 | _validate: function _validate() {
551 | Object.keys(controls).forEach(function (key) {
552 | controls[key]._validate();
553 | });
554 | },
555 | _reset: function _reset() {
556 | state.$submitted = false;
557 | state.$pending = false;
558 | state.$submittedState = {};
559 | Object.keys(controls).forEach(function (key) {
560 | var control = controls[key];
561 | control._hasFocused = false;
562 | control._setUntouched();
563 | control._setPristine();
564 | control.$submitted = false;
565 | control.$pending = false;
566 | });
567 | }
568 | };
569 |
570 | Object.keys(formstate).forEach(function (key) {
571 | _this2.$set(_this2.state, key, formstate[key]);
572 | });
573 |
574 | this.$watch('state', function () {
575 | var isDirty = false;
576 | var isValid = true;
577 | var isTouched = false;
578 | var isFocused = false;
579 | var isPending = false;
580 | Object.keys(controls).forEach(function (key) {
581 | var control = controls[key];
582 |
583 | control.$submitted = state.$submitted;
584 |
585 | if (control.$dirty) {
586 | isDirty = true;
587 | }
588 | if (control.$touched) {
589 | isTouched = true;
590 | }
591 | if (control.$focused) {
592 | isFocused = true;
593 | }
594 | if (control.$pending) {
595 | isPending = true;
596 | }
597 | if (!control.$valid) {
598 | isValid = false;
599 | // add control to errors
600 | _this2.$set(state.$error, control.$name, control);
601 | } else {
602 | _this2.$delete(state.$error, control.$name);
603 | }
604 | });
605 |
606 | state.$dirty = isDirty;
607 | state.$pristine = !isDirty;
608 | state.$touched = isTouched;
609 | state.$untouched = !isTouched;
610 | state.$focused = isFocused;
611 | state.$valid = isValid;
612 | state.$invalid = !isValid;
613 | state.$pending = isPending;
614 | }, {
615 | deep: true,
616 | immediate: true
617 | });
618 |
619 | /* watch pristine? if set to true, set all children to pristine
620 | Object.keys(controls).forEach((ctrl) => {
621 | controls[ctrl].setPristine();
622 | });*/
623 | },
624 |
625 | computed: {
626 | className: function className() {
627 | var classes = getClasses(this.vueFormConfig.formClasses, this.state);
628 | return classes;
629 | }
630 | },
631 | methods: {
632 | reset: function reset() {
633 | this.state._reset();
634 | },
635 | validate: function validate() {
636 | this.state._validate();
637 | }
638 | }
639 | };
640 |
641 | var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
642 |
643 |
644 |
645 |
646 |
647 | function createCommonjsModule(fn, module) {
648 | return module = { exports: {} }, fn(module, module.exports), module.exports;
649 | }
650 |
651 | var scope_eval = createCommonjsModule(function (module) {
652 | // Generated by CoffeeScript 1.10.0
653 | (function() {
654 | var hasProp = {}.hasOwnProperty,
655 | slice = [].slice;
656 |
657 | module.exports = function(source, scope) {
658 | var key, keys, value, values;
659 | keys = [];
660 | values = [];
661 | for (key in scope) {
662 | if (!hasProp.call(scope, key)) continue;
663 | value = scope[key];
664 | if (key === 'this') {
665 | continue;
666 | }
667 | keys.push(key);
668 | values.push(value);
669 | }
670 | return Function.apply(null, slice.call(keys).concat(["return eval(" + (JSON.stringify(source)) + ")"])).apply(scope["this"], values);
671 | };
672 |
673 | }).call(commonjsGlobal);
674 | });
675 |
676 | function findLabel(nodes) {
677 | if (!nodes) {
678 | return;
679 | }
680 | for (var i = 0; i < nodes.length; i++) {
681 | var vnode = nodes[i];
682 | if (vnode.tag === 'label') {
683 | return nodes[i];
684 | } else if (nodes[i].children) {
685 | return findLabel(nodes[i].children);
686 | }
687 | }
688 | }
689 |
690 | var messages = {
691 | inject: { vueFormConfig: vueFormConfig, vueFormState: vueFormState, vueFormParentForm: vueFormParentForm },
692 | render: function render(h) {
693 | var _this = this;
694 |
695 | var children = [];
696 | var field = this.formstate[this.fieldname];
697 | if (field && field.$error && this.isShown) {
698 | Object.keys(field.$error).forEach(function (key) {
699 | if (_this.$slots[key] || _this.$scopedSlots[key]) {
700 | var out = _this.$slots[key] || _this.$scopedSlots[key](field);
701 | if (_this.autoLabel) {
702 | var label = findLabel(out);
703 | if (label) {
704 | label.data = label.data || {};
705 | label.data.attrs = label.data.attrs || {};
706 | label.data.attrs.for = field._id;
707 | }
708 | }
709 | children.push(out);
710 | }
711 | });
712 | if (!children.length && field.$valid) {
713 | if (this.$slots.default || this.$scopedSlots.default) {
714 | var out = this.$slots.default || this.$scopedSlots.default(field);
715 | if (this.autoLabel) {
716 | var label = findLabel(out);
717 | if (label) {
718 | label.data = label.data || {};
719 | label.data.attrs = label.data.attrs || {};
720 | label.data.attrs.for = field._id;
721 | }
722 | }
723 | children.push(out);
724 | }
725 | }
726 | }
727 | return h(this.tag || this.vueFormConfig.messagesTag, children);
728 | },
729 |
730 | props: {
731 | state: Object,
732 | name: String,
733 | show: {
734 | type: String,
735 | default: ''
736 | },
737 | tag: {
738 | type: String
739 | },
740 | autoLabel: Boolean
741 | },
742 | data: function data() {
743 | return {
744 | formstate: null,
745 | fieldname: ''
746 | };
747 | },
748 | created: function created() {
749 | this.fieldname = this.name;
750 | this.formstate = this.state || this.vueFormState;
751 | },
752 |
753 | computed: {
754 | isShown: function isShown() {
755 | var field = this.formstate[this.fieldname];
756 | var show = this.show || this.vueFormParentForm.showMessages || this.vueFormConfig.showMessages;
757 |
758 | if (!show || !field) {
759 | return true;
760 | }
761 |
762 | return scope_eval(show, field);
763 | }
764 | }
765 | };
766 |
767 | var validate = {
768 | render: function render(h) {
769 | var _this = this;
770 |
771 | var foundVnodes = getVModelAndLabel(this.$slots.default, this.vueFormConfig);
772 | var vModelnodes = foundVnodes.vModel;
773 | var attrs = {
774 | for: null
775 | };
776 | if (vModelnodes.length) {
777 | this.name = getName(vModelnodes[0]);
778 |
779 | if (foundVnodes.messages) {
780 | this.$nextTick(function () {
781 | var messagesVm = foundVnodes.messages.componentInstance;
782 | if (messagesVm) {
783 | messagesVm.fieldname = messagesVm.fieldname || _this.name;
784 | }
785 | });
786 | }
787 |
788 | if (this.autoLabel) {
789 | var id = vModelnodes[0].data.attrs.id || this.fieldstate._id;
790 | this.fieldstate._id = id;
791 | vModelnodes[0].data.attrs.id = id;
792 | if (foundVnodes.label) {
793 | foundVnodes.label.data = foundVnodes.label.data || {};
794 | foundVnodes.label.data.attrs = foundVnodes.label.data.attrs || {};
795 | foundVnodes.label.data.attrs.for = id;
796 | } else if (this.tag === 'label') {
797 | attrs.for = id;
798 | }
799 | }
800 | vModelnodes.forEach(function (vnode) {
801 | if (!vnode.data.directives) {
802 | vnode.data.directives = [];
803 | }
804 | vnode.data.directives.push({ name: 'vue-form-validator', value: { fieldstate: _this.fieldstate, config: _this.vueFormConfig } });
805 | vnode.data.attrs['vue-form-validator'] = '';
806 | vnode.data.attrs['debounce'] = _this.debounce;
807 | });
808 | } else {
809 | //console.warn('Element with v-model not found');
810 | }
811 | return h(this.tag || this.vueFormConfig.validateTag, { 'class': this.className, attrs: attrs }, this.$slots.default);
812 | },
813 |
814 | props: {
815 | state: Object,
816 | custom: null,
817 | autoLabel: Boolean,
818 | tag: {
819 | type: String
820 | },
821 | debounce: Number
822 | },
823 | inject: { vueFormConfig: vueFormConfig, vueFormState: vueFormState, vueFormParentForm: vueFormParentForm },
824 | data: function data() {
825 | return {
826 | name: '',
827 | formstate: null,
828 | fieldstate: {}
829 | };
830 | },
831 |
832 | methods: {
833 | getClasses: function getClasses$$1(classConfig) {
834 | var s = this.fieldstate;
835 | return Object.keys(s.$error).reduce(function (classes, error) {
836 | classes[classConfig.invalid + '-' + hyphenate(error)] = true;
837 | return classes;
838 | }, getClasses(classConfig, s));
839 | }
840 | },
841 | computed: {
842 | className: function className() {
843 | return this.getClasses(this.vueFormConfig.validateClasses);
844 | },
845 | inputClassName: function inputClassName() {
846 | return this.getClasses(this.vueFormConfig.inputClasses);
847 | }
848 | },
849 | mounted: function mounted() {
850 | var _this2 = this;
851 |
852 | this.fieldstate.$name = this.name;
853 | this.formstate._addControl(this.fieldstate);
854 |
855 | var vModelEls = this.$el.querySelectorAll('[vue-form-validator]');
856 |
857 | // add classes to the input element
858 | this.$watch('inputClassName', function (value, oldValue) {
859 | var out = void 0;
860 |
861 | var _loop = function _loop(i, el) {
862 | if (oldValue) {
863 | Object.keys(oldValue).filter(function (k) {
864 | return oldValue[k];
865 | }).forEach(function (k) {
866 | return removeClass(el, k);
867 | });
868 | }
869 | out = [];
870 | Object.keys(value).filter(function (k) {
871 | return value[k];
872 | }).forEach(function (k) {
873 | out.push(k);
874 | addClass(el, k);
875 | });
876 | };
877 |
878 | for (var i = 0, el; el = vModelEls[i++];) {
879 | _loop(i, el);
880 | }
881 | _this2.fieldstate._className = out;
882 | }, {
883 | deep: true,
884 | immediate: true
885 | });
886 |
887 | this.$watch('name', function (value, oldValue) {
888 | _this2.formstate._removeControl(_this2.fieldstate);
889 | _this2.fieldstate.$name = value;
890 | _this2.formstate._addControl(_this2.fieldstate);
891 | });
892 | },
893 | created: function created() {
894 | var _this4 = this;
895 |
896 | this.formstate = this.state || this.vueFormState;
897 | var vm = this;
898 | var pendingValidators = [];
899 | var _val = void 0;
900 | var prevVnode = void 0;
901 | this.fieldstate = {
902 | $name: '',
903 | $dirty: false,
904 | $pristine: true,
905 | $valid: true,
906 | $invalid: false,
907 | $touched: false,
908 | $untouched: true,
909 | $focused: false,
910 | $pending: false,
911 | $submitted: false,
912 | $error: {},
913 | $attrs: {},
914 | _className: null,
915 | _id: 'vf' + randomId(),
916 | _setValidatorValidity: function _setValidatorValidity(validator, isValid) {
917 | if (isValid) {
918 | vm.$delete(this.$error, validator);
919 | } else {
920 | vm.$set(this.$error, validator, true);
921 | }
922 | },
923 | _setValidity: function _setValidity(isValid) {
924 | this.$valid = isValid;
925 | this.$invalid = !isValid;
926 | },
927 | _setDirty: function _setDirty() {
928 | this.$dirty = true;
929 | this.$pristine = false;
930 | },
931 | _setPristine: function _setPristine() {
932 | this.$dirty = false;
933 | this.$pristine = true;
934 | },
935 | _setTouched: function _setTouched() {
936 | this.$touched = true;
937 | this.$untouched = false;
938 | },
939 | _setUntouched: function _setUntouched() {
940 | this.$touched = false;
941 | this.$untouched = true;
942 | },
943 | _setFocused: function _setFocused(value) {
944 | this.$focused = typeof value === 'boolean' ? value : false;
945 | if (this.$focused) {
946 | this._setHasFocused();
947 | } else {
948 | this._setTouched();
949 | }
950 | },
951 | _setHasFocused: function _setHasFocused() {
952 | this._hasFocused = true;
953 | },
954 |
955 | _hasFocused: false,
956 | _validators: {},
957 | _validate: function _validate(vnode) {
958 | var _this3 = this;
959 |
960 | if (!vnode) {
961 | vnode = prevVnode;
962 | } else {
963 | prevVnode = vnode;
964 | }
965 | this.$pending = true;
966 | var isValid = true;
967 | var emptyAndRequired = false;
968 | var value = vModelValue(vnode.data);
969 | _val = value;
970 |
971 | var pending = {
972 | promises: [],
973 | names: []
974 | };
975 |
976 | pendingValidators.push(pending);
977 |
978 | var attrs = vnode.data.attrs || {};
979 | var childvm = vnode.componentInstance;
980 | if (childvm && childvm._vfValidationData_) {
981 | attrs = extend({}, attrs, childvm._vfValidationData_);
982 | }
983 |
984 | var propsData = vnode.componentOptions && vnode.componentOptions.propsData ? vnode.componentOptions.propsData : {};
985 |
986 | Object.keys(this._validators).forEach(function (validator) {
987 | // when value is empty and current validator is not the required validator, the field is valid
988 | if ((value === '' || value === undefined || value === null) && validator !== 'required') {
989 | _this3._setValidatorValidity(validator, true);
990 | emptyAndRequired = true;
991 | // return early, required validator will
992 | // fall through if it is present
993 | return;
994 | }
995 |
996 | var attrValue = typeof attrs[validator] !== 'undefined' ? attrs[validator] : propsData[validator];
997 | var isFunction = typeof _this3._validators[validator] === 'function';
998 |
999 | // match vue behaviour, ignore if attribute is null or undefined. But for type=email|url|number and custom validators, the value will be null, so allow with _allowNulls
1000 | if (isFunction && (attrValue === null || typeof attrValue === 'undefined') && !_this3._validators[validator]._allowNulls) {
1001 | return;
1002 | }
1003 |
1004 | if (attrValue) {
1005 | _this3.$attrs[validator] = attrValue;
1006 | }
1007 |
1008 | var result = isFunction ? _this3._validators[validator](value, attrValue, vnode) : vm.custom[validator];
1009 |
1010 | if (typeof result === 'boolean') {
1011 | if (result) {
1012 | _this3._setValidatorValidity(validator, true);
1013 | } else {
1014 | isValid = false;
1015 | _this3._setValidatorValidity(validator, false);
1016 | }
1017 | } else {
1018 | pending.promises.push(result);
1019 | pending.names.push(validator);
1020 | vm.vueFormParentForm.promises.push(result);
1021 | }
1022 | });
1023 |
1024 | if (pending.promises.length) {
1025 | vm.vueFormConfig.Promise.all(pending.promises).then(function (results) {
1026 |
1027 | // only concerned with the last promise results, in case
1028 | // async responses return out of order
1029 | if (pending !== pendingValidators[pendingValidators.length - 1]) {
1030 | //console.log('ignoring old promise', pending.promises);
1031 | return;
1032 | }
1033 |
1034 | pendingValidators = [];
1035 |
1036 | results.forEach(function (result, i) {
1037 | var name = pending.names[i];
1038 | if (result) {
1039 | _this3._setValidatorValidity(name, true);
1040 | } else {
1041 | isValid = false;
1042 | _this3._setValidatorValidity(name, false);
1043 | }
1044 | });
1045 | _this3._setValidity(isValid);
1046 | _this3.$pending = false;
1047 | });
1048 | } else {
1049 | this._setValidity(isValid);
1050 | this.$pending = false;
1051 | }
1052 | }
1053 | };
1054 |
1055 | // add custom validators
1056 | if (this.custom) {
1057 | Object.keys(this.custom).forEach(function (prop) {
1058 | if (typeof _this4.custom[prop] === 'function') {
1059 | _this4.custom[prop]._allowNulls = true;
1060 | _this4.fieldstate._validators[prop] = _this4.custom[prop];
1061 | } else {
1062 | _this4.fieldstate._validators[prop] = _this4.custom[prop];
1063 | }
1064 | });
1065 | }
1066 |
1067 | this.$watch('custom', function (v, oldV) {
1068 | if (!oldV || !v) {
1069 | return;
1070 | }
1071 | if (isShallowObjectDifferent(v, oldV)) {
1072 | _this4.fieldstate._validate();
1073 | }
1074 | }, {
1075 | deep: true
1076 | });
1077 | },
1078 | destroyed: function destroyed() {
1079 | this.formstate._removeControl(this.fieldstate);
1080 | }
1081 | };
1082 |
1083 | var field = {
1084 | inject: { vueFormConfig: vueFormConfig },
1085 | render: function render(h) {
1086 | var foundVnodes = getVModelAndLabel(this.$slots.default, this.vueFormConfig);
1087 | var vModelnodes = foundVnodes.vModel;
1088 | var attrs = {
1089 | for: null
1090 | };
1091 | if (vModelnodes.length) {
1092 | if (this.autoLabel) {
1093 | var id = vModelnodes[0].data.attrs && vModelnodes[0].data.attrs.id || 'vf' + randomId();
1094 | vModelnodes[0].data.attrs.id = id;
1095 | if (foundVnodes.label) {
1096 | foundVnodes.label.data = foundVnodes.label.data || {};
1097 | foundVnodes.label.data.attrs = foundVnodes.label.data.attrs = {};
1098 | foundVnodes.label.data.attrs.for = id;
1099 | } else if (this.tag === 'label') {
1100 | attrs.for = id;
1101 | }
1102 | }
1103 | }
1104 | return h(this.tag || this.vueFormConfig.fieldTag, { attrs: attrs }, this.$slots.default);
1105 | },
1106 |
1107 | props: {
1108 | tag: {
1109 | type: String
1110 | },
1111 | autoLabel: {
1112 | type: Boolean,
1113 | default: true
1114 | }
1115 | }
1116 | };
1117 |
1118 | var debouncedValidators = {};
1119 |
1120 | function addValidators(attrs, validators, fieldValidators) {
1121 | Object.keys(attrs).forEach(function (attr) {
1122 | var prop = attr === 'type' ? attrs[attr].toLowerCase() : attr.toLowerCase();
1123 |
1124 | if (validators[prop] && !fieldValidators[prop]) {
1125 | fieldValidators[prop] = validators[prop];
1126 | }
1127 | });
1128 | }
1129 |
1130 | function compareChanges(vnode, oldvnode, validators) {
1131 |
1132 | var hasChanged = false;
1133 | var attrs = vnode.data.attrs || {};
1134 | var oldAttrs = oldvnode.data.attrs || {};
1135 | var out = {};
1136 |
1137 | if (vModelValue(vnode.data) !== vModelValue(oldvnode.data)) {
1138 | out.vModel = true;
1139 | hasChanged = true;
1140 | }
1141 |
1142 | Object.keys(validators).forEach(function (validator) {
1143 | if (attrs[validator] !== oldAttrs[validator]) {
1144 | out[validator] = true;
1145 | hasChanged = true;
1146 | }
1147 | });
1148 |
1149 | // if is a component
1150 | if (vnode.componentOptions && vnode.componentOptions.propsData) {
1151 | var _attrs = vnode.componentOptions.propsData;
1152 | var _oldAttrs = oldvnode.componentOptions.propsData;
1153 | Object.keys(validators).forEach(function (validator) {
1154 | if (_attrs[validator] !== _oldAttrs[validator]) {
1155 | out[validator] = true;
1156 | hasChanged = true;
1157 | }
1158 | });
1159 | }
1160 |
1161 | if (hasChanged) {
1162 | return out;
1163 | }
1164 | }
1165 |
1166 | var vueFormValidator = {
1167 | name: 'vue-form-validator',
1168 | bind: function bind(el, binding, vnode) {
1169 | var fieldstate = binding.value.fieldstate;
1170 | var validators = binding.value.config.validators;
1171 |
1172 | var attrs = vnode.data.attrs || {};
1173 | var inputName = getName(vnode);
1174 |
1175 | if (!inputName) {
1176 | console.warn('vue-form: name attribute missing');
1177 | return;
1178 | }
1179 |
1180 | if (attrs.debounce) {
1181 | debouncedValidators[fieldstate._id] = debounce(function (fieldstate, vnode) {
1182 | if (fieldstate._hasFocused) {
1183 | fieldstate._setDirty();
1184 | }
1185 | fieldstate._validate(vnode);
1186 | }, attrs.debounce);
1187 | }
1188 |
1189 | // add validators
1190 | addValidators(attrs, validators, fieldstate._validators);
1191 |
1192 | // if is a component, a validator attribute could be a prop this component uses
1193 | if (vnode.componentOptions && vnode.componentOptions.propsData) {
1194 | addValidators(vnode.componentOptions.propsData, validators, fieldstate._validators);
1195 | }
1196 |
1197 | fieldstate._validate(vnode);
1198 |
1199 | // native listeners
1200 | el.addEventListener('blur', function () {
1201 | fieldstate._setFocused(false);
1202 | }, false);
1203 | el.addEventListener('focus', function () {
1204 | fieldstate._setFocused(true);
1205 | }, false);
1206 |
1207 | // component listeners
1208 | var vm = vnode.componentInstance;
1209 | if (vm) {
1210 | vm.$on('blur', function () {
1211 | fieldstate._setFocused(false);
1212 | });
1213 | vm.$on('focus', function () {
1214 | fieldstate._setFocused(true);
1215 | });
1216 |
1217 | vm.$once('vf:addFocusListeners', function () {
1218 | el.addEventListener('focusout', function () {
1219 | fieldstate._setFocused(false);
1220 | }, false);
1221 | el.addEventListener('focusin', function () {
1222 | fieldstate._setFocused(true);
1223 | }, false);
1224 | });
1225 |
1226 | vm.$on('vf:validate', function (data) {
1227 | if (!vm._vfValidationData_) {
1228 | addValidators(data, validators, fieldstate._validators);
1229 | }
1230 | vm._vfValidationData_ = data;
1231 | fieldstate._validate(vm.$vnode);
1232 | });
1233 | }
1234 | },
1235 | update: function update(el, binding, vnode, oldVNode) {
1236 | var validators = binding.value.config.validators;
1237 |
1238 | var changes = compareChanges(vnode, oldVNode, validators);
1239 | var fieldstate = binding.value.fieldstate;
1240 |
1241 |
1242 | var attrs = vnode.data.attrs || {};
1243 | var vm = vnode.componentInstance;
1244 | if (vm && vm._vfValidationData_) {
1245 | attrs = extend({}, attrs, vm[vm._vfValidationData_]);
1246 | }
1247 |
1248 | if (vnode.elm.className.indexOf(fieldstate._className[0]) === -1) {
1249 | vnode.elm.className = vnode.elm.className + ' ' + fieldstate._className.join(' ');
1250 | }
1251 |
1252 | if (!changes) {
1253 | return;
1254 | }
1255 |
1256 | if (changes.vModel) {
1257 | // re-validate all
1258 | if (attrs.debounce) {
1259 | fieldstate.$pending = true;
1260 | debouncedValidators[fieldstate._id](fieldstate, vnode);
1261 | } else {
1262 | if (fieldstate._hasFocused) {
1263 | fieldstate._setDirty();
1264 | }
1265 | fieldstate._validate(vnode);
1266 | }
1267 | } else {
1268 | // attributes have changed
1269 | // to do: loop through them and re-validate changed ones
1270 | //for(let prop in changes) {
1271 | // fieldstate._validate(vnode, validator);
1272 | //}
1273 | // for now
1274 | fieldstate._validate(vnode);
1275 | }
1276 | }
1277 | };
1278 |
1279 | function VueFormBase(options) {
1280 | var _components;
1281 |
1282 | var c = extend(true, {}, config, options);
1283 | this.provide = function () {
1284 | return defineProperty({}, vueFormConfig, c);
1285 | };
1286 | this.components = (_components = {}, defineProperty(_components, c.formComponent, vueForm), defineProperty(_components, c.messagesComponent, messages), defineProperty(_components, c.validateComponent, validate), defineProperty(_components, c.fieldComponent, field), _components);
1287 | this.directives = { vueFormValidator: vueFormValidator };
1288 | }
1289 |
1290 | var VueForm = function (_VueFormBase) {
1291 | inherits(VueForm, _VueFormBase);
1292 |
1293 | function VueForm() {
1294 | classCallCheck(this, VueForm);
1295 | return possibleConstructorReturn(this, (VueForm.__proto__ || Object.getPrototypeOf(VueForm)).apply(this, arguments));
1296 | }
1297 |
1298 | createClass(VueForm, null, [{
1299 | key: 'install',
1300 | value: function install(Vue, options) {
1301 | Vue.mixin(new this(options));
1302 | }
1303 | }, {
1304 | key: 'installed',
1305 | get: function get$$1() {
1306 | return !!this.install.done;
1307 | },
1308 | set: function set$$1(val) {
1309 | this.install.done = val;
1310 | }
1311 | }]);
1312 | return VueForm;
1313 | }(VueFormBase);
1314 |
1315 | VueFormBase.call(VueForm);
1316 | // temp fix for vue 2.3.0
1317 | VueForm.options = new VueForm();
1318 |
1319 | return VueForm;
1320 |
1321 | })));
1322 |
--------------------------------------------------------------------------------