Reputation: 9811
I have form with dynamic amount of inputs (admin email) however checking for uniqueness fails:
validationSchema={Yup.object().shape({
adminEmails: Yup.array()
.of(
Yup.string()
.notOneOf(Yup.ref('adminEmails'), 'E-mail is already used')
What is best approach here?
FYI, as a form helper I use Formik
.
Upvotes: 18
Views: 33529
Reputation: 319
import _ from "lodash";
import {addMethod, array, defaultLocale, Flags, Message, TestContext} from 'yup';
// https://github.com/jquense/yup?tab=readme-ov-file#extending-built-in-schema-with-new-methods
// https://github.com/jquense/yup/issues/345
declare module 'yup' {
interface ArraySchema<TIn extends any[] | null | undefined, TContext, TDefault = undefined, TFlags extends Flags = ''> {
unique(props?: UniqueProps): this;
}
interface ArrayLocale {
unique?: Message<UniqueExtra>;
}
}
export type UniqueProps = {
/** if the item of array is object, fieldName used to associate a unique field */
fieldName?: string,
/** get the duplicated values */
verbose?: boolean,
/** custom message */
message?: Message<UniqueExtra>,
};
export type UniqueExtra = {
/** if {@link UniqueProps#fieldName} assigned, this will be fieldLabel */
itemLabel: string;
/** value join by ',' */
rawValue?: string;
};
function rawValue(key: string, record: Record<string, number[]>) {
return `${key}[${record[key].join(",")}]`;
}
addMethod(array, 'unique', function (props: UniqueProps = {}) {
return this.test({
name: "unique",
// @ts-ignore
message: props.message || defaultLocale.array.unique || "${path} must be unique, ${itemLabel}${rawValue} is duplicated!",
params: {itemLabel: "value", rawValue: ""} as UniqueExtra,
test(value: any[] | undefined, context: TestContext) {
if (!value || value.length <= 1) return true;
// arraySchema, itemSchema, fieldSchema
let itemSchema = context.schema.innerType;
const fieldName = props.fieldName;
if (fieldName) {
// itemSchema <- fieldSchema
itemSchema = itemSchema.fields[fieldName];
value = value.map((item: any) => item[fieldName]);
}
const itemLabel = itemSchema.spec.label;
if (props.verbose !== true) {
return value.length === new Set(value).size
|| context.createError({
params: {itemLabel,},
});
}
const grouped = _(value)
.map((item, index) => ({value: item, index}))
.groupBy('value')
.pickBy((value, _key) => value.length > 1)
.mapValues((value, _key) => value.map(item => item.index))
.value()
;
const keys = Object.keys(grouped);
if (keys.length > 0) {
return context.createError({
params: {itemLabel, rawValue: keys.map(key => rawValue(key, grouped)).join(','),},
});
}
return true;
}
});
});
Upvotes: 1
Reputation: 11
Improving on this answer, this works for me
import { array, ArraySchema, Flags, number, object } from 'yup';
export const yupTestUnique = <
TIn extends any[] | null | undefined,
TContext,
TDefault,
TFlags extends Flags
>(params: {
arraySchema: ArraySchema<TIn, TContext, TDefault, TFlags>;
iteratee?: (
value: TIn extends readonly (infer ElementType)[] ? ElementType : never
) => any;
message?: string;
}) => {
return params.arraySchema.test(
"unique",
params.message ? params.message : 'must be unique',
(values, context) => {
return values?.length
? new Set(
values.map((value) =>
typeof params.iteratee === "function"
? params.iteratee(value)
: value
)
).size === values.length
: true;
}
);
};
// Example
const createProductUnitValidationSchema = object({
unitId: number().required(),
price: number().required().min(0),
});
const createProductValidationSchema = object({
productUnits: yupTestUnique({
arraySchema: array()
.required()
.of(createProductUnitValidationSchema)
.min(1),
iteratee: (value) => value.unitId,
}),
});
Upvotes: 1
Reputation: 1350
This is a simple inline solution to validate that an array of strings only contains unique elements:
Yup.array().of(Yup.string())
.test(
'unique',
'Only unique values allowed.',
(value) => value ? value.length === new Set(value)?.size : true
)
Upvotes: 8
Reputation: 1356
To improve the performance of Alex's answer, use something like this to pre-calculate the lookup (using lodash).
yup.addMethod(yup.mixed, 'uniqueIn', function (array = [], message) {
return this.test('uniqueIn', message, function (value) {
const cacheKey = 'groups';
if (!this.options.context[cacheKey]) {
this.options.context[cacheKey] = _.groupBy(array, x => x);
}
const groups = this.options.context[cacheKey];
return _.size(groups[value]) < 2;
});
});
then call validate with an object for context so we can store our calculated group for the duration of the validate call
schema.validate(data, {context: {}}).then(...);
Upvotes: 2
Reputation: 61
If you want to have the errors in each field and not in the array
Yup.addMethod(Yup.mixed, 'uniqueIn', function (array = [], message) {
return this.test('uniqueIn', message, function (value) {
return array.filter(item => item === value).length < 2;
});
});
Upvotes: 6
Reputation: 5410
Simply Do This It works for me
First Define this function in your react component
Yup.addMethod(Yup.array, "unique", function (message, mapper = (a) => a) {
return this.test("unique", message, function (list) {
return list.length === new Set(list.map(mapper)).size
})
})
Just Put this schema inside your Formik tag
<Formik
initialValues={{
hotelName: "",
hotelEmail: [""],
}}
validationSchema={Yup.object().shape({
hotelName: Yup.string().required("Please enter hotel name"),
hotelEmail: Yup.array()
.of(
Yup.object().shape({
email: Yup.string()
.email("Invalid email")
.required("Please enter email"),
}),
)
.unique("duplicate email", (a) => a.email),
})}
onSubmit={(values, { validate }) => {
getFormPostData(values)
}}
render={({ values, errors, touched }) => (
<Form>
<FieldArray
name="hotelEmail"
render={(arrayHelpers) => (
<>
{values.hotelEmail.map((hotel, index) => (
<div class="row" key={index}>
<div className="col-md-8 mt-3">
<div className="user-title-info user-details">
<div className="form-group d-flex align-items-center mb-md-4 mb-3">
<label className="mb-0" htmlFor="hotelName">
{lang("Hotelmanagement.hotelsystemadmin")}
<sup className="text-danger">*</sup>
</label>
<div className="w-100">
<Field
name={`hotelEmail.${index}.email`}
className="form-control"
id="hotelEmail"
placeholder={lang(
"Hotelmanagement.hotelsystemadmin",
)}
/>
<span className="text-danger d-block">
{errors &&
errors.hotelEmail &&
errors.hotelEmail[index] &&
errors.hotelEmail[index].email && (
<span className="text-danger d-block">
{errors.hotelEmail[index].email}
</span>
)}
{errors &&
errors.hotelEmail &&(
<span className="text-danger d-block">
{errors.hotelEmail}
</span>
)}
</span>
</div>
</div>
</div>
</div>
<div className="col-md-2 mt-3">
{index > 0 && (
<i
className="bx bx-minus addnewBtn "
onClick={() => arrayHelpers.remove(index)}
/>
)}
{index === values.hotelEmail.length - 1 && (
<i
className="bx bx-plus addnewBtn ml-5"
onClick={() => arrayHelpers.push("")}
/>
)}
</div>
</div>
))}
</>
)}
/>
not to show error do this following
{errors &&
errors.hotelEmail &&(
<span className="text-danger d-block">
{errors.hotelEmail}
</span>
)}
)}
/>
Upvotes: 3
Reputation: 31
Probably too late to respond, but anyways, you should use this.createError({path, message});
See example:
yup.addMethod(yup.array, 'growing', function(message) {
return this.test('growing', message, function(values) {
const len = values.length;
for (let i = 0; i < len; i++) {
if (i === 0) continue;
if (values[i - 1].intervalTime > values[i].intervalTime) return this.createError({
path: `intervals[${i}].intervalTime`,
message: 'Should be greater than previous interval',
});
}
return true;
});
});
Upvotes: 2
Reputation: 6820
Try this:
Yup.addMethod(Yup.array, 'unique', function(message, mapper = a => a) {
return this.test('unique', message, function(list) {
return list.length === new Set(list.map(mapper)).size;
});
});
Then use it like this:
const headersSchema = Yup.object().shape({
adminEmails: Yup.array().of(
Yup.string()
)
.unique('email must be unique')
})
Upvotes: 24