form icon indicating copy to clipboard operation
form copied to clipboard

"Touched" definition in documentation is wrong

Open michaelboyles opened this issue 10 months ago • 12 comments

The docs say

A field is "touched" when the user clicks/tabs into it, "pristine" until the user changes value in it, and "dirty" after the value has been changed.

https://tanstack.com/form/latest/docs/framework/react/guides/basic-concepts

But this is not true. form.Field doesn't seem to expose anything that would even allow it to be true. It exposes handleChange and handleBlur. Both of them set the touched flag, but blur only accounts for focus being lost, not focus being gained.

I think this is correct:

Image

michaelboyles avatar Feb 21 '25 19:02 michaelboyles

Oh hmm... Legit Q: is this chart you outlined also consistent with RHF and Formik? This is where we got this idea from.

crutchcorn avatar Feb 25 '25 10:02 crutchcorn

Hi,

I have made an example showing that the isTouched property is set to true (on submit) even if a field was not clicked or gained focus.

borntorun avatar Feb 25 '25 18:02 borntorun

@borntorun Correct. I didn't mention in the above, but submitting sets the touched flag. This is consistent with Formik.

@crutchcorn From my testing, none of the three are consistent with each other.

Formik: touched = blurred | submitted "Before submitting a form, Formik touches all fields". Changing a field without tabbing out doesn't touch it

RHF: touched = blurred. There's a feature request to touch all on submit which they didn't implement: https://github.com/react-hook-form/react-hook-form/issues/11366

Tanstack Form: touched = changed | blurred | submitted providing you attach both change and blur listeners - which you might not do. If you click on a field, it will be touched as soon as you either change it or tab out of it. What the documentation incorrectly claims is that touched = hasBeenFocussed

michaelboyles avatar Feb 25 '25 21:02 michaelboyles

@michaelboyles Yes, is like you said with a small correction: the part "providing you attach both change and blur listeners " is not what is happening. I removed all event handlers in the example and still touched is set to true on submit.

I think managing all these states causes a lot of confusion - that's why building forms is complicated and tedious... - most of the time you just need a flag/property that says "the field value is different from the initial one". The only way to retrieving the changed fields (on submit) is still comparing the value against the initial value - there is no direct property on the library that gives this.

borntorun avatar Feb 26 '25 10:02 borntorun

Some confusion with the current handling of isTouched brought me here. I'm using this meta property to surface the error message for the user at the right time.

I've adapted the example to show the issue I see: https://stackblitz.com/edit/vitejs-vite-offm7dia?file=src%2FApp.jsx

  1. Touch the first optional input (click and blur). Notice the error messages get set for all fields (correct for the onBlur schema). isTouched is correctly set to true for the first field and hence the relvant error message is visible.
  2. Now try submitting. isTouched isn't updated for the second or third input field. The error message aren't displayed. The user gets lost.
  3. The user has to touch the individual fields to see the error message. Imagine this UX in the context of a bigger form

Tracing relevant code:

https://github.com/TanStack/form/blob/8672e57a7bea0a7318435d29d22c321d465e0213/packages/form-core/src/FormApi.ts#L1152-L1156

Prior art (formik): https://formik.org/docs/guides/form-submission#frequently-asked-questions

Why does Formik touch all fields before submit?

It is common practice to only show an input's errors in the UI if it has been visited (a.k.a "touched"). Before submitting a form, Formik touches all fields so that all errors that may have been hidden will now be visible.

How do others solve this? Am I using tanstack-form incorrectly? Happy about feedback! Thanks for making the library, I find it the most powerful in modern TypeScript projects and I appreciate the considerations every feature gets.

leomelzer avatar Feb 27 '25 10:02 leomelzer

Hi,

The error messages are not visible because you are ony showing them if fields are touched.

I think each field validation only occurs if the field is not already in validation error state, if it is, the validation is skipped (my assumption), and the isTouched is not set for that field. (dont know if this is correct...)

If you try to submit not touching any field the messages will show (as long you remove the condition field.state.meta.isTouched && ...), and the isTouched changes to true

borntorun avatar Feb 27 '25 14:02 borntorun

@leomelzer Agree the behaviour seems strange. Notice that if you touch nothing and submit, all 3 fields get touched on submit. However, click in and out of the first field and submit, only that field is touched.

michaelboyles avatar Feb 27 '25 15:02 michaelboyles

Thanks @borntorun, I've taken the code from e.g. https://tanstack.com/form/latest/docs/framework/react/examples/simple and because I think that provides a good user experience (without isTouched, all fields would immediately show up red after the first touched field). That seems off to me.

@michaelboyles exactly! Thanks for rephrasing.

leomelzer avatar Feb 27 '25 21:02 leomelzer

Can confirm the behavior is still the same on v1. Would appreciate any pointers :) Thank you!

leomelzer avatar Mar 04 '25 11:03 leomelzer

Regarding isDirty there's discussions in #1080 & #1081 which I personally agree with.

CanRau avatar Mar 04 '25 22:03 CanRau

Some confusion with the current handling of isTouched brought me here. I'm using this meta property to surface the error message for the user at the right time.

I've adapted the example to show the issue I see: https://stackblitz.com/edit/vitejs-vite-offm7dia?file=src%2FApp.jsx

1. Touch the first optional input (click and blur). Notice the error messages get set for all fields (correct for the onBlur schema). `isTouched` is correctly set to true for the first field and hence the relvant error message is visible.

2. Now try submitting. `isTouched` isn't updated for the second or third input field. The error message aren't displayed. The user gets lost.

3. The user has to touch the individual fields to see the error message. Imagine this UX in the context of a bigger form

Tracing relevant code:

form/packages/form-core/src/FormApi.ts

Lines 1152 to 1156 in 8672e57 // If any fields are not touched if (!field.instance.state.meta.isTouched) { // Mark them as touched field.instance.setMeta((prev) => ({ ...prev, isTouched: true })) }

Prior art (formik): https://formik.org/docs/guides/form-submission#frequently-asked-questions

Why does Formik touch all fields before submit? It is common practice to only show an input's errors in the UI if it has been visited (a.k.a "touched"). Before submitting a form, Formik touches all fields so that all errors that may have been hidden will now be visible.

How do others solve this? Am I using tanstack-form incorrectly? Happy about feedback! Thanks for making the library, I find it the most powerful in modern TypeScript projects and I appreciate the considerations every feature gets.

For future users, just dropping this out there: https://stackblitz.com/edit/vitejs-vite-qtvagxcg?file=src%2FApp.jsx

This is a clean way to implement the behaviour talked about here. Here, we subscribe to the submission attempts

const isSubmitted = useStore(
    form.store,
    (state) => state.submissionAttempts > 0
  );

and do our checks based on that information:

{isSubmitted && field.state.meta.errors.length > 0 ? (
                  <p style={{ color: 'red' }}>
                    {field.state.meta.errors
                      .map((error) => error.message)
                      .join(', ')}
                  </p>
                ) : null}

Personally I too feel that the isSubmitted approach is not something that would naturally come to mind, so touching the fields would be easier. However, I don't know if this behaviour would be naturally understood by devs. Though it seems to be very widely known.

theVedanta avatar Mar 11 '25 15:03 theVedanta

@theVedanta I have the same issue you have described. I think the cause is the code here

https://github.com/TanStack/form/blob/8672e57a7bea0a7318435d29d22c321d465e0213/packages/form-core/src/FormApi.ts#L1563-L1572

i.e. if the form is not in a canSubmit state then it won't run validateAllFields and so won't set the isTouched state on them.

I have worked around by adding some extra code to my onSubmit handler:

<form
  onSubmit={async (e) => {
    e.preventDefault();
    e.stopPropagation();
    await form.handleSubmit();

    // ensure `isTouched` statuses are set when form is not valid...
    if (!form.state.canSubmit) {
      await form.validateAllFields('submit');
      await form.validate('submit');
    }
  }}
>

I think this shouldn't be necessary really, but not sure if better to create a new issue about it...

BenGladman avatar Mar 14 '25 12:03 BenGladman

@BenGladman thanks for your workaround. On react-native I was able to get the desired behavior using

if (form.state.canSubmit) {
  void form.handleSubmit();
} else {
  void form.validate('submit');
  void form.handleSubmit();
}

.validate worked alone always, while .validateAllFields didn't always work for some reason.

It seems .validate is marked as private, but something in it works better than .validateAllFields 😬

jahirfiquitiva avatar Mar 18 '25 20:03 jahirfiquitiva

Thank you so much @BenGladman for the workaround. However, inputs already showing errors are validated again, but it works. @theVedanta Also thank you for your workaround! cc @crutchcorn

j0rgedev avatar Mar 19 '25 06:03 j0rgedev