Last active
May 26, 2023 22:47
-
-
Save vladDotH/6672e6ccde143a97fa49f7976fde2e4b to your computer and use it in GitHub Desktop.
VueFormGenerator composition api adapter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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[] | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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