form
form copied to clipboard
Add a way to display only one error for a field
Describe the bug
Currently we return field.state.meta.touchedErrors
ValidationError[]
as a string by concataining the different errors of a field with commas.
It would be great to be able to display only one error.
e.g:
validators={{
onChange: v.string([
v.minLength(1, t(i18n)`This field is mandatory`),
v.custom(isDateStringValid, t(i18n)`The birth date format is incorrect`),
v.custom(isDateStringOver18, t(i18n)`You have to be at least 18 years`),
]),
}}
These valibot validators will end up on showing all three errors just by typing a letter and deleting it. Imo, this is not a good UX.
I would expect some kind of priority in the error messages, thus only displaying the first validation to return an error.
Your minimal, reproducible example
https://github.com/TanStack/form/
Steps to reproduce
validators={{
onChange: v.string([
v.minLength(1, t(i18n)`This field is mandatory`),
v.custom(isDateStringValid, t(i18n)`The birth date format is incorrect`),
v.custom(isDateStringOver18, t(i18n)`You have to be at least 18 years`),
]),
}}
Expected behavior
More control over the errors display
How often does this bug happen?
None
Screenshots or Videos
No response
Platform
macOS
TanStack Form adapter
react-form
TanStack Form version
0.19.0
TypeScript version
No response
Additional context
No response
This should be done at the validator level where you can pass transformErrors
to transformErrors: errors => errors[0]
rather than transformErrors: errors => errors.join(', ')
Will mark this as "Good first issue" and a feature request
@crutchcorn Hello, can I work on this issue? If possible, please assign it to me. 😊
Not sure if this is relevant to this issue or if I should create a separate one but being able to return an array from
validators={{
onChange: ["err1", "err2"] // string | string[]
}}
instead of having to return a single .join("\n")
ed string would be a very nice addition as well.
@jessekelly881 let's open a different issue on it :)
@seobbang PRs welcome! No need to assign it out, but do feel free to tackle the issue :)
Would love this fixed @seobbang did you manage to work on this?
@TheKnightCoder I'm going to try again 🤗
It would be nice to accept ValidatonError[]
as return value for FieldValidateOrFn
since there could be some cases when we want all errors to be shown from a single validation.
For example, when setting password we may want to show multiple errors under the input like 'length should be longer than 8', 'should contain special characters', ...
I quickly passed by here and was seeing something like this, it probably doesn't work and I don't have time to test/research in depth but maybe it will help:
import { safeParse, safeParseAsync } from 'valibot'
import type { BaseIssue, BaseSchema, BaseSchemaAsync } from 'valibot'
import type { Validator } from '@tanstack/form-core'
type Params = {
errorMap?: (
errors: [BaseIssue<unknown>, ...BaseIssue<unknown>[]],
) => BaseIssue<unknown>
}
export const valibotValidator = (params: Params) =>
(() => {
return {
validate({ value }, fn) {
if (fn.async) return
const result = safeParse(fn, value)
if (result.success) return
if (params.errorMap) {
return params.errorMap(result.issues)
}
return result.issues.map((i) => i.message).join(', ')
},
async validateAsync({ value }, fn) {
const result = await safeParseAsync(fn, value)
if (result.success) return
if (params.errorMap) {
return params.errorMap(result.issues)
}
return result.issues.map((i) => i.message).join(', ')
},
}
}) as Validator<
unknown,
| BaseSchema<unknown, unknown, BaseIssue<unknown>>
| BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>
>
@t-rosa @crutchcorn
I have a question.
According to what you(@t-rosa ) suggested, when valibotValidator
is used in useForm
it can be used like:
validatorAdapter: valibotValidator({errorMap: errors => errors[0]}),
But the problem is that even if I want to use the default value, I have to use it with function call:
validatorAdapter: valibotValidator(),
But because we've been used like:
validatorAdapter: valibotValidator,
it's going to be a problem for people who are already using this validator.
So, should I make another validator, for example, valibotValidatorWithOption`? Please tell me if I'm wrong or if there's any other way.
Thank you for your help
Maaaybe this? (Sorry, it's not very clean and I'm not sure at all)
import { safeParse, safeParseAsync } from 'valibot'
import type { BaseIssue, BaseSchema, BaseSchemaAsync } from 'valibot'
import type { Validator } from '@tanstack/form-core'
type Params = {
errorMap?: (
errors: [BaseIssue<unknown>, ...BaseIssue<unknown>[]],
) => BaseIssue<unknown>
}
type validateValue = { value: unknown }
type validateFn =
| BaseSchema<unknown, unknown, BaseIssue<unknown>>
| BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>
export const valibotValidator = ((params: Params = {}) => {
return {
validate({ value }: validateValue, fn: validateFn) {
if (fn.async) return
const result = safeParse(fn, value)
if (result.success) return
if (params.errorMap) {
return params.errorMap(result.issues)
}
return result.issues.map((i) => i.message).join(', ')
},
async validateAsync({ value }: validateValue, fn: validateFn) {
const result = await safeParseAsync(fn, value)
if (result.success) return
if (params.errorMap) {
return params.errorMap(result.issues)
}
return result.issues.map((i) => i.message).join(', ')
},
}
}) as Validator<
unknown,
| BaseSchema<unknown, unknown, BaseIssue<unknown>>
| BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>
> &
((
params: Params,
) => Validator<
unknown,
| BaseSchema<unknown, unknown, BaseIssue<unknown>>
| BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>
>)
@t-rosa this looks awesome! Let's move forward with it in a PR :)
@seobbang this WILL be a breaking change, but a very minor one. We're prior to 1.0 so that shouldn't be a problem
I'm sorry for my slow understanding.
The current type of validatorAdapter
:
export type Validator<Type, Fn = unknown> = () => {
validate(options: {
value: Type;
}, fn: Fn): ValidationError;
validateAsync(options: {
value: Type;
}, fn: Fn): Promise<ValidationError>;
};
But if i use valibotValidator
like this with @t-rosa 's code
validatorAdapter: valibotValidator({ errorMap: (errors) => errors[0] }),
valibotValidator return just
{
validate({ value }: validateValue, fn: validateFn) {
if (fn.async) return
const result = safeParse(fn, value)
if (result.success) return
if (params.errorMap) {
return params.errorMap(result.issues)
}
return result.issues.map((i) => i.message).join(', ')
},
async validateAsync({ value }: validateValue, fn: validateFn) {
const result = await safeParseAsync(fn, value)
if (result.success) return
if (params.errorMap) {
return params.errorMap(result.issues)
}
return result.issues.map((i) => i.message).join(', ')
},
}
Not function, just object.
So the types don't match. How can this be solved?
I'm sorry for my slow understanding.
The current type of
validatorAdapter
:export type Validator<Type, Fn = unknown> = () => { validate(options: { value: Type; }, fn: Fn): ValidationError; validateAsync(options: { value: Type; }, fn: Fn): Promise<ValidationError>; };
But if i use
valibotValidator
like this with @t-rosa 's codevalidatorAdapter: valibotValidator({ errorMap: (errors) => errors[0] }),
valibotValidator return just
{ validate({ value }: validateValue, fn: validateFn) { if (fn.async) return const result = safeParse(fn, value) if (result.success) return if (params.errorMap) { return params.errorMap(result.issues) } return result.issues.map((i) => i.message).join(', ') }, async validateAsync({ value }: validateValue, fn: validateFn) { const result = await safeParseAsync(fn, value) if (result.success) return if (params.errorMap) { return params.errorMap(result.issues) } return result.issues.map((i) => i.message).join(', ') }, }
Not function, just object.
So the types don't match. How can this be solved?
Yes, you're right.
I think we should use the first version, even if it's a mini breaking change. as crutchcorn said it shouldn't be a problem :)
So this one maybe the best option:
import { safeParse, safeParseAsync } from 'valibot'
import type { BaseIssue, BaseSchema, BaseSchemaAsync } from 'valibot'
import type { Validator } from '@tanstack/form-core'
type Params = {
errorMap?: (
errors: [BaseIssue<unknown>, ...BaseIssue<unknown>[]],
) => BaseIssue<unknown>
}
export const valibotValidator = (params: Params = {}) =>
(() => {
return {
validate({ value }, fn) {
if (fn.async) return
const result = safeParse(fn, value)
if (result.success) return
if (params.errorMap) {
return params.errorMap(result.issues)
}
return result.issues.map((i) => i.message).join(', ')
},
async validateAsync({ value }, fn) {
const result = await safeParseAsync(fn, value)
if (result.success) return
if (params.errorMap) {
return params.errorMap(result.issues)
}
return result.issues.map((i) => i.message).join(', ')
},
}
}) as Validator<
unknown,
| BaseSchema<unknown, unknown, BaseIssue<unknown>>
| BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>
>
It should be ok for valibot, the PR is open, we should be able to do the same for the other validators
I just submitted a PR to be able to pass a config to the valibot adaptator: https://github.com/TanStack/form/pull/980
Using the abortPipeEarly
option of valibot felt cleaner for me, and would fix this issue too...