Comparing version 2.0.1 to 3.0.0
@@ -8,61 +8,86 @@ (function (global, factory) { | ||
var config = { | ||
formComponent: 'vueForm', | ||
errorComponent: 'formError', | ||
errorsComponent: 'formErrors', | ||
validateComponent: 'validate', | ||
errorTag: 'span', | ||
errorsTag: 'div', | ||
classPrefix: 'vf-', | ||
dirtyClass: 'dirty', | ||
pristineClass: 'pristine', | ||
validClass: 'valid', | ||
invalidClass: 'invalid', | ||
submittedClass: 'submitted', | ||
touchedClass: 'touched', | ||
untouchedClass: 'untouched', | ||
pendingClass: 'pending', | ||
Promise: window.Promise | ||
formComponent: 'vueForm', | ||
messagesComponent: 'fieldMessages', | ||
validateComponent: 'validate', | ||
fieldComponent: 'field', | ||
messagesTag: 'div', | ||
fieldTag: 'div', | ||
classes: { | ||
form: { | ||
dirty: 'vf-form-dirty', | ||
pristine: 'vf-form-pristine', | ||
valid: 'vf-form-valid', | ||
invalid: 'vf-form-invalid', | ||
touched: 'vf-form-touched', | ||
untouched: 'vf-form-untouched', | ||
submitted: 'vf-form-submitted', | ||
pending: 'vf-form-pending' | ||
}, | ||
validate: { | ||
dirty: 'vf-field-dirty', | ||
pristine: 'vf-field-pristine', | ||
valid: 'vf-field-valid', | ||
invalid: 'vf-field-invalid', | ||
touched: 'vf-field-touched', | ||
untouched: 'vf-field-untouched', | ||
submitted: 'vf-field-submitted', | ||
pending: 'vf-field-pending' | ||
}, | ||
input: { | ||
dirty: 'vf-dirty', | ||
pristine: 'vf-pristine', | ||
valid: 'vf-valid', | ||
invalid: 'vf-invalid', | ||
touched: 'vf-touched', | ||
untouched: 'vf-untouched', | ||
submitted: 'vf-submitted', | ||
pending: 'vf-pending' | ||
} | ||
}, | ||
Promise: window.Promise | ||
}; | ||
var errorMixin = { | ||
computed: { | ||
isShown: function isShown() { | ||
var field = this.formstate[this.field]; | ||
if (!this.show || !field) { | ||
return true; | ||
} | ||
if (this.show.indexOf('&&') > -1) { | ||
// and logic - every | ||
var split = this.show.split('&&'); | ||
return split.every(function (v) { | ||
return field[v.trim()]; | ||
}); | ||
} else if (this.show.indexOf('||') > -1) { | ||
// or logic - some | ||
var _split = this.show.split('||'); | ||
return _split.some(function (v) { | ||
return field[v.trim()]; | ||
}); | ||
} else { | ||
// single | ||
return field[this.show]; | ||
} | ||
function findLabel(nodes) { | ||
if (!nodes) { | ||
return; | ||
} | ||
for (var i = 0; i < nodes.length; i++) { | ||
var vnode = nodes[i]; | ||
if (vnode.tag === 'label') { | ||
return nodes[i]; | ||
} else if (nodes[i].children) { | ||
return findLabel(nodes[i].children); | ||
} | ||
} | ||
}; | ||
} | ||
var formErrors = { | ||
mixins: [errorMixin], | ||
var messages = { | ||
render: function render(h) { | ||
var _this = this; | ||
//console.log('errors render'); | ||
var children = []; | ||
var field = this.formstate[this.field]; | ||
var field = this.formstate[this.name]; | ||
if (field && field.$error && this.isShown) { | ||
Object.keys(field.$error).forEach(function (key) { | ||
if (_this.autoLabel) { | ||
var label = findLabel(_this.$slots[key]); | ||
if (label) { | ||
label.data = label.data || {}; | ||
label.data.attrs = label.data.attrs || {}; | ||
label.data.attrs.for = field._id; | ||
} | ||
} | ||
children.push(_this.$slots[key]); | ||
}); | ||
if (!children.length) { | ||
if (this.autoLabel) { | ||
var label = findLabel(this.$slots.default); | ||
if (label) { | ||
label.data = label.data || {}; | ||
label.data.attrs = label.data.attrs || {}; | ||
label.data.attrs.for = field._id; | ||
} | ||
} | ||
children.push(this.$slots.default); | ||
} | ||
} | ||
@@ -74,3 +99,3 @@ return h(this.tag, children); | ||
state: Object, | ||
field: String, | ||
name: String, | ||
show: { | ||
@@ -82,4 +107,5 @@ type: String, | ||
type: String, | ||
default: config.errorsTag | ||
} | ||
default: config.messagesTag | ||
}, | ||
autoLabel: Boolean | ||
}, | ||
@@ -97,38 +123,29 @@ data: function data() { | ||
}); | ||
} | ||
}; | ||
var formError = { | ||
mixins: [errorMixin], | ||
render: function render(h) { | ||
var field = this.formstate[this.field]; | ||
if (field && field.$error[this.error] && this.isShown) { | ||
return h(this.tag, [this.$slots.default]); | ||
} | ||
}, | ||
props: { | ||
state: Object, | ||
field: String, | ||
error: String, | ||
show: { | ||
type: String, | ||
default: '' | ||
}, | ||
tag: { | ||
type: String, | ||
default: config.errorTag | ||
computed: { | ||
isShown: function isShown() { | ||
var field = this.formstate[this.name]; | ||
if (!this.show || !field) { | ||
return true; | ||
} | ||
var compare = function compare(v) { | ||
return field[v.trim()]; | ||
}; | ||
if (this.show.indexOf('&&') > -1) { | ||
// and logic - every | ||
var split = this.show.split('&&'); | ||
return split.every(compare); | ||
} else if (this.show.indexOf('||') > -1) { | ||
// or logic - some | ||
var _split = this.show.split('||'); | ||
return _split.some(compare); | ||
} else { | ||
// single | ||
return field[this.show]; | ||
} | ||
} | ||
}, | ||
data: function data() { | ||
return { | ||
formstate: {} | ||
}; | ||
}, | ||
mounted: function mounted() { | ||
var _this = this; | ||
this.$nextTick(function () { | ||
_this.formstate = _this.state || _this.$parent.formstate || _this.$parent.state; | ||
}); | ||
} | ||
@@ -190,2 +207,3 @@ }; | ||
_this.state.$submitted = true; | ||
_this.state._cloneState(); | ||
_this.$emit('submit', event); | ||
@@ -222,2 +240,11 @@ } | ||
$error: {}, | ||
$submittedState: {}, | ||
_id: '', | ||
_cloneState: function _cloneState() { | ||
var cloned = JSON.parse(JSON.stringify(state)); | ||
delete cloned.$submittedState; | ||
Object.keys(cloned).forEach(function (key) { | ||
_this2.$set(_this2.state.$submittedState, key, cloned[key]); | ||
}); | ||
}, | ||
_addControl: function _addControl(ctrl) { | ||
@@ -245,2 +272,5 @@ controls[ctrl.$name] = ctrl; | ||
var control = controls[key]; | ||
control.$submitted = state.$submitted; | ||
if (control.$dirty) { | ||
@@ -285,26 +315,25 @@ isDirty = true; | ||
var out = []; | ||
var c = config.classes.form; | ||
if (this.state.$dirty) { | ||
out.push(config.dirtyClass); | ||
out.push(c.dirty); | ||
} else { | ||
out.push(config.pristineClass); | ||
out.push(c.pristine); | ||
} | ||
if (this.state.$valid) { | ||
out.push(config.validClass); | ||
out.push(c.valid); | ||
} else { | ||
out.push(config.invalidClass); | ||
out.push(c.invalid); | ||
} | ||
if (this.state.$touched) { | ||
out.push(config.touchedClass); | ||
out.push(c.touched); | ||
} else { | ||
out.push(config.untouchedClass); | ||
out.push(c.untouched); | ||
} | ||
if (this.state.$submitted) { | ||
out.push(config.submittedClass); | ||
out.push(c.submitted); | ||
} | ||
if (this.state.$pending) { | ||
out.push(config.pendingClass); | ||
out.push(c.pending); | ||
} | ||
return out.map(function (v) { | ||
return config.classPrefix + 'form-' + v; | ||
}).join(' '); | ||
return out.join(' '); | ||
} | ||
@@ -341,4 +370,7 @@ } | ||
function getVModelNode(nodes) { | ||
var foundVnodes = []; | ||
function getVModelAndLabel(nodes) { | ||
var foundVnodes = { | ||
vModel: [], | ||
label: null | ||
}; | ||
@@ -348,2 +380,5 @@ function traverse(nodes) { | ||
var node = nodes[i]; | ||
if (node.tag === 'label' && !foundVnodes.label) { | ||
foundVnodes.label = node; | ||
} | ||
if (node.data) { | ||
@@ -355,6 +390,6 @@ if (node.data.directives) { | ||
if (match.length) { | ||
foundVnodes.push(node); | ||
foundVnodes.vModel.push(node); | ||
} | ||
} else if (node.data.model) { | ||
foundVnodes.push(node); | ||
foundVnodes.vModel.push(node); | ||
} | ||
@@ -386,2 +421,6 @@ } | ||
function randomId() { | ||
return Math.random().toString(36).substr(2, 10); | ||
} | ||
function compareChanges(vnode, oldvnode) { | ||
@@ -441,5 +480,5 @@ | ||
if (attr === 'type') { | ||
prop = attrs[attr]; | ||
prop = attrs[attr].toLowerCase(); | ||
} else { | ||
prop = attr; | ||
prop = attr.toLowerCase(); | ||
} | ||
@@ -507,4 +546,2 @@ if (validators[prop] && !fieldstate._validators[prop]) { | ||
// todo: Make getVModelNode recursive | ||
var validate = { | ||
@@ -514,11 +551,27 @@ render: function render(h) { | ||
var foundVnodes = getVModelNode(this.$slots.default); | ||
if (foundVnodes.length) { | ||
this.name = getName(foundVnodes[0]); | ||
foundVnodes.forEach(function (foundVnode) { | ||
if (!foundVnode.data.directives) { | ||
foundVnode.data.directives = []; | ||
var foundVnodes = getVModelAndLabel(this.$slots.default); | ||
var vModelnodes = foundVnodes.vModel; | ||
var attrs = { | ||
for: null | ||
}; | ||
if (vModelnodes.length) { | ||
this.name = getName(vModelnodes[0]); | ||
if (this.autoLabel) { | ||
var id = this.fieldstate._id || vModelnodes[0].data.attrs.id || 'vf' + randomId(); | ||
this.fieldstate._id = id; | ||
vModelnodes[0].data.attrs.id = id; | ||
if (foundVnodes.label) { | ||
foundVnodes.label.data = foundVnodes.label.data || {}; | ||
foundVnodes.label.data.attrs = foundVnodes.label.data.attrs = {}; | ||
foundVnodes.label.data.attrs.for = id; | ||
} else if (this.tag === 'label') { | ||
attrs.for = id; | ||
} | ||
foundVnode.data.directives.push({ name: 'vue-form-validator', value: _this.fieldstate }); | ||
foundVnode.data.attrs['vue-form-validator'] = ''; | ||
} | ||
vModelnodes.forEach(function (vnode) { | ||
if (!vnode.data.directives) { | ||
vnode.data.directives = []; | ||
} | ||
vnode.data.directives.push({ name: 'vue-form-validator', value: _this.fieldstate }); | ||
vnode.data.attrs['vue-form-validator'] = ''; | ||
}); | ||
@@ -528,5 +581,3 @@ } else { | ||
} | ||
return h(this.tag, { 'class': this.className.map(function (v) { | ||
return config.classPrefix + 'container-' + v; | ||
}) }, this.$slots.default); | ||
return h(this.tag, { 'class': this.className.join(' '), attrs: attrs }, this.$slots.default); | ||
}, | ||
@@ -537,5 +588,6 @@ | ||
custom: null, | ||
autoLabel: Boolean, | ||
tag: { | ||
type: String, | ||
default: 'span' | ||
default: 'div' | ||
} | ||
@@ -554,25 +606,53 @@ }, | ||
var out = []; | ||
var c = config.classes.validate; | ||
if (this.fieldstate.$dirty) { | ||
out.push(config.dirtyClass); | ||
out.push(c.dirty); | ||
} else { | ||
out.push(config.pristineClass); | ||
out.push(c.pristine); | ||
} | ||
if (this.fieldstate.$valid) { | ||
out.push(config.validClass); | ||
out.push(c.valid); | ||
} else { | ||
out.push(config.invalidClass); | ||
out.push(c.invalid); | ||
} | ||
if (this.fieldstate.$touched) { | ||
out.push(config.touchedClass); | ||
out.push(c.touched); | ||
} else { | ||
out.push(config.untouchedClass); | ||
out.push(c.untouched); | ||
} | ||
if (this.fieldstate.$pending) { | ||
out.push(config.pendingClass); | ||
out.push(c.pending); | ||
} | ||
Object.keys(this.fieldstate.$error).forEach(function (error) { | ||
out.push(config.invalidClass + '-' + hyphenate(error)); | ||
out.push(c.invalid + '-' + hyphenate(error)); | ||
}); | ||
return out; | ||
}, | ||
inputClassName: function inputClassName() { | ||
var out = []; | ||
var c = config.classes.input; | ||
if (this.fieldstate.$dirty) { | ||
out.push(c.dirty); | ||
} else { | ||
out.push(c.pristine); | ||
} | ||
if (this.fieldstate.$valid) { | ||
out.push(c.valid); | ||
} else { | ||
out.push(c.invalid); | ||
} | ||
if (this.fieldstate.$touched) { | ||
out.push(c.touched); | ||
} else { | ||
out.push(c.untouched); | ||
} | ||
if (this.fieldstate.$pending) { | ||
out.push(c.pending); | ||
} | ||
Object.keys(this.fieldstate.$error).forEach(function (error) { | ||
out.push(c.invalid + '-' + hyphenate(error)); | ||
}); | ||
return out; | ||
} | ||
@@ -588,7 +668,7 @@ }, | ||
// add classes to the input element | ||
this.$watch('className', function (value, oldValue) { | ||
this.$watch('inputClassName', function (value, oldValue) { | ||
if (oldValue) { | ||
var _loop = function _loop(i) { | ||
oldValue.forEach(function (v) { | ||
return removeClass(vModelEls[i], config.classPrefix + v); | ||
return removeClass(vModelEls[i], v); | ||
}); | ||
@@ -604,3 +684,3 @@ }; | ||
value.forEach(function (v) { | ||
return addClass(vModelEls[_i], config.classPrefix + v); | ||
return addClass(vModelEls[_i], v); | ||
}); | ||
@@ -632,3 +712,5 @@ }; | ||
$pending: false, | ||
$submitted: false, | ||
$error: {}, | ||
_id: '', | ||
_setValidatorVadility: function _setValidatorVadility(validator, isValid) { | ||
@@ -753,2 +835,54 @@ if (isValid) { | ||
var field = { | ||
render: function render(h) { | ||
var foundVnodes = getVModelAndLabel(this.$slots.default); | ||
var vModelnodes = foundVnodes.vModel; | ||
var attrs = { | ||
for: null | ||
}; | ||
if (vModelnodes.length) { | ||
if (this.autoLabel) { | ||
var id = vModelnodes[0].data.attrs.id || 'vf' + randomId(); | ||
vModelnodes[0].data.attrs.id = id; | ||
if (foundVnodes.label) { | ||
foundVnodes.label.data = foundVnodes.label.data || {}; | ||
foundVnodes.label.data.attrs = foundVnodes.label.data.attrs = {}; | ||
foundVnodes.label.data.attrs.for = id; | ||
} else if (this.tag === 'label') { | ||
attrs.for = id; | ||
} | ||
} | ||
} | ||
return h(this.tag, { attrs: attrs }, this.$slots.default); | ||
}, | ||
props: { | ||
tag: { | ||
type: String, | ||
default: config.fieldTag | ||
}, | ||
autoLabel: { | ||
type: Boolean, | ||
default: true | ||
} | ||
} | ||
}; | ||
var defineProperty = function (obj, key, value) { | ||
if (key in obj) { | ||
Object.defineProperty(obj, key, { | ||
value: value, | ||
enumerable: true, | ||
configurable: true, | ||
writable: true | ||
}); | ||
} else { | ||
obj[key] = value; | ||
} | ||
return obj; | ||
}; | ||
var _components; | ||
var main = { | ||
@@ -758,4 +892,4 @@ install: function install(Vue) { | ||
Vue.component(config.validateComponent, validate); | ||
Vue.component(config.errorsComponent, formErrors); | ||
Vue.component(config.errorComponent, formError); | ||
Vue.component(config.messagesComponent, messages); | ||
Vue.component(config.fieldComponent, field); | ||
Vue.directive('vue-form-validator', vueFormValidator); | ||
@@ -770,8 +904,3 @@ }, | ||
mixin: { | ||
components: { | ||
formErrors: formErrors, | ||
formError: formError, | ||
vueForm: vueForm, | ||
validate: validate | ||
}, | ||
components: (_components = {}, defineProperty(_components, config.formComponent, vueForm), defineProperty(_components, config.validateComponent, validate), defineProperty(_components, config.messagesComponent, messages), defineProperty(_components, config.fieldComponent, field), _components), | ||
directives: { | ||
@@ -778,0 +907,0 @@ vueFormValidator: vueFormValidator |
@@ -1,1 +0,1 @@ | ||
!function(global,factory){"object"==typeof exports&&"undefined"!=typeof module?module.exports=factory():"function"==typeof define&&define.amd?define(factory):global.vueForm=factory()}(this,function(){"use strict";function addClass(el,className){el.classList?el.classList.add(className):el.className+=" "+className}function removeClass(el,className){el.classList?el.classList.remove(className):el.className=el.className.replace(new RegExp("(^|\\b)"+className.split(" ").join("|")+"(\\b|$)","gi")," ")}function vModelValue(data){return data.model?data.model.value:data.directives.filter(function(v){return"model"===v.name})[0].value}function getVModelNode(nodes){function traverse(nodes){for(var i=0;i<nodes.length;i++){var node=nodes[i];if(node.data)if(node.data.directives){var match=node.data.directives.filter(function(v){return"model"===v.name});match.length&&foundVnodes.push(node)}else node.data.model&&foundVnodes.push(node);node.children&&traverse(node.children)}}var foundVnodes=[];return traverse(nodes),foundVnodes}function getName(vnode){return vnode.data&&vnode.data.attrs&&vnode.data.attrs.name?vnode.data.attrs.name:vnode.componentOptions&&vnode.componentOptions.propsData&&vnode.componentOptions.propsData.name?vnode.componentOptions.propsData.name:void 0}function hyphenate(str){return str.replace(hyphenateRE,"$1-$2").replace(hyphenateRE,"$1-$2").toLowerCase()}function compareChanges(vnode,oldvnode){var hasChanged=!1,attrs=vnode.data.attrs||{},oldAttrs=oldvnode.data.attrs||{},out={};if(vModelValue(vnode.data)!==vModelValue(oldvnode.data)&&(out.vModel=!0,hasChanged=!0),Object.keys(validators).forEach(function(validator){attrs[validator]!==oldAttrs[validator]&&(out[validator]=!0,hasChanged=!0)}),vnode.componentOptions&&vnode.componentOptions.propsData&&!function(){var attrs=vnode.componentOptions.propsData,oldAttrs=oldvnode.componentOptions.propsData;Object.keys(validators).forEach(function(validator){attrs[validator]!==oldAttrs[validator]&&(out[validator]=!0,hasChanged=!0)})}(),hasChanged)return out}var config={formComponent:"vueForm",errorComponent:"formError",errorsComponent:"formErrors",validateComponent:"validate",errorTag:"span",errorsTag:"div",classPrefix:"vf-",dirtyClass:"dirty",pristineClass:"pristine",validClass:"valid",invalidClass:"invalid",submittedClass:"submitted",touchedClass:"touched",untouchedClass:"untouched",pendingClass:"pending",Promise:window.Promise},errorMixin={computed:{isShown:function(){var field=this.formstate[this.field];if(!this.show||!field)return!0;if(this.show.indexOf("&&")>-1){var split=this.show.split("&&");return split.every(function(v){return field[v.trim()]})}if(this.show.indexOf("||")>-1){var _split=this.show.split("||");return _split.some(function(v){return field[v.trim()]})}return field[this.show]}}},formErrors={mixins:[errorMixin],render:function(h){var _this=this,children=[],field=this.formstate[this.field];return field&&field.$error&&this.isShown&&Object.keys(field.$error).forEach(function(key){children.push(_this.$slots[key])}),h(this.tag,children)},props:{state:Object,field:String,show:{type:String,default:""},tag:{type:String,default:config.errorsTag}},data:function(){return{formstate:{}}},mounted:function(){var _this2=this;this.$nextTick(function(){_this2.formstate=_this2.state||_this2.$parent.formstate||_this2.$parent.state})}},formError={mixins:[errorMixin],render:function(h){var field=this.formstate[this.field];if(field&&field.$error[this.error]&&this.isShown)return h(this.tag,[this.$slots.default])},props:{state:Object,field:String,error:String,show:{type:String,default:""},tag:{type:String,default:config.errorTag}},data:function(){return{formstate:{}}},mounted:function(){var _this=this;this.$nextTick(function(){_this.formstate=_this.state||_this.$parent.formstate||_this.$parent.state})}},emailRegExp=/^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i,urlRegExp=/^(http\:\/\/|https\:\/\/)(.{4,})$/,validators={email:function(value,attrValue,vnode){return emailRegExp.test(value)},number:function(value){return!isNaN(value)},url:function(value){return urlRegExp.test(value)},required:function(value,attrValue,vnode){return attrValue===!1||(0===value||(Array.isArray(value)?!!value.length:!!value))},minlength:function(value,length){return value.length>=length},maxlength:function(value,length){return length>=value.length},pattern:function(value,_pattern){var patternRegExp=new RegExp("^"+_pattern+"$");return patternRegExp.test(value)},min:function(value,_min){return 1*value>=1*_min},max:function(value,_max){return 1*_max>=1*value}},vueForm={render:function(h){var _this=this;return h("form",{on:{submit:function(event){_this.state.$submitted=!0,_this.$emit("submit",event)}},attrs:{novalidate:"",class:this.className}},[this.$slots.default])},props:{state:Object},data:function(){return{}},created:function(){var _this2=this,controls={},state=this.state,formstate={$dirty:!1,$pristine:!0,$valid:!0,$invalid:!1,$submitted:!1,$touched:!1,$untouched:!0,$pending:!1,$error:{},_addControl:function(ctrl){controls[ctrl.$name]=ctrl,_this2.$set(state,ctrl.$name,ctrl)},_removeControl:function(ctrl){delete controls[ctrl.$name],_this2.$delete(_this2.state,ctrl.$name),_this2.$delete(_this2.state.$error,ctrl.$name)}};Object.keys(formstate).forEach(function(key){_this2.$set(_this2.state,key,formstate[key])}),this.$watch("state",function(){var isDirty=!1,isValid=!0,isTouched=!1,isPending=!1;Object.keys(controls).forEach(function(key){var control=controls[key];control.$dirty&&(isDirty=!0),control.$touched&&(isTouched=!0),control.$pending&&(isPending=!0),control.$valid?_this2.$delete(state.$error,control.$name):(isValid=!1,_this2.$set(state.$error,control.$name,control))}),state.$dirty=isDirty,state.$pristine=!isDirty,state.$touched=isTouched,state.$untouched=!isTouched,state.$valid=isValid,state.$invalid=!isValid,state.$pending=isPending},{deep:!0,immediate:!0})},computed:{className:function(){var out=[];return this.state.$dirty?out.push(config.dirtyClass):out.push(config.pristineClass),this.state.$valid?out.push(config.validClass):out.push(config.invalidClass),this.state.$touched?out.push(config.touchedClass):out.push(config.untouchedClass),this.state.$submitted&&out.push(config.submittedClass),this.state.$pending&&out.push(config.pendingClass),out.map(function(v){return config.classPrefix+"form-"+v}).join(" ")}}},hyphenateRE=/([^-])([A-Z])/g,vueFormValidator={name:"vue-form-validator",bind:function(el,binding,vnode){var fieldstate=binding.value,attrs=vnode.data.attrs||{},inputName=getName(vnode);return inputName?(Object.keys(attrs).forEach(function(attr){var prop=void 0;prop="type"===attr?attrs[attr]:attr,validators[prop]&&!fieldstate._validators[prop]&&(fieldstate._validators[prop]=validators[prop])}),vnode.componentOptions&&vnode.componentOptions.propsData&&Object.keys(vnode.componentOptions.propsData).forEach(function(prop){validators[prop]&&!fieldstate._validators[prop]&&(fieldstate._validators[prop]=validators[prop])}),fieldstate._validate(vnode),el.addEventListener("blur",function(){fieldstate._setTouched()},!1),el.addEventListener("focus",function(){fieldstate._setFocused()},!1),void(vnode.componentInstance&&(vnode.componentInstance.$on("blur",function(){fieldstate._setTouched()}),vnode.componentInstance.$on("focus",function(){fieldstate._setFocused()})))):void console.warn("vue-form: name attribute missing")},update:function(el,binding,vnode,oldVNode){var changes=compareChanges(vnode,oldVNode),fieldstate=binding.value;changes&&(changes.vModel?(fieldstate._hasFocused&&fieldstate._setDirty(),fieldstate._validate(vnode)):fieldstate._validate(vnode))}},validate={render:function(h){var _this=this,foundVnodes=getVModelNode(this.$slots.default);return foundVnodes.length?(this.name=getName(foundVnodes[0]),foundVnodes.forEach(function(foundVnode){foundVnode.data.directives||(foundVnode.data.directives=[]),foundVnode.data.directives.push({name:"vue-form-validator",value:_this.fieldstate}),foundVnode.data.attrs["vue-form-validator"]=""})):console.warn("Element with v-model not found"),h(this.tag,{class:this.className.map(function(v){return config.classPrefix+"container-"+v})},this.$slots.default)},props:{state:Object,custom:null,tag:{type:String,default:"span"}},data:function(){return{name:"",formstate:{},fieldstate:{}}},computed:{className:function(){var out=[];return this.fieldstate.$dirty?out.push(config.dirtyClass):out.push(config.pristineClass),this.fieldstate.$valid?out.push(config.validClass):out.push(config.invalidClass),this.fieldstate.$touched?out.push(config.touchedClass):out.push(config.untouchedClass),this.fieldstate.$pending&&out.push(config.pendingClass),Object.keys(this.fieldstate.$error).forEach(function(error){out.push(config.invalidClass+"-"+hyphenate(error))}),out}},mounted:function(){this.fieldstate.$name=this.name,this.formstate=this.state||this.$parent.state,this.formstate._addControl(this.fieldstate);var vModelEls=this.$el.querySelectorAll("[vue-form-validator]");this.$watch("className",function(value,oldValue){if(oldValue)for(var _loop=function(i){oldValue.forEach(function(v){return removeClass(vModelEls[i],config.classPrefix+v)})},i=0;i<vModelEls.length;i++)_loop(i);for(var _loop2=function(_i){value.forEach(function(v){return addClass(vModelEls[_i],config.classPrefix+v)})},_i=0;_i<vModelEls.length;_i++)_loop2(_i)},{deep:!0,immediate:!0})},created:function(){var _this3=this,vm=this,pendingValidators=[],_val=void 0;this.fieldstate={$name:"",$dirty:!1,$pristine:!0,$valid:!0,$invalid:!1,$touched:!1,$untouched:!0,$pending:!1,$error:{},_setValidatorVadility:function(validator,isValid){isValid?vm.$delete(this.$error,validator):vm.$set(this.$error,validator,!0)},_setVadility:function(isValid){this.$valid=isValid,this.$invalid=!isValid},_setDirty:function(){this.$dirty=!0,this.$pristine=!1},_setPristine:function(){this.$dirty=!1,this.$pristine=!0},_setTouched:function(){this.$touched=!0,this.$untouched=!1},_setUntouched:function(){this.$touched=!1,this.$untouched=!0},_setFocused:function(){this._hasFocused=!0},_hasFocused:!1,_validators:{},_validate:function(vnode){var _this2=this;this.$pending=!0;var isValid=!0,emptyAndRequired=!1,value=vModelValue(vnode.data);_val=value;var pending={promises:[],names:[]};pendingValidators.push(pending);var attrs=vnode.data.attrs||{},propsData=vnode.componentOptions&&vnode.componentOptions.propsData?vnode.componentOptions.propsData:{};Object.keys(this._validators).forEach(function(validator){if((""===value||void 0===value||null===value)&&"required"!==validator)return _this2._setValidatorVadility(validator,!0),void(emptyAndRequired=!0);var attrValue="undefined"!=typeof attrs[validator]?attrs[validator]:propsData[validator],result=_this2._validators[validator](value,attrValue,vnode);"boolean"==typeof result?result?_this2._setValidatorVadility(validator,!0):(isValid=!1,_this2._setValidatorVadility(validator,!1)):(pending.promises.push(result),pending.names.push(validator))}),pending.promises.length?config.Promise.all(pending.promises).then(function(results){pending===pendingValidators[pendingValidators.length-1]&&(pendingValidators=[],results.forEach(function(result,i){var name=pending.names[i];result?_this2._setValidatorVadility(name,!0):(isValid=!1,_this2._setValidatorVadility(name,!1))}),_this2._setVadility(isValid),_this2.$pending=!1)}):(this._setVadility(isValid),this.$pending=!1)}},this.custom&&Object.keys(this.custom).forEach(function(prop){_this3.fieldstate._validators[prop]=_this3.custom[prop]})},destroyed:function(){this.formstate._removeControl(this.fieldstate)}},main={install:function(Vue){Vue.component(config.formComponent,vueForm),Vue.component(config.validateComponent,validate),Vue.component(config.errorsComponent,formErrors),Vue.component(config.errorComponent,formError),Vue.directive("vue-form-validator",vueFormValidator)},config:config,addValidator:function(key,fn){validators[key]=fn},mixin:{components:{formErrors:formErrors,formError:formError,vueForm:vueForm,validate:validate},directives:{vueFormValidator:vueFormValidator}}};return main}); | ||
!function(global,factory){"object"==typeof exports&&"undefined"!=typeof module?module.exports=factory():"function"==typeof define&&define.amd?define(factory):global.vueForm=factory()}(this,function(){"use strict";function findLabel(nodes){if(nodes)for(var i=0;i<nodes.length;i++){var vnode=nodes[i];if("label"===vnode.tag)return nodes[i];if(nodes[i].children)return findLabel(nodes[i].children)}}function addClass(el,className){el.classList?el.classList.add(className):el.className+=" "+className}function removeClass(el,className){el.classList?el.classList.remove(className):el.className=el.className.replace(new RegExp("(^|\\b)"+className.split(" ").join("|")+"(\\b|$)","gi")," ")}function vModelValue(data){return data.model?data.model.value:data.directives.filter(function(v){return"model"===v.name})[0].value}function getVModelAndLabel(nodes){function traverse(nodes){for(var i=0;i<nodes.length;i++){var node=nodes[i];if("label"!==node.tag||foundVnodes.label||(foundVnodes.label=node),node.data)if(node.data.directives){var match=node.data.directives.filter(function(v){return"model"===v.name});match.length&&foundVnodes.vModel.push(node)}else node.data.model&&foundVnodes.vModel.push(node);node.children&&traverse(node.children)}}var foundVnodes={vModel:[],label:null};return traverse(nodes),foundVnodes}function getName(vnode){return vnode.data&&vnode.data.attrs&&vnode.data.attrs.name?vnode.data.attrs.name:vnode.componentOptions&&vnode.componentOptions.propsData&&vnode.componentOptions.propsData.name?vnode.componentOptions.propsData.name:void 0}function hyphenate(str){return str.replace(hyphenateRE,"$1-$2").replace(hyphenateRE,"$1-$2").toLowerCase()}function randomId(){return Math.random().toString(36).substr(2,10)}function compareChanges(vnode,oldvnode){var hasChanged=!1,attrs=vnode.data.attrs||{},oldAttrs=oldvnode.data.attrs||{},out={};if(vModelValue(vnode.data)!==vModelValue(oldvnode.data)&&(out.vModel=!0,hasChanged=!0),Object.keys(validators).forEach(function(validator){attrs[validator]!==oldAttrs[validator]&&(out[validator]=!0,hasChanged=!0)}),vnode.componentOptions&&vnode.componentOptions.propsData&&!function(){var attrs=vnode.componentOptions.propsData,oldAttrs=oldvnode.componentOptions.propsData;Object.keys(validators).forEach(function(validator){attrs[validator]!==oldAttrs[validator]&&(out[validator]=!0,hasChanged=!0)})}(),hasChanged)return out}var _components,config={formComponent:"vueForm",messagesComponent:"fieldMessages",validateComponent:"validate",fieldComponent:"field",messagesTag:"div",fieldTag:"div",classes:{form:{dirty:"vf-form-dirty",pristine:"vf-form-pristine",valid:"vf-form-valid",invalid:"vf-form-invalid",touched:"vf-form-touched",untouched:"vf-form-untouched",submitted:"vf-form-submitted",pending:"vf-form-pending"},validate:{dirty:"vf-field-dirty",pristine:"vf-field-pristine",valid:"vf-field-valid",invalid:"vf-field-invalid",touched:"vf-field-touched",untouched:"vf-field-untouched",submitted:"vf-field-submitted",pending:"vf-field-pending"},input:{dirty:"vf-dirty",pristine:"vf-pristine",valid:"vf-valid",invalid:"vf-invalid",touched:"vf-touched",untouched:"vf-untouched",submitted:"vf-submitted",pending:"vf-pending"}},Promise:window.Promise},messages={render:function(h){var _this=this,children=[],field=this.formstate[this.name];if(field&&field.$error&&this.isShown&&(Object.keys(field.$error).forEach(function(key){if(_this.autoLabel){var label=findLabel(_this.$slots[key]);label&&(label.data=label.data||{},label.data.attrs=label.data.attrs||{},label.data.attrs.for=field._id)}children.push(_this.$slots[key])}),!children.length)){if(this.autoLabel){var label=findLabel(this.$slots.default);label&&(label.data=label.data||{},label.data.attrs=label.data.attrs||{},label.data.attrs.for=field._id)}children.push(this.$slots.default)}return h(this.tag,children)},props:{state:Object,name:String,show:{type:String,default:""},tag:{type:String,default:config.messagesTag},autoLabel:Boolean},data:function(){return{formstate:{}}},mounted:function(){var _this2=this;this.$nextTick(function(){_this2.formstate=_this2.state||_this2.$parent.formstate||_this2.$parent.state})},computed:{isShown:function(){var field=this.formstate[this.name];if(!this.show||!field)return!0;var compare=function(v){return field[v.trim()]};if(this.show.indexOf("&&")>-1){var split=this.show.split("&&");return split.every(compare)}if(this.show.indexOf("||")>-1){var _split=this.show.split("||");return _split.some(compare)}return field[this.show]}}},emailRegExp=/^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i,urlRegExp=/^(http\:\/\/|https\:\/\/)(.{4,})$/,validators={email:function(value,attrValue,vnode){return emailRegExp.test(value)},number:function(value){return!isNaN(value)},url:function(value){return urlRegExp.test(value)},required:function(value,attrValue,vnode){return attrValue===!1||(0===value||(Array.isArray(value)?!!value.length:!!value))},minlength:function(value,length){return value.length>=length},maxlength:function(value,length){return length>=value.length},pattern:function(value,_pattern){var patternRegExp=new RegExp("^"+_pattern+"$");return patternRegExp.test(value)},min:function(value,_min){return 1*value>=1*_min},max:function(value,_max){return 1*_max>=1*value}},vueForm={render:function(h){var _this=this;return h("form",{on:{submit:function(event){_this.state.$submitted=!0,_this.state._cloneState(),_this.$emit("submit",event)}},attrs:{novalidate:"",class:this.className}},[this.$slots.default])},props:{state:Object},data:function(){return{}},created:function(){var _this2=this,controls={},state=this.state,formstate={$dirty:!1,$pristine:!0,$valid:!0,$invalid:!1,$submitted:!1,$touched:!1,$untouched:!0,$pending:!1,$error:{},$submittedState:{},_id:"",_cloneState:function(){var cloned=JSON.parse(JSON.stringify(state));delete cloned.$submittedState,Object.keys(cloned).forEach(function(key){_this2.$set(_this2.state.$submittedState,key,cloned[key])})},_addControl:function(ctrl){controls[ctrl.$name]=ctrl,_this2.$set(state,ctrl.$name,ctrl)},_removeControl:function(ctrl){delete controls[ctrl.$name],_this2.$delete(_this2.state,ctrl.$name),_this2.$delete(_this2.state.$error,ctrl.$name)}};Object.keys(formstate).forEach(function(key){_this2.$set(_this2.state,key,formstate[key])}),this.$watch("state",function(){var isDirty=!1,isValid=!0,isTouched=!1,isPending=!1;Object.keys(controls).forEach(function(key){var control=controls[key];control.$submitted=state.$submitted,control.$dirty&&(isDirty=!0),control.$touched&&(isTouched=!0),control.$pending&&(isPending=!0),control.$valid?_this2.$delete(state.$error,control.$name):(isValid=!1,_this2.$set(state.$error,control.$name,control))}),state.$dirty=isDirty,state.$pristine=!isDirty,state.$touched=isTouched,state.$untouched=!isTouched,state.$valid=isValid,state.$invalid=!isValid,state.$pending=isPending},{deep:!0,immediate:!0})},computed:{className:function(){var out=[],c=config.classes.form;return this.state.$dirty?out.push(c.dirty):out.push(c.pristine),this.state.$valid?out.push(c.valid):out.push(c.invalid),this.state.$touched?out.push(c.touched):out.push(c.untouched),this.state.$submitted&&out.push(c.submitted),this.state.$pending&&out.push(c.pending),out.join(" ")}}},hyphenateRE=/([^-])([A-Z])/g,vueFormValidator={name:"vue-form-validator",bind:function(el,binding,vnode){var fieldstate=binding.value,attrs=vnode.data.attrs||{},inputName=getName(vnode);return inputName?(Object.keys(attrs).forEach(function(attr){var prop=void 0;prop="type"===attr?attrs[attr].toLowerCase():attr.toLowerCase(),validators[prop]&&!fieldstate._validators[prop]&&(fieldstate._validators[prop]=validators[prop])}),vnode.componentOptions&&vnode.componentOptions.propsData&&Object.keys(vnode.componentOptions.propsData).forEach(function(prop){validators[prop]&&!fieldstate._validators[prop]&&(fieldstate._validators[prop]=validators[prop])}),fieldstate._validate(vnode),el.addEventListener("blur",function(){fieldstate._setTouched()},!1),el.addEventListener("focus",function(){fieldstate._setFocused()},!1),void(vnode.componentInstance&&(vnode.componentInstance.$on("blur",function(){fieldstate._setTouched()}),vnode.componentInstance.$on("focus",function(){fieldstate._setFocused()})))):void console.warn("vue-form: name attribute missing")},update:function(el,binding,vnode,oldVNode){var changes=compareChanges(vnode,oldVNode),fieldstate=binding.value;changes&&(changes.vModel?(fieldstate._hasFocused&&fieldstate._setDirty(),fieldstate._validate(vnode)):fieldstate._validate(vnode))}},validate={render:function(h){var _this=this,foundVnodes=getVModelAndLabel(this.$slots.default),vModelnodes=foundVnodes.vModel,attrs={for:null};if(vModelnodes.length){if(this.name=getName(vModelnodes[0]),this.autoLabel){var id=this.fieldstate._id||vModelnodes[0].data.attrs.id||"vf"+randomId();this.fieldstate._id=id,vModelnodes[0].data.attrs.id=id,foundVnodes.label?(foundVnodes.label.data=foundVnodes.label.data||{},foundVnodes.label.data.attrs=foundVnodes.label.data.attrs={},foundVnodes.label.data.attrs.for=id):"label"===this.tag&&(attrs.for=id)}vModelnodes.forEach(function(vnode){vnode.data.directives||(vnode.data.directives=[]),vnode.data.directives.push({name:"vue-form-validator",value:_this.fieldstate}),vnode.data.attrs["vue-form-validator"]=""})}else console.warn("Element with v-model not found");return h(this.tag,{class:this.className.join(" "),attrs:attrs},this.$slots.default)},props:{state:Object,custom:null,autoLabel:Boolean,tag:{type:String,default:"div"}},data:function(){return{name:"",formstate:{},fieldstate:{}}},computed:{className:function(){var out=[],c=config.classes.validate;return this.fieldstate.$dirty?out.push(c.dirty):out.push(c.pristine),this.fieldstate.$valid?out.push(c.valid):out.push(c.invalid),this.fieldstate.$touched?out.push(c.touched):out.push(c.untouched),this.fieldstate.$pending&&out.push(c.pending),Object.keys(this.fieldstate.$error).forEach(function(error){out.push(c.invalid+"-"+hyphenate(error))}),out},inputClassName:function(){var out=[],c=config.classes.input;return this.fieldstate.$dirty?out.push(c.dirty):out.push(c.pristine),this.fieldstate.$valid?out.push(c.valid):out.push(c.invalid),this.fieldstate.$touched?out.push(c.touched):out.push(c.untouched),this.fieldstate.$pending&&out.push(c.pending),Object.keys(this.fieldstate.$error).forEach(function(error){out.push(c.invalid+"-"+hyphenate(error))}),out}},mounted:function(){this.fieldstate.$name=this.name,this.formstate=this.state||this.$parent.state,this.formstate._addControl(this.fieldstate);var vModelEls=this.$el.querySelectorAll("[vue-form-validator]");this.$watch("inputClassName",function(value,oldValue){if(oldValue)for(var _loop=function(i){oldValue.forEach(function(v){return removeClass(vModelEls[i],v)})},i=0;i<vModelEls.length;i++)_loop(i);for(var _loop2=function(_i){value.forEach(function(v){return addClass(vModelEls[_i],v)})},_i=0;_i<vModelEls.length;_i++)_loop2(_i)},{deep:!0,immediate:!0})},created:function(){var _this3=this,vm=this,pendingValidators=[],_val=void 0;this.fieldstate={$name:"",$dirty:!1,$pristine:!0,$valid:!0,$invalid:!1,$touched:!1,$untouched:!0,$pending:!1,$submitted:!1,$error:{},_id:"",_setValidatorVadility:function(validator,isValid){isValid?vm.$delete(this.$error,validator):vm.$set(this.$error,validator,!0)},_setVadility:function(isValid){this.$valid=isValid,this.$invalid=!isValid},_setDirty:function(){this.$dirty=!0,this.$pristine=!1},_setPristine:function(){this.$dirty=!1,this.$pristine=!0},_setTouched:function(){this.$touched=!0,this.$untouched=!1},_setUntouched:function(){this.$touched=!1,this.$untouched=!0},_setFocused:function(){this._hasFocused=!0},_hasFocused:!1,_validators:{},_validate:function(vnode){var _this2=this;this.$pending=!0;var isValid=!0,emptyAndRequired=!1,value=vModelValue(vnode.data);_val=value;var pending={promises:[],names:[]};pendingValidators.push(pending);var attrs=vnode.data.attrs||{},propsData=vnode.componentOptions&&vnode.componentOptions.propsData?vnode.componentOptions.propsData:{};Object.keys(this._validators).forEach(function(validator){if((""===value||void 0===value||null===value)&&"required"!==validator)return _this2._setValidatorVadility(validator,!0),void(emptyAndRequired=!0);var attrValue="undefined"!=typeof attrs[validator]?attrs[validator]:propsData[validator],result=_this2._validators[validator](value,attrValue,vnode);"boolean"==typeof result?result?_this2._setValidatorVadility(validator,!0):(isValid=!1,_this2._setValidatorVadility(validator,!1)):(pending.promises.push(result),pending.names.push(validator))}),pending.promises.length?config.Promise.all(pending.promises).then(function(results){pending===pendingValidators[pendingValidators.length-1]&&(pendingValidators=[],results.forEach(function(result,i){var name=pending.names[i];result?_this2._setValidatorVadility(name,!0):(isValid=!1,_this2._setValidatorVadility(name,!1))}),_this2._setVadility(isValid),_this2.$pending=!1)}):(this._setVadility(isValid),this.$pending=!1)}},this.custom&&Object.keys(this.custom).forEach(function(prop){_this3.fieldstate._validators[prop]=_this3.custom[prop]})},destroyed:function(){this.formstate._removeControl(this.fieldstate)}},field={render:function(h){var foundVnodes=getVModelAndLabel(this.$slots.default),vModelnodes=foundVnodes.vModel,attrs={for:null};if(vModelnodes.length&&this.autoLabel){var id=vModelnodes[0].data.attrs.id||"vf"+randomId();vModelnodes[0].data.attrs.id=id,foundVnodes.label?(foundVnodes.label.data=foundVnodes.label.data||{},foundVnodes.label.data.attrs=foundVnodes.label.data.attrs={},foundVnodes.label.data.attrs.for=id):"label"===this.tag&&(attrs.for=id)}return h(this.tag,{attrs:attrs},this.$slots.default)},props:{tag:{type:String,default:config.fieldTag},autoLabel:{type:Boolean,default:!0}}},defineProperty=function(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj},main={install:function(Vue){Vue.component(config.formComponent,vueForm),Vue.component(config.validateComponent,validate),Vue.component(config.messagesComponent,messages),Vue.component(config.fieldComponent,field),Vue.directive("vue-form-validator",vueFormValidator)},config:config,addValidator:function(key,fn){validators[key]=fn},mixin:{components:(_components={},defineProperty(_components,config.formComponent,vueForm),defineProperty(_components,config.validateComponent,validate),defineProperty(_components,config.messagesComponent,messages),defineProperty(_components,config.fieldComponent,field),_components),directives:{vueFormValidator:vueFormValidator}}};return main}); |
{ | ||
"name": "vue-form", | ||
"version": "2.0.1", | ||
"version": "3.0.0", | ||
"description": "Form validation for Vue.js", | ||
@@ -5,0 +5,0 @@ "main": "dist/vue-form.js", |
@@ -25,4 +25,7 @@ # vue-form | ||
Once installed you have access to four components (`vue-form`, `validate`, `form-errors`, `form-error`) for managing form state, validating form fields and displaying validation error messages. | ||
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. | ||
Live examples | ||
* Configured to work with Bootstrap styles: https://jsfiddle.net/fergal_doyle/zfqt4yhq/ | ||
Example | ||
@@ -38,3 +41,6 @@ | ||
<form-error field="name" error="required">Name is a required field</form-error> | ||
<field-messages name="name"> | ||
<div>Success!</div> | ||
<div slot="required">Name is a required field</div> | ||
</field-messages> | ||
</validate> | ||
@@ -46,6 +52,6 @@ | ||
<form-errors field="email"> | ||
<field-messages name="email"> | ||
<div slot="required">Email is a required field</div> | ||
<div slot="email">Email is not valid</div> | ||
</form-errors> | ||
</field-messages> | ||
</validate> | ||
@@ -97,2 +103,5 @@ | ||
}, | ||
"$submittedState": { | ||
// each form sumbit, state is cloned into this object | ||
}, | ||
"name": { | ||
@@ -127,4 +136,4 @@ "$name": "name", | ||
### Displaying errors | ||
Display single errors with `form-error` or multiple errors with `form-errors`. | ||
### Displaying messages | ||
Display validation errors or success messages with `field-messages`. | ||
@@ -261,5 +270,5 @@ The `show` prop supports simple expressions which specifiy when erros should be displayed based on the current state of the field, e.g: `$dirty`, `$dirty && $touched`, `$dirty || $touched` | ||
Input wrappers (e.g. the tag the `validate` component renders) will also get state classes, but with the `container` prefix, e.g. | ||
Input wrappers (e.g. the tag the `validate` component renders) will also get state classes, but with the `field` prefix, e.g. | ||
``` | ||
vf-container-dirty, vf-container-pristine, vf-container-valid, vf-container-invalid | ||
vf-field-dirty, vf-field-pristine, vf-field-valid, vf-field-invalid | ||
``` | ||
@@ -280,15 +289,10 @@ | ||
* `tag` String which specifies what element tag should be rendered by the `validate` component, defaults to `span` | ||
* `auto-label`: Automatically set `for` and `id` attributes of label and input elements found inside the `validate` component | ||
#### form-error | ||
#### field-messages | ||
* `state` Optional way of passing in the form state. If omitted form state will be found in the $parent | ||
* `field` String which specifies the related field name | ||
* `error` String which specifies the error key which the error should be shown for | ||
* `tag` String, defaults to `span` | ||
* `show`: String, show error dependant on form field state e.g. `$dirty`, `$dirty && $touched` | ||
#### form-errors | ||
* `state` Optional way of passing in the form state. If omitted form state will be found in the $parent | ||
* `field` String which specifies the related field name | ||
* `name` String which specifies the related field name | ||
* `tag` String, defaults to `div` | ||
* `show`: String, show error dependant on form field state e.g. `$touched`, `$dirty || $touched` | ||
* `show`: String, show error dependant on form field state e.g. `$touched`, `$dirty || $touched`, '$touched || $submitted' | ||
* `auto-label`: Automatically set the `for` attribute of labels found inside the `field-messages` component | ||
@@ -301,18 +305,41 @@ ### Config | ||
formComponent: 'vueForm', | ||
errorComponent: 'formError', | ||
errorsComponent: 'formErrors', | ||
messagesComponent: 'fieldMessages', | ||
validateComponent: 'validate', | ||
errorTag: 'span', | ||
errorsTag: 'div', | ||
classPrefix: 'vf-', | ||
dirtyClass: 'dirty', | ||
pristineClass: 'pristine', | ||
validClass: 'valid', | ||
invalidClass: 'invalid', | ||
submittedClass: 'submitted', | ||
touchedClass: 'touched', | ||
untouchedClass: 'untouched', | ||
pendingClass: 'pending', | ||
fieldComponent: 'field', | ||
messagesTag: 'div', | ||
fieldTag: 'div', | ||
classes: { | ||
form: { | ||
dirty: 'vf-form-dirty', | ||
pristine: 'vf-form-pristine', | ||
valid: 'vf-form-valid', | ||
invalid: 'vf-form-invalid', | ||
touched: 'vf-form-touched', | ||
untouched: 'vf-form-untouched', | ||
submitted: 'vf-form-submitted', | ||
pending: 'vf-form-pending' | ||
}, | ||
validate: { | ||
dirty: 'vf-field-dirty', | ||
pristine: 'vf-field-pristine', | ||
valid: 'vf-field-valid', | ||
invalid: 'vf-field-invalid', | ||
touched: 'vf-field-touched', | ||
untouched: 'vf-field-untouched', | ||
submitted: 'vf-field-submitted', | ||
pending: 'vf-field-pending' | ||
}, | ||
input: { | ||
dirty: 'vf-dirty', | ||
pristine: 'vf-pristine', | ||
valid: 'vf-valid', | ||
invalid: 'vf-invalid', | ||
touched: 'vf-touched', | ||
untouched: 'vf-untouched', | ||
submitted: 'vf-submitted', | ||
pending: 'vf-pending' | ||
} | ||
}, | ||
Promise: window.Promise | ||
} | ||
``` |
import { config } from '../config'; | ||
import { getVModelNode, vModelValue, addClass, removeClass, getName, hyphenate } from '../util'; | ||
import { getVModelAndLabel, vModelValue, addClass, removeClass, getName, hyphenate, randomId } from '../util'; | ||
import { validators } from '../validators'; | ||
// todo: Make getVModelNode recursive | ||
export default { | ||
render(h) { | ||
let foundVnodes = getVModelNode(this.$slots.default); | ||
if (foundVnodes.length) { | ||
this.name = getName(foundVnodes[0]); | ||
foundVnodes.forEach((foundVnode) => { | ||
if (!foundVnode.data.directives) { | ||
foundVnode.data.directives = []; | ||
let foundVnodes = getVModelAndLabel(this.$slots.default); | ||
const vModelnodes = foundVnodes.vModel; | ||
const attrs = { | ||
for: null | ||
}; | ||
if (vModelnodes.length) { | ||
this.name = getName(vModelnodes[0]); | ||
if(this.autoLabel) { | ||
const id = this.fieldstate._id || vModelnodes[0].data.attrs.id || 'vf' + randomId(); | ||
this.fieldstate._id = id; | ||
vModelnodes[0].data.attrs.id = id; | ||
if(foundVnodes.label) { | ||
foundVnodes.label.data = foundVnodes.label.data || {}; | ||
foundVnodes.label.data.attrs = foundVnodes.label.data.attrs = {}; | ||
foundVnodes.label.data.attrs.for = id; | ||
} else if (this.tag === 'label') { | ||
attrs.for = id; | ||
} | ||
foundVnode.data.directives.push({ name: 'vue-form-validator', value: this.fieldstate }); | ||
foundVnode.data.attrs['vue-form-validator'] = ''; | ||
} | ||
vModelnodes.forEach((vnode) => { | ||
if (!vnode.data.directives) { | ||
vnode.data.directives = []; | ||
} | ||
vnode.data.directives.push({ name: 'vue-form-validator', value: this.fieldstate }); | ||
vnode.data.attrs['vue-form-validator'] = ''; | ||
}); | ||
@@ -22,3 +36,3 @@ } else { | ||
} | ||
return h(this.tag, { 'class': this.className.map(v => config.classPrefix + 'container-' + v) }, this.$slots.default); | ||
return h(this.tag, { 'class': this.className.join(' '), attrs }, this.$slots.default); | ||
}, | ||
@@ -28,5 +42,6 @@ props: { | ||
custom: null, | ||
autoLabel: Boolean, | ||
tag: { | ||
type: String, | ||
default: 'span' | ||
default: 'div' | ||
} | ||
@@ -44,25 +59,53 @@ }, | ||
const out = []; | ||
const c = config.classes.validate; | ||
if (this.fieldstate.$dirty) { | ||
out.push(config.dirtyClass); | ||
out.push(c.dirty); | ||
} else { | ||
out.push(config.pristineClass) | ||
out.push(c.pristine) | ||
} | ||
if (this.fieldstate.$valid) { | ||
out.push(config.validClass); | ||
out.push(c.valid); | ||
} else { | ||
out.push(config.invalidClass) | ||
out.push(c.invalid) | ||
} | ||
if (this.fieldstate.$touched) { | ||
out.push(config.touchedClass); | ||
out.push(c.touched); | ||
} else { | ||
out.push(config.untouchedClass) | ||
out.push(c.untouched) | ||
} | ||
if (this.fieldstate.$pending) { | ||
out.push(config.pendingClass); | ||
out.push(c.pending); | ||
} | ||
Object.keys(this.fieldstate.$error).forEach((error) => { | ||
out.push(config.invalidClass + '-' + hyphenate(error)); | ||
out.push(c.invalid + '-' + hyphenate(error)); | ||
}); | ||
return out; | ||
}, | ||
inputClassName() { | ||
const out = []; | ||
const c = config.classes.input; | ||
if (this.fieldstate.$dirty) { | ||
out.push(c.dirty); | ||
} else { | ||
out.push(c.pristine) | ||
} | ||
if (this.fieldstate.$valid) { | ||
out.push(c.valid); | ||
} else { | ||
out.push(c.invalid) | ||
} | ||
if (this.fieldstate.$touched) { | ||
out.push(c.touched); | ||
} else { | ||
out.push(c.untouched) | ||
} | ||
if (this.fieldstate.$pending) { | ||
out.push(c.pending); | ||
} | ||
Object.keys(this.fieldstate.$error).forEach((error) => { | ||
out.push(c.invalid + '-' + hyphenate(error)); | ||
}); | ||
return out; | ||
} | ||
@@ -78,10 +121,10 @@ }, | ||
// add classes to the input element | ||
this.$watch('className', (value, oldValue) => { | ||
this.$watch('inputClassName', (value, oldValue) => { | ||
if (oldValue) { | ||
for (let i = 0; i < vModelEls.length; i++) { | ||
oldValue.forEach(v => removeClass(vModelEls[i], config.classPrefix + v)); | ||
oldValue.forEach(v => removeClass(vModelEls[i], v)); | ||
} | ||
} | ||
for (let i = 0; i < vModelEls.length; i++) { | ||
value.forEach(v => addClass(vModelEls[i], config.classPrefix + v)); | ||
value.forEach(v => addClass(vModelEls[i], v)); | ||
}; | ||
@@ -107,3 +150,5 @@ }, { | ||
$pending: false, | ||
$submitted: false, | ||
$error: {}, | ||
_id: '', | ||
_setValidatorVadility(validator, isValid) { | ||
@@ -110,0 +155,0 @@ if (isValid) { |
@@ -11,2 +11,3 @@ import { config } from '../config'; | ||
this.state.$submitted = true; | ||
this.state._cloneState(); | ||
this.$emit('submit', event); | ||
@@ -41,2 +42,11 @@ } | ||
$error: {}, | ||
$submittedState: {}, | ||
_id: '', | ||
_cloneState: () => { | ||
const cloned = JSON.parse(JSON.stringify(state)); | ||
delete cloned.$submittedState; | ||
Object.keys(cloned).forEach((key) => { | ||
this.$set(this.state.$submittedState, key, cloned[key]); | ||
}); | ||
}, | ||
_addControl: (ctrl) => { | ||
@@ -64,2 +74,5 @@ controls[ctrl.$name] = ctrl; | ||
const control = controls[key]; | ||
control.$submitted = state.$submitted; | ||
if (control.$dirty) { | ||
@@ -105,26 +118,27 @@ isDirty = true; | ||
const out = []; | ||
const c = config.classes.form; | ||
if (this.state.$dirty) { | ||
out.push(config.dirtyClass); | ||
out.push(c.dirty); | ||
} else { | ||
out.push(config.pristineClass); | ||
out.push(c.pristine); | ||
} | ||
if (this.state.$valid) { | ||
out.push(config.validClass); | ||
out.push(c.valid); | ||
} else { | ||
out.push(config.invalidClass); | ||
out.push(c.invalid); | ||
} | ||
if (this.state.$touched) { | ||
out.push(config.touchedClass); | ||
out.push(c.touched); | ||
} else { | ||
out.push(config.untouchedClass); | ||
out.push(c.untouched); | ||
} | ||
if (this.state.$submitted) { | ||
out.push(config.submittedClass); | ||
out.push(c.submitted); | ||
} | ||
if (this.state.$pending) { | ||
out.push(config.pendingClass); | ||
out.push(c.pending); | ||
} | ||
return out.map(v => config.classPrefix + 'form-' + v).join(' '); | ||
return out.join(' '); | ||
} | ||
} | ||
} |
export const config = { | ||
formComponent: 'vueForm', | ||
errorComponent: 'formError', | ||
errorsComponent: 'formErrors', | ||
messagesComponent: 'fieldMessages', | ||
validateComponent: 'validate', | ||
errorTag: 'span', | ||
errorsTag: 'div', | ||
classPrefix: 'vf-', | ||
dirtyClass: 'dirty', | ||
pristineClass: 'pristine', | ||
validClass: 'valid', | ||
invalidClass: 'invalid', | ||
submittedClass: 'submitted', | ||
touchedClass: 'touched', | ||
untouchedClass: 'untouched', | ||
pendingClass: 'pending', | ||
fieldComponent: 'field', | ||
messagesTag: 'div', | ||
fieldTag: 'div', | ||
classes: { | ||
form: { | ||
dirty: 'vf-form-dirty', | ||
pristine: 'vf-form-pristine', | ||
valid: 'vf-form-valid', | ||
invalid: 'vf-form-invalid', | ||
touched: 'vf-form-touched', | ||
untouched: 'vf-form-untouched', | ||
submitted: 'vf-form-submitted', | ||
pending: 'vf-form-pending' | ||
}, | ||
validate: { | ||
dirty: 'vf-field-dirty', | ||
pristine: 'vf-field-pristine', | ||
valid: 'vf-field-valid', | ||
invalid: 'vf-field-invalid', | ||
touched: 'vf-field-touched', | ||
untouched: 'vf-field-untouched', | ||
submitted: 'vf-field-submitted', | ||
pending: 'vf-field-pending' | ||
}, | ||
input: { | ||
dirty: 'vf-dirty', | ||
pristine: 'vf-pristine', | ||
valid: 'vf-valid', | ||
invalid: 'vf-invalid', | ||
touched: 'vf-touched', | ||
untouched: 'vf-untouched', | ||
submitted: 'vf-submitted', | ||
pending: 'vf-pending' | ||
} | ||
}, | ||
Promise: window.Promise | ||
}; |
@@ -0,1 +1,2 @@ | ||
import { config } from '../config'; | ||
import { validators } from '../validators'; | ||
@@ -56,5 +57,5 @@ import { vModelValue, getName } from '../util'; | ||
if (attr === 'type') { | ||
prop = attrs[attr]; | ||
prop = attrs[attr].toLowerCase(); | ||
} else { | ||
prop = attr; | ||
prop = attr.toLowerCase(); | ||
} | ||
@@ -61,0 +62,0 @@ if (validators[prop] && !fieldstate._validators[prop]) { |
import { config } from './config'; | ||
import formErrors from './components/form-errors'; | ||
import formError from './components/form-error'; | ||
import messages from './components/messages'; | ||
import vueForm from './components/vue-form'; | ||
import vueFormValidator from './directives/vue-form-validator'; | ||
import validate from './components/validate'; | ||
import field from './components/field'; | ||
import { validators } from './validators'; | ||
@@ -13,4 +13,4 @@ | ||
Vue.component(config.validateComponent, validate); | ||
Vue.component(config.errorsComponent, formErrors); | ||
Vue.component(config.errorComponent, formError); | ||
Vue.component(config.messagesComponent, messages); | ||
Vue.component(config.fieldComponent, field); | ||
Vue.directive('vue-form-validator', vueFormValidator); | ||
@@ -24,6 +24,6 @@ }, | ||
components: { | ||
formErrors, | ||
formError, | ||
vueForm, | ||
validate | ||
[config.formComponent]: vueForm, | ||
[config.validateComponent]: validate, | ||
[config.messagesComponent]: messages, | ||
[config.fieldComponent]: field | ||
}, | ||
@@ -30,0 +30,0 @@ directives: { |
@@ -31,4 +31,7 @@ export function addClass(el, className) { | ||
export function getVModelNode(nodes) { | ||
const foundVnodes = []; | ||
export function getVModelAndLabel(nodes) { | ||
const foundVnodes = { | ||
vModel: [], | ||
label: null | ||
}; | ||
@@ -38,2 +41,5 @@ function traverse(nodes) { | ||
let node = nodes[i]; | ||
if(node.tag === 'label' && !foundVnodes.label) { | ||
foundVnodes.label = node; | ||
} | ||
if (node.data) { | ||
@@ -43,6 +49,6 @@ if (node.data.directives) { | ||
if (match.length) { | ||
foundVnodes.push(node); | ||
foundVnodes.vModel.push(node); | ||
} | ||
} else if (node.data.model) { | ||
foundVnodes.push(node); | ||
foundVnodes.vModel.push(node); | ||
} | ||
@@ -76,1 +82,6 @@ } | ||
} | ||
export function randomId() { | ||
return Math.random().toString(36).substr(2, 10); | ||
} |
@@ -12,2 +12,3 @@ describe('vue-form', function() { | ||
vm.model.multicheck = ['Jack']; | ||
vm.model.sample = 'aaa'; | ||
} | ||
@@ -29,6 +30,6 @@ | ||
<input v-model="model.b" name="b" required type="text" minlength="6" /> | ||
<form-errors field="b" show="$dirty && $touched"> | ||
<span id="error-message-b" slot="required">required error</span> | ||
</form-errors> | ||
<form-error field="b" error="required" show="$touched"><span id="error-message-b-2"></span></form-error> | ||
<field-messages name="b" show="$dirty && $touched"> | ||
<span id="message-b-ok">Field is OK</span> | ||
<span id="message-b" slot="required">required error</span> | ||
</field-messages> | ||
</validate> | ||
@@ -40,8 +41,6 @@ | ||
</validate> | ||
<form-errors field="c"> | ||
<span id="error-message" slot="required">required error</span> | ||
<field-messages name="c"> | ||
<span id="message" slot="required">required error</span> | ||
<span id="minlength-message" slot="minlength">minlength error</span> | ||
</form-errors> | ||
<form-error field="c" error="required"><span id="error-message2"></span></form-error> | ||
<form-error field="c" error="minlength"><span id="minlength-message2"></span></form-error> | ||
</field-messages> | ||
</div> | ||
@@ -87,2 +86,48 @@ | ||
<field-messages name="sampleA" auto-label> | ||
<label id="field-messages-auto-label-a" slot="required">Sample is required</label> | ||
</field-messages> | ||
<field-messages name="sampleA" tag="ul" auto-label> | ||
<li id="field-messages-auto-label-b" slot="required"><label>Sample is required</label></li> | ||
</field-messages> | ||
<validate auto-label id="auto-label-a"> | ||
<label>Sample</label> | ||
<input name="sampleA" type="text" required v-model="model.sample" /> | ||
</validate> | ||
<validate auto-label id="auto-label-b"> | ||
<label>Sample <input name="sample" type="text" v-model="model.sample" /></label> | ||
</validate> | ||
<validate auto-label id="auto-label-c"> | ||
<label>Sample</label> | ||
<input name="sample" type="text" v-model="model.sample" id="existing" /> | ||
</validate> | ||
<validate tag="label" auto-label id="auto-label-d"> | ||
<span>Sample</span> | ||
<input name="sample" type="text" v-model="model.sample" /> | ||
</validate> | ||
<field id="field-auto-label-a"> | ||
<label>Sample</label> | ||
<input name="sample" type="text" v-model="model.sample" /> | ||
</field> | ||
<field id="field-auto-label-b"> | ||
<label>Sample <input name="sample" type="text" v-model="model.sample" /></label> | ||
</field> | ||
<field id="field-auto-label-c"> | ||
<label>Sample</label> | ||
<input name="sample" type="text" v-model="model.sample" id="existing" /> | ||
</field> | ||
<field tag="label" id="field-auto-label-d"> | ||
<span>Sample</span> | ||
<input name="sample" type="text" v-model="model.sample" /> | ||
</field> | ||
<button id="submit" type="submit"></button> | ||
@@ -111,3 +156,4 @@ | ||
custom2: 'custom2', | ||
multicheck: [] | ||
multicheck: [], | ||
sample: '' | ||
} | ||
@@ -379,3 +425,3 @@ }, | ||
expect(vm.formstate.$untouched).toBe(true); | ||
expect(Object.keys(vm.formstate.$error).length).toBe(3); | ||
expect(Object.keys(vm.formstate.$error).length).toBe(4); | ||
@@ -450,30 +496,46 @@ // emulate user interaction | ||
it('should show the correct form errors', (done) => { | ||
it('should set tie labels and inputs together with auto-label', (done) => { | ||
vm.$nextTick(() => { | ||
expect(vm.$el.querySelector('#auto-label-a > label').getAttribute('for')).toBeDefined(); | ||
expect(vm.$el.querySelector('#auto-label-b > label').getAttribute('for')).toBeDefined(); | ||
expect(vm.$el.querySelector('#auto-label-c > label').getAttribute('for')).toBeDefined(); | ||
expect(vm.$el.querySelector('#auto-label-c > label').getAttribute('for')).toBe('existing'); | ||
expect(vm.$el.querySelector('#auto-label-d').getAttribute('for')).toBeDefined(); | ||
expect(vm.$el.querySelector('#field-auto-label-a > label').getAttribute('for')).toBeDefined(); | ||
expect(vm.$el.querySelector('#field-auto-label-b > label').getAttribute('for')).toBeDefined(); | ||
expect(vm.$el.querySelector('#field-auto-label-c > label').getAttribute('for')).toBeDefined(); | ||
expect(vm.$el.querySelector('#field-auto-label-c > label').getAttribute('for')).toBe('existing'); | ||
expect(vm.$el.querySelector('#field-auto-label-d').getAttribute('for')).toBeDefined(); | ||
expect(vm.$el.querySelector('#field-messages-auto-label-a').getAttribute('for')).toBeDefined(); | ||
expect(vm.$el.querySelector('#field-messages-auto-label-b > label').getAttribute('for')).toBeDefined(); | ||
done(); | ||
}); | ||
}); | ||
it('should show the correct field messages', (done) => { | ||
vm.$nextTick(() => { | ||
// field b | ||
expect(vm.$el.querySelector('#error-message-b')).toBe(null); | ||
expect(vm.$el.querySelector('#error-message-b-2')).toBe(null); | ||
expect(vm.$el.querySelector('#message-b')).toBeNull(); | ||
expect(vm.$el.querySelector('#message-b-ok')).toBeNull(); | ||
vm.model.b = '123'; | ||
// field c | ||
expect(vm.$el.querySelector('#error-message')).not.toBeNull(null); | ||
expect(vm.$el.querySelector('#minlength-message')).toBe(null); | ||
expect(vm.$el.querySelector('#error-message2')).not.toBeNull(null); | ||
expect(vm.$el.querySelector('#minlength-message2')).toBe(null); | ||
expect(vm.$el.querySelector('#message')).not.toBeNull(); | ||
expect(vm.$el.querySelector('#minlength-message')).toBeNull(); | ||
vm.model.c = '123'; | ||
vm.$nextTick(() => { | ||
// field b could still be null | ||
expect(vm.$el.querySelector('#error-message-b')).toBe(null); | ||
expect(vm.$el.querySelector('#error-message-b-2')).toBe(null); | ||
// field b sould still be null | ||
expect(vm.$el.querySelector('#message-b')).toBeNull(); | ||
vm.$el.querySelector('[name=b]').focus(); | ||
vm.$el.querySelector('[name=b]').blur(); | ||
vm.model.b = ''; | ||
vm.model.b = '123456'; | ||
// field c | ||
expect(vm.$el.querySelector('#error-message')).toBe(null); | ||
expect(vm.$el.querySelector('#minlength-message')).not.toBeNull(null); | ||
expect(vm.$el.querySelector('#error-message2')).toBe(null); | ||
expect(vm.$el.querySelector('#minlength-message2')).not.toBeNull(null); | ||
expect(vm.$el.querySelector('#message')).toBeNull(); | ||
expect(vm.$el.querySelector('#minlength-message')).not.toBeNull(); | ||
vm.model.c = '123456'; | ||
@@ -484,13 +546,17 @@ | ||
// field b | ||
expect(vm.$el.querySelector('#error-message-b')).not.toBeNull(null); | ||
expect(vm.$el.querySelector('#error-message-b-2')).not.toBeNull(null); | ||
expect(vm.$el.querySelector('#message-b-ok')).not.toBeNull(); | ||
expect(vm.$el.querySelector('#message-b')).toBeNull(); | ||
vm.model.b = ''; | ||
// field c | ||
expect(vm.$el.querySelector('#error-message')).toBe(null); | ||
expect(vm.$el.querySelector('#minlength-message')).toBe(null); | ||
expect(vm.$el.querySelector('#error-message2')).toBe(null); | ||
expect(vm.$el.querySelector('#minlength-message2')).toBe(null); | ||
expect(vm.$el.querySelector('#message')).toBeNull(); | ||
expect(vm.$el.querySelector('#minlength-message')).toBeNull(); | ||
vm.$nextTick(()=>{ | ||
// field b | ||
expect(vm.$el.querySelector('#message-b-ok')).toBeNull(); | ||
expect(vm.$el.querySelector('#message-b')).not.toBeNull(); | ||
done(); | ||
}); | ||
done(); | ||
}); | ||
@@ -497,0 +563,0 @@ }); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
118971
26
2320
338