Comparing version 0.3.1 to 2.0.0
@@ -18,4 +18,6 @@ // Karma configuration | ||
files: [ | ||
'node_modules/vue/dist/vue.min.js', | ||
'vue-form.js', | ||
'https://www.promisejs.org/polyfills/promise-6.1.0.js', | ||
//'https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.js', | ||
'node_modules/vue/dist/vue.js', | ||
'dist/vue-form.js', | ||
'test/specs/*.js' | ||
@@ -26,4 +28,3 @@ ], | ||
// list of files to exclude | ||
exclude: [ | ||
], | ||
exclude: [], | ||
@@ -48,3 +49,3 @@ | ||
}, | ||
// test results reporter to use | ||
@@ -66,5 +67,9 @@ // possible values: 'dots', 'progress' | ||
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG | ||
logLevel: config.LOG_INFO, | ||
logLevel: config.LOG_DEBUG, | ||
client: { | ||
captureConsole: true | ||
}, | ||
// enable / disable watching file and executing tests whenever any file changes | ||
@@ -71,0 +76,0 @@ autoWatch: true, |
{ | ||
"name": "vue-form", | ||
"version": "0.3.1", | ||
"version": "2.0.0", | ||
"description": "Form validation for Vue.js", | ||
"main": "vue-form.js", | ||
"main": "dist/vue-form.js", | ||
"scripts": { | ||
"dev": "rollup -w -c", | ||
"test": "karma start", | ||
"dist": "uglifyjs --compress -- vue-form.js > vue-form.min.js" | ||
"dist": "rollup -c && uglifyjs --compress -- dist/vue-form.js > dist/vue-form.min.js" | ||
}, | ||
@@ -13,12 +14,18 @@ "author": "Fergal Doyle", | ||
"devDependencies": { | ||
"jasmine-core": "^2.3.4", | ||
"karma": "^0.13.11", | ||
"karma-babel-preprocessor": "^5.2.2", | ||
"babel-core": "^6.14.0", | ||
"babel-plugin-external-helpers": "^6.8.0", | ||
"babel-preset-es2015": "^6.14.0", | ||
"jasmine-core": "^2.5.2", | ||
"karma": "^1.5.0", | ||
"karma-babel-preprocessor": "^6.0.1", | ||
"karma-firefox-launcher": "^0.1.7", | ||
"karma-jasmine": "^0.3.6", | ||
"karma-phantomjs-launcher": "^0.2.1", | ||
"phantomjs": "^1.9.18", | ||
"uglify-js": "^2.5.0", | ||
"vue": "^1.0.21" | ||
"karma-jasmine": "^1.0.2", | ||
"karma-phantomjs-launcher": "^1.0.2", | ||
"phantomjs": "^2.1.7", | ||
"rollup": "^0.41.4", | ||
"rollup-plugin-babel": "^2.7.1", | ||
"rollup-watch": "^3.2.2", | ||
"uglify-js": "^2.7.3", | ||
"vue": "2.2.1" | ||
} | ||
} |
286
README.md
@@ -5,3 +5,3 @@ # vue-form | ||
Form validation for Vue.js 1.0+. Works along side `v-model` but can also be used on your custom form control components (tinymce, select2, tag-editor etc). | ||
Form validation for Vue.js 2.0+ | ||
@@ -13,38 +13,73 @@ ### Install | ||
``` js | ||
// es6: import * as vueForm from 'vue-form'; | ||
var vueForm = require('vue-form'); | ||
import vueForm from 'vue-form'; | ||
// install globally | ||
Vue.use(vueForm); | ||
// or use the mixin | ||
... | ||
mixins: [vueForm.mixin] | ||
... | ||
``` | ||
You can also directly include it with a `<script>` tag when you have Vue itself included globally. It will automatically install itself. | ||
### Usage | ||
This plugin registers two global directives, `v-form` and `v-form-ctrl`. Apply the `v-form` directive to a `form` element, and set the `name` attribute. This `name` will hold the overall form state object and is created on the current vm. | ||
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. | ||
Apply the `v-form-ctrl` directive to each of the form inputs. `v-form-ctrl` will watch `v-model` and validate on change. Use static or binding attributes to specify validators (`required`, `maxlength`, `type="email"`, `type="url"`, etc) | ||
Example | ||
```html | ||
<form v-form name="myform" @submit.prevent="onSubmit"> | ||
<div class="errors" v-if="myform.$submitted"> | ||
<p v-if="myform.name.$error.required">Name is required.</p> | ||
<p v-if="myform.email.$error.email">Email is not valid.</p> | ||
</div> | ||
<label> | ||
<span>Name *</span> | ||
<input v-model="model.name" v-form-ctrl required name="name" /> | ||
</label> | ||
<label> | ||
<span>Email</span> | ||
<input v-model="model.email" v-form-ctrl name="email" type="email" /> | ||
</label> | ||
<button type="submit">Submit</button> | ||
</form> | ||
<pre>{{ myform | json }}</pre> | ||
<div id="app"> | ||
<vue-form :state="formstate" @submit.prevent="onSubmit"> | ||
<validate tag="label"> | ||
<span>Name *</span> | ||
<input v-model="model.name" required name="name" /> | ||
<form-error field="name" error="required">Name is a required field</form-error> | ||
</validate> | ||
<validate tag="label"> | ||
<span>Email</span> | ||
<input v-model="model.email" name="email" type="email" required /> | ||
<form-errors field="email"> | ||
<div slot="required">Email is a required field</div> | ||
<div slot="email">Email is not valid</div> | ||
</form-errors> | ||
</validate> | ||
<button type="submit">Submit</button> | ||
</vue-form> | ||
<pre>{{ formstate }}</pre> | ||
</div> | ||
``` | ||
`myform` will be an object with the following properties: | ||
```js | ||
Vue.use(vueForm); | ||
new Vue({ | ||
el: '#app', | ||
data: { | ||
formstate: {}, | ||
model: { | ||
name: '', | ||
email: 'invalid-email' | ||
} | ||
}, | ||
methods: { | ||
onSubmit: function () { | ||
if(this.formstate.$invalid) { | ||
// alert user and exit early | ||
return; | ||
} | ||
// otherwise submit form | ||
} | ||
} | ||
}); | ||
``` | ||
The output of `formstate` will be: | ||
```js | ||
{ | ||
"$name": "myform", | ||
"$dirty": false, | ||
@@ -55,13 +90,7 @@ "$pristine": true, | ||
"$submitted": false, | ||
"$touched": false, | ||
"$untouched": true, | ||
"$pending": false, | ||
"$error": { | ||
"name": { | ||
"$name": "name", | ||
"$dirty": false, | ||
"$pristine": true, | ||
"$valid": false, | ||
"$invalid": true, | ||
"$error": { | ||
"required": true | ||
} | ||
} | ||
// fields with errors are copied into this object | ||
}, | ||
@@ -74,2 +103,5 @@ "name": { | ||
"$invalid": true, | ||
"$touched": false, | ||
"$untouched": true, | ||
"$pending": false, | ||
"$error": { | ||
@@ -83,5 +115,10 @@ "required": true | ||
"$pristine": true, | ||
"$valid": true, | ||
"$invalid": false, | ||
"$error": {} | ||
"$valid": false, | ||
"$invalid": true, | ||
"$touched": false, | ||
"$untouched": true, | ||
"$pending": false, | ||
"$error": { | ||
"email": true | ||
} | ||
} | ||
@@ -91,7 +128,20 @@ } | ||
### Validators | ||
### Displaying errors | ||
Display single errors with `form-error` or multiple errors with `form-errors`. | ||
#### Built in validators: | ||
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` | ||
```html | ||
<form-error field="fieldName" error="errorKey" show="$dirty">Error message</form-error> | ||
<form-errors field="fieldName" show="$dirty && $touched"> | ||
<div slot="errorKeyA">Error message A</div> | ||
<div slot="errorKeyB">Error message B</div> | ||
</form-errors> | ||
``` | ||
### Validators | ||
``` | ||
type="email" | ||
@@ -112,31 +162,40 @@ type="url" | ||
<!-- static validators --> | ||
<input type="email" name="email" v-model="model.email" v-form-ctrl required /> | ||
<input type="text" name="name" v-model="model.name" v-form-ctrl maxlength="25" minlength="5" /> | ||
<validate> | ||
<input type="email" name="email" v-model="model.email" required /> | ||
</validate> | ||
<validate> | ||
<input type="text" name="name" v-model="model.name" maxlength="25" minlength="5" /> | ||
</validate> | ||
<!-- bound validators --> | ||
<input type="email" name="email" v-model="model.email" v-form-ctrl :required="isRequired" /> | ||
<input type="text" name="name" v-model="model.name" v-form-ctrl :maxlength="maxLen" :minlength="minLen" /> | ||
<validate> | ||
<input type="email" name="email" v-model="model.email" :required="isRequired" /> | ||
</validate> | ||
<validate> | ||
<input type="text" name="name" v-model="model.name" :maxlength="maxLen" :minlength="minLen" /> | ||
</validate> | ||
``` | ||
#### State classes | ||
#### Custom validators | ||
You can register global and local custom validators. | ||
As form and input validation states change, state classes are added and removed | ||
Possible form classes: | ||
Global custom validator | ||
```js | ||
vueForm.addValidator('my-custom-validator', function (value, attrValue, vnode) { | ||
// return true to set input as $valid, false to set as $invalid | ||
return value === 'custom'; | ||
}); | ||
``` | ||
vf-dirty, vf-pristine, vf-valid, vf-invalid, vf-submitted | ||
``` | ||
Possible input classes: | ||
```html | ||
<validate> | ||
<input v-model="something" name="something" my-custom-validator /> | ||
</validate> | ||
``` | ||
vf-dirty, vf-pristine, vf-valid, vf-invalid | ||
// also for every validation error, a class will be added, e.g. | ||
vf-invalid-required, vf-invalid-minlength, vf-invalid-max, etc | ||
``` | ||
#### Custom validator: | ||
Local custom validator | ||
```html | ||
<input v-model="something" v-form-ctrl name="something" custom-validator="customValidator" /> | ||
<validate :custom="{customValidator: customValidator}"> | ||
<input v-model="something" name="something" /> | ||
</validate> | ||
``` | ||
@@ -155,16 +214,105 @@ | ||
#### Async validators: | ||
### Custom form control component | ||
Async validators are custom validators which return a Promise. `resolve()` `true` or `false` to set field vadility. | ||
```js | ||
// ... | ||
methods: { | ||
customValidator (value) { | ||
return new Promise((resolve, reject) => { | ||
setTimeout(() => { | ||
resolve(value === 'ajax'); | ||
}, 100); | ||
}); | ||
} | ||
} | ||
// ... | ||
``` | ||
You can also use `vue-form` on your own form components. Simply wrap your component with an element with `v-form-ctrl`, `name` and any validation attributes. Set `v-form-ctrl` to the same property you will be updating via two-way binding in your component. You can also get a hook into the internals of `v-form-ctrl` to mange control state. | ||
Async validator with debounce (example uses lodash debounce) | ||
```js | ||
methods: { | ||
debounced: _.debounce(function (value, resolve, reject) { | ||
fetch('https://httpbin.org/get').then(function(response){ | ||
resolve(response.isValid); | ||
}); | ||
}, 500), | ||
customValidator (value) { | ||
return new Promise((resolve, reject) => { | ||
this.debounced(value, resolve, reject); | ||
}); | ||
} | ||
} | ||
``` | ||
[See custom tinymce component validation example ](https://github.com/fergaldoyle/vue-form/tree/master/example) | ||
### State classes | ||
As form and input validation states change, state classes are added and removed | ||
```html | ||
<div> | ||
<span>Rich text *</span> | ||
<span v-form-ctrl="model.html" name="html" required> | ||
<tinymce id="inline-editor" :model.sync="model.html"></tinymce> | ||
</span> | ||
</div> | ||
Possible form classes: | ||
``` | ||
vf-form-dirty, vf-form-pristine, vf-form-valid, vf-form-invalid, vf-form-submitted | ||
``` | ||
Possible input classes: | ||
``` | ||
vf-dirty, vf-pristine, vf-valid, vf-invalid | ||
// also for every validation error, a class will be added, e.g. | ||
vf-invalid-required, vf-invalid-minlength, vf-invalid-max, etc | ||
``` | ||
Input wrappers (e.g. the tag the `validate` component renders) will also get state classes, but with the `container` prefix, e.g. | ||
``` | ||
vf-container-dirty, vf-container-pristine, vf-container-valid, vf-container-invalid | ||
``` | ||
### Custom components | ||
When writing custom form field components, e.g. `<my-checkbox v-model="foo"></my-checkbox>` 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. | ||
### Component props | ||
#### vue-form | ||
* `state` Object on which form state is set | ||
#### validate | ||
* `state` Optional way of passing in the form state. If omitted form state will be found in the $parent | ||
* `custom` Object containing one or many custom validators. `{validatorName: validatorFunction}` | ||
* `tag` String which specifies what element tag should be rendered by the `validate` component, defaults to `span` | ||
#### form-error | ||
* `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 | ||
* `tag` String, defaults to `div` | ||
* `show`: String, show error dependant on form field state e.g. `$touched`, `$dirty || $touched` | ||
### Config | ||
Set config options using `vueForm.config`, defaults: | ||
```js | ||
{ | ||
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 | ||
} | ||
``` |
@@ -1,34 +0,95 @@ | ||
describe('vue-form', function () { | ||
var vm; | ||
describe('vue-form', function() { | ||
let vm; | ||
beforeEach(function (done) { | ||
Vue.use(vueForm); | ||
console.log('Vue version', Vue.version); | ||
function setValid() { | ||
vm.model.b = '123456'; | ||
vm.model.c = '12346'; | ||
vm.model.multicheck = ['Jack']; | ||
} | ||
beforeEach(function(done) { | ||
const div = document.createElement('div'); | ||
document.body.appendChild(div); | ||
vm = new Vue({ | ||
el: 'body', | ||
replace: false, | ||
el: div, | ||
template: ` | ||
<form v-form name="myform"> | ||
<input v-model="model.a" v-form-ctrl name="a" required type="text" /> | ||
<input v-model="model.b" v-form-ctrl name="b" required type="text" /> | ||
<input v-model="model.c" v-form-ctrl name="c" type="text" /> | ||
<input v-model="model.d" v-form-ctrl name="d" :required="isRequired" type="text" /> | ||
<input v-model="model.e" v-form-ctrl name="e" :required="isRequired" type="text" /> | ||
<input v-model="model.f" v-form-ctrl name="f" type="email" /> | ||
<input v-model="model.g" v-form-ctrl name="g" type="number" /> | ||
<input v-model="model.h" v-form-ctrl name="h" type="text" minlength="6" /> | ||
<input v-model="model.i" v-form-ctrl name="i" type="text" maxlength="10" /> | ||
<input v-model="model.j" v-form-ctrl name="j" type="number" min="10" /> | ||
<input v-model="model.k" v-form-ctrl name="k" type="number" max="10" /> | ||
<input v-model="model.l" v-form-ctrl name="l" type="url" /> | ||
<input v-model="model.m" v-form-ctrl name="m" type="text" :pattern="'[A-Za-z]{3}'" /> | ||
<input v-model="model.n" v-form-ctrl name="n" type="email" required minlength="8" /> | ||
<input v-model="model.o" v-form-ctrl name="o" type="text" custom-validator="customValidator" /> | ||
<input type="checkbox" value="Jack" v-model="multicheck" v-form-ctrl required name="multicheck"/> | ||
<input type="checkbox" value="John" v-model="multicheck" v-form-ctrl required name="multicheck"/> | ||
<input type="checkbox" value="Mike" v-model="multicheck" v-form-ctrl required name="multicheck"/> | ||
<vue-form :state="formstate" @submit.prevent="onSubmit"> | ||
</form> | ||
<validate> | ||
<input v-model="model.a" name="a" required type="text" /> | ||
</validate> | ||
<validate :state="formstate"> | ||
<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> | ||
</validate> | ||
<div v-if="isCEnabled"> | ||
<validate> | ||
<input v-model="model.c" name="c" :required="isRequired" :minlength="minlength" type="text" /> | ||
</validate> | ||
<form-errors field="c"> | ||
<span id="error-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> | ||
</div> | ||
<validate> | ||
<input v-model="model.email" name="email" type="email" /> | ||
</validate> | ||
<validate> | ||
<input v-model="model.number" name="number" type="number" required /> | ||
</validate> | ||
<validate> | ||
<input v-model="model.url" name="url" type="url" /> | ||
</validate> | ||
<validate> | ||
<input v-model="model.length" name="length" type="text" minlength="4" maxlength="8" /> | ||
</validate> | ||
<validate> | ||
<input v-model="model.minmax" name="minmax" type="number" min="4" max="8" /> | ||
</validate> | ||
<validate> | ||
<input v-model="model.pattern" name="pattern" type="text" pattern="\\d\\d\\d\\d" /> | ||
</validate> | ||
<validate :custom="{ 'custom-key': customValidator }"> | ||
<input v-model="model.custom" name="custom" type="text" /> | ||
</validate> | ||
<validate v-if="asyncEnabled" :custom="{ customAsync: customValidatorAsync }"> | ||
<input v-model="model.custom2" name="custom2" type="text" /> | ||
</validate> | ||
<validate> | ||
<input type="checkbox" value="Jack" v-model="model.multicheck" required name="multicheck"/> | ||
<input type="checkbox" value="John" v-model="model.multicheck" required name="multicheck"/> | ||
<input type="checkbox" value="Mike" v-model="model.multicheck" required name="multicheck"/> | ||
</validate> | ||
<button id="submit" type="submit"></button> | ||
</vue-form> | ||
`, | ||
data: { | ||
hasSubmitted: false, | ||
formstate: {}, | ||
isRequired: true, | ||
minlength: 5, | ||
isCEnabled: true, | ||
asyncEnabled: false, | ||
model: { | ||
@@ -38,14 +99,10 @@ a: 'aaa', | ||
c: null, | ||
d: '', | ||
e: 'eee', | ||
f: 'foo.bar@com.com', | ||
g: '3', | ||
h: '12', | ||
i: '', | ||
j: 11, | ||
k: 5, | ||
l: 'non url', | ||
m: 'x', | ||
n: '', | ||
o: 'abc', | ||
email: 'joe.doe@foo.com', | ||
number: 1, | ||
url: 'https://foo.bar.com', | ||
length: '12345', | ||
minmax: 5, | ||
pattern: '1234', | ||
custom: 'custom', | ||
custom2: 'custom2', | ||
multicheck: [] | ||
@@ -55,4 +112,14 @@ } | ||
methods: { | ||
customValidator: function (value) { | ||
onSubmit() { | ||
this.hasSubmitted = true; | ||
}, | ||
customValidator(value) { | ||
return value === 'custom'; | ||
}, | ||
customValidatorAsync(value) { | ||
return new Promise((resolve, reject) => { | ||
setTimeout(() => { | ||
resolve(value === 'custom2'); | ||
}, 100); | ||
}); | ||
} | ||
@@ -64,3 +131,3 @@ } | ||
afterEach(function (done) { | ||
afterEach(function(done) { | ||
vm.$destroy(); | ||
@@ -70,291 +137,458 @@ Vue.nextTick(done); | ||
it('should create an object in the current vm', function () { | ||
expect(vm.myform).toBeDefined(); | ||
it('should create a form tag and listen to submit event', () => { | ||
expect(vm.$el.tagName).toBe('FORM'); | ||
vm.$el.querySelector('button[type=submit]').click(); | ||
expect(vm.hasSubmitted).toBe(true); | ||
}); | ||
it('should work on an element with no validation attributes', function () { | ||
expect(vm.myform.c).toBeDefined(); | ||
it('should populate formstate', () => { | ||
expect(vm.formstate.$valid).toBeDefined(); | ||
}); | ||
it('should validate against static attributes', function () { | ||
expect(vm.myform.a.$valid).toBe(true); | ||
expect(vm.myform.b.$invalid).toBe(true); | ||
it('should automatically find parent formstate and also work by passing state as a prop', () => { | ||
expect(vm.formstate.a).toBeDefined(true); | ||
expect(vm.formstate.b).toBeDefined(true); | ||
}); | ||
it('should validate against binding attributes', function () { | ||
expect(vm.myform.d.$invalid).toBe(true); | ||
expect(vm.myform.e.$valid).toBe(true); | ||
it('should validate required fields', (done) => { | ||
expect(vm.formstate.a.$valid).toBe(true); | ||
expect(vm.formstate.a.$error.required).toBeUndefined(); | ||
vm.model.a = ''; | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.a.$valid).toBe(false); | ||
expect(vm.formstate.a.$error.required).toBe(true); | ||
done(); | ||
}); | ||
}); | ||
it('should react to model changes', function (done) { | ||
expect(vm.myform.a.$valid).toBe(true); | ||
vm.model.a = ''; | ||
Vue.nextTick(function () { | ||
expect(vm.myform.a.$valid).toBe(false); | ||
Vue.nextTick(done); | ||
it('should not show other validators if required validator fails', (done) => { | ||
expect(vm.formstate.b.$error.required).toBe(true); | ||
expect(vm.formstate.b.$error.minlength).toBeUndefined(); | ||
vm.model.b = 'acb'; | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.b.$error.required).toBeUndefined(); | ||
expect(vm.formstate.b.$error.minlength).toBe(true); | ||
done(); | ||
}); | ||
}); | ||
it('should react to attribue binding changes', function (done) { | ||
expect(vm.myform.d.$valid).toBe(false); | ||
it('should react to bound validators', (done) => { | ||
expect(vm.formstate.c.$error.required).toBe(true); | ||
vm.isRequired = false; | ||
Vue.nextTick(function () { | ||
expect(vm.myform.d.$valid).toBe(true); | ||
Vue.nextTick(done); | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.c.$error.required).toBeUndefined(); | ||
vm.model.c = '1234'; | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.c.$error.minlength).toBe(true); | ||
vm.minlength = 2; | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.c.$error.minlength).toBeUndefined(); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
it('should validate [type=email]', function (done) { | ||
expect(vm.myform.f.$valid).toBe(true); | ||
vm.model.f = 'not a real email'; | ||
Vue.nextTick(function () { | ||
expect(vm.myform.f.$valid).toBe(false); | ||
Vue.nextTick(done); | ||
it('should validate [type=email]', (done) => { | ||
expect(vm.formstate.email.$valid).toBe(true); | ||
expect(vm.formstate.email.$error.email).toBeUndefined(); | ||
vm.model.email = 'not a real email'; | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.email.$valid).toBe(false); | ||
expect(vm.formstate.email.$error.email).toBe(true); | ||
done(); | ||
}); | ||
}); | ||
it('should validate [type=number]', function (done) { | ||
expect(vm.myform.g.$valid).toBe(true); | ||
vm.model.g = 'not a real email'; | ||
Vue.nextTick(function () { | ||
expect(vm.myform.g.$valid).toBe(false); | ||
Vue.nextTick(done); | ||
it('should validate [type=number]', (done) => { | ||
expect(vm.formstate.number.$valid).toBe(true); | ||
expect(vm.formstate.number.$error.number).toBeUndefined(); | ||
vm.model.number = 'a string'; | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.number.$valid).toBe(false); | ||
expect(vm.formstate.number.$error.number).toBe(true); | ||
done(); | ||
}); | ||
}); | ||
it('should validate [type=url]', function (done) { | ||
expect(vm.myform.l.$valid).toBe(false); | ||
vm.model.l = 'http://foo.bar/baz'; | ||
Vue.nextTick(function () { | ||
expect(vm.myform.l.$valid).toBe(true); | ||
Vue.nextTick(done); | ||
it('should validate required [type=number] === 0', (done) => { | ||
vm.model.number = 0; | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.number.$valid).toBe(true); | ||
expect(vm.formstate.number.$error.number).toBeUndefined(); | ||
expect(vm.formstate.number.$error.required).toBeUndefined(); | ||
done(); | ||
}); | ||
}); | ||
it('should validate [required]', function (done) { | ||
expect(vm.myform.a.$valid).toBe(true); | ||
vm.model.a = ''; | ||
Vue.nextTick(function () { | ||
expect(vm.myform.a.$valid).toBe(false); | ||
Vue.nextTick(done); | ||
it('should validate [type=url]', (done) => { | ||
expect(vm.formstate.url.$valid).toBe(true); | ||
expect(vm.formstate.url.$error.url).toBeUndefined(); | ||
vm.model.url = 'not a real url'; | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.url.$valid).toBe(false); | ||
expect(vm.formstate.url.$error.url).toBe(true); | ||
done(); | ||
}); | ||
}); | ||
it('should validate [minlength]', function (done) { | ||
expect(vm.myform.h.$valid).toBe(false); | ||
vm.model.h = '123456'; | ||
Vue.nextTick(function () { | ||
expect(vm.myform.h.$valid).toBe(true); | ||
Vue.nextTick(done); | ||
it('should validate [minlength]', (done) => { | ||
expect(vm.formstate.length.$valid).toBe(true); | ||
expect(vm.formstate.length.$error.minlength).toBeUndefined(); | ||
vm.model.length = '1'; | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.length.$valid).toBe(false); | ||
expect(vm.formstate.length.$error.minlength).toBe(true); | ||
done(); | ||
}); | ||
}); | ||
it('should validate [maxlength]', function (done) { | ||
expect(vm.myform.i.$valid).toBe(true); | ||
vm.model.i = '123456789100'; | ||
Vue.nextTick(function () { | ||
expect(vm.myform.i.$valid).toBe(false); | ||
Vue.nextTick(done); | ||
it('should validate [maxlength]', (done) => { | ||
expect(vm.formstate.length.$valid).toBe(true); | ||
expect(vm.formstate.length.$error.maxlength).toBeUndefined(); | ||
vm.model.length = '1234567890'; | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.length.$valid).toBe(false); | ||
expect(vm.formstate.length.$error.maxlength).toBe(true); | ||
done(); | ||
}); | ||
}); | ||
it('should validate [number][min]', function (done) { | ||
expect(vm.myform.j.$valid).toBe(true); | ||
vm.model.j = 9; | ||
Vue.nextTick(function () { | ||
expect(vm.myform.j.$valid).toBe(false); | ||
Vue.nextTick(done); | ||
it('should validate [number][min]', (done) => { | ||
expect(vm.formstate.minmax.$valid).toBe(true); | ||
expect(vm.formstate.minmax.$error.min).toBeUndefined(); | ||
vm.model.minmax = 1; | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.minmax.$valid).toBe(false); | ||
expect(vm.formstate.minmax.$error.min).toBe(true); | ||
done(); | ||
}); | ||
}); | ||
it('should validate [number][max]', function (done) { | ||
expect(vm.myform.k.$valid).toBe(true); | ||
vm.model.k = 15; | ||
Vue.nextTick(function () { | ||
expect(vm.myform.k.$valid).toBe(false); | ||
Vue.nextTick(done); | ||
it('should validate [number][max]', (done) => { | ||
expect(vm.formstate.minmax.$valid).toBe(true); | ||
expect(vm.formstate.minmax.$error.max).toBeUndefined(); | ||
vm.model.minmax = 100; | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.minmax.$valid).toBe(false); | ||
expect(vm.formstate.minmax.$error.max).toBe(true); | ||
done(); | ||
}); | ||
}); | ||
it('should validate [pattern]', function (done) { | ||
expect(vm.myform.m.$valid).toBe(false); | ||
vm.model.m = 'abc'; | ||
Vue.nextTick(function () { | ||
expect(vm.myform.m.$valid).toBe(true); | ||
Vue.nextTick(done); | ||
it('should validate [pattern]', (done) => { | ||
expect(vm.formstate.pattern.$valid).toBe(true); | ||
expect(vm.formstate.pattern.$error.pattern).toBeUndefined(); | ||
vm.model.pattern = 'not four numbers'; | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.pattern.$valid).toBe(false); | ||
expect(vm.formstate.pattern.$error.pattern).toBe(true); | ||
done(); | ||
}); | ||
}); | ||
it('should validate multiple validators', function (done) { | ||
expect(vm.myform.n.$valid).toBe(false); | ||
// pass required | ||
vm.model.n = 'abc'; | ||
Vue.nextTick(function () { | ||
// email will be invalid | ||
expect(vm.myform.n.$valid).toBe(false); | ||
// pass email | ||
vm.model.n = 'a@b.c'; | ||
Vue.nextTick(function () { | ||
// minlength will be invalid | ||
expect(vm.myform.n.$valid).toBe(false); | ||
// pass minlength | ||
vm.model.n = 'aa@bb.xxxx'; | ||
Vue.nextTick(function () { | ||
expect(vm.myform.n.$valid).toBe(true); | ||
Vue.nextTick(done); | ||
}); | ||
}); | ||
it('should validate custom validators', function(done) { | ||
expect(vm.formstate.custom.$valid).toBe(true); | ||
expect(vm.formstate.custom.$error['custom-key']).toBeUndefined(); | ||
vm.model.custom = 'custom invalid value'; | ||
vm.$nextTick(function() { | ||
expect(vm.formstate.custom.$valid).toBe(false); | ||
expect(vm.formstate.custom.$error['custom-key']).toBe(true); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
it('should validate custom-validator', function (done) { | ||
expect(vm.myform.o.$valid).toBe(false); | ||
vm.model.o = 'custom'; | ||
Vue.nextTick(function () { | ||
expect(vm.myform.o.$valid).toBe(true); | ||
Vue.nextTick(done); | ||
it('should validate async validators', function(done) { | ||
vm.asyncEnabled = true; | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.custom2.$pending).toBe(true); | ||
expect(vm.formstate.custom2.$valid).toBe(true); | ||
setTimeout(() => { | ||
expect(vm.formstate.custom2.$pending).toBe(false); | ||
expect(vm.formstate.custom2.$valid).toBe(true); | ||
vm.model.custom2 = 'foo'; | ||
setTimeout(() => { | ||
expect(vm.formstate.custom2.$pending).toBe(false); | ||
expect(vm.formstate.custom2.$valid).toBe(false); | ||
done(); | ||
}, 150); | ||
}, 150); | ||
}); | ||
}); | ||
it('should validate checkbox array', function (done) { | ||
expect(vm.myform.multicheck.$valid).toBe(false); | ||
it('should validate checkbox array', (done) => { | ||
expect(vm.formstate.multicheck.$valid).toBe(false); | ||
expect(vm.formstate.multicheck.$error.required).toBe(true); | ||
vm.$el.querySelector('[name=multicheck]').click(); | ||
Vue.nextTick(function () { | ||
expect(vm.myform.multicheck.$valid).toBe(true); | ||
Vue.nextTick(done); | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.multicheck.$valid).toBe(true); | ||
expect(vm.formstate.multicheck.$error.required).toBeUndefined(); | ||
done(); | ||
}); | ||
}); | ||
it('should set input dirty when changed', function (done) { | ||
expect(vm.myform.d.$dirty).toBe(false); | ||
vm.model.d = 'abc'; | ||
Vue.nextTick(function () { | ||
expect(vm.myform.d.$dirty).toBe(true); | ||
Vue.nextTick(done); | ||
it('should set $dirty when model changed by user', (done) => { | ||
expect(vm.formstate.a.$dirty).toBe(false); | ||
// non user change | ||
vm.model.a = 'abc'; | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.a.$dirty).toBe(false); | ||
// user interacted with field then changed text | ||
vm.$el.querySelector('[name=a]').focus(); | ||
vm.model.a = 'abcc'; | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.a._hasFocused).toBe(true); | ||
expect(vm.formstate.a.$dirty).toBe(true); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
it('should set form dirty when child changed', function (done) { | ||
expect(vm.myform.$dirty).toBe(false); | ||
vm.model.d = 'abc'; | ||
Vue.nextTick(function () { | ||
expect(vm.myform.$dirty).toBe(true); | ||
Vue.nextTick(done); | ||
it('should set $touched on blur', (done) => { | ||
expect(vm.formstate.a.$touched).toBe(false); | ||
vm.$el.querySelector('[name=a]').focus(); | ||
vm.$el.querySelector('[name=a]').blur(); | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.a.$touched).toBe(true); | ||
done(); | ||
}); | ||
}); | ||
it('should set form invalid when child is invald', function () { | ||
expect(vm.myform.$invalid).toBe(true); | ||
it('should set form properties when child properties change', (done) => { | ||
// starts off invalid | ||
expect(vm.formstate.$valid).toBe(false); | ||
expect(vm.formstate.$invalid).toBe(true); | ||
expect(vm.formstate.$dirty).toBe(false); | ||
expect(vm.formstate.$pristine).toBe(true); | ||
expect(vm.formstate.$touched).toBe(false); | ||
expect(vm.formstate.$untouched).toBe(true); | ||
expect(Object.keys(vm.formstate.$error).length).toBe(3); | ||
// emulate user interaction | ||
vm.$el.querySelector('[name=b]').focus(); | ||
vm.$el.querySelector('[name=b]').blur(); | ||
setValid(); | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.$valid).toBe(true); | ||
expect(vm.formstate.$invalid).toBe(false); | ||
expect(vm.formstate.$dirty).toBe(true); | ||
expect(vm.formstate.$pristine).toBe(false); | ||
expect(vm.formstate.$touched).toBe(true); | ||
expect(vm.formstate.$untouched).toBe(false); | ||
expect(Object.keys(vm.formstate.$error).length).toBe(0); | ||
done(); | ||
}); | ||
}); | ||
it('should add and remove state classes from inputs', function (done) { | ||
var classes = vm.$el.querySelector('[name=b]').className; | ||
expect(classes.indexOf('vf-pristine')).not.toBe(-1); | ||
expect(classes.indexOf('vf-invalid')).not.toBe(-1); | ||
expect(classes.indexOf('vf-invalid-required')).not.toBe(-1); | ||
vm.model.b = 'abc'; | ||
Vue.nextTick(function () { | ||
classes = vm.$el.querySelector('[name=b]').className; | ||
expect(classes.indexOf('vf-pristine')).toBe(-1); | ||
expect(classes.indexOf('vf-invalid')).toBe(-1); | ||
expect(classes.indexOf('vf-invalid-required')).toBe(-1); | ||
expect(classes.indexOf('vf-valid')).not.toBe(-1); | ||
Vue.nextTick(done); | ||
it('should add and remove state classes', (done) => { | ||
// starts off invalid, pristine and untouched | ||
expect(vm.$el.classList.contains('vf-form-pristine')).toBe(true); | ||
expect(vm.$el.classList.contains('vf-form-invalid')).toBe(true); | ||
expect(vm.$el.classList.contains('vf-form-untouched')).toBe(true); | ||
const input = vm.$el.querySelector('[name=b]'); | ||
expect(input.classList.contains('vf-pristine')).toBe(true); | ||
expect(input.classList.contains('vf-invalid')).toBe(true); | ||
expect(input.classList.contains('vf-untouched')).toBe(true); | ||
expect(input.classList.contains('vf-invalid-required')).toBe(true); | ||
// set valid and interacted | ||
input.focus(); | ||
input.blur(); | ||
setValid(); | ||
vm.$nextTick(() => { | ||
expect(vm.$el.classList.contains('vf-form-dirty')).toBe(true); | ||
expect(vm.$el.classList.contains('vf-form-valid')).toBe(true); | ||
expect(vm.$el.classList.contains('vf-form-touched')).toBe(true); | ||
expect(input.classList.contains('vf-dirty')).toBe(true); | ||
expect(input.classList.contains('vf-valid')).toBe(true); | ||
expect(input.classList.contains('vf-touched')).toBe(true); | ||
expect(input.classList.contains('vf-invalid-required')).toBe(false); | ||
done(); | ||
}); | ||
}); | ||
it('should add and remove state classes from form', function (done) { | ||
var classes = vm.$el.querySelector('[name="myform"]').className; | ||
expect(classes.indexOf('vf-invalid')).not.toBe(-1); | ||
expect(classes.indexOf('vf-pristine')).not.toBe(-1); | ||
vm.model.b = 'abc'; | ||
Vue.nextTick(function () { | ||
classes = vm.$el.querySelector('[name="myform"]').className; | ||
//expect(classes.indexOf('vf-pristine')).toBe(-1); | ||
//expect(classes.indexOf('vf-invalid')).toBe(-1); | ||
//expect(classes.indexOf('vf-valid')).not.toBe(-1); | ||
Vue.nextTick(done); | ||
}); | ||
it('should add and remove a field from overall state when inside v-if', (done) => { | ||
setValid(); | ||
vm.model.c = ''; | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.c).toBeDefined(); | ||
expect(vm.formstate.c.$invalid).toBe(true); | ||
expect(vm.formstate.$invalid).toBe(true); | ||
vm.isCEnabled = false; | ||
vm.$nextTick(() => { | ||
expect(vm.formstate.c).toBeUndefined(); | ||
expect(vm.formstate.$valid).toBe(true); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
it('should work with v-for scope', function (done) { | ||
vm.$destroy(); | ||
it('should show the correct form errors', (done) => { | ||
vm.$nextTick(() => { | ||
var vmx = new Vue({ | ||
el: 'body', | ||
replace: false, | ||
// field b | ||
expect(vm.$el.querySelector('#error-message-b')).toBe(null); | ||
expect(vm.$el.querySelector('#error-message-b-2')).toBe(null); | ||
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); | ||
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); | ||
vm.$el.querySelector('[name=b]').focus(); | ||
vm.$el.querySelector('[name=b]').blur(); | ||
vm.model.b = ''; | ||
// 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); | ||
vm.model.c = '123456'; | ||
vm.$nextTick(() => { | ||
// field b | ||
expect(vm.$el.querySelector('#error-message-b')).not.toBeNull(null); | ||
expect(vm.$el.querySelector('#error-message-b-2')).not.toBeNull(null); | ||
// 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); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
it('should work with v-for', function(done) { | ||
vm.$destroy(); | ||
const div = document.createElement('div'); | ||
document.body.appendChild(div); | ||
new Vue({ | ||
el: div, | ||
template: ` | ||
<form v-form name="myform"> | ||
<label v-for="input in inputs"> | ||
<label> {{input.label}} <br> | ||
<input v-form-ctrl type="text" :name="input.name" v-model="input.model" :required="input.required" /> | ||
</label> | ||
</label> | ||
</form> | ||
<vue-form :state="formstate"> | ||
<validate tag="label" v-for="input in inputs" :key="input.name"> | ||
{{input.label}} <br> | ||
<input type="text" :name="input.name" v-model="input.model" :required="input.required" /> | ||
</validate> | ||
</vue-form> | ||
`, | ||
data: { | ||
data: { | ||
inputs: [{ | ||
label: 'Input A', | ||
name: 'a', | ||
model: '', | ||
required: true | ||
label: 'Input A', | ||
name: 'a', | ||
model: '', | ||
required: true | ||
}, { | ||
label: 'Input B', | ||
name: 'b', | ||
model: '', | ||
required: false | ||
label: 'Input B', | ||
name: 'b', | ||
model: '', | ||
required: false | ||
}, { | ||
label: 'Input C', | ||
name: 'c', | ||
model: 'abc', | ||
required: true | ||
label: 'Input C', | ||
name: 'c', | ||
model: 'abc', | ||
required: true | ||
}], | ||
myform: {} | ||
formstate: {} | ||
}, | ||
ready: function () { | ||
setTimeout(function () { | ||
expect(vmx.myform.a.$valid).toBe(false); | ||
expect(vmx.myform.b.$valid).toBe(true); | ||
expect(vmx.myform.c.$valid).toBe(true); | ||
mounted: function() { | ||
this.$nextTick(() => { | ||
expect(this.formstate.a.$valid).toBe(false); | ||
expect(this.formstate.b.$valid).toBe(true); | ||
expect(this.formstate.c.$valid).toBe(true); | ||
done(); | ||
}, 100); | ||
}); | ||
} | ||
}); | ||
}); | ||
}); | ||
it('should work with v-bind object syntax', function (done) { | ||
vm.$destroy(); | ||
var vmx = new Vue({ | ||
el: 'body', | ||
replace: false, | ||
it('should work with components, even if some validation attributes are also component props', function(done) { | ||
vm.$destroy(); | ||
const div = document.createElement('div'); | ||
document.body.appendChild(div); | ||
new Vue({ | ||
el: div, | ||
components: { | ||
test: { | ||
props: ['value'], | ||
template: '<span></span>' | ||
}, | ||
test2: { | ||
props: ['value', 'required', 'name'], | ||
template: '<span><input type="text" id="input" /></span>', | ||
mounted () { | ||
this.$el.querySelector('input').addEventListener('focus', this.$emit('focus')); | ||
this.$el.querySelector('input').addEventListener('blur', this.$emit('blur')); | ||
} | ||
} | ||
}, | ||
template: ` | ||
<form v-form name="myform"> | ||
<input v-model="model.a" v-form-ctrl v-bind="{'name': 'a', required: true}" /> | ||
<input v-model="model.b" v-form-ctrl :="{'name': 'b', required: false}" /> | ||
</form> | ||
<vue-form :state="formstate"> | ||
<validate> | ||
<test name="test" v-model="model.test" required ></test> | ||
</validate> | ||
<validate> | ||
<test2 name="test2" v-model="model.test2" required ></test2> | ||
</validate> | ||
</vue-form> | ||
`, | ||
data: { | ||
data: { | ||
model: { | ||
b: 'xxx' | ||
test: '', | ||
test2: '' | ||
}, | ||
myform: {} | ||
formstate: {} | ||
}, | ||
ready: function () { | ||
this.$nextTick(function () { | ||
expect(vmx.myform.a.$valid).toBe(false); | ||
expect(vmx.myform.b.$valid).toBe(true); | ||
/*vmx.model.a = 'aaa'; | ||
vmx.model.b = 'aa'; | ||
this.$nextTick(function () { | ||
console.log(vmx.model); | ||
expect(vmx.myform.a.$valid).toBe(true); | ||
expect(vmx.myform.b.$valid).toBe(false); | ||
done(); | ||
});*/ | ||
mounted: function() { | ||
expect(this.formstate.test).toBeDefined(); | ||
expect(this.formstate.test.$error.required).toBe(true); | ||
expect(this.formstate.test2).toBeDefined(); | ||
expect(this.formstate.test2.$error.required).toBe(true); | ||
expect(this.formstate.test2.$dirty).toBe(false); | ||
this.$el.querySelector('#input').focus(); | ||
this.$el.querySelector('#input').blur(); | ||
this.model.test = 'xxx'; | ||
this.model.test2 = 'xxx'; | ||
this.$nextTick(() => { | ||
expect(this.formstate.test.$error.required).toBeUndefined(); | ||
expect(this.formstate.test.$dirty).toBe(false); | ||
expect(this.formstate.test2.$dirty).toBe(true); | ||
expect(this.formstate.test2.$touched).toBe(true); | ||
done(); | ||
@@ -365,4 +599,4 @@ }); | ||
}); | ||
}); | ||
}); |
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
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
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
121540
25
1997
1
311
15
1