From 973c6fe2c9bcc4f2becda1e7e7f79349aa6e398c Mon Sep 17 00:00:00 2001 From: dbssman Date: Sat, 2 Sep 2023 12:25:57 +0200 Subject: [PATCH] Improve type clarity and dx --- docs/api/use-form-handler/build.md | 7 +- docs/api/use-form-handler/modified-values.md | 7 +- docs/api/use-form-handler/register.md | 25 ++++ docs/api/use-form-handler/values.md | 2 +- src/logic/index.ts | 2 - src/logic/refFn.ts | 89 +++++++------- src/logic/transformValidations.ts | 59 +++++---- src/logic/validateField.ts | 31 ----- src/logic/validateForm.ts | 8 -- src/types/formHandler.ts | 15 +-- src/types/validations.ts | 14 +-- src/useFormHandler.ts | 119 +++++++++++++------ src/utils/objectKeys.ts | 4 +- src/utils/pattern.ts | 7 +- test/useFormHandler.test.ts | 4 +- 15 files changed, 211 insertions(+), 182 deletions(-) delete mode 100644 src/logic/validateField.ts delete mode 100644 src/logic/validateForm.ts diff --git a/docs/api/use-form-handler/build.md b/docs/api/use-form-handler/build.md index 37cb7b3..48c9de7 100644 --- a/docs/api/use-form-handler/build.md +++ b/docs/api/use-form-handler/build.md @@ -75,7 +75,10 @@ Notice how the template looks much cleaner with this approach, and this helps us ## Type Declarations ```ts -export type Build = >( +export type Build = Record> = < + TBuild extends Partial>, +>( configuration: TBuild | Ref | ComputedRef -) => ComputedRef>> +) => ComputedRef>>> + ``` diff --git a/docs/api/use-form-handler/modified-values.md b/docs/api/use-form-handler/modified-values.md index 3f604f7..e9d238e 100644 --- a/docs/api/use-form-handler/modified-values.md +++ b/docs/api/use-form-handler/modified-values.md @@ -1,6 +1,6 @@ # modifiedValues -Returns the current modified fields of the form. +Computed property that holds the current modified values of the form e.g values that are different from initial value if any initial value was indicated or different than the default value for the field if no initial values were given. ## Demo @@ -15,7 +15,7 @@ Coming soon...
-            {{ modifiedValues() }} //should be initially
+            {{ modifiedValues }} //should initially be an empty object
         
@@ -41,5 +41,6 @@ Let's say your form is initialized as above, because you're editing an existing
 ## Type Declarations
 
 ```ts
-export type ModifiedValues = () => TModified
+type ModifiedValues = ComputedRef>// T being the form interface
+
 ```
diff --git a/docs/api/use-form-handler/register.md b/docs/api/use-form-handler/register.md
index 82d4b40..1d5573f 100644
--- a/docs/api/use-form-handler/register.md
+++ b/docs/api/use-form-handler/register.md
@@ -243,6 +243,31 @@ Custom validations are kept very simple, can be synchronous or asynchronous. We
 ## Type Declarations
 
 ```ts
+
+interface ValidationWithMessage {
+  value: number | string | RegExp
+  message: string
+}
+
+interface ValidationsConfiguration {
+  required?: boolean | string
+  min?: number | ValidationWithMessage
+  max?: number | ValidationWithMessage
+  minLength?: number | ValidationWithMessage
+  maxLength?: number | ValidationWithMessage
+  pattern?: string | RegExp | ValidationWithMessage
+}
+
+export interface RegisterOptions extends ValidationsConfiguration {
+  native?: boolean
+  defaultValue?: any
+  validate?: Validations
+  withDetails?: boolean
+  disabled?: boolean
+  useNativeValidation?: boolean
+  dependentFields?: string[]
+}
+
 export type Register = (
   name: keyof T,
   options?: RegisterOptions
diff --git a/docs/api/use-form-handler/values.md b/docs/api/use-form-handler/values.md
index 8da6f96..7f212ed 100644
--- a/docs/api/use-form-handler/values.md
+++ b/docs/api/use-form-handler/values.md
@@ -40,5 +40,5 @@ const { register, values } = useFormHandler()
 ## Type Declarations
 
 ```ts
-export type Values = T
+export type Values = Partial
 ```
diff --git a/src/logic/index.ts b/src/logic/index.ts
index cb1ebcc..8ef82a9 100644
--- a/src/logic/index.ts
+++ b/src/logic/index.ts
@@ -1,6 +1,4 @@
 export { default as getNativeFieldValue } from './getNativeFieldValue'
 export { default as getDefaultFieldValue } from './getDefaultFieldValue'
-export { default as validateField } from './validateField'
-export { default as validateForm } from './validateForm'
 export { default as refFn } from './refFn'
 export { default as transformValidations } from './transformValidations'
diff --git a/src/logic/refFn.ts b/src/logic/refFn.ts
index bf62c15..fa8af91 100644
--- a/src/logic/refFn.ts
+++ b/src/logic/refFn.ts
@@ -6,52 +6,55 @@ import {
   isRadioInput,
 } from '../utils'
 
-export default (name: keyof T, _refs: Refs, values: T) => (fieldRef: any) => {
-  if (!fieldRef) {
-    delete _refs[name]
-    return
-  }
-  if (fieldRef.$el || !fieldRef.nodeName || !isNativeControl(fieldRef)) {
-    //TODO: Correctly type this in order to expect a fixed data structure
-    _refs[name].ref = {
-      type: 'custom',
+export default (name: keyof T, _refs: Refs, values: Partial) =>
+  (fieldRef: any) => {
+    if (!fieldRef) {
+      delete _refs[name]
+      return
     }
-    return
-  }
-  const isFirstRegister =
-    _refs[name] &&
-    (!_refs[name].ref ||
-      (Array.isArray(_refs[name].ref) &&
-        isRadioInput(fieldRef) &&
-        !(_refs[name].ref as FieldReference[]).some(
-          (option: any) => option.value === fieldRef.value
-        )))
-  if (isFirstRegister) {
-    _refs[name].ref = isRadioInput(fieldRef)
-      ? [...((_refs[name].ref as FieldReference[]) || []), fieldRef]
-      : fieldRef
-  }
-  if (isRadioInput(fieldRef)) {
-    if (isFirstRegister && !!fieldRef.checked) {
-      values[name] = fieldRef.value
+    if (fieldRef.$el || !fieldRef.nodeName || !isNativeControl(fieldRef)) {
+      //TODO: Correctly type this in order to expect a fixed data structure
+      _refs[name].ref = {
+        type: 'custom',
+      }
       return
     }
-    fieldRef.checked = values[name] === fieldRef.value
-    return
-  }
-  if (isCheckboxInput(fieldRef)) {
-    if (isFirstRegister && !!fieldRef.checked) {
-      values[name] = true as any
+    const isFirstRegister =
+      _refs[name] &&
+      (!_refs[name].ref ||
+        (Array.isArray(_refs[name].ref) &&
+          isRadioInput(fieldRef) &&
+          !(_refs[name].ref as FieldReference[]).some(
+            (option: any) => option.value === fieldRef.value
+          )))
+    if (isFirstRegister) {
+      _refs[name].ref = isRadioInput(fieldRef)
+        ? [...((_refs[name].ref as FieldReference[]) || []), fieldRef]
+        : fieldRef
+    }
+    if (isRadioInput(fieldRef)) {
+      if (isFirstRegister && !!fieldRef.checked) {
+        values[name] = fieldRef.value
+        return
+      }
+      fieldRef.checked = values[name] === fieldRef.value
       return
     }
-    fieldRef.checked = !!values[name]
-    return
-  }
-  if (isMultipleSelect(fieldRef)) {
-    ;[...fieldRef.options].forEach((option: any, index) => {
-      fieldRef[index].selected = !!((values[name] as any)?.includes(option.value))
-    })
-    return
+    if (isCheckboxInput(fieldRef)) {
+      if (isFirstRegister && !!fieldRef.checked) {
+        values[name] = true as any
+        return
+      }
+      fieldRef.checked = !!values[name]
+      return
+    }
+    if (isMultipleSelect(fieldRef)) {
+      ;[...fieldRef.options].forEach((option: any, index) => {
+        fieldRef[index].selected = !!(values[name] as any)?.includes(
+          option.value
+        )
+      })
+      return
+    }
+    fieldRef.value = values[name]
   }
-  fieldRef.value = values[name]
-}
diff --git a/src/logic/transformValidations.ts b/src/logic/transformValidations.ts
index 0b5b02a..8f64975 100644
--- a/src/logic/transformValidations.ts
+++ b/src/logic/transformValidations.ts
@@ -5,6 +5,22 @@ import {
 } from '../types'
 import { max, maxLength, min, minLength, pattern, required } from '../utils'
 
+const isValidationWithMessage = (
+  validation: any
+): validation is ValidationWithMessage => {
+  return (
+    typeof validation === 'object' &&
+    validation !== null &&
+    'value' in validation &&
+    'message' in validation
+  )
+}
+
+const getValidationForConfiguration = (validation: any, fn: Function) =>
+  isValidationWithMessage(validation)
+    ? fn(validation.value, validation.message)
+    : fn(validation)
+
 export default (validations: ValidationsConfiguration = {}): Validations => {
   return {
     ...(validations.required && {
@@ -14,46 +30,25 @@ export default (validations: ValidationsConfiguration = {}): Validations => {
           : required(),
     }),
     ...(validations.min && {
-      min:
-        typeof validations.min === 'number'
-          ? min(validations.min)
-          : min(validations.min.value as number, validations.min.message),
+      min: getValidationForConfiguration(validations.min, min),
     }),
     ...(validations.max && {
-      max:
-        typeof validations.max === 'number'
-          ? max(validations.max)
-          : max(validations.max.value as number, validations.max.message),
+      max: getValidationForConfiguration(validations.max, max),
     }),
     ...(validations.minLength && {
-      minLength:
-        typeof validations.minLength === 'number'
-          ? minLength(validations.minLength)
-          : minLength(
-              validations.minLength.value as number,
-              validations.minLength.message
-            ),
+      minLength: getValidationForConfiguration(
+        validations.minLength,
+        minLength
+      ),
     }),
     ...(validations.maxLength && {
-      maxLength:
-        typeof validations.maxLength === 'number'
-          ? maxLength(validations.maxLength)
-          : maxLength(
-              validations.maxLength.value as number,
-              validations.maxLength.message
-            ),
+      maxLength: getValidationForConfiguration(
+        validations.maxLength,
+        maxLength
+      ),
     }),
     ...(validations.pattern && {
-      pattern: !(validations.pattern as ValidationWithMessage)?.value
-        ? pattern(
-            (typeof validations.pattern === 'string'
-              ? new RegExp(validations.pattern)
-              : validations.pattern) as RegExp
-          )
-        : pattern(
-            (validations.pattern as ValidationWithMessage).value as RegExp,
-            (validations.pattern as ValidationWithMessage).message as string
-          ),
+      pattern: getValidationForConfiguration(validations.pattern, pattern),
     }),
   }
 }
diff --git a/src/logic/validateField.ts b/src/logic/validateField.ts
deleted file mode 100644
index e8c143b..0000000
--- a/src/logic/validateField.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { FormState } from "../types"
-
-export interface ValidateField{
-  name: keyof T
-  values: T
-  formState: FormState
-  _refs: any
-} 
-
-//TODO: review this
-export default async ({
-  name,
-  values,
-  formState,
-  _refs,
-}:ValidateField): Promise => {
-  if (!Object.keys(_refs[name]?._validations ?? {}).length) {
-    return
-  }
-  if (_refs[name]._disabled) {
-    return
-  }
-  for (const validation of Object.values(_refs[name]._validations)) {
-    const result = await (validation as any)(values[name])
-    if (result !== true) {
-      formState.errors[name] = result
-      break
-    }
-    delete formState.errors[name]
-  }
-}
diff --git a/src/logic/validateForm.ts b/src/logic/validateForm.ts
deleted file mode 100644
index 135ae91..0000000
--- a/src/logic/validateForm.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import validateField, { ValidateField } from './validateField'
-
-//TODO: review this
-export default async >(params: Omit,'name'>) => {
-  for (const name of Object.keys(params.values)) {
-    await validateField({ ...params, name })
-  }
-}
diff --git a/src/types/formHandler.ts b/src/types/formHandler.ts
index ad915c7..378e44d 100644
--- a/src/types/formHandler.ts
+++ b/src/types/formHandler.ts
@@ -17,14 +17,14 @@ export type HandleSubmitErrorFn = (errors: FormState['errors']) => void
 export type HandleSubmitSuccessFn = (values: Record) => void
 
 export type Build = Record> = <
-  TBuild extends Record,
+  TBuild extends Partial>,
 >(
   configuration: TBuild | Ref | ComputedRef
 ) => ComputedRef>>>
 
 export interface PartialReturn {
   /** Current form values */
-  values: T
+  values: Partial
 
   /** Current form state */
   formState: FormState
@@ -51,7 +51,7 @@ export interface PartialReturn {
   clearField: (name: keyof T) => void
 
   /** Function that returns the modified values of the form */
-  modifiedValues: () => TModified
+  modifiedValues: ComputedRef>
 }
 
 export interface InterceptorParams extends PartialReturn {
@@ -87,18 +87,15 @@ export type SubmitValidation = (
 
 export type InjectionKey = string | Symbol
 
-export interface FormHandlerParams<
-  TValues extends Record,
-  TInitial extends TValues = TValues,
-> {
+export interface FormHandlerParams {
   /** Values to initialize the form */
   initialValues?: TInitial | Ref | ComputedRef
 
   /** Field change interceptor */
-  interceptor?: Interceptor
+  interceptor?: Interceptor
 
   /** Validation function to execute before submitting (when using this individual validations are invalidated) */
-  validate?: (values: TValues) => Promise | boolean
+  validate?: (values: Partial) => Promise | boolean
 
   /** Validation behavior options */
   validationMode?: 'onChange' | 'onBlur' | 'onSubmit' | 'always'
diff --git a/src/types/validations.ts b/src/types/validations.ts
index f8ab738..9e2c3f8 100644
--- a/src/types/validations.ts
+++ b/src/types/validations.ts
@@ -1,6 +1,6 @@
-export interface ValidationWithMessage {
+export interface ValidationWithMessage {
   /** Validation value */
-  value: number | string | RegExp
+  value: T
 
   /** Validation message */
   message: string
@@ -35,25 +35,25 @@ export interface ValidationsConfiguration {
   /** Min validation */
   /** If a number, the field must be greater than or equal to the number */
   /** If an object, the field must be greater than or equal to the object.value and the object.message is the validation message */
-  min?: number | ValidationWithMessage
+  min?: number | ValidationWithMessage
 
   /** Max validation */
   /** If a number, the field must be less than or equal to the number */
   /** If an object, the field must be less than or equal to the object.value and the object.message is the validation message */
-  max?: number | ValidationWithMessage
+  max?: number | ValidationWithMessage
 
   /** MinLength validation */
   /** If a number, the field must be greater than or equal to the number */
   /** If an object, the field must be greater than or equal to the object.value and the object.message is the validation message */
-  minLength?: number | ValidationWithMessage
+  minLength?: number | ValidationWithMessage
 
   /** MaxLength validation */
   /** If a number, the field must be less than or equal to the number */
   /** If an object, the field must be less than or equal to the object.value and the object.message is the validation message */
-  maxLength?: number | ValidationWithMessage
+  maxLength?: number | ValidationWithMessage
 
   /** Pattern validation */
   /** If a RegExp, the field must match the RegExp */
   /** If an object, the field must match the object.value and the object.message is the validation message */
-  pattern?: string | RegExp | ValidationWithMessage
+  pattern?: string | RegExp | ValidationWithMessage
 }
diff --git a/src/useFormHandler.ts b/src/useFormHandler.ts
index cfc581e..8f2b74b 100644
--- a/src/useFormHandler.ts
+++ b/src/useFormHandler.ts
@@ -24,8 +24,6 @@ import {
 import { isEqual } from 'lodash-es'
 import {
   getNativeFieldValue,
-  validateField,
-  validateForm,
   getDefaultFieldValue,
   refFn,
   transformValidations,
@@ -43,29 +41,36 @@ export const initialState = () => ({
 
 type Refs = Record
 
+interface ValidateField {
+  name: keyof T
+  values: T
+  formState: FormState
+  _refs: any
+}
+
 export const useFormHandler = <
-  T extends Record = Record,
-  R extends T = T,
+  TForm extends Record = Record,
+  TInitial extends Partial = Partial,
 >({
-  initialValues = {} as R,
+  initialValues = {} as TInitial,
   interceptor,
   validate,
   validationMode = 'onChange',
   injectionKey = defaultInjectionKey,
-}: FormHandlerParams = {}) => {
-  const values: T = reactive({ ...unref(initialValues) })
+}: FormHandlerParams = {}) => {
+  const values: Partial = reactive({ ...unref(initialValues) })
 
   const formState = reactive({
-    ...(initialState() as FormState),
-  }) as FormState
+    ...(initialState() as FormState),
+  }) as FormState
 
-  let _refs: Refs = {} as Refs
+  let _refs: Refs = {} as Refs
 
-  const _getDefault = (name: keyof T): T[keyof T] =>
+  const _getDefault = (name: keyof TForm): TForm[keyof TForm] =>
     _refs[name]?._defaultValue ?? getDefaultFieldValue(_refs[name]?.ref)
-  const _getInitial = (name: keyof T): any =>
+  const _getInitial = (name: keyof TForm): any =>
     unref(initialValues)?.[name] ?? _getDefault(name)
-  const _initControl = (name: keyof T, options: RegisterOptions) => {
+  const _initControl = (name: keyof TForm, options: RegisterOptions) => {
     const needsReset = options.disabled && _refs[name] && !_refs[name]._disabled
     _refs[name] = {
       ...(_refs[name] || {}),
@@ -101,10 +106,40 @@ export const useFormHandler = <
     formState.isTouched = Object.values(formState.touched).some(Boolean)
   }
 
-  const clearError = (name?: keyof T) => {
+  const _validateField = async ({
+    name,
+    values,
+    formState,
+    _refs,
+  }: ValidateField>) => {
+    if (!Object.keys(_refs[name]?._validations ?? {}).length) {
+      return
+    }
+    if (_refs[name]._disabled) {
+      return
+    }
+    for (const validation of Object.values(_refs[name]._validations)) {
+      const result = await (validation as any)(values[name])
+      if (result !== true) {
+        formState.errors[name] = result
+        break
+      }
+      delete formState.errors[name]
+    }
+  }
+
+  const _validateForm = async >(
+    params: Omit, 'name'>
+  ) => {
+    for (const name of Object.keys(params.values)) {
+      await _validateField({ ...params, name })
+    }
+  }
+
+  const clearError = (name?: keyof TForm) => {
     try {
       if (!name) {
-        formState.errors = {} as FormState['errors']
+        formState.errors = {} as FormState['errors']
         return
       }
       delete formState.errors[name]
@@ -113,19 +148,19 @@ export const useFormHandler = <
     }
   }
 
-  const triggerValidation = async (name?: keyof T) => {
+  const triggerValidation = async (name?: keyof TForm) => {
     try {
       if (!name) {
-        await validateForm({ formState, values, _refs })
+        await _validateForm({ formState, values, _refs })
         return
       }
-      await validateField({ formState, name, _refs, values })
+      await _validateField({ formState, name, _refs, values })
     } finally {
       _updateValidState()
     }
   }
 
-  const setDirty = (name: keyof T, dirty: boolean) => {
+  const setDirty = (name: keyof TForm, dirty: boolean) => {
     if (formState.dirty[name] !== dirty) {
       if (dirty) {
         formState.dirty[name] = true
@@ -137,7 +172,7 @@ export const useFormHandler = <
     }
   }
 
-  const setTouched = (name: keyof T, touched: boolean) => {
+  const setTouched = (name: keyof TForm, touched: boolean) => {
     if (formState.touched[name] !== touched) {
       if (touched) {
         formState.touched[name] = true
@@ -149,7 +184,7 @@ export const useFormHandler = <
     }
   }
 
-  const resetField = (name: keyof T) => {
+  const resetField = (name: keyof TForm) => {
     values[name] = _getInitial(name)
     setTouched(name, false)
     setDirty(name, false)
@@ -165,18 +200,21 @@ export const useFormHandler = <
     Object.assign(formState, initialState())
   }
 
-  const setError = (name: keyof T, error: string) => {
+  const setError = (name: keyof TForm, error: string) => {
     formState.errors[name] = error
     _updateValidState()
   }
 
-  const modifiedValues = (): TModified => {
+  const modifiedValues = computed(() => {
     return Object.fromEntries(
       Object.entries(values).filter(([name]) => formState.dirty[name])
-    ) as TModified
-  }
+    ) as Partial
+  })
 
-  const setValue = async (name: keyof T, value: any = DEFAULT_FIELD_VALUE) => {
+  const setValue = async (
+    name: keyof TForm,
+    value: any = DEFAULT_FIELD_VALUE
+  ) => {
     const field = _refs[name]
     if (
       !field?._disabled &&
@@ -192,7 +230,7 @@ export const useFormHandler = <
           setError,
           setValue,
           triggerValidation,
-          value: value as T[keyof T],
+          value: value as TForm[keyof TForm],
           values,
         })))
     ) {
@@ -220,14 +258,17 @@ export const useFormHandler = <
     }
   }
 
-  const handleBlur = (name: keyof T) => {
+  const handleBlur = (name: keyof TForm) => {
     setTouched(name, true)
     if (['always', 'onBlur'].includes(validationMode)) {
       triggerValidation(name)
     }
   }
 
-  const handleChange = async (name: keyof T, value = DEFAULT_FIELD_VALUE) => {
+  const handleChange = async (
+    name: keyof TForm,
+    value = DEFAULT_FIELD_VALUE
+  ) => {
     await setValue(name, value)
     setTouched(name, true)
     if (['always', 'onChange'].includes(validationMode)) {
@@ -235,7 +276,7 @@ export const useFormHandler = <
     }
   }
 
-  const clearField = async (name: keyof T) => {
+  const clearField = async (name: keyof TForm) => {
     const defaultValue: any = _getDefault(name)
     if (defaultValue !== values[name]) {
       await setValue(name, defaultValue)
@@ -243,7 +284,7 @@ export const useFormHandler = <
     }
   }
 
-  const unregister = (name: keyof T) => {
+  const unregister = (name: keyof TForm) => {
     delete _refs[name]
     delete values[name]
     delete formState.errors[name]
@@ -254,7 +295,7 @@ export const useFormHandler = <
     _updateValidState()
   }
 
-  const register = (name: keyof T, options: RegisterOptions = {}) => {
+  const register = (name: keyof TForm, options: RegisterOptions = {}) => {
     const {
       validate,
       defaultValue,
@@ -271,7 +312,7 @@ export const useFormHandler = <
       modelValue: values[name],
       'onUpdate:modelValue': async (value: any) =>
         await handleChange(name, value),
-      ref: refFn(name, _refs, values),
+      ref: refFn(name, _refs, values),
       onBlur: () => handleBlur(name),
       onClear: () => clearField(name),
       ...(disabled ? { disabled: true } : { disabled: undefined }),
@@ -304,12 +345,12 @@ export const useFormHandler = <
     }
   }
 
-  const build: Build = (configuration) => {
+  const build: Build = (configuration) => {
     const plainConfig = unref(configuration)
     const built = computed(() =>
       objectKeys(plainConfig).reduce(
         (acc, key) => {
-          acc[key] = register(key as unknown as keyof T, plainConfig[key])
+          acc[key] = register(key as unknown as keyof TForm, plainConfig[key])
           return acc
         },
         {} as Record<
@@ -330,11 +371,11 @@ export const useFormHandler = <
   }
 
   const handleSubmit = async (
-    successFn: HandleSubmitSuccessFn,
-    errorFn?: HandleSubmitErrorFn
+    successFn: HandleSubmitSuccessFn,
+    errorFn?: HandleSubmitErrorFn
   ) => {
     if (await isValidForm()) {
-      successFn(values)
+      successFn(values as TForm)
       return
     }
     if (errorFn) {
@@ -356,7 +397,7 @@ export const useFormHandler = <
     clearField,
     formState: readonly(formState),
     handleSubmit,
-    modifiedValues,
+    modifiedValues: readonly(modifiedValues),
     register,
     resetField,
     resetForm,
diff --git a/src/utils/objectKeys.ts b/src/utils/objectKeys.ts
index 69ffb9c..29da424 100644
--- a/src/utils/objectKeys.ts
+++ b/src/utils/objectKeys.ts
@@ -1 +1,3 @@
-export default (obj: T) => Object.keys(obj) as (keyof T)[]
+export default function objectKeys(obj: T) {
+  return Object.keys(obj) as (keyof T)[]
+}
diff --git a/src/utils/pattern.ts b/src/utils/pattern.ts
index a872bff..aa0688f 100644
--- a/src/utils/pattern.ts
+++ b/src/utils/pattern.ts
@@ -1,4 +1,4 @@
-import isNil from "./isNil"
+import isNil from './isNil'
 
 /**
  * @param pattern - RegExp pattern
@@ -10,8 +10,11 @@ import isNil from "./isNil"
  * pattern('abc') // true
  * pattern('123') // 'This field is invalid'
  */
-export default (pattern: RegExp, message = 'This field is invalid') =>
+export default (pattern: string | RegExp, message = 'This field is invalid') =>
   (value: any) => {
+    if (typeof pattern === 'string') {
+      pattern = new RegExp(pattern)
+    }
     if (!isNil(value) && !pattern.test(value)) {
       return message
     }
diff --git a/test/useFormHandler.test.ts b/test/useFormHandler.test.ts
index 107a1db..0519349 100644
--- a/test/useFormHandler.test.ts
+++ b/test/useFormHandler.test.ts
@@ -189,8 +189,8 @@ describe('useFormHandler()', () => {
           field: 'something',
         },
       })
-      await setValue('field2', 'another thing')
-      expect(modifiedValues()).toStrictEqual({ field2: 'another thing' })
+      await setValue('field', 'another thing')
+      expect(modifiedValues.value).toStrictEqual({ field: 'another thing' })
     })
     it('should register field properly via build', () => {
       const { build, values } = useFormHandler()