1
0
mirror of synced 2024-11-25 23:06:02 +03:00
vue-formulario/src/FormularioInput.vue
2020-05-22 14:22:56 +03:00

332 lines
12 KiB
Vue

<template>
<div
class="formulario-input"
:data-has-errors="hasErrors"
:data-is-showing-errors="hasVisibleErrors"
:data-type="type"
>
<slot :id="id" :context="context" :errors="errors" :validationErrors="validationErrors" />
</div>
</template>
<script>
import context from './libs/context'
import { shallowEqualObjects, parseRules, snakeToCamel, has, arrayify, groupBails } from './libs/utils'
export default {
name: 'FormularioInput',
inheritAttrs: false,
provide () {
return {
// Allows sub-components of this input to register arbitrary rules.
formularioRegisterRule: this.registerRule,
formularioRemoveRule: this.removeRule
}
},
inject: {
formularioSetter: { default: undefined },
formularioFieldValidation: { default: () => () => ({}) },
formularioRegister: { default: undefined },
formularioDeregister: { default: undefined },
getFormValues: { default: () => () => ({}) },
observeErrors: { default: undefined },
removeErrorObserver: { default: undefined },
path: { default: '' }
},
model: {
prop: 'formularioValue',
event: 'input'
},
props: {
type: {
type: String,
default: 'text'
},
name: {
type: String,
required: true
},
/* eslint-disable */
formularioValue: {
default: ''
},
value: {
default: false
},
/* eslint-enable */
id: {
type: [String, Boolean, Number],
default: false
},
errors: {
type: [String, Array, Boolean],
default: false
},
validation: {
type: [String, Boolean, Array],
default: false
},
validationName: {
type: [String, Boolean],
default: false
},
errorBehavior: {
type: String,
default: 'blur',
validator: function (value) {
return ['blur', 'live', 'submit'].includes(value)
}
},
showErrors: {
type: Boolean,
default: false
},
imageBehavior: {
type: String,
default: 'preview'
},
uploadUrl: {
type: [String, Boolean],
default: false
},
uploader: {
type: [Function, Object, Boolean],
default: false
},
uploadBehavior: {
type: String,
default: 'live'
},
preventWindowDrops: {
type: Boolean,
default: true
},
validationMessages: {
type: Object,
default: () => ({})
},
validationRules: {
type: Object,
default: () => ({})
},
disableErrors: {
type: Boolean,
default: false
}
},
data () {
return {
defaultId: this.$formulario.nextId(this),
localAttributes: {},
localErrors: [],
proxy: this.getInitialValue(),
behavioralErrorVisibility: (this.errorBehavior === 'live'),
formShouldShowErrors: false,
validationErrors: [],
pendingValidation: Promise.resolve(),
// These registries are used for injected messages registrants only (mostly internal).
ruleRegistry: [],
messageRegistry: {}
}
},
computed: {
...context,
parsedValidationRules () {
const parsedValidationRules = {}
Object.keys(this.validationRules).forEach(key => {
parsedValidationRules[snakeToCamel(key)] = this.validationRules[key]
})
return parsedValidationRules
},
messages () {
const messages = {}
Object.keys(this.validationMessages).forEach((key) => {
messages[snakeToCamel(key)] = this.validationMessages[key]
})
Object.keys(this.messageRegistry).forEach((key) => {
messages[snakeToCamel(key)] = this.messageRegistry[key]
})
return messages
}
},
watch: {
'$attrs': {
handler (value) {
this.updateLocalAttributes(value)
},
deep: true
},
proxy (newValue, oldValue) {
this.performValidation()
if (!this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
this.context.model = newValue
}
},
formularioValue (newValue, oldValue) {
if (this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
this.context.model = newValue
}
},
showValidationErrors: {
handler (val) {
this.$emit('error-visibility', val)
},
immediate: true
}
},
created () {
this.applyInitialValue()
if (this.formularioRegister && typeof this.formularioRegister === 'function') {
this.formularioRegister(this.nameOrFallback, this)
}
if (!this.disableErrors && typeof this.observeErrors === 'function') {
this.observeErrors({ callback: this.setErrors, type: 'input', field: this.nameOrFallback })
}
this.updateLocalAttributes(this.$attrs)
this.performValidation()
},
beforeDestroy () {
if (!this.disableErrors && typeof this.removeErrorObserver === 'function') {
this.removeErrorObserver(this.setErrors)
}
if (typeof this.formularioDeregister === 'function') {
this.formularioDeregister(this.nameOrFallback)
}
},
methods: {
getInitialValue () {
if (has(this.$options.propsData, 'value')) {
return this.value
} else if (has(this.$options.propsData, 'formularioValue')) {
return this.formularioValue
}
return ''
},
applyInitialValue () {
// This should only be run immediately on created and ensures that the
// proxy and the model are both the same before any additional registration.
if (!shallowEqualObjects(this.context.model, this.proxy)) {
this.context.model = this.proxy
}
},
updateLocalAttributes (value) {
if (!shallowEqualObjects(value, this.localAttributes)) {
this.localAttributes = value
}
},
performValidation () {
let rules = parseRules(this.validation, this.$formulario.rules(this.parsedValidationRules))
// Add in ruleRegistry rules. These are added directly via injection from
// children and not part of the standard validation rule set.
rules = this.ruleRegistry.length ? this.ruleRegistry.concat(rules) : rules
this.pendingValidation = this.runRules(rules)
.then(messages => this.didValidate(messages))
return this.pendingValidation
},
runRules (rules) {
const run = ([rule, args, ruleName, modifier]) => {
var res = rule({
value: this.context.model,
getFormValues: this.getFormValues.bind(this),
name: this.context.name
}, ...args)
res = (res instanceof Promise) ? res : Promise.resolve(res)
return res.then(result => result ? false : this.getMessage(ruleName, args))
}
return new Promise(resolve => {
const resolveGroups = (groups, allMessages = []) => {
const ruleGroup = groups.shift()
if (Array.isArray(ruleGroup) && ruleGroup.length) {
Promise.all(ruleGroup.map(run))
.then(messages => messages.filter(m => !!m))
.then(messages => {
messages = Array.isArray(messages) ? messages : []
// The rule passed or its a non-bailing group, and there are additional groups to check, continue
if ((!messages.length || !ruleGroup.bail) && groups.length) {
return resolveGroups(groups, allMessages.concat(messages))
}
return resolve(allMessages.concat(messages))
})
} else {
resolve([])
}
}
resolveGroups(groupBails(rules))
})
},
didValidate (messages) {
const validationChanged = !shallowEqualObjects(messages, this.validationErrors)
this.validationErrors = messages
if (validationChanged) {
const errorObject = this.getErrorObject()
this.$emit('validation', errorObject)
if (this.formularioFieldValidation && typeof this.formularioFieldValidation === 'function') {
this.formularioFieldValidation(errorObject)
}
}
},
getMessage (ruleName, args) {
return this.getMessageFunc(ruleName)({
args,
name: this.mergedValidationName,
value: this.context.model,
vm: this,
formValues: this.getFormValues()
})
},
getMessageFunc (ruleName) {
ruleName = snakeToCamel(ruleName)
if (this.messages && typeof this.messages[ruleName] !== 'undefined') {
switch (typeof this.messages[ruleName]) {
case 'function':
return this.messages[ruleName]
case 'string':
case 'boolean':
return () => this.messages[ruleName]
}
}
return (context) => this.$formulario.validationMessage(ruleName, context, this)
},
hasValidationErrors () {
return new Promise(resolve => {
this.$nextTick(() => {
this.pendingValidation.then(() => resolve(!!this.validationErrors.length))
})
})
},
getValidationErrors () {
return new Promise(resolve => {
this.$nextTick(() => this.pendingValidation.then(() => resolve(this.getErrorObject())))
})
},
getErrorObject () {
return {
name: this.context.nameOrFallback || this.context.name,
errors: this.validationErrors.filter(s => typeof s === 'string'),
hasErrors: !!this.validationErrors.length
}
},
setErrors (errors) {
this.localErrors = arrayify(errors)
},
registerRule (rule, args, ruleName, message = null) {
if (!this.ruleRegistry.some(r => r[2] === ruleName)) {
// These are the raw rule format since they will be used directly.
this.ruleRegistry.push([rule, args, ruleName])
if (message !== null) {
this.messageRegistry[ruleName] = message
}
}
},
removeRule (key) {
const ruleIndex = this.ruleRegistry.findIndex(r => r[2] === key)
if (ruleIndex >= 0) {
this.ruleRegistry.splice(ruleIndex, 1)
delete this.messageRegistry[key]
}
}
}
}
</script>