best-doctor/ke

View on GitHub
src/django-spa/cdk/Forms/Form.hook.ts

Summary

Maintainability
A
2 hrs
Test Coverage
B
85%
import { useCallback, useEffect, useState } from 'react'
import { isEqual } from '@utils/Types'

import { FieldKey, RecordData, FieldData, RootProviderDesc } from './Fields'
import { FormData, ValueData } from './types'
import { RecordValidator } from './Validation'

export function useForm<K extends FieldKey>(
  useRecord: (val: Record<K, unknown>, onChange: (val: RecordData<K>) => void) => RootProviderDesc,
  useValidation: (
    v: Record<K, unknown>,
    validator?: RecordValidator
  ) => { errorsRoot: RootProviderDesc; recursiveValidate: RecordValidator },
  value: Record<K, unknown>,
  onFormChange: (value: Record<K, unknown>, meta: Omit<FormData<K>, 'value'>) => void,
  validator?: RecordValidator
): UseFormResult {
  const { errorsRoot, recursiveValidate } = useValidation(value, validator)

  const [formValue, setFormValue] = useState((): ValueData<K> => makeDefaultForm(value))

  useEffect(() => {
    onFormChange(formValue.value, {
      relatedRefs: formValue.relatedRefs,
      isTouched: formValue.isTouched,
      validate: () => recursiveValidate(formValue.value),
    })
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [formValue, recursiveValidate]) // We explicitly do not want to change data on onFormChange changes

  useEffect(() => {
    setFormValue((prev) => (value === prev.value ? prev : replaceFormValue(prev, value)))
  }, [value])

  const handleChange = useCallback((record: RecordData<K>) => {
    setFormValue((prev) => {
      const changedValue = extract(record, 'value')
      const changed = {
        value: isEqual(prev.value, changedValue) ? prev.value : changedValue,
        relatedRefs: extract(record, 'relatedRef'),
        isTouched: !!Object.values(record).find((field) => (field as FieldData).isTouched),
      }
      return isEqual(prev, changed) ? prev : changed
    })
  }, [])

  return {
    valuesRoot: useRecord(value, handleChange),
    errorsRoot,
  }
}

function extract<K extends FieldKey, FK extends keyof FieldData>(
  data: RecordData<K>,
  fieldKey: FK
): Record<K, FieldData[FK]> {
  return Object.fromEntries(
    Object.entries(data).map(([key, field]) => [key, (field as FieldData)[fieldKey]])
  ) as Record<K, FieldData[FK]>
}

function makeDefaultForm<K extends FieldKey>(value: Record<K, unknown>): ValueData<K> {
  return {
    value,
    relatedRefs: Object.fromEntries(Object.keys(value).map((key) => [key, null])) as Record<K, null>,
    isTouched: false,
  }
}

function replaceFormValue<K extends FieldKey>(prev: ValueData<FieldKey>, value: Record<K, unknown>): ValueData<K> {
  const replaced = {
    value,
    relatedRefs: Object.fromEntries(Object.keys(value).map((key) => [key, prev.relatedRefs[key] || null])) as Record<
      K,
      null
    >,
    isTouched: false,
  }

  return isEqual(prev, replaced) ? prev : replaced
}

export interface UseFormResult {
  valuesRoot: RootProviderDesc
  errorsRoot: RootProviderDesc
}