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

liveValidate should optionally not validate "pristine" fields (at least until a submit)

Open glasserc opened this issue 7 years ago • 30 comments

Prerequisites

  • [x] I have read the documentation;
  • [x] In the case of a bug report, I understand that providing a SSCCE example is tremendously useful to the maintainers.

Description

In the playground, the "Validation" tab, when live validation is enabled, starts with an error ("passwords don't match"). However, the user hasn't put in a password yet, so it doesn't make sense as part of the user experience to show this error, at least until they start typing in the field.

Similarly, in the "Errors" tab, the initial state shows a bunch of errors. However, for some user experiences, this doesn't seem necessary or helpful; we're telling the user about errors that pre-existed, not errors they themselves have made.

Per https://github.com/mozilla-services/react-jsonschema-form/issues/246#issuecomment-286277405, maybe we should not validate fields until they've been "touched" somehow.

glasserc avatar Mar 14 '17 17:03 glasserc

In the playground, the "Validation" tab, when live validation is enabled, starts with an error ("passwords don't match")

I can't reproduce this behavior with the latest version. Maybe I'm missing a step? If not, how has this been fixed, that would been nice to know.

n1k0 avatar Mar 24 '17 09:03 n1k0

Similarly, in the "Errors" tab, the initial state shows a bunch of errors

This one is reproducible, though in this case we provide actual formData, so the form immediately immediately renders errors matching the provided data. This makes sense to me, but maybe just me? If not, would this need to be documented somehow?

n1k0 avatar Mar 24 '17 09:03 n1k0

Ha ha, so apparently my browser had saved a password for localhost:8080 from when I was working on something two jobs ago, and it autofilled that password when I was hacking on rjsf. But obviously it didn't autofill the second one, so "passwords don't match". Never mind...

glasserc avatar Apr 03 '17 16:04 glasserc

The issue still surfaces for example if any of the fields in the "Validation" example are marked as required.

Playground example

In the example, the entire form lights up as the validation fails for the root element (the fieldset containing the fields).

Any ideas on how to get rid of the required errors initially if the form hasn't been touched?

cowbellerina avatar Apr 21 '17 10:04 cowbellerina

This is a tough one. This example is a great use case for why you might want to live validate passwords as well as required fields, but it's not really great UX to show the user that fields later in the form aren't valid when they haven't even added anything to them. On the other hand, it doesn't make sense to wait until the user "touches" a required field to validate it, because the validation is the mere fact that it hasn't been touched.

Here's an off-the-cuff idea for handling this: maybe we can maintain a state which indicates what the "latest" field that was touched was, and liveValidate only up to that? So for this example, when you enter pass1, we validate the password fields, and then when you get to age, we validate password fields plus the age, and then if you skip ahead to another field, we'd validate password, age, and everything up to the field you were on. I don't think we have the bandwidth to implement anything like this, but I'd try to review a PR..

glasserc avatar Apr 21 '17 15:04 glasserc

Use formContext to maintain the pristine state and use refer to the field's state in the FieldTemplate.

I just provide a Form component wrapper with a formContext that looks like this:

const formContext = {
      lastUpdated: null,
      formControlState: {},
      setTouched: function (id) {
        this.formControlState[id] = 'touched'
      },

      setDirty: function (id) {
        this.formControlState[id] = 'dirty'
      },

      isTouched: function (id) {
        return this.formControlState[id] === 'touched'
      },

      isPristine: function (id) {
        return this.formControlState[id] == null
      },

      isDirty: function (id) {
        return this.formControlState[id] === 'dirty'
      }
    }

I just curry the validate function and pass this formContext to it within Form wrapper component:

const validate = (formContext, formData, errors) => {
  const {password, confirm} = formData
  // hard coding confirm id
  const confirmId = 'root_confirm'
  const confirmModified =
    formContext.isTouched(confirmId) || formContext.isDirty(confirmId)
  if (password != null && confirmModified) {
    if (password !== confirm) {
      // errors.password.addError("Passwords don't match")
      errors.confirm.addError("Passwords don't match")
    }
  }
  return errors
}

Create a higher order component for widgets and fields, do a custom onChange implementation which wraps the onChange provided by the library where update touched, dirty and lastUpdated values within the formContext.

However, I cannot get the name of the field within formData here and must resort to using an id. The library should ideally be implementing this and wrap each widget and field with a HoC to figure out when to perform validation.

Additionally, I am finding it hard to show error on confirm only confirm when password or confirm has been updated (in the example above) so maybe the idea of fields within uiSchema would make sense so that validation occurs only when related fields are changed.

Then we can validate using:

const canShowErrors =
    !isPristine(id) && (id === lastUpdated || isRelatedField)

inside the FieldTemplate component.

vamsiampolu avatar Apr 28 '17 05:04 vamsiampolu

I just came across this issue too. My form instantly has errors when no fields have been manipulated.

j avatar Oct 03 '17 18:10 j

@vamsiampolu How would your solution address "required" fields that the user skips over? I agree that validating only "touched" fields is necessary, but not sufficient.

glasserc avatar Oct 06 '17 16:10 glasserc

Imho live validation should be trigger if a input control looses focus. That behaviour has some nice benifits:

  • only touched controls are validated
  • you only get validation errors when you are finished typing, not immediatly

The latter one is a nice thing, as often while typing the editor is in an invalid state (password does not yet match, string is not yet an email-address, and so on)

jannikbuschke avatar Jan 16 '18 12:01 jannikbuschke

IMHO, instead of modifying the validate function, its better to show or hide errors based on touched or not. A field should always be invalid if it does not have valid data irrespective of touched or not. However, the error message display need not happen if its not touched.

rsaiprasad avatar Feb 28 '19 04:02 rsaiprasad

Sounds like this discussion is on the right track. @rsaiprasad @jannikbuschke are you interested in making a PR that implements this change?

epicfaace avatar Mar 16 '19 22:03 epicfaace

Where is this at? Would be nice to only validate touched fields! Happy to get involved if there is a branch open otherwise I can start one. Really would like this feature so I can improve the POC and actually get this library approved for our project going forward.

pmonty avatar Oct 28 '19 05:10 pmonty

I would to be willing to work on it as well, I am also in need of this functionality for my project.

priscillacamargo avatar Nov 14 '19 15:11 priscillacamargo

Feel free to submit a PR / start a branch to work on this issue. It would be good to get it into v2 because it would probably entail a breaking change.

epicfaace avatar Dec 09 '19 05:12 epicfaace

To clarify, I think invalid fields that have not been touched yet would need to wait until submit before they showed their error messages.

timkindberg avatar Mar 11 '20 18:03 timkindberg

Two years pasted and any update on this issue?

calpa avatar Jun 17 '20 12:06 calpa

Good news for the issue followers, I code a fix of this issue.

https://codesandbox.io/s/rjsf-live-validation-ryv3l

calpa avatar Jun 17 '20 13:06 calpa

@calpa cool, would you like to make a PR?

epicfaace avatar Jun 17 '20 15:06 epicfaace

@epicfaace I think this is a demo instead of code changes, so need PR?

calpa avatar Jun 17 '20 15:06 calpa

Would be great to have some resolution on this problem, its been open since 2017 and I would consider this a fairly fundamental feature for live-validation to work in a sane way.

ost-ing avatar Jul 08 '20 12:07 ost-ing

Any updates on this? It's 2021 now!

TejeshJadhav-cactus avatar Jan 11 '21 06:01 TejeshJadhav-cactus

It's been over 4 years since this request to support very commonly-implemented form functionality. I'm not sure why it isn't a higher priority. Is this issue ever going to be addressed? @epicfaace ?

Sammii avatar Aug 18 '21 17:08 Sammii

https://github.com/rjsf-team/react-jsonschema-form/issues/2418

On Wed, Aug 18, 2021, 1:54 PM Samantha @.***> wrote:

It's been over 4 years since this request to support very commonly-implemented form functionality. I'm not sure why it isn't a higher priority. Is this issue ever going to be addressed? @epicfaace https://github.com/epicfaace ?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/rjsf-team/react-jsonschema-form/issues/512#issuecomment-901313511, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAM4MX3LFD3WHL3SGKFFBATT5PXVPANCNFSM4DDTQ3KA .

epicfaace avatar Aug 18 '21 18:08 epicfaace

Might be useful for someone. Solved a similar problem using a form ref and transformErrors function.

// simplified example
function transformErrors(form: Form<any>) {
  // @ts-expect-error
  const formData = form?.state?.formData ?? {};

  return (errors: AjvError[]) => (
    errors.reduce((errorsList, error) => {
      const isPropDirty = error.property.slice(1) in formData;

      return isPropDirty ? [...errorsList, error] : errorsList;
    }, [])
  )
}

const TemplateForm = (formProps) => {
  // WARN: not sure which type is correct for form ref
  const formRef = useRef<Form<any>>();

  return (
    <Form
      {...formProps}
      ref={formRef}
      liveValidate
      transformErrors={transformErrors(formRef.current)}
    />
}
Gif with example how it works

ezgif-1-385a431b1a

theexplay avatar Feb 16 '22 01:02 theexplay

Thank you @theexplay for the good work, I think T in formRef should be same type than formData properties of the <Form>

ltruchot avatar Mar 07 '22 11:03 ltruchot

@theexplay Hi.., can you produce the same in code Sandbox ? i am not getting the exact behaviour as shown in the attached GIF. Just want to check what i am missing to get the similar behaviour.

Dave-Rushabh avatar Aug 02 '22 05:08 Dave-Rushabh

@Dave-Rushabh sure - https://codesandbox.io/s/serene-archimedes-px0skd?file=/src/TemplateForm.tsx (simplified version from code)

I have wrapper with form data controlled, on change data fire to change state, it rerender Form component and useRef value updated, smth like this)

theexplay avatar Aug 03 '22 15:08 theexplay

@theexplay This works!! Thank you brother..

Dave-Rushabh avatar Aug 04 '22 03:08 Dave-Rushabh

Might be useful for someone. Solved a similar problem using a form ref and transformErrors function.

But in this case, when we click submit button on the empty form (not touching any fields), we don't see any errors. Especially when noHtml5Validate option used.

I wonder if we can somehow determine if validation process called from liveValidation or from submit. If this possible, then we could show all errors on submit and only touched fields on live validation.

beholdr avatar Oct 12 '22 09:10 beholdr

Thanks to @theexplay , I solved my problem. Now the error list remains hidden until the first submit, and I won't submit if there are any errors even for the first submit.

Example:

const CompiledForm = (props: {
  onSubmit: () => void;
    //... props 
}) => {
  //... business logic
  const formRef = useRef<Form>(null);

  // You have to use Ref here, or ths transformErrors will still return [] on first submit
  // which make validation failed on the first submit
  const hasSubmittedRef = useRef(false);

  const transformErrors = useCallback(
    (errors: RJSFValidationError[]) => (hasSubmittedRef.current ? errors : []),
    []
  );

  return (
    <Form
      // ...
      onSubmit={() => {
        hasSubmittedRef.current = true;
        if (!formRef.current?.validateForm()) {
          return;
        }
        onSubmit();
      }}
      liveValidate={true}
      transformErrors={transformErrors}
    >
      {/* Business Button */}
    </Form>
  );
};

AndyBoat avatar Nov 01 '23 08:11 AndyBoat