Skip to content

Commit

Permalink
feat: add ability to walk through nested $each declarations.
Browse files Browse the repository at this point in the history
  • Loading branch information
dobromir-hristov committed Jul 25, 2018
1 parent f71eafd commit 2821014
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 29 deletions.
4 changes: 2 additions & 2 deletions src/multi-error-extractor-mixin.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { flattenValidatorObjects } from './utils'
import { flattenValidatorObjects, get } from './utils'
import baseErrorsMixin from './base-errors-mixin'

export default {
Expand All @@ -21,7 +21,7 @@ export default {
errors () {
return flattenValidatorObjects(this.preferredValidator).map(error => {
const params = Object.assign({}, error.params, {
attribute: this.mergedAttributes[error.propName]
attribute: get(this.mergedAttributes, error.propName, error.propName)
})
return Object.assign({}, error, { params })
})
Expand Down
17 changes: 11 additions & 6 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,25 @@ export function getValidationObject (validationKey, key, params = {}) {
}
}

function getAttribute (attributes, attribute, label, name) {
function getAttribute (attributes, attribute, label, name = '') {
if (attribute) return attribute
if (attributes[name]) return attributes[name]
if (attributes[label]) return attributes[label]
return label
if (!name) return label
// strip out the $each
const normalizedName = name.replace(/\$each\.\d\./g, '')
return attributes[normalizedName] || normalizedName
}

export function flattenValidatorObjects (validator, propName) {
return Object.entries(validator)
.filter(([key, value]) => !key.startsWith('$'))
.filter(([key, value]) => !key.startsWith('$') || key === '$each')
.reduce((errors, [key, value]) => {
// its probably a deeply nested object
if (typeof value === 'object') {
return errors.concat(flattenValidatorObjects(value, propName ? `${propName}.${key}` : key))
const nestedValidatorName =
(key === '$each' || !isNaN(parseInt(key))) ? propName
: propName ? `${propName}.${key}`
: key
return errors.concat(flattenValidatorObjects(value, nestedValidatorName))
} // else its the validated prop
const params = Object.assign({}, validator.$params[key])
delete params.type
Expand Down
3 changes: 2 additions & 1 deletion test/dev/dev-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ Vue.use(vuelidateErrorExtractor, {
email: 'Email',
'address.street': 'Street',
'address.city': 'City',
'address.postal': 'Postal Code'
'address.postal': 'Postal Code',
'phones.model': 'Phone Model'
}
// validationKeys: configs.laravel
})
Expand Down
23 changes: 22 additions & 1 deletion test/dev/testForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@
v-model="nestedObject.address.postal"
@input="$v.nestedObject.address.postal.$touch()">
</form-group>
<form-group name="phones.$each.0.model" label="First Phone models">
<template slot-scope="{ attributes, events }">
<input
v-bind="attributes"
v-on="events"
type="text"
v-model="nestedObject.phones[0].model">
</template>
</form-group>
<button class="button" @click="$v.nestedObject.$touch()">Touch</button>
</form-wrapper>
</div>
Expand All @@ -61,7 +70,13 @@ export default {
street: '',
city: '',
postal: ''
}
},
phones: [
{
model: '',
brand: ''
}
]
},
attributesMap: {
first_name: 'First Name',
Expand Down Expand Up @@ -90,6 +105,12 @@ export default {
street: { required, minLength: minLength(5) },
city: { required, minLength: minLength(5) },
postal: { required }
},
phones: {
$each: {
model: { required },
brand: { minLength: minLength(8) }
}
}
}
}
Expand Down
78 changes: 59 additions & 19 deletions test/unit/single-error-extractor-mixin.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ function createWrapper (opts = {}) {
$vuelidateErrorExtractor: {
validationKeys: {},
i18n: 'validation',
attributes: {}
attributes: {
text: 'Text override'
}
}
}
}, mountOpts)
Expand Down Expand Up @@ -139,10 +141,7 @@ describe('single-error-extractor-mixin', () => {
mocks: {
$vuelidateErrorExtractor: {
i18n: false,
messages: i18nMessages.en.validation,
attributes: {
text: 'Text override'
}
messages: i18nMessages.en.validation
}
}
}
Expand Down Expand Up @@ -174,26 +173,37 @@ describe('single-error-extractor-mixin', () => {

it('uses the globally provided attributes as attribute prop', () => {
wrapper = createWrapper({
componentOpts: { template: '<div><form-element :validator="$v.text" label="Label" attribute="text"/></div>' },
componentOpts: { template: '<div><form-element :validator="$v.text" label="Label" name="text"/></div>' },
mountOpts: {
mocks: {
$vuelidateErrorExtractor: {
i18n: false,
messages: i18nMessages.en.validation,
attributes: {
text: 'Text override'
}
messages: i18nMessages.en.validation
}
}
}
})
wrapper.vm.$v.$touch()
wrapper.setData({
text: ''
})
const component = wrapper.find(formElement)
expect(component.vm.activeErrorMessages).toContain('Field Text override is required')
})

it('overrides the globally provided attributes by a local attribute', () => {
wrapper = createWrapper({
componentOpts: { template: '<div><form-element :validator="$v.text" attribute="Local Text" name="text"/></div>' },
mountOpts: {
mocks: {
$vuelidateErrorExtractor: {
i18n: false,
messages: i18nMessages.en.validation
}
}
}
})
wrapper.vm.$v.$touch()
const component = wrapper.find(formElement)
expect(component.vm.activeErrorMessages).toContain('Field Local Text is required')
})
})

describe('using validationKeys', () => {
Expand Down Expand Up @@ -248,25 +258,55 @@ describe('single-error-extractor-mixin', () => {
})

describe('using a form wrapper', () => {
let wrapper
it('uses the injected validator', () => {
wrapper = createWrapper({
componentOpts: { template: '<div><form-wrapper :validator="$v"><form-element label="Label" attribute="text" name="text"/></form-wrapper></div>' },
it('uses the injected validator and global attributes', () => {
const wrapper = createWrapper({
componentOpts: { template: '<div><form-wrapper :validator="$v"><form-element label="Label" name="text"/></form-wrapper></div>' },
mountOpts: {
mocks: {
$vuelidateErrorExtractor: {
i18n: false,
messages: i18nMessages.en.validation
}
}
}
})
wrapper.vm.$v.$touch()
const component = wrapper.find(formElement)
expect(component.vm.activeErrorMessages).toContain('Field Text override is required')
})
})

describe('using with nested $each rules', () => {
it('finds the proper validator and assigns proper attributes', () => {
const wrapper = createWrapper({
componentOpts: {
template: '<form-wrapper :validator="$v"><form-element name="phones.$each.0.batteries.$each.0.model"/></form-wrapper>',
data: () => ({ phones: [{ batteries: [{ model: null }] }] }),
validations: {
phones: {
$each: {
batteries: {
$each: { model: { required } }
}
}
}
}
},
mountOpts: {
mocks: {
$vuelidateErrorExtractor: {
i18n: false,
messages: i18nMessages.en.validation,
attributes: {
text: 'Text override'
'phones.batteries.model': 'Phone Battery model'
}
}
}
}
})
wrapper.vm.$v.$touch()
const component = wrapper.find(formElement)
expect(component.vm.activeErrorMessages).toContain('Field Text override is required')
expect(component.vm.activeErrorMessages).toContain('Field Phone Battery model is required')
})
})
})

0 comments on commit 2821014

Please sign in to comment.