react-final-form icon indicating copy to clipboard operation
react-final-form copied to clipboard

Trigger validation on both onChange and onBlur

Open Zn4rK opened this issue 7 years ago • 17 comments

Feature request

What is the current behavior?

If true, validation will happen on blur. If false, validation will happen on change. Defaults to false.

What is the expected behavior?

I would suggest adding another config option; validateOnChange: bool validateOnBlur: bool

Other information

I'm considering abandoning my own attempt at a form library (it was a great learning experience though), since you certainly know more about the different pain points users have, and the internal workings of React and how to properly do performant libraries, considering your background with forms and React.

One thing I really liked with my solution was the ability to do sync validation onChange, and async (i.e network requests) on onBlur.

I haven't figured out another way to do this just yet. But If we can have the library to validate on both onChange and onBlur this would be possible.

Zn4rK avatar Dec 18 '17 15:12 Zn4rK

Not sure why this makes sense. What does { validateOnBlur: true, validateOnChange: true }, or the false false equivalent, even mean? If you're validating on change, doing so on blur is meaningless, right?

erikras avatar Dec 18 '17 15:12 erikras

If you look at your example here, the verifyUsername function is hit on every keystroke (if the sync rules passes) https://codesandbox.io/s/kl9n295n5

What I essentially want to do is to create a validate function that checks whether the field is currently active, if it is, I can disregard my async rules and only run the sync rules. When it's not active, but the validation triggers, it is blurred, and I can run my async rules if no sync errors exists.

To do this, I also need the FormState in the validate function. Right now I'm using a reference to get it. But I was thinking of opening another issue depending on your response to this one.

{ validateOnBlur: true, validateOnChange: true } means simply run on both onBlur and onChange.

{ validateOnBlur: false, validateOnChange: false } would disable the validation.

Zn4rK avatar Dec 18 '17 15:12 Zn4rK

Just to demonstrate my point a bit better, I put together a simple example here: https://codesandbox.io/s/jn1lwj06lv

The sync validation (required check) vill run on onChange, but the the async validation verifyUsername will not fire until you leave the field.

Zn4rK avatar Dec 18 '17 22:12 Zn4rK

Wow, @Zn4rK, that's some impressive FormSpy hacking to get on-blur validation almost-working.

erikras avatar Jan 11 '18 11:01 erikras

@Zn4rK Your example inspired me to create this example. It uses setFieldData to maintain a completely separate layer of errors that are validated on blur. What do you think? It seems like you could use both this "blur layer" and the built-in onChange layer to achieve your goals.

Edit 🏁 React Final Form - Custom Validation Engine

erikras avatar Jan 12 '18 15:01 erikras

@erikras Wow! This looks promising, I think I'll be able to use this!

I had a feeling it would be possible with FormSpy, but I did not have that much time to figure out how it all fit together when I created this issue.

IMO this issue is resolved with the above example...

Zn4rK avatar Jan 13 '18 00:01 Zn4rK

A little update on this issue; I've been experimenting with this for a few hours today, and initially I thought it would work great. Don't get me wrong, it does work great and I can probably still use it - but lifting parts or all validation responsibility from the library introduces another set of issues;

  • ~~We have no way of knowing if the form is done validating in onSubmit() (edit: this was bad phrasing on my part, we can of course read the "validating"-props in onSubmit, and I guess we could subscribe to changes to "data" to verify that the validation is done). Just disabling the button and waiting for the form to validate it self is not good UX - we could build a promise based solution in OnBlurValidation, but that's something that the library already does when using the "normal" validations.~~

  • If a user submits the form our extra layer of validation will not be run in the current example. We'll have to build something that triggers all extra validation when the form is submitted. Try it - insert anything in the "lastName" field to make the form not be pristine, and then submit.

I only have loose thoughts on how we could go about resolving these issues (or the original one).

I've almost managed to hack up a solution using a <FormSpy /> to detect changes to active fields to trigger a validation using a mutator (which in turn does nothing, but mutators always triggers the validation), and then using a <Field /> to add a field-level validator the reads the current reactFinalForm.getState() via context. It's ugly, and I was mostly curious of what it would take to make it work.

The only issue with this is that the mutators always trigger the validations so if I want to set some field data in the validate (loading indicator) I get a recursive loop. I could use reactFinalForm.pauseValidation() since I'm already using the context, but then I'm introducing a race condition - another field might not validate.

My thoughts I had while I was hacking this^ up was;

  • FieldState should have a validating property.
  • The validators should have access to final-form's FormState
  • There should be a way to call runValidation() via FormRenderProps.
  • It would be awesome if field-level validators could be specified via FormProps directly

I'll end this update in some pseduo code:

<Form
  render={(formRenderProps) => {
    <React.Fragment>
      {/* Other stuff */}
      <Field
        name={name}
        // It would probably be awesome to have FieldState here as well...
        validate={(value, values, FormState) => {
          // Normal sync validation
          if(!value) {
            return 'This field is required';
          }
  
          // Check if we can run async validation
          if(FormState.active === undefined) {
            return new Promise(resolve => {
              setTimeout(() => {
                if(value === 'admin') {
                  resolve('No way!');
                }
                
                resolve();
              }, 2000);
            })
          }
        }}
      />
      {/* Other stuff */}
      <FormSpy
        subscription={{ active: true }}
        onChange={({ active }) => {
          const { active: previouslyActiveField } = formRenderProps;
  
          if (active === undefined && previouslyActiveField !== undefined) {
            // A blur occured, run the validation for the previously active field
            formRenderProps.validate(previouslyActiveField);
          }
        }}
      />
    </React.Fragment>
  }}
/>

Hopefully my thoughts here makes sense. If they do, we can extract some of them to new issues...

I'm re-opening this to facilitate the discussion. Feel free to close it if we should take the discussion somewhere else :)

Zn4rK avatar Feb 18 '18 21:02 Zn4rK

@erikras I think this issue is not actionable without your decisions regarding those newly proposed features. If you ask me, the idea seems to make sense. It's important to figure out an intuitive API for it.

maciejmyslinski avatar Sep 16 '18 15:09 maciejmyslinski

My 2c: I think validateOnBlur should not disable validateOnChange. Here is one example why. I'm triggering form submission on mouse down on a save button, and that doesn't blur the currently active field, so the form is submitted even though the validation doesn't pass.

Why do I use onMouseDown instead of just onClick? That's because I display the errors from the validation under the inputs. When there are errors at submission, the save button is pushed down, sometimes way inder the current mouse pointer, and this cancels the click event.

fzaninotto avatar Dec 19 '19 10:12 fzaninotto

A possibly related behaviour I quite like is having "onBlur" validation initially, and after the initial blur, validation is done onChange.

Dakkers avatar Jan 11 '20 19:01 Dakkers

Any updates on this? The scenario @Dakkers describes is required in my project and is preventing my team to migrate to react-final-form

rpedretti avatar Mar 05 '21 01:03 rpedretti

Well, maybe I found an unorthodox way to come up with a solution about it. Here the validation is only executed after the onBlur. I started from the idea of memorizing an asynchronous validation, in the example: https://codesandbox.io/s/wy7z7q5zx5?file=/index.js:615-632; from there continue passing as a parameter to the Field (in my case it is a component) the instance of the Form: form, so that it could control when it paused and continued the validation (pauseValidation and resumeValidation methods of the form). So for the onChange event, I stop the validation, and for the onBlur it continues. Here are some code snippets:

// Similar to `simpleMemoize` only adapted to this need.
const memorizeValidator = (fn: any) => {
  let lastResult = '';
  return (value: any, values: object, meta: FieldState<any>) => {
    if (!meta.pristine) {
      lastResult = fn(value, values, meta);
    }
    return lastResult;
  };
};
const asyncValidator = memorizeValidator(
    async (value: string) => {
      try {
        const resp = await ...;
        return 'Error after response';
      } catch (e) {
        return 'Error on request';
      }
    },
  );
// Note that the unorthodox thing was to pass the instance of the `form`, so far I did not see how to get it directly from the` field` 
<Field
    name="name"
    component={TextInputComponent}
    .
    .
    .
    validate={asyncValidator}
    form={form}
  />
// These are the handlers for the respective input events in the component.
const handleOnChange = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      form.pauseValidation();
      input.onChange(event.target.value);
      .
      .
      .
    },
    [input, form],
  );
  const handleOnBlur = useCallback(() => {
    form.resumeValidation();
    input.onBlur();
  }, [input, form]);

LizanLycan avatar Apr 13 '21 02:04 LizanLycan

I encountered a very similar issue today and I think I found an almost ideal solution.

At first, the documentation for Field validate caught my attention - it passes field meta as a 3rd argument, thus we can detect in our field validator if we validate on blur or on change -- the meta.active is true during onChange validation and false during onBlur. Note, that Form validatate receives only the values parameter, thus is unable to detect which validation we are in, but I favor Field validation in all cases as it is more declarative, simpler, and easier to maintain.

But now, how to trigger validation on the change as well as on blur? We could simply leave the Form validateOnBlur property untouched, thus our validate function will run on change only, then write some of our validators to skip validation, when the field is in meta.active and then rerun the validation onBlur.

Well, it turns out, triggering the validation is another nut to crack! Unfortunately, I did not found a public API for triggering a validation, but I dig into the source code and found the mighty runValidation function, which is not exported but is triggered by change, which we can control, so I created a simple mutator which, when called, "changes" the value of the specified field, to the same, unchanged value -- it fakes "change" to just trigger validation.

And, with all these little components, we are able to run some validations on change and some validation on blur.

Check the CodeSandbox and note the beautiful validator composition, which runs my async validator onBlur only when all synchronous validator passes, whilst synchronous validators are run on every change. https://codesandbox.io/s/trigger-validation-onblur-mx7vx

NOTE: the example is forked from another example and a little bit complicated than necessary, but the important part shall be clear. The fundamental parts are triggerValidation mutator and an async field-level validation function that tests meta.active.

czabaj avatar Apr 13 '21 11:04 czabaj

This is an old issue now and I don't actually have a problem I haven't been able to solve here, but I did discover a slight flaw in this implementation future users of this technique might want to be aware of.

This custom call to triggerValidation will not honor the validateFields={[]} prop on the Fields component. All validators will be run for any input change in the mutator. I imagine this is not an easy issue to resolve, nor am I asking anyone to. Simply warning those who land here in the future not to use this technique if they need to limit field validation to fewer than all fields.

Demo: https://codesandbox.io/s/trigger-validation-onblur-forked-tl6w5z?file=/src/index.js

dabstractor avatar Jun 08 '23 20:06 dabstractor

anyone landing here at this point, I'd recommend to move to react-hook-form if possible. that library is maintained (for now, anyway)

Dakkers avatar Jun 08 '23 22:06 Dakkers

Thank you, I've actually used it before a few years back but forgot about it when I started writing this library. And I was about to wrap it up today too, looks like it'll be a few more days :rofl:

dabstractor avatar Jun 09 '23 17:06 dabstractor

even though react-hook-form is maintained, it's bad designed. Don't recommend to use it if you have huge complicated forms and performance is important to you

anyone landing here at this point, I'd recommend to move to react-hook-form if possible. that library is maintained (for now, anyway) even though react-hook-form is maintained, it's bad designed. Don't recommend to use it if you have huge complicated forms and performance is important to you. We try to switch from hook-form to final-form just because of many performance issues

EvilaMany avatar Feb 05 '24 12:02 EvilaMany