@detools/vue-form
Version:
Form State Management for VueJS
390 lines (301 loc) • 9.37 kB
JavaScript
import Vue from 'vue'
import {
union,
without,
has,
isNil,
noop,
isFunction,
mapValues,
merge,
get,
set,
cloneDeep,
isPlainObject,
} from 'lodash'
import { validate, asyncValidate } from '../validators/validate'
import isValid from '../utils/isValid'
import CONSTANTS from '../constants'
export const VueFormStoreParams = {
defaultNormalizer: ({ value, name }) => ({ value, name }),
data() {
return {
// { [fieldName]: Any }
state: {},
// { [fieldName]: String }
syncErrors: {},
asyncErrors: {},
// { [fieldName]: Promise }
asyncValidations: {},
// Array<String>
formFields: [],
// Array<String>
removedFields: [],
// { [fieldName]: true }
touchedFields: {},
// { [fieldName]: true }
dirtyFields: {},
// { [fieldName]: Function }
normalizers: {},
form: {
submitting: false,
validating: false,
dirty: false,
},
props: {
// { [fieldName]: Any }
initialValues: {},
handleModelChange: noop,
keepValueOnRemove: false,
},
}
},
computed: {
isValid() {
return isValid([this.syncErrors, this.asyncErrors])
},
isPristine() {
return Boolean(this.formFields.length) && !this.form.dirty
},
isDisabled() {
const { submitting, validating } = this.form
return submitting || validating
},
allErrors() {
return merge({}, this.syncErrors, this.asyncErrors)
},
allErrorsFields() {
return Object.keys(this.allErrors)
},
},
methods: {
// ON MOUNT FORM START
setInitialValues(initialValues) {
this.props.initialValues = JSON.parse(JSON.stringify(initialValues))
return this
},
setHandleModelChange(handleModelChange) {
this.props.handleModelChange = handleModelChange
},
setBehaviourOnRemoveControl(keepValueOnRemove) {
this.props.keepValueOnRemove = keepValueOnRemove
},
// ON MOUNT FORM END
registerFormControl(params) {
const {
name,
fieldLevelInitialValue,
validators,
isComponentPartOfArrayField,
normalize,
} = params
const vm = this
vm.addFormField(name)
if (normalize) {
vm.$set(vm.normalizers, name, normalize)
}
const setError = vm.createSetError(name)
const setAsyncError = vm.createSetAsyncError(name)
const setValue = vm.createSetValue(name, setError)
const cleanFormValue = vm.createCleanFormValue(name)
const validateOnReinitialize = vm.createValidateOnReinitialize(name)
const setTouched = vm.createSetTouched(name)
const setDirty = vm.createSetDirty(name)
const formLevelInitialValue = get(vm.props.initialValues, name)
const value = !isNil(formLevelInitialValue) ? formLevelInitialValue : fieldLevelInitialValue
if (!has(vm.state, name)) {
setValue(validators)(value)
} else {
setError(validators)()
}
return {
setError,
setAsyncError,
cleanFormValue,
validateOnReinitialize,
setTouched,
setDirty,
isComponentPartOfArrayField,
asyncValidations: vm.asyncValidations,
useState: syncValidators => {
const isFieldTouched = vm.touchedFields[name]
const isFieldDirty = vm.dirtyFields[name]
return [
// Current value
get(vm.state, name),
// Value handler — setValue
setValue(syncValidators, setDirty),
// Current error
isFieldTouched && isFieldDirty && vm.allErrors[name],
// Touched indicator
isFieldTouched,
// Initial value
get(vm.props.initialValues, name),
]
},
}
},
addFormField(name) {
this.formFields = union(this.formFields, [name])
this.removedFields = without(this.removedFields, name)
},
removeFormField(name) {
this.formFields = without(this.formFields, name)
this.removedFields = this.removedFields.concat(name)
if (!this.props.keepValueOnRemove) {
this.$delete(this.state, name)
}
this.removeFormFieldErrors(name)
},
removeFormFieldErrors(name) {
// Control Level
this.$delete(this.syncErrors, name)
this.$delete(this.asyncErrors, name)
this.$delete(this.touchedFields, name)
this.$delete(this.dirtyFields, name)
// Form Level
this.form.dirty = Boolean(Object.keys(this.dirtyFields).length)
},
addFormSyncErrors(syncErrors) {
// Let's try use Form Level Sync errors as base
// And override them with existing sync errors
this.syncErrors = merge({}, syncErrors, this.syncErrors)
},
createSetError(name) {
const vm = this
return validators => (nextValue = get(vm.state, name)) => {
if (validators) {
const error = validate(validators, nextValue, name)
const method = error ? vm.$set : vm.$delete
method(vm.syncErrors, name, error)
}
}
},
createSetAsyncError(name) {
const vm = this
const on = {
success: () => vm.$delete(vm.asyncErrors, name),
error: error => vm.$set(vm.asyncErrors, name, error),
}
return asyncValidators => (nextValue = get(vm.state, name)) => {
if (!vm.syncErrors[name] && asyncValidators) {
const promise = asyncValidate(asyncValidators, nextValue, name)
const off = vm.manageValidatingState(name, promise)
promise
.then(on.success)
.catch(on.error)
.then(off)
return promise
}
return Promise.resolve()
}
},
createSetValue(passedName, setError) {
const vm = this
const normalizer = vm.normalizers[passedName] || this.$options.defaultNormalizer
return (validators, setDirty) => nextValue => {
const { name = passedName, value } = normalizer({
value: nextValue,
name: passedName,
state: vm.state,
})
const useLodashSet = /[[]/.test(name)
if (!useLodashSet) {
vm.$set(vm.state, name, value)
} else {
vm.state = merge({}, set(vm.state, name, value))
}
// When control value changes we need to clean async errors
if (vm.asyncErrors[name]) {
vm.$delete(vm.asyncErrors, name)
}
if (isFunction(vm.props.handleModelChange)) {
vm.props.handleModelChange(vm.state)
}
// setDirty passed only for setValue for Control
if (setDirty) {
setDirty()
}
setError(validators)(value)
}
},
createCleanFormValue(name) {
const vm = this
return () => {
vm.removeFormField(name)
}
},
createSetTouched(name) {
const vm = this
return () => {
vm.$set(vm.touchedFields, name, true)
}
},
createSetDirty(name) {
const vm = this
return () => {
vm.$set(vm.dirtyFields, name, true)
vm.$set(this.form, 'dirty', true)
}
},
createValidateOnReinitialize() {
const vm = this
return callback => {
vm.$on(CONSTANTS.VUE_FORM_REINITIALIZE, callback)
return () => vm.$off(CONSTANTS.VUE_FORM_REINITIALIZE, callback)
}
},
manageValidatingState(name, promise) {
const vm = this
vm.form.validating = true
vm.$set(vm.asyncValidations, name, promise)
return () => {
vm.form.validating = false
vm.$delete(vm.asyncValidations, name)
}
},
manageSubmittingState() {
const vm = this
vm.form.submitting = true
return () => {
vm.form.submitting = false
}
},
manageTouchedFieldsState() {
const fields = this.formFields.reduce((memo, name) => ({ ...memo, [name]: true }), {})
this.touchedFields = fields
this.dirtyFields = fields
},
resetValues() {
this.reinitializeValues(this.props.initialValues)
this.props.handleModelChange(this.props.initialValues)
},
reinitializeValues(nextInitialValues) {
this.setInitialValues(nextInitialValues)
this.state = mapValues(this.state, (value, name) => {
const initialValue = nextInitialValues[name]
this.removeFormFieldErrors(name)
if (!isNil(initialValue)) {
return Array.isArray(initialValue) ? cloneDeep(initialValue) : initialValue
}
// Array that does not have formLevel value
// Do not reset it state to empty array
if (Array.isArray(value)) {
return value
}
// Plaing object that does not have formLevel value
// Do not reset it state to empty nil value
// Form controls already created default value for it
if (isPlainObject(value)) {
return value
}
return undefined
})
this.$emit(CONSTANTS.VUE_FORM_REINITIALIZE, this.state)
},
creatDetached(initialValues) {
return new Vue(VueFormStoreParams).setInitialValues(initialValues).registerFormControl
},
},
}
export default new Vue(VueFormStoreParams)