Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make v-form validation great again #1581

Merged
merged 9 commits into from
Sep 7, 2017
98 changes: 66 additions & 32 deletions src/components/VForm/VForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,20 @@ export default {

data () {
return {
inputs: 0,
inputs: [],
errorBag: {}
}
},

props: {
value: Boolean
value: Boolean,
lazyValidation: Boolean
},

watch: {
errorBag: {
handler () {
const keys = Object.keys(this.errorBag)
if (keys.length < this.inputs) return false

const errors = keys.reduce((errors, key) => {
errors = errors || this.errorBag[key]
return errors
}, false)
const errors = Object.values(this.errorBag).includes(true)

this.$emit('input', !errors)

Expand All @@ -35,41 +30,80 @@ export default {

methods: {
getInputs () {
return this.$children.filter(child => {
return typeof child.errorBucket !== 'undefined'
const results = []

const search = (children, depth = 0) => {
for (const child of children) {
if (child.errorBucket !== undefined) {
results.push(child)
} else {
search(child.$children, depth + 1)
}
}
if (depth === 0) return results
}

return search(this.$children)
},
watchInputs (inputs) {
inputs === undefined && (inputs = this.getInputs())

for (const child of inputs) {
if (this.inputs.includes(child)) {
continue // We already know about this input
}

this.inputs.push(child)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove space

this.watchChild(child)
}
},
watchChild (child) {
const watcher = (child) => {
child.$watch('valid', (val) => {
this.$set(this.errorBag, child._uid, !val)
}, { immediate: true })
}

if (!this.lazyValidation) return watcher(child)

// Only start watching inputs if we need to
child.$watch('shouldValidate', (val) => {
if (!val) return

// Only watch if we're not already doing it
if (this.errorBag.hasOwnProperty(child._uid)) return

watcher(child)
})
},
validate () {
const errors = this.getInputs().reduce((errors, child) => {
const error = !child.validate(true)
return errors || error
}, false)

return !errors
return !this.inputs.filter(input => input.validate(true)).length
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could/should be !this.inputs.every(input => input.validate(true))

Copy link
Member Author

@KaelWD KaelWD Sep 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had that in 9a5063b, but it stops at the first invalid field. I can change it to ...length !== 0 to make it more clear what is happening.

Edit: or

const errors = this.inputs.filter ... .length
return !errors

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could also possibly use this.inputs.some(input => !input.validate(true))

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.some has the same problem as .every.

The every method executes the provided callback function once for each element present in the array until it finds one where callback returns a falsy value. If such an element is found, the every method immediately returns false.

some() executes the callback function once for each element present in the array until it finds one where callback returns a truthy value (a value that becomes true when converted to a Boolean). If such an element is found, some() immediately returns true.

input.validate() must be called on every element so that validation errors are forced to display.

},
reset () {
this.getInputs().forEach((input) => input.reset())
this.inputs.forEach((input) => input.reset())
Object.keys(this.errorBag).forEach(key => this.$delete(this.errorBag, key))
}
},

mounted () {
this.$vuetify.load(() => {
this.getInputs().forEach((child) => {
this.inputs += 1
this.$vuetify.load(() => this.watchInputs())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be this.$vuetify.load(this.watchInputs)

Copy link
Member Author

@KaelWD KaelWD Sep 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That will pass the global Window object to watchInputs, instead of undefined.

Is .load() even needed? Nothing seems to change if I remove it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's for SSR protection lol

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also valid point, no need to change this.

},

// Only start watching inputs if we need to
child.$watch('shouldValidate', (val) => {
if (!val) return
updated () {
const inputs = this.getInputs()

// Only watch if we're not already doing it
if (this.errorBag.hasOwnProperty(child._uid)) return
if (inputs.length < this.inputs.length) {
// Something was removed, we don't want it in the errorBag any more
const removed = this.inputs.filter(i => !inputs.includes(i))

child.$watch('valid', (val) => {
this.$set(this.errorBag, child._uid, !val)
}, { immediate: true })
})
})
})
for (const input of removed) {
this.$delete(this.errorBag, input._uid)
this.$delete(this.inputs, this.inputs.indexOf(input))
}
}

this.watchInputs(inputs)
},

render (h) {
Expand Down