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:
parent
9038415aa1
commit
15ec7c7743
@ -4,7 +4,7 @@ Vue Formulario is a library, based on <a href="https://vueformulate.com">Vue For
|
||||
|
||||
## 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:
|
||||
```json
|
||||
@ -39,7 +39,7 @@ The example below creates the authorization form from data:
|
||||
v-for="(error, index) in vSlot.context.allErrors"
|
||||
:key="index"
|
||||
>
|
||||
{{ error }}
|
||||
{{ error.message }}
|
||||
</span>
|
||||
</div>
|
||||
</FormularioInput>
|
||||
|
@ -83,6 +83,7 @@
|
||||
"dependencies": {
|
||||
"is-plain-object": "^3.0.0",
|
||||
"is-url": "^1.2.4",
|
||||
"nanoid": "^2.1.11"
|
||||
"nanoid": "^2.1.11",
|
||||
"vue-i18n": "^8.17.7"
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import library from './libs/library'
|
||||
import rules from './libs/rules'
|
||||
import mimes from './libs/mimes'
|
||||
import FileUpload from './FileUpload'
|
||||
import RuleValidationMessages from './RuleValidationMessages'
|
||||
import { arrayify, parseLocale, has } from './libs/utils'
|
||||
import isPlainObject from 'is-plain-object'
|
||||
import fauxUploader from './libs/faux-uploader'
|
||||
@ -33,7 +34,8 @@ class Formulario {
|
||||
fileUrlKey: 'url',
|
||||
uploadJustCompleteDuration: 1000,
|
||||
errorHandler: (err) => err,
|
||||
plugins: [],
|
||||
plugins: [ RuleValidationMessages ],
|
||||
validationMessages: {},
|
||||
idPrefix: 'formulario-'
|
||||
}
|
||||
this.registry = new Map()
|
||||
@ -145,26 +147,15 @@ class Formulario {
|
||||
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.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -231,7 +231,7 @@ export default {
|
||||
name: this.context.name
|
||||
}, ...args)
|
||||
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 => {
|
||||
@ -266,14 +266,21 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
getMessage (ruleName, args) {
|
||||
return this.getMessageFunc(ruleName)({
|
||||
getMessageObject (ruleName, args) {
|
||||
let context = {
|
||||
args,
|
||||
name: this.mergedValidationName,
|
||||
value: this.context.model,
|
||||
vm: this,
|
||||
formValues: this.getFormValues()
|
||||
})
|
||||
};
|
||||
let message = this.getMessageFunc(ruleName)(context);
|
||||
|
||||
return {
|
||||
message: message,
|
||||
rule: ruleName,
|
||||
context: context
|
||||
}
|
||||
},
|
||||
getMessageFunc (ruleName) {
|
||||
ruleName = snakeToCamel(ruleName)
|
||||
@ -303,7 +310,7 @@ export default {
|
||||
getErrorObject () {
|
||||
return {
|
||||
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
|
||||
}
|
||||
},
|
||||
|
223
src/RuleValidationMessages.js
Normal file
223
src/RuleValidationMessages.js
Normal 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
|
||||
})
|
||||
}
|
@ -166,13 +166,17 @@ function createOptionList (options) {
|
||||
* These are errors we that have been explicity passed to us.
|
||||
*/
|
||||
function explicitErrors () {
|
||||
return arrayify(this.errors)
|
||||
let result = arrayify(this.errors)
|
||||
.concat(this.localErrors)
|
||||
.concat(arrayify(this.error))
|
||||
result = result.map(message => ({'message': message, 'rule': null, 'context': null}))
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* The merged errors computed property.
|
||||
* Each error is an object with fields message (translated message), rule (rule name) and context
|
||||
*/
|
||||
function allErrors () {
|
||||
return this.explicitErrors
|
||||
|
@ -6,7 +6,19 @@ import FormSubmission from '../../src/FormSubmission.js'
|
||||
import FormularioForm from '@/FormularioForm.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', () => {
|
||||
it('render a form DOM element', () => {
|
||||
@ -354,7 +366,7 @@ describe('FormularioForm', () => {
|
||||
slots: {
|
||||
default: `
|
||||
<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>
|
||||
`
|
||||
}
|
||||
@ -362,8 +374,11 @@ describe('FormularioForm', () => {
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
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 () => {
|
||||
const wrapper = mount({
|
||||
template: `
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Vue from 'vue'
|
||||
import VueI18n from 'vue-i18n'
|
||||
import flushPromises from 'flush-promises'
|
||||
import { mount, createLocalVue } from '@vue/test-utils'
|
||||
import Formulario from '@/Formulario.js'
|
||||
@ -7,7 +8,19 @@ import FormularioInput from '@/FormularioInput.vue'
|
||||
|
||||
const globalRule = jest.fn((context) => { return false })
|
||||
|
||||
function validationMessages (instance) {
|
||||
instance.extend({
|
||||
validationMessages: {
|
||||
required: () => 'required',
|
||||
'in': () => 'in',
|
||||
min: () => 'min',
|
||||
globalRule: () => 'globalRule',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Vue.use(Formulario, {
|
||||
plugins: [validationMessages],
|
||||
rules: {
|
||||
globalRule
|
||||
}
|
||||
@ -24,7 +37,7 @@ describe('FormularioInput', () => {
|
||||
value: 'other value'
|
||||
},
|
||||
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()
|
||||
@ -41,7 +54,7 @@ describe('FormularioInput', () => {
|
||||
value: 'other value'
|
||||
},
|
||||
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()
|
||||
@ -63,7 +76,7 @@ describe('FormularioInput', () => {
|
||||
value: 'bar'
|
||||
},
|
||||
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()
|
||||
@ -85,7 +98,7 @@ describe('FormularioInput', () => {
|
||||
value: 'bar'
|
||||
},
|
||||
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()
|
||||
@ -118,7 +131,11 @@ describe('FormularioInput', () => {
|
||||
expect(errorObject).toEqual({
|
||||
name: 'testinput',
|
||||
errors: [
|
||||
expect.any(String)
|
||||
{
|
||||
message: expect.any(String),
|
||||
rule: expect.stringContaining('required'),
|
||||
context: expect.any(Object)
|
||||
}
|
||||
],
|
||||
hasErrors: true
|
||||
})
|
||||
@ -227,7 +244,7 @@ describe('FormularioInput', () => {
|
||||
default: `
|
||||
<div>
|
||||
<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>
|
||||
`
|
||||
}
|
||||
@ -246,7 +263,7 @@ describe('FormularioInput', () => {
|
||||
slots: {
|
||||
default: `
|
||||
<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>
|
||||
`
|
||||
}
|
||||
|
@ -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"
|
||||
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:
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/vue-jest/-/vue-jest-3.0.5.tgz#d6f124b542dcbff207bf9296c19413f4c40b70c9"
|
||||
|
Loading…
Reference in New Issue
Block a user