Created
November 3, 2019 23:40
-
-
Save uriannrima/69942996f7dc900d6b62ae3d8f48bc20 to your computer and use it in GitHub Desktop.
useForm Vue Composition API (Hook) using useFormik as reference.
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
/* eslint-disable @typescript-eslint/no-non-null-assertion*/ | |
import _set from 'lodash.set'; | |
import _get from 'lodash.get'; | |
import { reactive, computed, watch } from '@vue/composition-api'; | |
import { ObjectSchema } from 'yup'; | |
export const isObject = (obj: object) => obj !== null && typeof obj === 'object'; | |
export const toSpreadable = <FieldValueType>({ | |
value, | |
onChange: change, | |
onBlur: blur, | |
...rest | |
}: FormHookFieldProps<FieldValueType>) => ({ | |
props: { value }, | |
domProps: { value }, | |
on: { | |
change, | |
blur, | |
}, | |
...rest, | |
}); | |
export function setNestedObjectValues(object: any, value: any, visited = new WeakMap(), response: any = {}) { | |
for (let k of Object.keys(object)) { | |
const val = object[k]; | |
if (isObject(val)) { | |
if (!visited.get(val)) { | |
visited.set(val, true); | |
// In order to keep array values consistent for both dot path and | |
// bracket syntax, we need to check if this is an array so that | |
// this will output { friends: [true] } and not { friends: { "0": true } } | |
response[k] = Array.isArray(val) ? [] : {}; | |
setNestedObjectValues(val, value, visited, response[k]); | |
} | |
} else { | |
response[k] = value; | |
} | |
} | |
return response; | |
} | |
/** | |
* An object containing error messages whose keys correspond to FormikValues. | |
* Should be always be and object of strings, but any is allowed to support i18n libraries. | |
*/ | |
export type FormHookErrors<Values> = { | |
[K in keyof Values]?: Values[K] extends any[] | |
? Values[K][number] extends object // [number] is the special sauce to get the type of array's element. More here https://github.com/Microsoft/TypeScript/pull/21316 | |
? FormHookErrors<Values[K][number]>[] | string | string[] | |
: string | string[] | |
: Values[K] extends object | |
? FormHookErrors<Values[K]> | |
: string; | |
}; | |
export type FormHookTouched<Values> = { | |
[K in keyof Values]?: Values[K] extends any[] | |
? Values[K][number] extends object // [number] is the special sauce to get the type of array's element. More here https://github.com/Microsoft/TypeScript/pull/21316 | |
? FormHookTouched<Values[K][number]>[] | |
: boolean | |
: Values[K] extends object | |
? FormHookTouched<Values[K]> | |
: boolean; | |
}; | |
export interface FormHookState<Values> { | |
/** Form values */ | |
values: Values; | |
/** map of field names to specific error for that field */ | |
errors: FormHookErrors<Values>; | |
/** map of field names to whether the field has been touched */ | |
touched: FormHookTouched<Values>; | |
/** whether the form is currently submitting */ | |
isSubmitting: boolean; | |
/** whether the form is currently validating (prior to submission) */ | |
isValidating: boolean; | |
/** Number of times user tried to submit the form */ | |
submitCount: number; | |
submitError?: Error; | |
} | |
export interface FormHookProps<FormValuesType> { | |
initialValues: FormValuesType; | |
onSubmit: (values: FormValuesType) => Promise<void>; | |
validate?: (values: FormValuesType) => FormHookErrors<FormValuesType>; | |
validationSchema?: ObjectSchema; | |
validateOnChange: boolean; | |
validateOnBlur: boolean; | |
validateOnMount: boolean; | |
} | |
export interface FormHookFieldProps<FieldValue> { | |
value: FieldValue; | |
onChange: HTMLElementEventMap['change']; | |
onBlur: HTMLElementEventMap['blur']; | |
meta: { | |
touched: boolean; | |
error: string; | |
}; | |
} | |
const yupToFormErrors = (yupError: any) => { | |
let errors = {}; | |
if (yupError.inner) { | |
if (yupError.inner.length === 0) { | |
return _set(errors, yupError.path, yupError.message); | |
} | |
for (let err of yupError.inner) { | |
if (!_get(errors, err.path)) { | |
errors = _set(errors, err.path, err.message); | |
} | |
} | |
} | |
return errors; | |
}; | |
export interface FormValues { | |
[field: string]: any; | |
} | |
function getSelectedValues(options: any[]) { | |
return Array.from(options) | |
.filter(el => el.selected) | |
.map(el => el.value); | |
} | |
const isString = (obj: any): obj is string => Object.prototype.toString.call(obj) === '[object String]'; | |
function getValueForCheckbox(currentValue: string | any[], checked: boolean, valueProp: any) { | |
// eslint-disable-next-line eqeqeq | |
if (valueProp == 'true' || valueProp == 'false') { | |
return !!checked; | |
} | |
if (checked) { | |
return Array.isArray(currentValue) ? currentValue.concat(valueProp) : [valueProp]; | |
} | |
if (!Array.isArray(currentValue)) { | |
return !!currentValue; | |
} | |
const index = currentValue.indexOf(valueProp); | |
if (index < 0) { | |
return currentValue; | |
} | |
return currentValue.slice(0, index).concat(currentValue.slice(index + 1)); | |
} | |
export default <FormValuesType extends FormValues = FormValues>({ | |
initialValues, | |
onSubmit, | |
validate, | |
validationSchema, | |
validateOnChange = true, | |
validateOnBlur = true, | |
validateOnMount = false, | |
}: FormHookProps<FormValuesType>) => { | |
const state = reactive({ | |
values: initialValues, | |
errors: {}, | |
touched: {}, | |
isSubmitting: false, | |
isValidating: false, | |
submitCount: 0, | |
submitError: undefined, | |
hasErrors: computed(() => Object.keys(state.errors).length >= 1), | |
}) as FormHookState<FormValuesType>; | |
const onSubmitAttempt = () => { | |
state.isSubmitting = true; | |
state.touched = setNestedObjectValues(state.values, true); | |
}; | |
const onSubmitError = (submitError?: Error) => { | |
state.isSubmitting = false; | |
state.submitError = submitError; | |
}; | |
const setErrors = (errors: FormHookErrors<FormValuesType>) => { | |
state.errors = errors; | |
}; | |
const onSubmitSuccess = () => { | |
state.isSubmitting = false; | |
}; | |
const handleValidation = async (values: FormValuesType) => { | |
if (!validate) return {}; | |
return validate(values); | |
}; | |
const handleValidationSchema = async (values: FormValuesType) => { | |
if (!validationSchema) return {}; | |
try { | |
await validationSchema.validate(values, { | |
abortEarly: false, | |
}); | |
} catch (validationError) { | |
return yupToFormErrors(validationError); | |
} | |
}; | |
const handleFormValidation = async (values: FormValuesType) => { | |
const errors = await handleValidation(values); | |
const schemaErrors = await handleValidationSchema(values); | |
Object.assign(state.errors, { | |
...errors, | |
...schemaErrors, | |
}); | |
}; | |
const handleSubmit = async (event: Event) => { | |
event.preventDefault(); | |
onSubmitAttempt(); | |
await handleFormValidation(state.values); | |
if (!Object.keys(state.errors).length) { | |
try { | |
await onSubmit(state.values); | |
onSubmitSuccess(); | |
} catch (submitError) { | |
onSubmitError(submitError); | |
} | |
} else { | |
onSubmitError(); | |
} | |
}; | |
const executeChange = (eventOrTextValue: string | Event, maybePath?: string) => { | |
let field = maybePath; | |
let val = eventOrTextValue; | |
let parsed; | |
if (!isString(eventOrTextValue)) { | |
const { type, name, id, value, checked, options, multiple } = (eventOrTextValue as any).target; | |
field = maybePath ? maybePath : name ? name : id; | |
val = /number|range/.test(type) | |
? ((parsed = parseFloat(value)), isNaN(parsed) ? '' : parsed) | |
: /checkbox/.test(type) | |
? getValueForCheckbox(_get(state.values, field!), checked, value) | |
: multiple | |
? getSelectedValues(options) | |
: value; | |
} | |
if (field) { | |
_set(state.values, field, val); | |
validateOnChange && handleFormValidation(state.values); | |
} | |
}; | |
const executeBlur = (e: Event, path?: string) => { | |
const { name, id } = e.target as any; | |
const field = path ? path : name ? name : id; | |
_set(state.touched, field, true); | |
validateOnBlur && handleFormValidation(state.values); | |
}; | |
const handleChange = (eventOrPath: string | Event) => { | |
if (isString(eventOrPath)) { | |
return (event: string | Event) => executeChange(event, eventOrPath); | |
} | |
executeChange(eventOrPath); | |
}; | |
const handleBlur = (eventOrPath: string | Event) => { | |
if (isString(eventOrPath)) { | |
return (event: Event) => executeBlur(event, eventOrPath); | |
} | |
executeBlur(eventOrPath); | |
}; | |
const getFieldProps = <FieldKey extends keyof FormValuesType, FieldValueType extends FormValuesType[FieldKey]>( | |
fieldName: string & FieldKey, | |
): FormHookFieldProps<FieldValueType> => { | |
return reactive({ | |
value: computed(() => _get(state.values, fieldName)), | |
onChange: handleChange, | |
onBlur: handleBlur, | |
meta: computed(() => ({ | |
touched: _get(state.touched, fieldName), | |
error: _get(state.errors, fieldName), | |
})), | |
}) as FormHookFieldProps<FieldValueType>; | |
}; | |
const getSpreadableFieldProps = <FieldKey extends keyof FormValuesType>(fieldName: string & FieldKey) => | |
toSpreadable(getFieldProps(fieldName)); | |
/** TODO: Ref still doesn't accept a function. */ | |
const register = () => {}; | |
watch( | |
() => state.values, | |
() => { | |
if (validate) { | |
const errors = validate(state.values); | |
setErrors(errors); | |
} | |
}, | |
); | |
validateOnMount && handleFormValidation(state.values); | |
return { | |
handleSubmit, | |
form: state, | |
getFieldProps, | |
getSpreadableFieldProps, | |
handleChange, | |
handleBlur, | |
register, | |
}; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment