How to set dynamic error messages in Yup async validation? - reactjs

I am trying async validation in Formik using Yup's .test() method and need to set the error message that I get from the API. Error messages are going to be different based on some conditions in backend.
Tried few solutions mentioned here
https://github.com/jquense/yup/issues/222 and Dynamic Validation Messages Using Yup and Typescript
But Yup is throwing the default error message given in test().
Documentation says that
All tests must provide a name, an error message and a validation function that must return true or false or a ValidationError. To make a test async return a promise that resolves true or false or a ValidationError.
I am resolving a new ValidationError with the error message but still, it throws the default error.
Here is the code.
const schema = Yup.object().shape({
email: Yup.string().test(
"email_async_validation",
"Email Validation Error", // YUP always throws this error
value => {
return new Promise((resolve, reject) => {
emailValidationApi(value)
.then(res => {
const { message } = res.data; // I want this error message to be shown in form.
resolve(new Yup.ValidationError(message));
})
.catch(e => {
console.log(e);
});
});
}
)
});

I got it working using the function syntax instead of arrow function for validation function.
Doc says:
test functions are called with a special context, or this value, that
exposes some useful metadata and functions. Note that to use this
context, the test function must be a function expression (function test(value) {}), not an arrow function, since arrow functions have
lexical context.
Here is the working code.
const schema = Yup.object().shape({
email: Yup.string()
.email("Not a valid email")
.required("Required")
.test("email_async_validation", "Email Validation Error", function (value) { // Use function
return emailValidationApi(value)
.then((res) => {
const message = res;
console.log("API Response:", message);
return this.createError({ message: message });
// return Promise.resolve(this.createError({ message: message })); // This also works
})
.catch((e) => {
console.log(e);
});
})
});

Actually you're almost correct.You just need to use the following:
resolve(this.createError({ message: message }));
Let me know if it still doesn't work ya

I am able to do this with arrow function as well.
const schema = Yup.object().shape({
email: Yup.string()
.email("Not a valid email")
.required("Required")
.test("email_async_validation", "Email Validation Error", (value, {createError}) {
return emailValidationApi(value)
.then((res) => {
const message = res;
console.log("API Response:", message);
return createError({ message: message });
})
.catch((e) => {
console.log(e);
});
})
});

Don't pass second parameter, as we generally pass it as a error message instead create your own custom message using "createError" and return it on your condition.
import * as yup from "yup";
const InitiateRefundSchema = yup.object().shape({
amountPaid: yup.number(),
refundAmount: yup
.number()
.test("test-compare a few values", function (value) {
let value1 = this.resolve(yup.ref("amountPaid"));
let value2 = this.resolve(yup.ref("refundAmount"));
if (value1 < value2) {
return this.createError({
message: `refund amount cannot be greater than paid amount '${value1}'`,
path: "refundAmount", // Fieldname
});
} else return true;
}),
})

With Internatilization ('vue-i18n') and Yup ('yup') options you could use as:
const fullname = yup.string().required(this.$t('validate.required')).min(8, this.$t('validate.min', {min: '8'}))
So with this line below, the text error message will be changed as the locale option change.
this.$t('validate.min')
pt.ts
validation: {
required: 'Esse campo é obrigatório',
size_min: 'Deve no minimo {min} caracteres',
}
en.ts
validation: {
required: 'Required please',
size_min: 'Please only {min} caracteres',
}

Related

react-hook-form's validate function always returns errors

This is the one that occurs problem.
<input
type="text"
name="nickname"
{...register('nickname', {
required: true,
validate: async (value) =>
value !== profile?.nickname &&
(await validateNickname(value).catch(() => {
return 'Invalid nickname.';
})),
})}
placeholder="Nickname"
/>
If the input value is not same with the defaultValue
and It's duplicated with another nickname, (validateNickname function returns error)
Then, error message is registered 'Invalid nickname.'
and the errors object looks like this below.
{
nickname:
message: "Invalid nickname."
ref:...
type: "validate"
}
but the problem is
If I input the value which is same with the defaultValue,
or If I input not duplicated value,
The errors object should be empty.
but it still returns error like this below.
{
nickname:
message: ""
ref:...
type: "validate"
}
so there's no error message registered, but somehow, error is exist.
Please let me know if there's anything I'm doing wrong.
Thanks!
This could be the problem:
(await validateNickname(value).catch(() => {
return 'Invalid nickname.';
}))
You want to return a boolean in your AND expression, but you are returning a string, which would be saying the validation is correct.
If you want to handle that validation error so as to display the message in html you can do a simple error handling approach like this one (using named validations):
// ... your validation set up
validate:{
validateNickname_ : async (value) => {
let response = false;
await validateNickname(value)
.then( () => { response = true;})
.catch(() => { response = false;});
return value !== profile?.nickname && response;
}
}
// ... showing your error message
{
errors.nickname && errors.nickname.type=="validateNickname_" &&
<p className='error-form-msg'>
{"Your custom error message here ..."}
</p>
}

How to get Yup to perform more than one custom validation?

I'm working on a ReactJS project. I'm learning to use Yup for Validation with FormIk . The following code works fine:
const ValidationSchema = Yup.object().shape({
paymentCardName: Yup.string().required(s.validation.paymentCardName.required),
paymentCardNumber: Yup.string()
/*
.test(
"test-num",
"Requires 16 digits",
(value) => !isEmpty(value) && value.replace(/\s/g, "").length === 16
)
*/
.test(
"test-ctype",
"We do not accept this card type",
(value) => getCardType(value).length > 0
)
.required(),
But the moment I uncomment the test-num the developer tools complain about an uncaught promise:
How do I get Yup to give me a different error string based on the validation failure that I detect?
You can use the addMethod method to create two custom validation methods like this.
Yup.addMethod(Yup.string, "creditCardType", function (errorMessage) {
return this.test(`test-card-type`, errorMessage, function (value) {
const { path, createError } = this;
return (
getCardType(value).length > 0 ||
createError({ path, message: errorMessage })
);
});
});
Yup.addMethod(Yup.string, "creditCardLength", function (errorMessage) {
return this.test(`test-card-length`, errorMessage, function (value) {
const { path, createError } = this;
return (
(value && value.length === 16) ||
createError({ path, message: errorMessage })
);
});
});
const validationSchema = Yup.object().shape({
creditCard: Yup.string()
.creditCardType("We do not accept this card type")
.creditCardLength('Too short')
.required("Required"),
});

How to reset recaptcha when using react-redux-firebase

I am working with React-Redux-Firebase. I implemented signing in with phone number. Now I am trying to implement error handling. When number is invalid I display window alert with error message. The only thing left to do is to reset recaptcha. Without it, I am getting error:
reCAPTCHA has already been rendered in this element
I was trying to do according to Firebase documentation
grecaptcha.reset(window.recaptchaWidgetId);
// Or, if you haven't stored the widget ID:
window.recaptchaVerifier.render().then(function(widgetId) {
grecaptcha.reset(widgetId);
}
but it does not work in my code. I dont have grecaptcha implemented. I tried to add it with react-grecaptcha, but it did not work.
Could someone give me a hint how to reset recaptcha after each error, please?
state = {
phone: "",
confirmationResult: {},
};
handleClick = () => {
const recaptchaVerifier = new firebase.auth.RecaptchaVerifier(
"sign-in-button",
{
size: "invisible",
}
);
firebase
.signInWithPhoneNumber(`+${this.state.phone}`, recaptchaVerifier)
.then((confirmationResult) => {
this.setState({ confirmationResult });
})
.catch((error) => {
// Error; SMS not sent
// Handle Errors Here
window.alert(`${error.code}, ${error.message}`);
recaptchaVerifier.reset(); // How can I do that?
});
};
I've been struggling with this problem for several days, maybe my answer will help someone.
export const requestRecaptchVerifier = () => {
window.recaptchaVerifier = new RecaptchaVerifier(
"recapcha-container",
{
size: "invisible",
},
auth
);
};
I then call signInWithPhone from another function and handle the error like this:
await signInWithPhone(formik.values.phone)
.then(() => {
// ... my code
})
.catch(() => {
window.recaptchaVerifier.recaptcha.reset();
window.recaptchaVerifier.clear();
});
All the difference in
window.recaptchaVerifier.recaptcha.reset()
And
window.recaptchaVerifier.clear()
I'm no expert but from the documentation and by talking with you in the comment section I think you need to pass a callback. Like this:
const recaptchaVerifier = new firebase.auth.RecaptchaVerifier('sign-in-button', {
'size': 'invisible',
'callback': function(response) {
// reCAPTCHA solved, allow signInWithPhoneNumber.
firebase
.signInWithPhoneNumber(`+${this.state.phone}`, recaptchaVerifier)
.then((confirmationResult) => {
this.setState({ confirmationResult });
})
.catch((error) => {
// Error; SMS not sent
// Handle Errors Here
window.alert(`${error.code}, ${error.message}`);
recaptchaVerifier.reset();
});
}
});
Reference: https://firebase.google.com/docs/auth/web/phone-auth#use-invisible-recaptcha
Hope this helps!

Handling errors from api with Formik

I am catching errors from api and showing them in form, and that is working fine. But the problem is when I change one field in form all errors disappear. For form I am using Formik and for validation Yup.
const handleSubmit = (values, {setSubmitting, setFieldError, setStatus}) => {
someApiCall(values)
.then(
() => {
},
(error) => {
// example of setting error
setFieldError('email', 'email is already used');
})
.finally(() => {
setSubmitting(false)
});
};
I tried with adding third parametar false to setFieldError, but nothing changed.
Here's my working example: https://codesandbox.io/s/formik-example-dynamic-server-rendered-values-1uv4l
There's a callback validate available in Formik: https://jaredpalmer.com/formik/docs/api/formik#validate-values-values-formikerrors-values-promise-any using which you can probably try to do something like below.
I initiated emailsAlreadyInUse with empty array and then in your API call once error gets returned then add that user to the array and once user uses the same email again and tried to validate, although it will pass Yup validation but it will be caught in validate callback which I believe runs after Yup validation (though I could be wrong but in your case doesn't matter).
const emailsAlreadyInUse= [];
const handleSubmit = (values, {
setSubmitting,
setFieldError,
setStatus
}) => {
someApiCall(values)
.then(
() => {
// Do something
// possibly reset emailsAlreadyInUse if needed unless component is going to be unmounted.
},
(error) => {
// example of setting error
setFieldError('email', 'email is already used');
// Assuming error object you receive has data object that has email property
emailsAlreadyInUse.push(error.data.email);
})
.finally(() => {
setSubmitting(false)
});
};
<Formik
...
...
validate = {
values => {
let errors = {};
if (emailsAlreadyInUse.includes(values.email)) {
errors.email = 'email is already used';
}
return errors;
}
}
/>
I found simplier method to make API validation errors always visible than using validate method. You can set validateOnBlur and validateOnChange on your form to false. It will cause validation only on submit and errors returned from API will remain after changing field value.
Usage:
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
validateOnBlur={false}
validateOnChange={false}
>
...form fields...
</Formik>

How to emit multiple actions in one epic using redux-observable?

I'm new to rxjs/redux observable and want to do two things:
1) improve this epic to be more idiomatic
2) dispatch two actions from a single epic
Most of the examples I see assume that the api library will throw an exception when a fetch fails, but i've designed mine to be a bit more predictable, and with Typescript union types I'm forced to check an ok: boolean value before I can unpack the results, so understanding how to do this in rxjs has been a bit more challenging.
What's the best way to improve the following? If the request is successful, I'd like to emit both a success action (meaning the user is authorized) and also a 'fetch account' action, which is a separate action because there may be times where I need to fetch the account outside of just 'logging in'. Any help would be greatly appreciated!
const authorizeEpic: Epic<ActionTypes, ActionTypes, RootState> = action$ =>
action$.pipe(
filter(isActionOf(actions.attemptLogin.request)),
switchMap(async val => {
if (!val.payload) {
try {
const token: Auth.Token = JSON.parse(
localStorage.getItem(LOCAL_STORAGE_KEY) || ""
);
if (!token) {
throw new Error();
}
return actions.attemptLogin.success({
token
});
} catch (e) {
return actions.attemptLogin.failure({
error: {
title: "Unable to decode JWT"
}
});
}
}
const resp = await Auth.passwordGrant(
{
email: val.payload.email,
password: val.payload.password,
totp_passcode: ""
},
{
url: "localhost:8088",
noVersion: true,
useHttp: true
}
);
if (resp.ok) {
return [
actions.attemptLogin.success({
token: resp.value
})
// EMIT action 2, etc...
];
}
return actions.attemptLogin.failure(resp);
})
);
The docs for switchMap indicate the project function (the lambda in your example) may return the following:
type ObservableInput<T> = SubscribableOrPromise<T> | ArrayLike<T> | Iterable<T>
When a Promise<T> is returned, the resolved value is simply emitted. In your example, if you return an array from your async scope, the array will be sent directly to the Redux store. Assuming you have no special Redux middlewares setup to handle an array of events, this is likely not what you want. Instead, I would recommend returning an observable in the project function. It's a slight modification to your example:
const authorizeEpic: Epic<ActionTypes, ActionTypes, RootState> = action$ =>
action$.pipe(
filter(isActionOf(actions.attemptLogin.request)), // or `ofType` from 'redux-observable'?
switchMap(action => {
if (action.payload) {
try {
const token: Auth.Token = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) || "")
if (!token) {
throw new Error()
}
// return an observable that emits a single action...
return of(actions.attemptLogin.success({
token
}))
} catch (e) {
// return an observable that emits a single action...
return of(actions.attemptLogin.failure({
error: {
title: "Unable to decode JWT"
}
}))
}
}
// return an observable that eventually emits one or more actions...
return from(Auth.passwordGrant(
{
email: val.payload.email,
password: val.payload.password,
totp_passcode: ""
},
{
url: "localhost:8088",
noVersion: true,
useHttp: true
}
)).pipe(
mergeMap(response => response.ok
? of(
actions.attemptLogin.success({ token: resp.value }),
// action 2, etc...
)
: of(actions.attemptLogin.failure(resp))
),
)
}),
)
I don't have your TypeScript type definitions, so I can't verify the example above works exactly. However, I've had quite good success with the more recent versions of TypeScript, RxJS, and redux-observable. Nothing stands out in the above that makes me think you should encounter any issues.
You could zip your actions and return them.
zip(actions.attemptLogin.success({
token: resp.value
})
// EMIT action 2, etc...
So that, now both your actions will be called.

Resources