jaredpalmer / formik

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

Submit Payload #1792

Open kserjey opened 5 years ago

kserjey commented 5 years ago

🚀 Feature request

Current Behavior

People do hack with setTimeout + setFieldValue - #214

Desired Behavior

It would be great if we have an option to pass something with submitForm(payload) to submit handler or get an event from submitted button.

Who does this impact? Who is this for?

For all people who need to render two submit buttons with different submitting logic!

johnrom commented 5 years ago

The answer to #214 was not to use a hack, but to perform an action before submit. Any form should only have 1 submit action. That action can take different actions depending on factors that occur before it. So if there are two buttons, one with a normal submit and one with a secondary action, one could wrap a callback which will split the decision beforehand:

const getSubmitCallback = saving => 
    values => {
        if (saving) {
            doSave(values);
        } else {
            doSubmit(values);
        };
};

const MyDualActionForm = () => {
    const onSave = React.useCallback(getSubmitCallback(true), []);
    const onSubmit = React.useCallback(getSubmitCallback(false), []);

    return (
        <Formik onSubmit={onSubmit}>
            <input type="button" onClick={onSave} value="Save" />
            <input type="submit" value="Submit">
        </Formik>
    );
}

Or one could use a class and do:

onClick={() => this.setState({saving: true}, formikProps.onSubmit)}

where onSubmit is now a callback that occurs after the state has been updated.

kserjey commented 5 years ago

I understand, but I think it could be simpler if there was an option to pass submit context. In my case I have two submit buttons. One of them to save item as draft and the other to save item as it is. So I have two handlers for two buttons. They looks similar:

withHandlers({
  handleSubmitDraft: ({ submitForm, setLifeState }) => {
    setLifeState('DRAFT');
    setTimeout(submitForm);
  },
  handleSubmitActual: ({ submitForm, setLifeState }) => {
    setLifeState('NORMAL');
    setTimeout(submitForm);
  }
})

And then, in submit handler I get lifeState state and append it to rest fields:

withFormik({
  handleSubmit: (values, { props }) => {
    const item = { ...values, lifeState: props.lifeState };
    createItem(item).then(() => { /* handle success submission */ });
  }
})

And if there was a way to pass context, It would be simpler and doesn't require extra state:

withFormik({
  handleSubmit: (values, { submitContext }) => {
    const item = { ...values, ...submitContext });
    createItem(item).then(() => { /* handle success submission */ });
  }
}),
withHandlers({
  handleSubmitDraft: ({ submitForm, setLifeState }) => {
    submitForm({ lifeState: 'DRAFT' });
  },
  handleSubmitActual: ({ submitForm, setLifeState }) => {
    submitForm({ lifeState: 'NORMAL' });
  }
})

And if submission was called by handleSubmit:

<button onClick={handleSubmit}>Save</button>
<button onClick={handleSubmit}>Save as Draft</button>

submitContext could be a click event of a clicked button.

johnrom commented 5 years ago

So all you're asking for is the submit event during onSubmit? I don't have anything against that, necessarily, but I'm not the one to make the final for API changes. I'd recommend modifying this issue from "Submit Payload" to something like "Pass Submit Event to onSubmit Handler" to help future maintainers understand what is being asked for!

kserjey commented 5 years ago

Yes, but also I would like to pass some data to submitForm, like is done in third example. So in submit handler there would be an event (from handleSubmit) or data passed via submitForm.

florentdestremau commented 4 years ago

So all you're asking for is the submit event during onSubmit?

YES ! I have exactly this problem right now. I want a "save as draft" and a "save & publish" button, very easy to handle, I would simply like to add a query parameter to the post URL, but I have been looking for any clean solution for the last 2 hours and can't find any.. Having an event helping me to find out what is the name of the submit button used would solve the case.

florentdestremau commented 4 years ago

What I would like is being able to have 2 buttons like this

    <button type="submit" name="save_draft">Save draft</button>
    <button type="submit" name="save_and_publish">Save and publish</button>

And then in my <Formik> instance, having a new form property like this


onSubmit = async (values, form)  => {
  // my saving ajax request

  if (form.submittedButtonName === 'save_and_publish') {
    publish();
  }
}
SamKirkland commented 4 years ago

@florentdestremau that's exactly what I was thinking. We have some very complex forms (legacy app unfortunately) with some having up to 6 submit actions. Think save, save & approve, save & create new, save & decline, etc...

We've been working around this for a few months, if everyone can agree on a pattern i can throw together a PR.

dmudro commented 4 years ago

I also handle my two submit buttons use case with state:

onClick={() => this.setState({customFlag: true}, submitForm())

...and then read the flag inside handleSubmit().

FYI the particular scenario is that I want to redirect the user to another route if one of the two submit actions were called.

This works but is slightly suboptimal:

I entertained by the idea to save the flag with Formik's setStatus() instead of using the state but it turns out you can't read status in handleSubmit():https://jaredpalmer.com/formik/docs/api/withFormik#the-formikbag @jaredpalmer that's probably for a reason?

All in all having the ability to pass a custom payload submitFom() sounds like a great idea for a variety of valid use cases.

HectorRicardo commented 4 years ago

The answer to #214 was not to use a hack, but to perform an action before submit. Any form should only have 1 submit action. That action can take different actions depending on factors that occur before it. So if there are two buttons, one with a normal submit and one with a secondary action, one could wrap a callback which will split the decision beforehand:

const getSubmitCallback = saving => 
    values => {
        if (saving) {
            doSave(values);
        } else {
            doSubmit(values);
        };
};

const MyDualActionForm = () => {
    const onSave = React.useCallback(getSubmitCallback(true), []);
    const onSubmit = React.useCallback(getSubmitCallback(false), []);

    return (
        <Formik onSubmit={onSubmit}>
            <input type="button" onClick={onSave} value="Save" />
            <input type="submit" value="Submit">
        </Formik>
    );
}

Or one could use a class and do:

onClick={() => this.setState({saving: true}, formikProps.onSubmit)}

where onSubmit is now a callback that occurs after the state has been updated.

@johnrom Correct me if I am wrong, but the onSave callback is not really a submit. That means that when you click onSave, the submission phases described in here won't run.

johnrom commented 4 years ago

@hectorricardo that's true! in the case above, as a user I would probably expect that an autosave would save an invalid state. However, that might not always be the case. This issue is still open because passing context to onSubmit is probably a good idea. However, in the meantime it seems like a good time for a ref!

const getSubmitCallback = async savingRef => 
  values => {
    if (savingRef.current) {
      await doSave(values);
      savingRef.current = false;

    } else {
      await doSubmit(values);
    };
};

const MyDualActionForm = () => {
  const savingRef = React.useRef(false);
  const onSubmit = React.useCallback(getSubmitCallback(savingRef), []);

  return (
    <Formik onSubmit={onSubmit}>
      {({ handleSubmit }) => (
        <>
          <input type="button" onClick={() => {
            savingRef.current = true;
            handleSubmit();
          }} value="Save" />
          <input type="submit" value="Submit" />
        </>
      )}
    </Formik>
  );
}
WegDamit commented 4 years ago

i tried that above solution from @johnrom and didn't get it to work. here's a sandbox can anybody help me getting this working? i think this solution might work with the usual submit on enter, so i'd prefer this on over that one (using js on the buttons)