Skip to content

Instantly share code, notes, and snippets.

@uriannrima
Created November 3, 2019 23:40
Show Gist options
  • Save uriannrima/69942996f7dc900d6b62ae3d8f48bc20 to your computer and use it in GitHub Desktop.
Save uriannrima/69942996f7dc900d6b62ae3d8f48bc20 to your computer and use it in GitHub Desktop.
useForm Vue Composition API (Hook) using useFormik as reference.
/* 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