"Touched" definition in documentation is wrong
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:
Oh hmm... Legit Q: is this chart you outlined also consistent with RHF and Formik? This is where we got this idea from.
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 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 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.
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
- Touch the first optional input (click and blur). Notice the error messages get set for all fields (correct for the onBlur schema).
isTouchedis correctly set to true for the first field and hence the relvant error message is visible. - Now try submitting.
isTouchedisn't updated for the second or third input field. The error message aren't displayed. The user gets lost. - 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.
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
@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.
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.
Can confirm the behavior is still the same on v1. Would appreciate any pointers :) Thank you!
Regarding isDirty there's discussions in #1080 & #1081 which I personally agree with.
Some confusion with the current handling of
isTouchedbrought 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 formTracing 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 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 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 😬
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