Create a version of yup that uses useIntl
This commit is contained in:
parent
fa5664d555
commit
de840411fb
4 changed files with 115 additions and 207 deletions
|
@ -1,13 +0,0 @@
|
||||||
export * from 'yup';
|
|
||||||
export * from './yup';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NEVER CALL THIS FUNCTION FROM YOUR CODE. IT WILL THROW AN EXCEPTION
|
|
||||||
*
|
|
||||||
* setLocale is handled in `app/javascripts/common/yup/yup.ts`
|
|
||||||
* @throws Error
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
||||||
export function setLocale():never {
|
|
||||||
throw new Error('setLocale is handled in `app/javascripts/common/yup/yup.ts`. NEVER call this function from your code');
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
// License: LGPL-3.0-or-later
|
|
||||||
// yup errors are super confusing until formik turns them into something logical
|
|
||||||
import { validateYupSchema, yupToFormErrors } from 'formik';
|
|
||||||
|
|
||||||
import * as yup from './';
|
|
||||||
import {createMessage, YupFail} from './';
|
|
||||||
|
|
||||||
|
|
||||||
describe("yup", () => {
|
|
||||||
describe('.createMessage', () => {
|
|
||||||
const nameLabel = 'Name';
|
|
||||||
const customTranslationId = 'translation.id.path';
|
|
||||||
it('createMessage creates the proper functions', async() => {
|
|
||||||
expect.assertions(4);
|
|
||||||
|
|
||||||
const schema = yup.object({
|
|
||||||
name: yup.string().label(nameLabel).min(20),
|
|
||||||
id: yup.string().required(createMessage(({ path}) => (new YupFail( customTranslationId, {path})))),
|
|
||||||
address: yup.object({
|
|
||||||
city: yup.string().required(),
|
|
||||||
state: yup.string(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// This is the equivalent of getting errors from the FormikContext
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
||||||
let errors:any = null;
|
|
||||||
try {
|
|
||||||
await validateYupSchema({name: "not 20 chars"}, schema, false);
|
|
||||||
}
|
|
||||||
catch(e) {
|
|
||||||
//turn into useful errors
|
|
||||||
errors = yupToFormErrors(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(errors.name.message).toStrictEqual([{id:"yup.string.min"}, {path: nameLabel, min: 20}]);
|
|
||||||
expect(errors.id.message).toStrictEqual([{id:customTranslationId}, {path: 'id'}]);
|
|
||||||
expect(errors.address.city.message).toStrictEqual([{id:'yup.mixed.required'}, {path: 'address.city'}]);
|
|
||||||
expect(errors.address.state).toBeUndefined();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,148 +0,0 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { setLocale, LocaleObject, TestMessageParams } from 'yup';
|
|
||||||
import type {IntlShape} from '../../components/intl';
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps a validation failure from yup
|
|
||||||
* @date 2020-10-24
|
|
||||||
* @export
|
|
||||||
* @class YupFail
|
|
||||||
* @example
|
|
||||||
* const schema = yup.schema({
|
|
||||||
* // we use createMessage to make sure we provide the correct function to min
|
|
||||||
* name: yup.string.min(20, createMessage(
|
|
||||||
* //we're taking the path and min properties of the min function's result
|
|
||||||
* ({path, label, min}) => {
|
|
||||||
* if (label) {
|
|
||||||
* // in yup messages, path represents the name of the field.
|
|
||||||
* // if label is provided, that's a better name
|
|
||||||
* path = label;
|
|
||||||
* }
|
|
||||||
* return new YupFail('translation.id.path',
|
|
||||||
* {path, min} // you HAVE to pass path here, even if the message doesn't use it
|
|
||||||
* );
|
|
||||||
* }
|
|
||||||
* ))
|
|
||||||
* })
|
|
||||||
*
|
|
||||||
* // the yup schema was passed into Formik and formikConfig.errors has been set for the name
|
|
||||||
* // field. We assume the `errors` const has latest formikConfig.errors
|
|
||||||
*
|
|
||||||
* // the below code is part of a TSX component
|
|
||||||
* <ul className="errorListForName">
|
|
||||||
* {
|
|
||||||
* errors.name.map((yupFail:YupFail) => (
|
|
||||||
* <li key={yupFail.id} className="errorItem">{formatMessage(...yupFail.message)</li>
|
|
||||||
* )
|
|
||||||
* )
|
|
||||||
* }
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export class YupFail {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an instance of Description.
|
|
||||||
* @date 2020-10-24
|
|
||||||
* @param id the translation ID of the failure message
|
|
||||||
* @param values the values to be interpolated into the failure message ID'd by {@link id}
|
|
||||||
*/
|
|
||||||
constructor(readonly id: string, readonly values: Parameters<ToPropResult>[0]) {
|
|
||||||
this.id = id;
|
|
||||||
this.values = values;
|
|
||||||
Object.bind(this, this.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
get message(): Parameters<IntlShape['formatMessage']> {
|
|
||||||
return [{id:this.id}, this.values];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
type RequiredValueProps = {path:string};
|
|
||||||
/**
|
|
||||||
* A spec
|
|
||||||
*/
|
|
||||||
type TestOptionsMessageFn<Extra extends Record<string, any> = Record<string, any>, R = any> =
|
|
||||||
| ((params: Extra & Partial<TestMessageParams> & RequiredValueProps) => R)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export type ToPropResult<Extra extends Record<string, any> = Record<string, any>> = TestOptionsMessageFn<Extra, YupFail>
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A function used for improving typescript safety when manually creating your
|
|
||||||
* own validation messages
|
|
||||||
* @param fn a function
|
|
||||||
*/
|
|
||||||
export function createMessage<Extra extends Record<string, any> = Record<string, any>>(fn: ToPropResult<Extra>): ToPropResult<Extra> {
|
|
||||||
return fn;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
type HoudiniYupLocaleObject = Required<LocaleObject>;
|
|
||||||
|
|
||||||
// TODO: we could optimize this and only run it the first time the module is
|
|
||||||
// loaded
|
|
||||||
|
|
||||||
const locale: HoudiniYupLocaleObject = {
|
|
||||||
mixed: {
|
|
||||||
default: message("yup.mixed.default"),
|
|
||||||
required: message("yup.mixed.required"),
|
|
||||||
oneOf: message("yup.mixed.oneOf"),
|
|
||||||
notOneOf: message("yup.mixed.notOneOf"),
|
|
||||||
},
|
|
||||||
string: {
|
|
||||||
length: message("yup.string.length"),
|
|
||||||
min: message("yup.string.min"),
|
|
||||||
max: message("yup.string.max"),
|
|
||||||
matches: message("yup.string.regex"),
|
|
||||||
email: message("yup.string.email"),
|
|
||||||
url: message("yup.string.url"),
|
|
||||||
uuid: message("yup.string.uuid"),
|
|
||||||
trim: message("yup.string.trim"),
|
|
||||||
lowercase: message("yup.string.lowercase"),
|
|
||||||
uppercase: message("yup.string.uppercase"),
|
|
||||||
},
|
|
||||||
number: {
|
|
||||||
min: message("yup.number.min"),
|
|
||||||
max: message("yup.string.max"),
|
|
||||||
lessThan: message("yup.number.lessThan"),
|
|
||||||
moreThan: message("yup.number.moreThan"),
|
|
||||||
|
|
||||||
positive: message("yup.number.postive"),
|
|
||||||
negative: message("yup.number.negative"),
|
|
||||||
integer: message("yup.number.integer"),
|
|
||||||
},
|
|
||||||
date: {
|
|
||||||
min: message("yup.date.min"),
|
|
||||||
max: message("yup.data.max"),
|
|
||||||
},
|
|
||||||
|
|
||||||
boolean: {
|
|
||||||
|
|
||||||
},
|
|
||||||
object: {
|
|
||||||
noUnknown: message("yup.object.noUnknown"),
|
|
||||||
},
|
|
||||||
|
|
||||||
array: {
|
|
||||||
min: message("yup.array.min"),
|
|
||||||
max: message("yup.array.max"),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function message<Extra extends Record<string, any> = Record<string, unknown>>(id: string): ToPropResult<Extra> {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
return createMessage(({ path, label, value, originalValue, ...other }) => {
|
|
||||||
if (label) {
|
|
||||||
path = label;
|
|
||||||
}
|
|
||||||
return new YupFail(id, { path, ...other });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setLocale(locale);
|
|
115
app/javascript/hooks/useYup.ts
Normal file
115
app/javascript/hooks/useYup.ts
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import set from 'lodash/set';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useIntl } from '../components/intl';
|
||||||
|
import type { IntlShape } from '../components/intl';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simply yup but with the default validation messages set to the current locale from `IntlProvider`
|
||||||
|
* @description
|
||||||
|
* @date 2020-11-05
|
||||||
|
* @export
|
||||||
|
* @returns yup
|
||||||
|
*/
|
||||||
|
export default function useYup(): typeof yup {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { locale, messages } = intl;
|
||||||
|
useEffect(() => {
|
||||||
|
yup.setLocale(generateYupLocale(messages));
|
||||||
|
}, [locale, messages]);
|
||||||
|
return yup;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function generateYupLocale(messages: IntlShape['messages']) : typeof yupValidationMessagesToTranslationKeys {
|
||||||
|
const newLocale:Partial<typeof yupValidationMessagesToTranslationKeys> = {};
|
||||||
|
|
||||||
|
for (const childKey in yupValidationMessagesToTranslationKeys) {
|
||||||
|
const child = get(yupValidationMessagesToTranslationKeys, childKey);
|
||||||
|
const newChild= {};
|
||||||
|
for (const subchildKey in child) {
|
||||||
|
const subchild = get(child, subchildKey);
|
||||||
|
const result = ((messages[subchild] as string) || subchild).replace(/{/gi, "${");
|
||||||
|
set(newChild, subchildKey, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
set(newLocale, childKey, newChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newLocale as typeof yupValidationMessagesToTranslationKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const yupValidationMessagesToTranslationKeys = {
|
||||||
|
mixed: {
|
||||||
|
default: "yup.mixed.default",
|
||||||
|
required: "yup.mixed.required",
|
||||||
|
oneOf: "yup.mixed.oneOf",
|
||||||
|
notOneOf: "yup.mixed.notOneOf",
|
||||||
|
},
|
||||||
|
string: {
|
||||||
|
length: "yup.string.length",
|
||||||
|
min: "yup.string.min",
|
||||||
|
max: "yup.string.max",
|
||||||
|
matches: "yup.string.regex",
|
||||||
|
email: "yup.string.email",
|
||||||
|
url: "yup.string.url",
|
||||||
|
uuid: "yup.string.uuid",
|
||||||
|
trim: "yup.string.trim",
|
||||||
|
lowercase: "yup.string.lowercase",
|
||||||
|
uppercase: "yup.string.uppercase",
|
||||||
|
},
|
||||||
|
number: {
|
||||||
|
min: "yup.number.min",
|
||||||
|
max: "yup.string.max",
|
||||||
|
lessThan: "yup.number.lessThan",
|
||||||
|
moreThan: "yup.number.moreThan",
|
||||||
|
|
||||||
|
positive: "yup.number.postive",
|
||||||
|
negative: "yup.number.negative",
|
||||||
|
integer: "yup.number.integer",
|
||||||
|
},
|
||||||
|
date: {
|
||||||
|
min: "yup.date.min",
|
||||||
|
max: "yup.data.max",
|
||||||
|
},
|
||||||
|
|
||||||
|
boolean: {
|
||||||
|
|
||||||
|
},
|
||||||
|
object: {
|
||||||
|
noUnknown: "yup.object.noUnknown",
|
||||||
|
},
|
||||||
|
|
||||||
|
array: {
|
||||||
|
min: "yup.array.min",
|
||||||
|
max: "yup.array.max",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type RequiredValueProps = {path:string};
|
||||||
|
/**
|
||||||
|
* A spec
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type TestOptionsMessageFn<Extra extends Record<string, any> = Record<string, any>, R = any> =
|
||||||
|
| ((params: Extra & Partial<yup.TestMessageParams> & RequiredValueProps) => R)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type ToPropResult<Extra extends Record<string, any> = Record<string, any>> = TestOptionsMessageFn<Extra>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function used for improving typescript safety when manually creating your
|
||||||
|
* own validation messages
|
||||||
|
* @param fn a function
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export function createMessage<Extra extends Record<string, any> = Record<string, any>>(fn: ToPropResult<Extra>): ToPropResult<Extra> {
|
||||||
|
return fn;
|
||||||
|
}
|
Loading…
Reference in a new issue