jaredpalmer / formik

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

RFC: The ability to centralize onChange behavior with the Formik component #1347

Open mauricedb opened 5 years ago

mauricedb commented 5 years ago

🚀 Feature request

Current Behavior

At the moment form validation can be done by using the validate or validationSchema props on the Formik component. However it isn't possible to apply other business logic at the same central place. Instead if one value needs to be updated when another value changes this has to be coded up using a custom component and setFieldValue calls. This leads to business logic spread out and intertwined with UI logic. An example would be a component like this to keep width and height the same if the shape is a square:

class SquareShape extends React.Component {
  handleChange = e => {
    this.props.form.handleChange(e);

    if (this.props.form.values.shape === "square") {
      const { name, value } = e.target;

      this.props.form.setFieldValue(
        name === "width" ? "height" : "width",
        value
      );
    }
  };

Desired Behavior

A much better approach would be to centralize this logic at the Formik level, just like validation and keep it out of the UI logic.

Suggested Solution

A much better approach would be to add a callback to the Formik component to run after each change and be able to apply any business rules.

const applyBusinessRules = (values, change) => {
  if (values.shape === "square") {
    switch (change.name) {
      case "width":
        return { ...values, height: change.value };
      case "height":
        return { ...values, width: change.value };
    }
  } else if (change.name === "shape" && change.value === "square") {
    return { ...values, width: values.height };
  }

  return values;
};

And then wire up the business logic as follows:

<Formik 
  initialValues={{ shape: "square", width: 10, height: 10 }} 
  businessRules={applyBusinessRules}>
.....
</Formik>

The default of applyBusinessRules would be to just leave the values as they where.

Who does this impact? Who is this for?

This would be a useful feature for every developer creating complex forms with business rules that update other values based on changes a user makes.

Describe alternatives you've considered

  1. Adding the update logic to the UI. However this leads to hard to track logic making it very error prone. It also results in many custom components that would otherwise not be needed.
  2. Adding the logic to validation code. However validation logic should not mutate the values. This might also introduce unwanted side effects because a value is first validated and then updated. The validation can also take too long as result of other asynchronous validations.
  3. Applying the business rules when submitting. However this would mean the the effects of these business rules are not visible until the user saves and they would not be visible on screen before.

Additional context

Martin Fowler on separating UI code

jaredpalmer commented 5 years ago

Interesting idea. This is effectively another approach to #812. applyBusinessRules is really just a state reducer.

jaredpalmer commented 5 years ago

Going to mark as a duplicate.

Andreyco commented 5 years ago

I would mention #401 as well here...

mauricedb commented 5 years ago

@jaredpalmer Didn't want to call it a reducer because of the slightly different signature but making this a proper reducer makes a lot of sense to me. It makes it easy to differentiate between updates coming from setValues() or setFieldValue().

The basic signature could look something like:

type ReducerAction<Values> = {
      type: "setFieldValue";
      payload: {
        field: string;
        value: any;
      };
    }
  | {
      type: "setValues";
      payload: Partial<Values>;
    };

export class Formik<Values>
  reducer = (values: Values, action: ReducerAction<Values>): Values => {
    return values;
  };

  // Other code
}

With a Values definition of:

type Shape = {
  shape: string;
  width: number;
  height: number;
};

The complete reducer for the business rule above would look something like:

  const reducer = (values: Shape, action: ReducerAction<Shape>): Shape => {
    if (action.type === "setFieldValue") {
      const { field, value } = action.payload;

      if (values.shape === "square") {
        switch (field) {
          case "width":
            return { ...values, height: value };
          case "height":
            return { ...values, width: value };
        }
      } else if (field === "shape" && value === "square") {
        return { ...values, width: values.height };
      }
    } else {
      const { shape, height } = action.payload;

      if (shape === "square") {
        return { ...values, width: height };
      }
    }
    return values;
  };

And a nice side benefit of this approach is the possibility to wire up and be able to track changes to values using the Redux Devtools.

maddhruv commented 4 years ago

Closing as duplicate and stale

johnrom commented 4 years ago

@maddhruv interestingly, this issue has never been solved so I would leave it open. A few proposals have been made. All issues this is mentioned as being a duplicate of are closed.

Also discussed here: https://github.com/formium/formik/discussions/2474

github-actions[bot] commented 3 years ago

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 60 days