Skip to content

Instantly share code, notes, and snippets.

@vladDotH
Last active May 26, 2023 22:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vladDotH/6672e6ccde143a97fa49f7976fde2e4b to your computer and use it in GitHub Desktop.
Save vladDotH/6672e6ccde143a97fa49f7976fde2e4b to your computer and use it in GitHub Desktop.
VueFormGenerator composition api adapter
import Vue, { ExtractPropTypes, PropType } from 'vue'
import { FieldSchema, FormOptions } from 'vue-form-generator'
export const FieldPropsObject = {
vfg: { type: Object as PropType<any> },
model: Object as PropType<any>,
schema: { type: Object as PropType<FieldSchema>, required: true },
formOptions: Object as PropType<FormOptions>,
disabled: Boolean as PropType<boolean>
} as const
export type FieldProps = ExtractPropTypes<typeof FieldPropsObject>
export const FieldEmitsObject = {
validated(isValid: boolean, errors: any[], instance?: Vue) {
return true
},
'model-updated': function (newValue: any, model?: string) {
return true
}
} as const
export type FieldEmits = {
(event: 'validated', isValid: boolean, errors: any[], instance?: Vue): void
(event: 'model-updated', value: any, model?: string): void
}
export interface FieldExpose {
validate: (calledParent?: any) => Promise<any[]> | any[]
clearValidationErrors: () => any[]
}
import _ from 'lodash'
import { computed, getCurrentInstance, ref } from 'vue'
import { FieldEmits, FieldProps } from './types'
import { FieldSchema, schema, validators } from 'vue-form-generator'
const slugifyFormID = schema.slugifyFormID
function convertValidator(validator: string | CallableFunction) {
if (_.isString(validator)) {
if (validators[validator] != null) {
return validators[validator]
} else {
console.warn(`'${validator}' is not a validator function!`)
return null // caller need to handle null
}
}
return validator
}
export const useField = (props: FieldProps, emit: FieldEmits) => {
const instance = getCurrentInstance()
const errors = ref<any[]>([]),
debouncedValidateFunc = ref<any>(null),
debouncedFormatFunc = ref<any>(null)
const getFieldClasses = () => props.schema.fieldClasses ?? [],
formatValueToField = ref((value: any) => value),
formatValueToModel = ref((value: any) => value)
const getFieldID = (schema: FieldSchema, unique = false) => {
const idPrefix = props.formOptions?.fieldIdPrefix ?? ''
return slugifyFormID(schema, idPrefix) + (unique ? '-' + _.uniqueId() : '')
}
const clearValidationErrors = () => errors.value.splice(0) as any[]
const validate = (calledParent?: any) => {
clearValidationErrors()
const validateAsync = props.formOptions?.validateAsync ?? false
const validateDisabled = props.formOptions?.validateDisabled ?? false
const validateReadonly = props.formOptions?.validateReadonly ?? false
let results: any[] = []
if (
props.schema.validator &&
(props.schema.readonly !== true || validateReadonly) &&
(props.disabled !== true || validateDisabled)
) {
const validators = []
if (!_.isArray(props.schema.validator)) {
const converted = convertValidator(props.schema.validator)
if (converted) validators.push(converted.bind(instance?.proxy))
} else {
_.forEach(props.schema.validator, (validator) => {
const converted = convertValidator(validator)
if (converted) validators.push(converted.bind(instance?.proxy))
})
}
_.forEach(validators, (validator) => {
if (validateAsync) {
results.push(validator(value.value, props.schema, props.model))
} else {
const result = validator(value.value, props.schema, props.model)
if (result && _.isFunction(result.then)) {
result.then((err: any) => {
if (err) {
errors.value.push(...err)
}
const isValid = errors.value.length === 0
emit('validated', isValid, errors.value, instance?.proxy)
})
} else if (result) {
results = results.concat(result)
}
}
})
}
const handleErrors = (errorsHandled: any[]) => {
let fieldErrors: any[] = []
_.forEach(_.uniq(errorsHandled), (err) => {
if (_.isArray(err) && err.length > 0) {
fieldErrors = fieldErrors.concat(err)
} else if (_.isString(err)) {
fieldErrors.push(err)
}
})
if (_.isFunction(props.schema.onValidated)) {
props.schema.onValidated.call(
instance?.proxy,
props.model,
fieldErrors,
props.schema
)
}
const isValid = fieldErrors.length === 0
if (!calledParent) {
emit('validated', isValid, fieldErrors, instance?.proxy)
}
errors.value = fieldErrors
return fieldErrors
}
if (!validateAsync) {
return handleErrors(results)
}
return Promise.all(results).then(handleErrors)
}
const debouncedValidate = () => {
if (!_.isFunction(debouncedValidateFunc.value)) {
debouncedValidateFunc.value = _.debounce(
validate,
props.schema.validateDebounceTime ??
props.formOptions?.validateDebounceTime ??
500
)
}
debouncedValidateFunc.value()
}
const setModelValueByPath = (model: any, path: string, value: any) => {
// convert array indexes to properties
let s = path.replace(/\[(\w+)\]/g, '.$1')
// strip a leading dot
s = s.replace(/^\./, '')
const keys = s.split('.')
let i = 0
const n = keys.length
while (i < n) {
const key = keys[i]
if (i < n - 1) {
if (model[key] !== undefined) {
// Found parent property. Step in
model = model[key]
} else {
// Create missing property (new level)
model[key] = {}
model = model[key]
}
} else {
// Set final property value
model[key] = value
return
}
++i
}
}
const updateModelValue = (newValue: any, oldValue: any) => {
let changed = false
if (_.isFunction(props.schema.set)) {
props.schema.set(props.model, newValue)
changed = true
} else if (props.schema.model) {
setModelValueByPath(props.model, props.schema.model, newValue)
changed = true
}
if (changed) {
emit('model-updated', newValue, props.schema.model)
if (_.isFunction(props.schema.onChanged)) {
props.schema.onChanged(props.model, newValue, oldValue, props.schema)
}
if (_.get(props.formOptions, 'validateAfterChanged', false) === true) {
if (
_.get(
props.schema,
'validateDebounceTime',
_.get(props.formOptions, 'validateDebounceTime', 0)
) > 0
) {
debouncedValidate()
} else {
validate()
}
}
}
}
const value = computed({
get: () => {
let val
if (_.isFunction(props.schema.get)) {
val = props.schema.get(props.model)
} else if (props.schema.model) {
val = _.get(props.model, props.schema.model)
}
return formatValueToField.value(val)
},
set: (newValue) => {
const oldValue = value.value
newValue = formatValueToModel.value(newValue)
if (_.isFunction(newValue)) {
newValue(newValue, oldValue)
} else {
updateModelValue(newValue, oldValue)
}
}
})
return {
errors,
debouncedValidateFunc,
debouncedFormatFunc,
getFieldClasses,
formatValueToField,
formatValueToModel,
getFieldID,
clearValidationErrors,
validate,
debouncedValidate,
setModelValueByPath,
updateModelValue,
value
}
}
declare module 'vue-form-generator' {
import { PluginObject } from 'vue'
export type ValidatorFn = (
value: any,
field: FieldSchema,
model: any,
messages: { [key: string]: string }
) => string[] | null
export type LocaledValidatorFn = (
value: any,
field: FieldSchema,
model: any
) => string[] | null
export interface FormButton {
classes?: string
label?: string
type?: string
onclick?: (model: any, field: FieldSchema, event: any, wrapper: any) => void
[key: string]: any
}
export type FieldSchemaCallBack<T, M = any> = (
model: M,
field: FieldSchema<M>,
wrapper: any
) => T
export interface FieldSchema<M = object, V = any> {
type: string
label?: string
model?: string
default?: any
id?: string
inputName?: string
inputType?: string
accept?: string
get?: (model: M) => V
set?: (model: M, value: V) => void
validator?: string | LocaledValidatorFn | (string | LocaledValidatorFn)[]
onChanged?: (
model: M,
newVal: V,
oldVal: V,
field: FieldSchema<M, V>
) => void
onValidated?: (model: M, errors: any[], field: FieldSchema<M, V>) => void
validateDebounceTime?: number
required?: boolean | FieldSchemaCallBack<boolean>
readonly?: boolean | FieldSchemaCallBack<boolean>
disabled?: boolean | FieldSchemaCallBack<boolean>
featured?: boolean | FieldSchemaCallBack<boolean>
visible?: boolean
multi?: boolean
placeholder?: string
fieldClasses?: string
styleClasses?: string | string[]
labelClasses?: string
help?: string | FieldSchemaCallBack<string>
hint?: string
buttons?: FormButton[]
attributes?: object
[key: string]: any
}
export type ValidationResults<M = object, V = any> = {
field: FieldSchema<M, V>
error: string
index?: number
}[]
export type ValidationResultsLocal<M = object, V = any> = {
[k: number]: {
field: FieldSchema<M, V>
error: string
index?: number
}[]
}
export interface FormGroup<M = object, V = any> {
legend: string
fields: FieldSchema<M, V>[]
}
export interface FormSchema<M = object, V = any> {
fields?: FieldSchema<M, V>[]
groups?: FormGroup<M, V>[]
}
export interface FormOptions {
validateAfterLoad?: boolean
validateAfterChanged?: boolean
fieldIdPrefix?: string
validateAsync?: boolean
validationErrorClass?: string
validationSuccessClass?: string
validateDebounceTime?: number
validateReadonly?: boolean
validateDisabled?: boolean
}
export const abstractField: any
export type buildInValidator = ValidatorFn & {
locale: (messages: { [key: string]: string }) => LocaledValidatorFn
}
export const validators: {
[key: string]: buildInValidator
}
const VueFormGenerator: PluginObject<any>
export default VueFormGenerator
export const schema: {
createDefaultObject(schema: FormSchema, obj?: object): object
getMultipleFields(schema: FormSchema): FieldSchema[]
mergeMultiObjectFields(schema: FormSchema, objs: object[]): object
slugifyFormID(schema, prefix?: string): string
slugify(name?: string): string
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment