jaredpalmer / formik

Build forms in React, without the tears 😭
https://formik.org
Apache License 2.0
34k stars 2.79k forks source link

Support custom fields without workarounds #3645

Open kneczaj opened 2 years ago

kneczaj commented 2 years ago

Feature request

Current Behavior

This is the interface of Formik's handle change:

handleChange: {
        /** Classic React change handler, keyed by input name */
        (e: React.ChangeEvent<any>): void;
        /** Preact-like linkState. Will return a handleChange function.  */
        <T = string | React.ChangeEvent<any>>(field: T): T extends React.ChangeEvent<any> ? void : (e: string | React.ChangeEvent<any>) => void;
    };

The Field's Value can be any like here:

export interface FieldInputProps<Value> {
    /** Value of the field */
    value: Value;
    /** Name of the field */
    name: string;
    /** Multiple select? */
    multiple?: boolean;
    /** Is the field checked? */
    checked?: boolean;
    /** Change event handler */
    onChange: FormikHandlers['handleChange'];
    /** Blur event handler */
    onBlur: FormikHandlers['handleBlur'];
}

Just HTML <input> tag produces React.ChangeEvent what means that Formik cannot be attached with its handleChange function to a custom field which does not produce it.

The error in the browser when I use it with e.g. boolean is:

Uncaught TypeError: Cannot read properties of undefined (reading 'type')
    at formik.esm.js:721:1
    at formik.esm.js:755:1
    at formik.esm.js:1256:1
    at n (project44-manifest-ds.js:2:48822)
    at project44-manifest-ds.js:2:49089
    at Object.setSelected (project44-manifest-ds.js:2:222034)
    at onChange (project44-manifest-ds.js:2:221812)
    at Object.vi (react-dom.production.min.js:202:330)
    at ui (react-dom.production.min.js:32:27)
    at xi (react-dom.production.min.js:32:81)

and it happens at this line: https://github.com/jaredpalmer/formik/blob/b9cc2536a1edb9f2d69c4cd20ecf4fa0f8059ade/packages/formik/src/Formik.tsx#L611

Workaround which I currently use is:

  const { values, setFieldValue } = useFormikContext<SearchFormProps>();
  const [field, meta] = useField<string>('test');

  return (
    <CheckboxField
      isSelected={field.value}
      // onChange here has signature of `(value: boolean) => void` 
      onChange={(value) => {
        setFieldValue(field.name, value);
      }}
      ....

Desired Behavior

A boolean should be possible to save with handleChange so I may not use the workaround.

Suggested Solution

Please support signature

handleChange: <Value>(field: Value | React.ChangeEvent<any>) => void

How to do it?

Instead of if (!isString(eventOrTextValue)) check here https://github.com/jaredpalmer/formik/blob/b9cc2536a1edb9f2d69c4cd20ecf4fa0f8059ade/packages/formik/src/Formik.tsx#L600 this could be used:

function isChangeEvent<T extends HTMLElement = HTMLElement>(
  eventOrValue: ChangeEvent<T> | unknown,
): eventOrValue is ChangeEvent<T> {
  return (eventOrValue as ChangeEvent<T>).target !== undefined;
}

Who does this impact? Who is this for?

Describe alternatives you've considered

The workaround is the alternative.

Additional context

Noting

delfouly commented 1 month ago

I created this hook to handle Textinputs and checkboxes, sharing it here in case it helps anyone 😊

import {useFormik} from 'formik';
import * as Yup from 'yup';

type FormValues = Record<string, string | boolean>;

export type FormProps<T extends FormValues> = {
  initialValues: T;
  onSubmit: (values: T) => void;
  schema: Yup.ObjectShape;
};

/** Hook used to handle forms values, validations and submission */
export const useForm = <T extends FormValues>({
  initialValues,
  onSubmit,
  schema,
}: FormProps<T>) => {
  const {values, handleChange, handleSubmit, errors, setFieldValue} = useFormik(
    {
      initialValues,
      onSubmit,
      validationSchema: createSchemaObject(schema),
      enableReinitialize: true,
      validateOnBlur: false,
      validateOnChange: false,
    },
  );

  /**Same as handleChange but for boolean fields */
  const handleToggle = (fieldName: keyof typeof initialValues) => {
    const value = values[fieldName];
    return setFieldValue(fieldName as string, !value);
  };

  return {
    form: values,
    handleChange,
    handleToggle,
    handleSubmit,
    errors,
  };
};