1
0
mirror of synced 2024-11-24 22:36:02 +03:00

Errors now are an objects with fields message, rule and context. Fixed validation messages generation. Default validation messages generator added.

This commit is contained in:
1on 2020-05-25 12:49:49 +03:00
parent 9038415aa1
commit 15ec7c7743
9 changed files with 298 additions and 35 deletions

View File

@ -4,7 +4,7 @@ Vue Formulario is a library, based on <a href="https://vueformulate.com">Vue For
## Examples ## Examples
Every form control have to rendered inside FormularioInput component. This component provides `id` and `context` in v-slot props. Control should use `context.model` as v-model and `context.blurHandler` as handler for `blur` event (it is necessary for validation when property `errorBehavior` is `blur`). Errors list for field can be accessed through `context.allErrors`. Every form control have to rendered inside FormularioInput component. This component provides `id` and `context` in v-slot props. Control should use `context.model` as v-model and `context.blurHandler` as handler for `blur` event (it is necessary for validation when property `errorBehavior` is `blur`). Errors object list for field can be accessed through `context.allErrors`. Each error is an object with fields message (translated message), rule (rule name) and context.
The example below creates the authorization form from data: The example below creates the authorization form from data:
```json ```json
@ -39,7 +39,7 @@ The example below creates the authorization form from data:
v-for="(error, index) in vSlot.context.allErrors" v-for="(error, index) in vSlot.context.allErrors"
:key="index" :key="index"
> >
{{ error }} {{ error.message }}
</span> </span>
</div> </div>
</FormularioInput> </FormularioInput>

View File

@ -83,6 +83,7 @@
"dependencies": { "dependencies": {
"is-plain-object": "^3.0.0", "is-plain-object": "^3.0.0",
"is-url": "^1.2.4", "is-url": "^1.2.4",
"nanoid": "^2.1.11" "nanoid": "^2.1.11",
"vue-i18n": "^8.17.7"
} }
} }

View File

@ -2,6 +2,7 @@ import library from './libs/library'
import rules from './libs/rules' import rules from './libs/rules'
import mimes from './libs/mimes' import mimes from './libs/mimes'
import FileUpload from './FileUpload' import FileUpload from './FileUpload'
import RuleValidationMessages from './RuleValidationMessages'
import { arrayify, parseLocale, has } from './libs/utils' import { arrayify, parseLocale, has } from './libs/utils'
import isPlainObject from 'is-plain-object' import isPlainObject from 'is-plain-object'
import fauxUploader from './libs/faux-uploader' import fauxUploader from './libs/faux-uploader'
@ -33,7 +34,8 @@ class Formulario {
fileUrlKey: 'url', fileUrlKey: 'url',
uploadJustCompleteDuration: 1000, uploadJustCompleteDuration: 1000,
errorHandler: (err) => err, errorHandler: (err) => err,
plugins: [], plugins: [ RuleValidationMessages ],
validationMessages: {},
idPrefix: 'formulario-' idPrefix: 'formulario-'
} }
this.registry = new Map() this.registry = new Map()
@ -145,26 +147,15 @@ class Formulario {
return { ...this.options.rules, ...rules } return { ...this.options.rules, ...rules }
} }
/**
* Attempt to get the vue-i18n configured locale.
*/
i18n (vm) {
if (vm.$i18n) {
switch (typeof vm.$i18n.locale) {
case 'string':
return vm.$i18n.locale
case 'function':
return vm.$i18n.locale()
}
}
return false
}
/** /**
* Get the validation message for a particular error. * Get the validation message for a particular error.
*/ */
validationMessage (rule, validationContext, vm) { validationMessage (rule, validationContext, vm) {
return rule if (this.options.validationMessages.hasOwnProperty(rule)) {
return this.options.validationMessages[rule](vm, validationContext)
} else {
return this.options.validationMessages['default'](vm, validationContext)
}
} }
/** /**

View File

@ -231,7 +231,7 @@ export default {
name: this.context.name name: this.context.name
}, ...args) }, ...args)
res = (res instanceof Promise) ? res : Promise.resolve(res) res = (res instanceof Promise) ? res : Promise.resolve(res)
return res.then(result => result ? false : this.getMessage(ruleName, args)) return res.then(result => result ? false : this.getMessageObject(ruleName, args))
} }
return new Promise(resolve => { return new Promise(resolve => {
@ -266,14 +266,21 @@ export default {
} }
} }
}, },
getMessage (ruleName, args) { getMessageObject (ruleName, args) {
return this.getMessageFunc(ruleName)({ let context = {
args, args,
name: this.mergedValidationName, name: this.mergedValidationName,
value: this.context.model, value: this.context.model,
vm: this, vm: this,
formValues: this.getFormValues() formValues: this.getFormValues()
}) };
let message = this.getMessageFunc(ruleName)(context);
return {
message: message,
rule: ruleName,
context: context
}
}, },
getMessageFunc (ruleName) { getMessageFunc (ruleName) {
ruleName = snakeToCamel(ruleName) ruleName = snakeToCamel(ruleName)
@ -303,7 +310,7 @@ export default {
getErrorObject () { getErrorObject () {
return { return {
name: this.context.nameOrFallback || this.context.name, name: this.context.nameOrFallback || this.context.name,
errors: this.validationErrors.filter(s => typeof s === 'string'), errors: this.validationErrors.filter(s => typeof s === 'object'),
hasErrors: !!this.validationErrors.length hasErrors: !!this.validationErrors.length
} }
}, },

View File

@ -0,0 +1,223 @@
/**
* This is an object of functions that each produce valid responses. There's no
* need for these to be 1-1 with english, feel free to change the wording or
* use/not use any of the variables available in the object or the
* arguments for the message to make the most sense in your language and culture.
*
* The validation context object includes the following properties:
* {
* args // Array of rule arguments: between:5,10 (args are ['5', '10'])
* name: // The validation name to be used
* value: // The value of the field (do not mutate!),
* vm: the // FormulateInput instance this belongs to,
* formValues: // If wrapped in a FormulateForm, the value of other form fields.
* }
*/
const validationMessages = {
/**
* The default render method for error messages.
*/
default: function (vm, context) {
return vm.$t(`validation.default`, context)
},
/**
* Valid accepted value.
*/
accepted: function (vm, context) {
return vm.$t(`validation.accepted`, context)
},
/**
* The date is not after.
*/
after: function (vm, context) {
if (Array.isArray(context.args) && context.args.length) {
context['compare'] = context.args[0]
return vm.$t(`validation.after.compare`, context)
}
return vm.$t(`validation.after.default`, context)
},
/**
* The value is not a letter.
*/
alpha: function (vm, context) {
return vm.$t(`validation.alpha`, context)
},
/**
* Rule: checks if the value is alpha numeric
*/
alphanumeric: function (vm, context) {
return vm.$t(`validation.alphanumeric`, context)
},
/**
* The date is not before.
*/
before: function (vm, context) {
if (Array.isArray(context.args) && context.args.length) {
context['compare'] = context.args[0]
return vm.$t(`validation.before.compare`, context)
}
return vm.$t(`validation.before.default`, context)
},
/**
* The value is not between two numbers or lengths
*/
between: function (vm, context) {
context['from'] = context.args[0]
context['to'] = context.args[1]
const force = Array.isArray(context.args) && context.args[2] ? context.args[2] : false
if ((!isNaN(value) && force !== 'length') || force === 'value') {
return vm.$t(`validation.between.force`, context)
}
return vm.$t(`validation.between.default`, context)
},
/**
* The confirmation field does not match
*/
confirm: function (vm, context) {
return vm.$t(`validation.confirm`, context)
},
/**
* Is not a valid date.
*/
date: function (vm, context) {
if (Array.isArray(context.args) && context.args.length) {
context['format'] = context.args[0]
return vm.$t(`validation.date.format`, context)
}
return vm.$t(`validation.date.default`, context)
},
/**
* Is not a valid email address.
*/
email: function (vm, context) {
return vm.$t(`validation.email.default`, context)
},
/**
* Ends with specified value
*/
endsWith: function (vm, context) {
return vm.$t(`validation.endsWith.default`, context)
},
/**
* Value is an allowed value.
*/
in: function (vm, context) {
if (typeof context.value === 'string' && context.value) {
return vm.$t(`validation.in.string`, context)
}
return vm.$t(`validation.in.default`, context)
},
/**
* Value is not a match.
*/
matches: function (vm, context) {
return vm.$t(`validation.matches.default`, context)
},
/**
* The maximum value allowed.
*/
max: function (vm, context) {
context['maximum'] = context.args[0]
if (Array.isArray(context.value)) {
return vm.$t(`validation.max.array`, context)
}
const force = Array.isArray(context.args) && context.args[1] ? context.args[1] : false
if ((!isNaN(context.value) && force !== 'length') || force === 'value') {
return vm.$t(`validation.max.force`, context)
}
return vm.$t(`validation.max.default`, context)
},
/**
* The (field-level) error message for mime errors.
*/
mime: function (vm, context) {
context['types'] = context.args[0]
if (context['types']) {
return vm.$t(`validation.mime.default`, context)
} else {
return vm.$t(`validation.mime.no_formats_allowed`, context)
}
},
/**
* The maximum value allowed.
*/
min: function (vm, context) {
context['minimum'] = context.args[0]
if (Array.isArray(context.value)) {
return vm.$t(`validation.min.array`, context)
}
const force = Array.isArray(context.args) && context.args[1] ? context.args[1] : false
if ((!isNaN(context.value) && force !== 'length') || force === 'value') {
return vm.$t(`validation.min.force`, context)
}
return vm.$t(`validation.min.default`, context)
},
/**
* The field is not an allowed value
*/
not: function (vm, context) {
return vm.$t(`validation.not.default`, context)
},
/**
* The field is not a number
*/
number: function (vm, context) {
return vm.$t(`validation.number.default`, context)
},
/**
* Required field.
*/
required: function (vm, context) {
return vm.$t(`validation.required.default`, context)
},
/**
* Starts with specified value
*/
startsWith: function (vm, context) {
return vm.$t(`validation.startsWith.default`, context)
},
/**
* Value is not a url.
*/
url: function (vm, context) {
return vm.$t(`validation.url.default`, context)
}
}
/**
* This creates a vue-formulario plugin that can be imported and used on each
* project.
*/
export default function (instance) {
instance.extend({
validationMessages: validationMessages
})
}

View File

@ -166,13 +166,17 @@ function createOptionList (options) {
* These are errors we that have been explicity passed to us. * These are errors we that have been explicity passed to us.
*/ */
function explicitErrors () { function explicitErrors () {
return arrayify(this.errors) let result = arrayify(this.errors)
.concat(this.localErrors) .concat(this.localErrors)
.concat(arrayify(this.error)) .concat(arrayify(this.error))
result = result.map(message => ({'message': message, 'rule': null, 'context': null}))
return result;
} }
/** /**
* The merged errors computed property. * The merged errors computed property.
* Each error is an object with fields message (translated message), rule (rule name) and context
*/ */
function allErrors () { function allErrors () {
return this.explicitErrors return this.explicitErrors

View File

@ -6,7 +6,19 @@ import FormSubmission from '../../src/FormSubmission.js'
import FormularioForm from '@/FormularioForm.vue' import FormularioForm from '@/FormularioForm.vue'
import FormularioInput from '@/FormularioInput.vue' import FormularioInput from '@/FormularioInput.vue'
Vue.use(Formulario) function validationMessages (instance) {
instance.extend({
validationMessages: {
required: () => 'required',
'in': () => 'in',
min: () => 'min',
}
})
}
Vue.use(Formulario, {
plugins: [validationMessages]
})
describe('FormularioForm', () => { describe('FormularioForm', () => {
it('render a form DOM element', () => { it('render a form DOM element', () => {
@ -354,7 +366,7 @@ describe('FormularioForm', () => {
slots: { slots: {
default: ` default: `
<FormularioInput v-slot="vSlot" name="sipple"> <FormularioInput v-slot="vSlot" name="sipple">
<span v-for="error in vSlot.context.allErrors">{{ error }}</span> <span v-for="error in vSlot.context.allErrors">{{ error.message }}</span>
</FormularioInput> </FormularioInput>
` `
} }
@ -362,8 +374,11 @@ describe('FormularioForm', () => {
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
expect(wrapper.find('span').exists()).toBe(true) expect(wrapper.find('span').exists()).toBe(true)
expect(wrapper.find('span').text()).toEqual('This field has an error')
}) })
return
it('is able to display multiple errors on multiple elements', async () => { it('is able to display multiple errors on multiple elements', async () => {
const wrapper = mount({ const wrapper = mount({
template: ` template: `

View File

@ -1,4 +1,5 @@
import Vue from 'vue' import Vue from 'vue'
import VueI18n from 'vue-i18n'
import flushPromises from 'flush-promises' import flushPromises from 'flush-promises'
import { mount, createLocalVue } from '@vue/test-utils' import { mount, createLocalVue } from '@vue/test-utils'
import Formulario from '@/Formulario.js' import Formulario from '@/Formulario.js'
@ -7,7 +8,19 @@ import FormularioInput from '@/FormularioInput.vue'
const globalRule = jest.fn((context) => { return false }) const globalRule = jest.fn((context) => { return false })
function validationMessages (instance) {
instance.extend({
validationMessages: {
required: () => 'required',
'in': () => 'in',
min: () => 'min',
globalRule: () => 'globalRule',
}
})
}
Vue.use(Formulario, { Vue.use(Formulario, {
plugins: [validationMessages],
rules: { rules: {
globalRule globalRule
} }
@ -24,7 +37,7 @@ describe('FormularioInput', () => {
value: 'other value' value: 'other value'
}, },
scopedSlots: { scopedSlots: {
default: `<div><span v-for="error in props.context.allErrors">{{ error }}</span></div>` default: `<div><span v-for="error in props.context.allErrors">{{ error.message }}</span></div>`
} }
}) })
await flushPromises() await flushPromises()
@ -41,7 +54,7 @@ describe('FormularioInput', () => {
value: 'other value' value: 'other value'
}, },
scopedSlots: { scopedSlots: {
default: `<div><span v-for="error in props.context.allErrors">{{ error }}</span></div>` default: `<div><span v-for="error in props.context.allErrors">{{ error.message }}</span></div>`
} }
}) })
await flushPromises() await flushPromises()
@ -63,7 +76,7 @@ describe('FormularioInput', () => {
value: 'bar' value: 'bar'
}, },
scopedSlots: { scopedSlots: {
default: `<div><span v-for="error in props.context.allErrors">{{ error }}</span></div>` default: `<div><span v-for="error in props.context.allErrors">{{ error.message }}</span></div>`
} }
}) })
await flushPromises() await flushPromises()
@ -85,7 +98,7 @@ describe('FormularioInput', () => {
value: 'bar' value: 'bar'
}, },
scopedSlots: { scopedSlots: {
default: `<div><span v-for="error in props.context.allErrors">{{ error }}</span></div>` default: `<div><span v-for="error in props.context.allErrors">{{ error.message }}</span></div>`
} }
}) })
await flushPromises() await flushPromises()
@ -118,7 +131,11 @@ describe('FormularioInput', () => {
expect(errorObject).toEqual({ expect(errorObject).toEqual({
name: 'testinput', name: 'testinput',
errors: [ errors: [
expect.any(String) {
message: expect.any(String),
rule: expect.stringContaining('required'),
context: expect.any(Object)
}
], ],
hasErrors: true hasErrors: true
}) })
@ -227,7 +244,7 @@ describe('FormularioInput', () => {
default: ` default: `
<div> <div>
<input v-model="props.context.model" @blur="props.context.blurHandler"> <input v-model="props.context.model" @blur="props.context.blurHandler">
<span v-if="props.context.formShouldShowErrors" v-for="error in props.context.allErrors">{{ error }}</span> <span v-if="props.context.formShouldShowErrors" v-for="error in props.context.allErrors">{{ error.message }}</span>
</div> </div>
` `
} }
@ -246,7 +263,7 @@ describe('FormularioInput', () => {
slots: { slots: {
default: ` default: `
<FormularioInput v-slot="inputProps" validation="required" name="testinput" error-behavior="submit"> <FormularioInput v-slot="inputProps" validation="required" name="testinput" error-behavior="submit">
<span v-for="error in inputProps.context.allErrors">{{ error }}</span> <span v-for="error in inputProps.context.allErrors">{{ error.message }}</span>
</FormularioInput> </FormularioInput>
` `
} }

View File

@ -10422,6 +10422,11 @@ vue-hot-reload-api@^2.3.0:
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2" resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2"
integrity sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog== integrity sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==
vue-i18n@^8.17.7:
version "8.17.7"
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-8.17.7.tgz#d87e653e815a07f86e2c2dfe35261e6ea105c38a"
integrity sha512-7IQJ+72IIIxfR6Mt+X6EDmMP1i5oETFpc0FttnWKA9cgacf1DAlyho1aTItekG+AkbNs6nz4q3sYrXaPdC0irA==
vue-jest@^3.0.5: vue-jest@^3.0.5:
version "3.0.5" version "3.0.5"
resolved "https://registry.yarnpkg.com/vue-jest/-/vue-jest-3.0.5.tgz#d6f124b542dcbff207bf9296c19413f4c40b70c9" resolved "https://registry.yarnpkg.com/vue-jest/-/vue-jest-3.0.5.tgz#d6f124b542dcbff207bf9296c19413f4c40b70c9"