OtherResonChage only required if ReasonChange array include "Other" in it else that field is not required
export const formikValues = {
reasonChangeJob: [],
otherReasonChangeJob: "",
}
export const questionnaireSchema = Yup.object({
reasonChangeJob: Yup.array()
.required("req")
.min(1, "min q"),
otherReasonChangeJob: Yup.string().when(
"reasonChangeJob",
(reasonChangeJob) => {
if (reasonChangeJob.include("other")) {
return Yup.string().required(" req");
}
}
)
});
I'm using include instead of includes and this is the better way to handle conditional validations
otherReasonChangeJob: Yup.string().when("reasonChangeJob", {
is: (reasonChangeJob) => reasonChangeJob.includes("other"),
then: Yup.string().required("Require please"),
}),
Related
I'm using a hook that fills in my inputs automatically, according to the zip code the user enters. Then the user's address, street, etc are filled in automatically.
However, for the input to be filled in automatically, the component is re-rendering.
As my form is a modal it opens and closes again because of rendering. I need to make the user fill in the zip code, the inputs are filled in real time.
Can you help me with this?
useCEP Hook:
import { useState } from 'react'
import { api } from 'services/apiClient'
interface Cep {
bairro: string
logradouro: string
localidade: string
uf: string
}
export function useCep() {
const [checkCep, setCheckCep] = useState<Cep>()
const getCEP = async (e) => {
const cep = e.target.value.replace(/\D/g, '')
try {
const { data } = await api.get(`https://viacep.com.br/ws/${cep}/json/`)
setCheckCep(data)
} catch (err) {
console.log(err)
}
}
return { checkCep, getCEP }
}
Component:
const { control, formState, register, reset } = useFormContext()
const { checkCep, getCEP } = useCep()
useEffect(() => {
reset({
responsible: [
{
address: checkCep?.logradouro,
district: checkCep?.bairro,
city: checkCep?.localidade,
state: checkCep?.uf,
name: '',
email: '',
student_name: [],
cep: '',
residence: '',
telephone: '',
sex: ''
}
]
})
}, [checkCep])
<Input
name="cep"
type="number"
label="Cep"
{...register(`responsible.${index}.cep`)}
error={errors?.responsible?.[index]?.cep}
onBlur={(e) => getCEP(e)}
/>
{...}
Maybe you can try to use the setValue method instead of reset as it will reset the form.
https://react-hook-form.com/api/useform/setvalue
I'm trying to set course attribute from parameter to my input so that I can be able the insert the value on it but I cannot access the course attribute. ends_level and course becomes any instead of string means I haven't indexed properly the ends_level which the array of object course. I really need your eyes to see something that have missed or missed up. I hope I have explained it well. Thanks in advance y'all.
import { React, useState, useEffect } from 'react';
import Button from 'react-bootstrap/Button';
import { TextField } from '#material-ui/core';
import { useDispatch, useSelector } from 'react-redux';
import { updateProfile } from '../../../actions/profile';
const Profile = ({ data }) => {
const dispatch = useDispatch();
const currentId = data._id;
const [postData, setPostData] = useState(
{
profile: {
name: "",
},
skills: [
{
end: "",
ends_level: [
{
course: "",
level: ""
}
]
},
],
}
);
const profile = useSelector((state) => currentId ? state.profile.find((p) => p._id === currentId) : null);
useEffect(() => {
if(profile) setPostData(profile);
}, [profile])
const handleSubmit = (e) => {
e.preventDefault();
if(currentId) {
dispatch(updateProfile(currentId, postData));
}
}
return (
<form onSubmit={handleSubmit}>
<TextField
value={postData.profile.name}
onChange={(e) =>
setPostData(
{
...postData, profile:
{
...postData.profile, name: e.target.value
}
}
)
}
/>
<TextField
onChange={(e) =>
setPostData(
{
...postData, skills:
{
...postData.skills, ends_level:
{
...postData.skills.ends_level, course: e.target.value
}
}
}
)
}
/>
<Button variant="primary" size="md" type="submit" block >Save</Button>
</form>
);
}
export default Profile;
The problem seems to be you are using spread operators to spread objects into positions where you originally had arrays. Also when trying to sort out an answer, I feel like you aren't addressing a plurality problem with these arrays. Namely, you have a single target value which may apply to an array full of skills and an array full of ends_levels in each skill, so you have to decide how/what you want the new changed to value to apply to.
Here is a good start, with TypeScript enhancements, that lays out the schema and attempts to craft a new "post data" object which looks like the old one. You will have to fill out the commented section below to affect the array(s) with the new value as you see fit.
interface SkillType {
end: string,
ends_level: [
{
course: string,
level: string
}
]
}
interface DataType {
profile: {
name: string
},
skills: SkillType[]
}
let postData: DataType = {
profile: {
name: "",
},
skills: [
{
end: "",
ends_level: [
{
course: "",
level: ""
}
]
},
],
};
// Create a new holder with all the old skills
let newSkills : SkillType[] = [... postData.skills ];
// Will have multiple skills per post-data
// Each post-data will have multiple "ends_levels" in it
// Do you want to merge it so all of these blocks now have the new value e.target.value ??
// If so, manipulate newSkills
let newPostData: DataType = {
...postData,
skills: newSkills
}
I have a validation that only validates its fields by checking if they are break its only rules, but it allows duplications inside the Array.
I want some sort of condition that won't allow duplicated values inside of it.
My object:
respostas: Yup.array()
.of(
Yup.object().shape({
nome: Yup.string()
.trim()
.max(1000, Messages.RESPOSTA_ENQUETE_TAMANHO_MAXIMO)
.required(Messages.CAMPO_OBRIGATORIO)
})
)
.required(Messages.RESPOSTA_QUANTIDADE_MINIMA)
For example, I would have an array like this: [aaa,bbb,aaa] allowed, but I don't want this duplication.
To validate this you can write your own .test(). You can add the following test to the Yup object.
.test("Unique", "Values need te be unique", values => {
return (new Set(values)).size === values.length;
})
The error will be added to the object in general, not at the specific field.
const { addOrEdit, recordForEdit, fabricTypeList } = props;
const duplicateNameCheck = (list, value) => {
for (var i = 0; i < list.length; i++) {
if (value === list[i].name) {
return false;
} else {
return true;
}
}
};
const validationSchema = yup.object().shape({
name: yup
.string()
.required("Name is required")
.test("Unique", "Name needs te be unique", (values) => {
return duplicateNameCheck(fabricTypeList, values);
}),
});
const formik = useFormik({
initialValues: initialValues,
validationSchema: validationSchema,
onSubmit: (values, { setSubmitting, resetForm }) => {
setSubmitting(true);
addOrEdit(values, resetForm, setSubmitting);
},
});
I have two stores: formStore and profileStore
FormStore
export class ProfileFormStore {
#observable editing = false;
profileStore = new ProfileStore(this.roleId);
originalValue?: ApiModel | null;
#action.bound
startEdit() {
// this.originalValue = this.profileStore.toJson();
/* if uncomment above, next error thrown
RangeError: Maximum call stack size exceeded
at initializeInstance (mobx.module.js:391)
at ProfileStore.get (mobx.module.js:381)
at ProfileStore.get
*/
this.editing = true;
}
}
ProfileStore
export class ProfileStore {
#observable userProfile: ApiModel = {
userProfile: {
newsAndUpdates: false,
email: "",
phone: "",
lastName: "",
firstName: "",
},
};
#observable email = "";
#action.bound
fetch() {
// this.fromJson(this.actions.fetch());
console.log("start");
this.email = "qwe";
console.log("end");
}
#computed
toJson(): ApiModel {
return {
userProfile: {
firstName: this.userProfile.userProfile.firstName,
lastName: this.userProfile.userProfile.lastName,
phone: this.userProfile.userProfile.phone,
email: this.userProfile.userProfile.email,
newsAndUpdates: this.userProfile.userProfile.newsAndUpdates,
},
};
}
}
And I want to use contexts
const formStore = new ProfileFormStore();
export const profileFormContext = React.createContext({
formStore,
profileStore: formStore.profileStore,
});
export const useProfileContext = () => React.useContext(profileFormContext);
And there are two components: form and formControl
const controls = {
admin: (<><ProfileName /><Email /></>),
user: (<><ProfileName /></>)
};
export const Form = () => {
const { formStore, profileStore } = useProfileContext();
// this.fromJson(this.actions.fetch()); // if uncomment throws 'Missing option for computed get'
return <form>(controls.admin)</form>
}
export const ProfileName = () => {
const { formStore, profileStore } = useProfileContext();
formStore.startEdit(); // check form store, when assigning from profileStore get overflow error
return formStore.editing ? <input value='test' /> : <label>Test</label>
}
So there are two kinds of errors:
When accessing observables from ProfileStore that is part of FormStore
When updating observables in ProfileStore that is part of FormStore
the FormStore working well
both stores injecting via React.useContext have followed these example https://mobx-react.js.org/recipes-context , however their stores are not nested. I made them nested, beacuse I wanted to get access to profileStore from formStore
What do these errors mean? How to fix them?
Actually it is not the answer :) But the solution I have used
export class ProfileStore {
#observable editing;
#observablt userProfile: UserProfile;
...
}
That's all - instead of using two stores, now there is one store, I happy that solution is working. I assume that error was that I forgot to write get at toJson. If in future I encounter same error and understand why it happened. I will try not to forget to update this answer.
I have this piece of a code. I want to add error messages depending on user's locale, but yup throws errors, same if fields are filled in incorrectly
[missing "en.login.emailRequiredError" translation]
[missing "en.login.passRequiredError" translation]
const schema = yup.object().shape({
email: yup
.string()
.email(i18n.t('login.emailSpellError'))
.required(i18n.t('login.emailRequiredError')),
password: yup
.string()
.matches(/^((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,15})$/i, i18n.t('login.passSpellError'))
.required(i18n.t('login.passRequiredError')),
});
i18n.t('login.passRequiredError') works fine when I put it into a render method for checking it but it does not work with the yup. Any suggestions? Thanks in advance
In your schema, replace:
.email(i18n.t('login.emailSpellError'))
with
.email('login.emailSpellError')
then in your render method:
{t(`form.errors.${form.errors.email}`)}
This assumes your translation file has an entry like this:
"form": { "errors": {"login": {"emailSpellError": "Your email is invalid"}}}}
The goal here is to move the t() method into your render method and have all translations happen there.
Yup Validation method,
// You define the key mentioned in the translation file, in my example 'Invalid email' and 'Required'
let ForgotPasswordSchema = yup.object().shape({
email: yup.string().email('Invalid email').required('Required'),
});
In render method,
// As per your definition
isInvalid={(!!errors.email) && this.context.t(!!errors.email)}
invalidText={(errors.email) && this.context.t(errors.email)}
Translation File
export const translations = {
"cy": {
"Required":"Gofynnol",
"Invalid email":"Nid yw'r cyfeiriad ebost yn ddilys",
}
};
A solution will be to make a function that returns your validation schema. Then call that function in your component with the result memoized.
This way, you are guaranteed that translations for validation messages are computed on the fly.
Another advantage here is you translate at the source of the message.
// Translation file
{
"validation.invalid-email": "Email is invalid",
"validation.field-required": "Field is required"
}
// Validation schema
const forgotPasswordSchema = () => {
return yup.object().shape({
email: yup
.string()
.email(i18n.t('validation.invalid-email'))
.required(i18n.t('validation.field-required')),
});
};
// Your component
const FormComponent = () => {
const schema = useMemo(() => forgotPasswordSchema(), [i18n.language]); // NB: `[i18n.language]` is optional and `[]` will suffice depending on how you're handling language change
return <>...</>;
}
I've created a few custom hooks for this approach
This one to refresh error messages inside schema when is changing app language
import { yupResolver } from '#hookform/resolvers/yup';
import { useRouter } from 'next/router';
import { useMemo } from 'react';
const useSchema = (getSchema) => {
const { locale } = useRouter();
const resolver = useMemo(getSchema, [locale]);
return yupResolver(resolver);
};
export default useSchema;
And this one to set global in App component localised error messages
import { useTranslation } from 'react-i18next';
import { setLocale } from 'yup';
export const useLocalisedYupSchema = () => {
const { t } = useTranslation('common');
setLocale({
mixed: {
required: t('validation.required')
},
string: {
min: ({ min }) => t('validation.min', { min }),
max: ({ max }) => t('validation.max', { max })
},
});
};
Also usage of schemas inside component with React Hook Form
import { getChangePasswordSchema } from 'static/schemas/changePassword';
import useSchema from 'utils/hooks/useSchema';
import { useForm } from 'react-hook-form';
const AccountContentSecurity = () => {
...
const resolver = useSchema(getChangePasswordSchema);
const { reset, control, handleSubmit } = useForm({
defaultValues: {
'current_password': '',
'new_password': '',
'password_confirmation': '',
},
resolver,
});
...
and schema
import { passwordSchema } from 'static/schemas';
import { object } from 'yup';
export const getChangePasswordSchema = () => object({
'current_password': passwordSchema,
'new_password': passwordSchema,
'password_confirmation': passwordSchema,
});