formContext.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import {createContext, useCallback, useContext, useState} from 'react';
  2. interface FormState<FormFields extends Record<string, any>> {
  3. /**
  4. * State for each field in the form.
  5. */
  6. fields: {
  7. [K in keyof FormFields]: {
  8. hasChanged: boolean;
  9. initialValue: FormFields[K];
  10. onChange: (value: FormFields[K]) => void;
  11. value: FormFields[K];
  12. error?: string;
  13. };
  14. };
  15. /**
  16. * Whether the form has changed from the initial values.
  17. */
  18. hasChanged: boolean;
  19. /**
  20. * Whether the form is valid.
  21. * A form is valid if all fields pass validation.
  22. */
  23. isValid: boolean;
  24. /**
  25. * Resets the form state to the initial values.
  26. */
  27. reset: () => void;
  28. /**
  29. * Saves the form state by setting the initial values to the current values.
  30. */
  31. save: () => void;
  32. }
  33. export type FormValidators<FormFields extends Record<string, any>> = {
  34. [K in keyof FormFields]?: (value: FormFields[K]) => string | undefined;
  35. };
  36. type InitialValues<FormFields extends Record<string, any>> = {
  37. [K in keyof FormFields]: FormFields[K];
  38. };
  39. /**
  40. * Creates a form state object with fields and validation for a given set of form fields.
  41. */
  42. export const useFormState = <FormFields extends Record<string, any>>(config: {
  43. initialValues: InitialValues<FormFields>;
  44. validators?: FormValidators<FormFields>;
  45. }): FormState<FormFields> => {
  46. const [initialValues, setInitialValues] = useState(config.initialValues);
  47. const [values, setValues] = useState(initialValues);
  48. const [errors, setErrors] = useState<{[K in keyof FormFields]?: string}>({});
  49. const setValue = useCallback(
  50. <K extends keyof FormFields>(name: K, value: FormFields[K]) => {
  51. setValues(old => ({...old, [name]: value}));
  52. },
  53. []
  54. );
  55. const setError = useCallback(
  56. <K extends keyof FormFields>(name: K, error: string | undefined) => {
  57. setErrors(old => ({...old, [name]: error}));
  58. },
  59. []
  60. );
  61. /**
  62. * Validates a field by running the field's validator function.
  63. */
  64. const validateField = useCallback(
  65. <K extends keyof FormFields>(name: K, value: FormFields[K]) => {
  66. const validator = config.validators?.[name];
  67. return validator?.(value);
  68. },
  69. [config.validators]
  70. );
  71. const handleFieldChange = <K extends keyof FormFields>(
  72. name: K,
  73. value: FormFields[K]
  74. ) => {
  75. setValue(name, value);
  76. setError(name, validateField(name, value));
  77. };
  78. return {
  79. fields: Object.entries(values).reduce((acc, [name, value]) => {
  80. acc[name as keyof FormFields] = {
  81. value,
  82. onChange: inputValue => handleFieldChange(name as keyof FormFields, inputValue),
  83. error: errors[name as keyof FormFields],
  84. hasChanged: value !== initialValues[name],
  85. initialValue: initialValues[name],
  86. };
  87. return acc;
  88. }, {} as any),
  89. isValid: Object.values(errors).every(error => !error),
  90. hasChanged: Object.entries(values).some(
  91. ([name, value]) => value !== initialValues[name]
  92. ),
  93. save: () => {
  94. setInitialValues(values);
  95. },
  96. reset: () => {
  97. setValues(initialValues);
  98. setErrors({});
  99. },
  100. };
  101. };
  102. /**
  103. * Creates a form context and hooks for a form with a given set of fields to enable type-safe form handling.
  104. */
  105. export const createForm = <FormFields extends Record<string, any>>({
  106. validators,
  107. }: {
  108. validators?: FormValidators<FormFields>;
  109. }) => {
  110. const FormContext = createContext<FormState<FormFields> | undefined>(undefined);
  111. function FormProvider({
  112. children,
  113. formState,
  114. }: {
  115. children: React.ReactNode;
  116. formState: FormState<FormFields>;
  117. }) {
  118. return <FormContext.Provider value={formState}>{children}</FormContext.Provider>;
  119. }
  120. const useFormField = <K extends keyof FormFields>(name: K) => {
  121. const formState = useContext(FormContext);
  122. if (!formState) {
  123. throw new Error('useFormField must be used within a FormProvider');
  124. }
  125. return formState.fields[name];
  126. };
  127. return {
  128. useFormState: (initialValues: InitialValues<FormFields>) =>
  129. useFormState<FormFields>({initialValues, validators}),
  130. FormProvider,
  131. useFormField,
  132. };
  133. };