diff --git a/index.html b/index.html new file mode 100644 index 0000000..81d5cb6 --- /dev/null +++ b/index.html @@ -0,0 +1,16 @@ + + + + + + + + Vite + Vue + TS + + + +
+ + + + \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index df20226..6143919 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,14 +1,16 @@ import { BaseControlEmits } from "./types" +export const DEFAULT_FIELD_VALUE = null; + export const BaseInputProps = { name: { type: String, required: true }, - isDirty: {type:Boolean, default:()=>false}, - isTouched: {type:Boolean, default:()=>false}, - errors: { type: Object, default:()=> {} }, + isDirty: { type: Boolean, default: () => false }, + isTouched: { type: Boolean, default: () => false }, + errors: { type: Object, default: () => { } }, onBlur: { type: Function, required: true }, - onClear: { type: Function, default:()=> null }, - modelValue: { type: [String, Object, Array, Number,Boolean, null], required:true}, + onClear: { type: Function, default: () => null }, + modelValue: { type: [String, Object, Array, Number, Boolean, null], required: true }, 'onUpdate:modelValue': { type: Function, required: true }, } -export const BaseInputEmits:BaseControlEmits = ['update:modelValue', 'blur', 'clear'] \ No newline at end of file +export const BaseInputEmits: BaseControlEmits = ['update:modelValue', 'blur', 'clear'] \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index cf2ebd4..cfd473d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,5 @@ -import FormHandler from './FormHandler' -import useFormHandler from './useFormHandler' +export { default as FormHandler } from './FormHandler' +export { default as useFormHandler } from './useFormHandler' export * from './constants' -export * from './types' -export { useFormHandler, FormHandler } \ No newline at end of file +export * from './types' \ No newline at end of file diff --git a/src/logic/getDefaultFieldValue.ts b/src/logic/getDefaultFieldValue.ts new file mode 100644 index 0000000..e5aea9f --- /dev/null +++ b/src/logic/getDefaultFieldValue.ts @@ -0,0 +1,12 @@ +import { isCheckboxInput } from "../utils" +import { DEFAULT_FIELD_VALUE } from "../constants" + +export default (el: any) => { + if (!el) { + return DEFAULT_FIELD_VALUE + } + if (isCheckboxInput(el)) { + return false + } + return DEFAULT_FIELD_VALUE +} \ No newline at end of file diff --git a/src/logic/getNativeFieldValue.ts b/src/logic/getNativeFieldValue.ts new file mode 100644 index 0000000..c5a1ded --- /dev/null +++ b/src/logic/getNativeFieldValue.ts @@ -0,0 +1,22 @@ +import { DEFAULT_FIELD_VALUE } from './../constants'; +import { isCheckboxInput, isMultipleSelect, isRadioInput } from "../utils" + +export default (el: any) => { + if (!el) { + return DEFAULT_FIELD_VALUE + } + if (Array.isArray(el)) { + if (isRadioInput(el[0])) { + return el.find(({ checked, disabled }) => checked && !disabled)?.value || el[0].value + } + return el[0].value + } + if (isCheckboxInput(el)) { + return el.checked + } + if (isMultipleSelect(el)) { + const values = [...el.options]?.filter(({ selected }: any) => selected).map(({ value }: any) => value) + return !values.length ? DEFAULT_FIELD_VALUE : values + } + return el.value || DEFAULT_FIELD_VALUE +} \ No newline at end of file diff --git a/src/logic/index.ts b/src/logic/index.ts new file mode 100644 index 0000000..d1f1a03 --- /dev/null +++ b/src/logic/index.ts @@ -0,0 +1,2 @@ +export { default as getNativeFieldValue } from './getNativeFieldValue' +export { default as getDefaultFieldValue } from './getNativeFieldValue' \ No newline at end of file diff --git a/src/playground/App.vue b/src/playground/App.vue new file mode 100644 index 0000000..18d2afa --- /dev/null +++ b/src/playground/App.vue @@ -0,0 +1,63 @@ + + + + + \ No newline at end of file diff --git a/src/playground/main.ts b/src/playground/main.ts new file mode 100644 index 0000000..43d27f6 --- /dev/null +++ b/src/playground/main.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import App from './App.vue' + +const app = createApp(App) +app.mount('#app') diff --git a/src/playground/vite-env.d.ts b/src/playground/vite-env.d.ts new file mode 100644 index 0000000..c71587c --- /dev/null +++ b/src/playground/vite-env.d.ts @@ -0,0 +1,7 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/src/types.ts b/src/types.ts index be14214..677045e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,34 +1,34 @@ /** Props for a base control */ -export interface BaseControlProps { +export interface BaseControlProps { /** Name of the control */ name: string, /** Current errors of the control */ - errors?:string[] + errors?: string[] /** Current dirty state of the control */ - isDirty?:boolean + isDirty?: boolean /** Current touched state of the control */ - isTouched?:boolean - + isTouched?: boolean + /** Value binding for native inputs */ - value?:any, + ref: any, /** Handler binding for native inputs */ - onInput?:(el:any)=>void, + onChange?: (el: any) => void, - /** Value binding for custom inputs */ - modelValue?: any, + /** Value binding for custom inputs */ + modelValue: any, /** Handler binding for custom inputs */ - 'onUpdate:modelValue'?:(value:any)=>void, + 'onUpdate:modelValue': (value: any) => void, /** Blur handler */ - onBlur?:()=>void, + onBlur?: () => void, /** Clear handler */ - onClear?:()=>void, + onClear?: () => void, } /** Each emit the handler could be expecting */ @@ -47,34 +47,34 @@ export interface FormState { isValid: boolean /** Object holding the dirty fields of the form */ - dirty: Record + dirty: Record /** Object holding the touched fields of the form */ - touched: Record + touched: Record /** Object holding the fields with errors (one or multiple, check:validationErrors)*/ - errors: Record + errors: Record } /** Function returning true for correct validation or a string with an error if it's invalid */ -export type ValidationFn = (_:any)=>Promise|boolean|string +export type ValidationFn = (_: any) => Promise | boolean | string /** Validations collection as an object */ -export type Validations = Record +export type Validations = Record export interface RegisterOptions { /** Indicates wether the input is native or not, set to false if the extra listeners are not desired */ native?: boolean /** Indicates wether the input is clearable or not */ - clearable?:boolean + clearable?: boolean /** Default value for the field */ defaultValue?: any /** Required indicator for the control */ - required?:boolean - + required?: boolean + /** Validations for the field */ validations?: Validations @@ -83,46 +83,46 @@ export interface RegisterOptions { } /** Gets the initial/default/fallback value for a control */ -export type GetInitValueForControl = (name:string) => any +export type GetInitValueForControl = (name: string) => any /** Field initializer */ -export type InitControl = (name:string, options:RegisterOptions) => void +export type InitControl = (name: string, options: RegisterOptions) => void /** Function that allows you to register a control to interact with the form */ -export type Register = (name:string, options?: RegisterOptions) => BaseControlProps +export type Register = (name: string, options?: RegisterOptions) => BaseControlProps /** Sets dirty state of a control */ -export type SetDirty = (name:string, dirty:boolean) => void +export type SetDirty = (name: string, dirty: boolean) => void /** Sets touched state of a control */ -export type SetTouched = (name:string, touched:boolean) => void +export type SetTouched = (name: string, touched: boolean) => void /** Function to set a value programmatically */ -export type SetValue = (name:string, value:any) => Promise +export type SetValue = (name: string, value: any) => Promise /** Function to trigger validations programmatically */ -export type TriggerValidation = (name?:string) => Promise +export type TriggerValidation = (name?: string) => Promise /** Control blur handler */ -export type HandleBlur = (name:string) => void +export type HandleBlur = (name: string) => void /** Control change handler */ -export type HandleChange = (name:string, value?:any) => Promise +export type HandleChange = (name: string, value?: any) => Promise /** Function to set an error programmatically */ -export type ClearField = (name:string) => Promise +export type ClearField = (name: string) => Promise /** Function to reset a control programmatically*/ -export type ResetField = (name:string) => void +export type ResetField = (name: string) => void /** Function to reset the whole form programmatically*/ export type ResetForm = () => void /** Function to set an error programmatically */ -export type SetError = (name:string, error:any, replace?:boolean) => void +export type SetError = (name: string, error: any, replace?: boolean) => void /** Function to clear an error programmatically */ -export type ClearErrors = (name?:string, errors?:string|string[]) => void +export type ClearErrors = (name?: string, errors?: string | string[]) => void /** Function to get the modified values of the form */ export type ModifiedValues = () => Object @@ -131,85 +131,82 @@ export type ModifiedValues = () => Object export type HandleSubmitSuccessFn = (values: Object) => void /** Optional function to be called after a form failed to submit */ -export type HandleSubmitErrorFn = (errors:Object) => void +export type HandleSubmitErrorFn = (errors: Object) => void /** Submit handler */ -export type HandleSubmit = (successFn:HandleSubmitSuccessFn, errorFn?:HandleSubmitErrorFn)=>void +export type HandleSubmit = (successFn: HandleSubmitSuccessFn, errorFn?: HandleSubmitErrorFn) => void export interface InterceptorParams { /** Name of the field that is currently about to be set*/ - name:string, + name: string, /** Value of the field that is currently about to be set */ - value:any, + value: any, /** Current form values */ - values: Record + values: Record /** Current form state */ formState: FormState /** Triggers the validation of a field */ - triggerValidation:TriggerValidation + triggerValidation: TriggerValidation /** Function to reset a field */ resetField: ResetField /** Function to reset the whole form */ resetForm: ResetForm - + /** Function to set an error on a field programmatically */ setError: SetError /** Function to clear one or more errors on a desired field or the whole form*/ - clearErrors:ClearErrors + clearErrors: ClearErrors /** Function that returns the modified values of the form */ modifiedValues: ModifiedValues } -export interface FormHandlerOptions{ +export interface FormHandlerOptions { /** Set to submit if validations are desired before sending the form */ validationBehaviour?: 'always' | 'submit' - - /** Set to all if we want to track all the validation errors a field could have */ - validationErrors?: 'first' | 'all' } export interface FormHandlerParams { /** Values to initialize the form */ - initialValues?: Record + initialValues?: Record /** Field change interceptor */ - interceptor?:(_:InterceptorParams)=>Promise + interceptor?: (_: InterceptorParams) => Promise /** Validation function to execute before submitting (when using this individual validations are invalidated) */ - validate?:()=>Promise|boolean + validate?: () => Promise | boolean /** Options for the form handler */ - options?:FormHandlerOptions + options?: FormHandlerOptions } -export interface FormHandlerReturn { +export interface FormHandlerReturn { /** Current form values */ - values: Record + values: Record /** Current form state */ formState: FormState /** Triggers the validation of a field */ - triggerValidation:TriggerValidation + triggerValidation: TriggerValidation /** Function to reset a field */ resetField: ResetField /** Function to reset the whole form */ resetForm: ResetForm - + /** Function to set an error on a field programmatically */ setError: SetError /** Function to clear one or more errors on a desired field or the whole form*/ - clearErrors:ClearErrors + clearErrors: ClearErrors /** Function that returns the modified values of the form */ modifiedValues: ModifiedValues @@ -218,7 +215,7 @@ export interface FormHandlerReturn { setValue: SetValue /** Function to clear a desired field*/ - clearField:ClearField + clearField: ClearField /** Method to register a field and make it interact with the current form */ register: Register @@ -228,4 +225,4 @@ export interface FormHandlerReturn { } /** Form handler solution as a composable function */ -export type FormHandler = (_?:FormHandlerParams)=>FormHandlerReturn +export type FormHandler = (_?: FormHandlerParams) => FormHandlerReturn diff --git a/src/useFormHandler.ts b/src/useFormHandler.ts index 2541f0a..b785b7c 100644 --- a/src/useFormHandler.ts +++ b/src/useFormHandler.ts @@ -1,7 +1,11 @@ +import { DEFAULT_FIELD_VALUE } from './constants'; import { ModifiedValues, TriggerValidation, Validations, FormHandler, ResetField, ResetForm, InitControl, SetError, ClearErrors, SetValue, ClearField, SetDirty, SetTouched, HandleBlur, HandleChange, GetInitValueForControl } from './types'; import { FormState, HandleSubmit, Register } from './types'; import { reactive, readonly, watch } from 'vue' import { isEqual } from 'lodash-es' +import { getNativeFieldValue } from './logic'; +import { isCheckboxInput, isRadioInput, isMultipleSelect, isNativeControl, isDefined } from './utils'; +import getDefaultFieldValue from './logic/getDefaultFieldValue'; export const initialState = () => ({ touched: {}, @@ -13,21 +17,24 @@ export const initialState = () => ({ }) //TODO: separate synchronous from asynchronous validations -//TODO: extend validation behaviours -const useFormHandler:FormHandler = ({ - initialValues = {}, - interceptor, +//TODO: extend validation behaviors +const useFormHandler: FormHandler = ({ + initialValues = {}, + interceptor, validate, - options = { validationBehaviour: 'always', validationErrors: 'first' } + // TODO: if no further options, validation behavior should be considered along with the base params + options = { validationBehaviour: 'always' } } = {}) => { - const values:Record = reactive({ ...initialValues }) - const formState = reactive({...initialState()}) + const values: Record = reactive({ ...initialValues }) + const formState = reactive({ ...initialState() }) - let validations:Record = {} - let defaultValues:Record = {} + let validations: Record = {} + let defaultValues: Record = {} + let refs: Record = {} - const getInitValueForControl:GetInitValueForControl = (name) => initialValues[name] ?? (defaultValues[name] ?? null) - const initControl:InitControl = (name, options) => { + const getDefaultValueForControl = (name: string) => defaultValues[name] ?? getDefaultFieldValue(refs[name]) + const getInitValueForControl: GetInitValueForControl = (name) => initialValues[name] ?? getDefaultValueForControl(name) + const initControl: InitControl = (name, options) => { validations = { ...validations, [name]: options.validations || {} @@ -36,8 +43,8 @@ const useFormHandler:FormHandler = ({ ...defaultValues, [name]: options.defaultValue } - if(initialValues[name] === undefined && values[name] === undefined){ - values[name] = options.defaultValue ?? null + if (initialValues[name] === undefined && values[name] === undefined) { + values[name] = getDefaultValueForControl(name) } } @@ -45,112 +52,126 @@ const useFormHandler:FormHandler = ({ formState.isValid = !Object.keys(formState.errors).some(field => Object.keys(formState.errors[field]).length) } - const triggerValidation:TriggerValidation = async (name) => { - if(!Object.keys(validations).length){ + const clearErrors: ClearErrors = (name, errors) => { + if (!name) { + formState.errors = {} + return + } + if (!errors) { + delete formState.errors[name] return } - if(!name){ - for(const field of Object.keys(values)){ + formState.errors[name] = Object.fromEntries(Object + .entries(formState.errors[name]) + .filter(([errorName]) => Array.isArray(errors) + ? !errors.includes(errorName) + : errors !== errorName)) + + if (Object.entries(formState.errors[name]).length < 1) { + delete formState.errors[name] + } + } + + const triggerValidation: TriggerValidation = async (name) => { + if (!Object.keys(validations).length) { + return + } + if (!name) { + for (const field of Object.keys(values)) { await triggerValidation(field) } return } - if(!Object.keys(validations[name]).length){ + if (!Object.keys(validations[name]).length) { return } - formState.errors[name] = {} - for(const [validationName, validation] of Object.entries(validations[name])){ + for (const [validationName, validation] of Object.entries(validations[name])) { const result = await validation(values[name]) formState.errors[name] = { - ...formState.errors[name], - ...(result !== true && {[validationName]: result}) + ...(result !== true && { [validationName]: result }) } - if(result !== true && options.validationErrors !== 'all'){ + if (result !== true) { break; } + //TODO: improve this + clearErrors(name) } } - const setDirty:SetDirty = (name, dirty) => { - if(formState.dirty[name] !== dirty){ - formState.dirty[name] = dirty - formState.isDirty = dirty || Object.values(formState.dirty).some(Boolean) + const setDirty: SetDirty = (name, dirty) => { + if (formState.dirty[name] !== dirty) { + if (dirty) { + formState.dirty[name] = true + formState.isDirty = true + return + } + delete formState.dirty[name] + formState.isDirty = Object.values(formState.touched).some(Boolean) } } - const setTouched:SetTouched = (name, touched) => { - if(formState.touched[name] !== touched){ - formState.touched[name] = touched - formState.isTouched = touched || Object.values(formState.touched).some(Boolean) + const setTouched: SetTouched = (name, touched) => { + if (formState.touched[name] !== touched) { + if (touched) { + formState.touched[name] = true + formState.isTouched = true + return + } + delete formState.touched[name] + formState.isTouched = Object.values(formState.touched).some(Boolean) } } - const resetField:ResetField = (name) => { + const resetField: ResetField = (name) => { values[name] = getInitValueForControl(name) setTouched(name, false) setDirty(name, false) } - const resetForm:ResetForm = () => { - Object.keys(values).forEach((key)=>{ + const resetForm: ResetForm = () => { + Object.keys(values).forEach((key) => { resetField(key) }) } - const setError:SetError = (name, error, replace = false) => { + const setError: SetError = (name, error, replace = false) => { formState.errors[name] = { ...(!replace && formState.errors[name]), ...error } } - const clearErrors:ClearErrors = (name, errors) => { - if(!name){ - formState.errors = {} - return; - } - if(!errors){ - formState.errors[name] = {} - return; - } - formState.errors[name] = Object.fromEntries(Object - .entries(formState.errors[name]) - .filter(([errorName])=> Array.isArray(errors) - ? !errors.includes(errorName) - : errors !== errorName)) - } - - const modifiedValues:ModifiedValues = () => { + const modifiedValues: ModifiedValues = () => { return Object.fromEntries(Object .entries(values) .filter(([name]) => formState.dirty[name])) } - const setValue:SetValue = async (name, value = null) => { - if(!interceptor || await interceptor({name, value, values, formState, clearErrors, modifiedValues, resetField, resetForm, setError, triggerValidation})) { + const setValue: SetValue = async (name, value = DEFAULT_FIELD_VALUE) => { + if (!interceptor || await interceptor({ name, value, values, formState, clearErrors, modifiedValues, resetField, resetForm, setError, triggerValidation })) { values[name] = value setDirty(name, !isEqual(value, getInitValueForControl(name))) } } - const handleBlur:HandleBlur = (name) => { - setTouched(name,true) + const handleBlur: HandleBlur = (name) => { + setTouched(name, true) triggerValidation(name) } - const handleChange:HandleChange = async (name, value = null) => { + const handleChange: HandleChange = async (name, value = DEFAULT_FIELD_VALUE) => { await setValue(name, value) - setTouched(name,true) + setTouched(name, true) triggerValidation(name) } - const clearField:ClearField = async (name) => { - await setValue(name, defaultValues[name] ?? null) + const clearField: ClearField = async (name) => { + await setValue(name, getDefaultValueForControl(name)) } - const register:Register = (name, options = {}) => { - initControl(name,options); - return({ + const register: Register = (name, options = {}) => { + initControl(name, options); + return ({ name, errors: Object.values(formState.errors[name] || {}) || [], onBlur: () => handleBlur(name), @@ -158,32 +179,70 @@ const useFormHandler:FormHandler = ({ isDirty: !!formState.dirty[name], isTouched: !!formState.touched[name], }), - ...(options.native !== true && { - modelValue: values[name], - 'onUpdate:modelValue': (value:any) => handleChange(name,value), - }), + modelValue: values[name], + 'onUpdate:modelValue': (value: any) => handleChange(name, value), + ref: (fieldRef: any) => { + if (!fieldRef || !fieldRef.nodeName || !isNativeControl(fieldRef)) { + refs = { + ...refs, + [name]: { + type: 'custom' + } + } + return + } + if (!refs[name] || (isRadioInput(fieldRef) && !refs[name].some((option: any) => option.value === fieldRef.value))) { + refs = { + ...refs, + [name]: isRadioInput(fieldRef) ? [...(refs[name] || []), fieldRef] : fieldRef + } + } + if (isRadioInput(fieldRef)) { + if (fieldRef.checked) { + values[name] = fieldRef.value + return + } + fieldRef.checked = (values[name] === fieldRef.value) + return + } + if (isCheckboxInput(fieldRef)) { + if (isDefined(fieldRef.checked)) { + values[name] = !!fieldRef.checked + return + } + fieldRef.checked = !!values[name] + return + } + if (isMultipleSelect(fieldRef)) { + [...fieldRef.options].forEach((option: any, index) => { + fieldRef[index].selected = !!values[name]?.includes(option.value) + }) + return + } + fieldRef.value = values[name] + }, ...(options.native !== false && { - value: values[name], - onInput: (el:any) => handleChange(name,el && (el.target && el.target.value)), - ...(options.required && {required: true}) + onChange: () => handleChange(name, getNativeFieldValue(refs[name] as HTMLInputElement)), + ...(options.required && { required: true }) }), - ...(options.clearable && {onClear: () => resetField(name)}) - })} + ...(options.clearable && { onClear: () => resetField(name) }) + }) + } - const handleSubmit:HandleSubmit = async (successFn, errorFn) => { + const handleSubmit: HandleSubmit = async (successFn, errorFn) => { let valid = false; - if(validate){ + if (validate) { valid = await validate() } - else{ + else { await triggerValidation() valid = formState.isValid } - if(valid){ + if (valid) { successFn(values) return } - if(errorFn){ + if (errorFn) { errorFn(formState.errors) return } @@ -191,10 +250,10 @@ const useFormHandler:FormHandler = ({ } watch( - ()=> formState.errors, - ()=> { - isValid(); - }, {deep:true, immediate:true}) + () => formState.errors, + () => { + isValid(); + }, { deep: true, immediate: true }) return { values: readonly(values), diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..ace2176 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,5 @@ +export { default as isCheckboxInput } from './isCheckboxInput' +export { default as isNativeControl } from './isNativeControl' +export { default as isRadioInput } from './isRadioInput' +export { default as isMultipleSelect } from './isMultipleSelect' +export { default as isDefined } from './isDefined' \ No newline at end of file diff --git a/src/utils/isCheckboxInput.ts b/src/utils/isCheckboxInput.ts new file mode 100644 index 0000000..33e3134 --- /dev/null +++ b/src/utils/isCheckboxInput.ts @@ -0,0 +1,2 @@ +export default (element: HTMLInputElement) => + element.type === 'checkbox'; diff --git a/src/utils/isDefined.ts b/src/utils/isDefined.ts new file mode 100644 index 0000000..249f489 --- /dev/null +++ b/src/utils/isDefined.ts @@ -0,0 +1 @@ +export default (element: unknown) => element !== undefined \ No newline at end of file diff --git a/src/utils/isMultipleSelect.ts b/src/utils/isMultipleSelect.ts new file mode 100644 index 0000000..250027c --- /dev/null +++ b/src/utils/isMultipleSelect.ts @@ -0,0 +1,2 @@ +export default (element: HTMLInputElement) => + element.type === `select-multiple`; diff --git a/src/utils/isNativeControl.ts b/src/utils/isNativeControl.ts new file mode 100644 index 0000000..87ba065 --- /dev/null +++ b/src/utils/isNativeControl.ts @@ -0,0 +1,2 @@ +export default (element: HTMLElement) => + ['input', 'select', 'textarea'].includes(element.nodeName.toLowerCase()) diff --git a/src/utils/isRadioInput.ts b/src/utils/isRadioInput.ts new file mode 100644 index 0000000..eec4495 --- /dev/null +++ b/src/utils/isRadioInput.ts @@ -0,0 +1,2 @@ +export default (element: HTMLInputElement) => + element.type === 'radio'; \ No newline at end of file diff --git a/test/useFormHandler.test.ts b/test/useFormHandler.test.ts index 19f0715..6c38384 100644 --- a/test/useFormHandler.test.ts +++ b/test/useFormHandler.test.ts @@ -1,29 +1,29 @@ import useFormHandler, { initialState } from '../src/useFormHandler'; import { expect, it, describe } from "vitest" -const sleep = () => new Promise((resolve) => setTimeout(()=> resolve(true),50)) +const sleep = () => new Promise((resolve) => setTimeout(() => resolve(true), 50)) describe('Form handler testing', () => { it('Initial form state and values', () => { - const {values, formState} = useFormHandler(); + const { values, formState } = useFormHandler(); expect(values).toStrictEqual({}) - expect(formState).toStrictEqual({...initialState()}) + expect(formState).toStrictEqual({ ...initialState() }) }) it('Initial values should be applied without', () => { const initialValues = { field: 'test' } - const {values} = useFormHandler({initialValues}); + const { values } = useFormHandler({ initialValues }); expect(values).toStrictEqual(initialValues) }) it('Setting a value programmatically', async () => { - const {values, setValue, formState} = useFormHandler(); + const { values, setValue, formState } = useFormHandler(); await setValue('field', 'oneTwoThree'); expect(values.field).toBe('oneTwoThree') expect(formState.isDirty).toBeTruthy() }) it('Clearing a field programmatically', async () => { - const {values, setValue, formState, clearField} = useFormHandler(); + const { values, setValue, formState, clearField } = useFormHandler(); await setValue('field', 'oneTwoThree'); expect(values.field).toBe('oneTwoThree') expect(formState.isDirty).toBeTruthy() @@ -32,8 +32,8 @@ describe('Form handler testing', () => { expect(formState.isDirty).toBeFalsy() }) it('Clearing an initialized field leaves it dirty', async () => { - const {values, formState, clearField} = useFormHandler({ - initialValues:{ field: 'value' } + const { values, formState, clearField } = useFormHandler({ + initialValues: { field: 'value' } }); expect(values.field).toBe('value') expect(formState.isDirty).toBeFalsy() @@ -42,38 +42,38 @@ describe('Form handler testing', () => { expect(formState.isDirty).toBeTruthy() }) it('Setting an error programmatically', async () => { - const {formState,setError} = useFormHandler(); - setError('field', {error: 'some error'}) - expect(formState.errors).toStrictEqual({field:{error: 'some error'}}) + const { formState, setError } = useFormHandler(); + setError('field', { error: 'some error' }) + expect(formState.errors).toStrictEqual({ field: { error: 'some error' } }) await sleep() expect(formState.isValid).toBeFalsy() }) it('Clearing one error of a control programmatically', async () => { - const {formState,setError,clearErrors} = useFormHandler(); - const errors = {error: 'some error' , error2: 'some other error'} + const { formState, setError, clearErrors } = useFormHandler(); + const errors = { error: 'some error', error2: 'some other error' } setError('field', errors) await sleep() expect(formState.isValid).toBeFalsy() clearErrors('field', 'error') - expect(formState.errors.field).toStrictEqual({error2: 'some other error'}) + expect(formState.errors.field).toStrictEqual({ error2: 'some other error' }) await sleep() expect(formState.isValid).toBeFalsy() }) it('Clearing all errors of a control programmatically', async () => { - const {formState,setError,clearErrors} = useFormHandler(); - const errors = {error: 'some error' , error2: 'some other error'} + const { formState, setError, clearErrors } = useFormHandler(); + const errors = { error: 'some error', error2: 'some other error' } setError('field', errors) await sleep() expect(formState.isValid).toBeFalsy() clearErrors('field') - expect(formState.errors.field).toStrictEqual({}) + expect(formState.errors.field).toBeUndefined() await sleep() expect(formState.isValid).toBeTruthy() }) it('Clearing all errors of the form programmatically', async () => { - const {formState,setError,clearErrors} = useFormHandler(); - const errorField1 = {error1: 'some error'} - const errorField2 = {error2: 'some error'} + const { formState, setError, clearErrors } = useFormHandler(); + const errorField1 = { error1: 'some error' } + const errorField2 = { error2: 'some error' } setError('field1', errorField1) setError('field2', errorField2) expect(formState.errors.field1).toStrictEqual(errorField1) @@ -87,8 +87,8 @@ describe('Form handler testing', () => { expect(formState.isValid).toBeTruthy() }) it('Resetting a field it back to its initial values and state', async () => { - const {values, formState, resetField, setValue} = useFormHandler({ - initialValues:{ field: 'value' } + const { values, formState, resetField, setValue } = useFormHandler({ + initialValues: { field: 'value' } }); expect(values.field).toBe('value') expect(formState.isDirty).toBeFalsy() @@ -99,20 +99,20 @@ describe('Form handler testing', () => { expect(values.field).toBe('value') expect(formState.isDirty).toBeFalsy() }) - it('Expecting modifiedValues to work', async() => { - const {modifiedValues, setValue} = useFormHandler({ - initialValues:{ - field:'something' + it('Expecting modifiedValues to work', async () => { + const { modifiedValues, setValue } = useFormHandler({ + initialValues: { + field: 'something' } }); await setValue('field2', 'another thing') - expect(modifiedValues()).toStrictEqual({field2:'another thing'}) + expect(modifiedValues()).toStrictEqual({ field2: 'another thing' }) }) }) describe('Register function testing', () => { it('Registering a field', () => { - const {values, register} = useFormHandler(); + const { values, register } = useFormHandler(); const field = register('field') expect(field.name).toBe('field') expect(field.errors).toStrictEqual([]) @@ -120,66 +120,62 @@ describe('Register function testing', () => { expect(field.isDirty).toBeUndefined() expect(field.isTouched).toBeUndefined() expect(field.onClear).toBeUndefined() - expect(field.value).toBe(null) - expect(field.onInput).toBeDefined() + expect(field.onChange).toBeDefined() expect(field.modelValue).toBe(null) expect(field['onUpdate:modelValue']).toBeDefined() expect(values.field).toBe(null) }) - it('Specified native field shouldn\'t have custom handlers', () => { - const {register} = useFormHandler(); - const field = register('field', {native:true}) - expect(field.value).toBeDefined() - expect(field.onInput).toBeDefined() - expect(field.modelValue).toBeUndefined() - expect(field['onUpdate:modelValue']).toBeUndefined() + it('Specified native field should have native handlers', () => { + const { register } = useFormHandler(); + const field = register('field', { native: true }) + expect(field.ref).toBeDefined() + expect(field.onChange).toBeDefined() }) it('Specified custom field shouldn\'t have native handlers', () => { - const {register} = useFormHandler(); - const field = register('field', {native:false}) - expect(field.value).toBeUndefined() - expect(field.onInput).toBeUndefined() + const { register } = useFormHandler(); + const field = register('field', { native: false }) + expect(field.onChange).toBeUndefined() expect(field.modelValue).toBeDefined() expect(field['onUpdate:modelValue']).toBeDefined() }) it('Input registered with details receives dirty and touched states', () => { - const {register} = useFormHandler(); - const field = register('field', {withDetails:true}) + const { register } = useFormHandler(); + const field = register('field', { withDetails: true }) expect(field.isDirty).toBeDefined() expect(field.isTouched).toBeDefined() }) it('Clearable inputs should have a bound clear handler', () => { - const {register} = useFormHandler(); - const field = register('field', {clearable:true}) + const { register } = useFormHandler(); + const field = register('field', { clearable: true }) expect(field.onClear).toBeDefined() }) it('Registering a field with default value', () => { - const {values, register} = useFormHandler(); - register('field', {defaultValue: 'something'}) + const { values, register } = useFormHandler(); + register('field', { defaultValue: 'something' }) expect(values.field).toBe('something') }) - it('Registered validations work on update via handler', async() => { - const {values, register, formState} = useFormHandler(); + it('Registered validations work on update via handler', async () => { + const { values, register, formState } = useFormHandler(); const field = register('field', { validations: { - error: (val) => val !== 'error' || 'Error with your field' + error: (val) => val !== 'error' || 'Error with your field' } }) - if(field['onUpdate:modelValue']){ + if (field['onUpdate:modelValue']) { field['onUpdate:modelValue']('error') await sleep() expect(values.field).toBe('error') expect(formState.isValid).toBeFalsy() } }) - it('Registered validations work on update via setter', async() => { - const {values, register, formState ,setValue, triggerValidation} = useFormHandler(); + it('Registered validations work on update via setter', async () => { + const { values, register, formState, setValue, triggerValidation } = useFormHandler(); register('field', { validations: { - error: (val) => val !== 'error' || 'Error with your field' + error: (val) => val !== 'error' || 'Error with your field' } }) - await setValue('field','error') + await setValue('field', 'error') await triggerValidation('field') expect(values.field).toBe('error') expect(formState.isValid).toBeFalsy() diff --git a/vite.config.ts b/vite.config.ts index 986947d..85680ca 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,7 +2,7 @@ /// import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' -import {resolve} from 'path' +import { resolve } from 'path' import dts from 'vite-plugin-dts' // https://vitejs.dev/config/ @@ -12,7 +12,8 @@ export default defineConfig({ }, test: { includeSource: ['test/*'], - environment: 'happy-dom' + environment: 'happy-dom', + reporters: 'verbose', }, build: { lib: {