Yup doesn't work properly with i18n - reactjs

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,
});

Related

How to set a value in input automatically

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

Yup conditional schema without nested object

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"),
}),

WPGraphQL useMutation GraphQL error: Unknown argument "username" on field "registerUser" of type "RootMutation"

I'm building a headless wordpress website using react, nextjs and wpgraphql. I'm trying to create a mutation to register user, but I'm getting the following errors after submitting my form:
Error: GraphQL error: Unknown argument "username" on field "registerUser" of type "RootMutation".
GraphQL error: Unknown argument "email" on field "registerUser" of type "RootMutation".
GraphQL error: Unknown argument "clientMutationId" on field "registerUser" of type "RootMutation".
GraphQL error: Field "registerUser" argument "input" of type "RegisterUserInput!" is required but not provided.
Everything works as expected when I test the mutation directly in wordpress using the GraphiQL:
mutation registerUser {
registerUser(input: {username: "new_user", clientMutationId: "39slh", email: "test#test.com"}) {
user {
id
name
}
}
}
This is the code I'm using:
import React, { useState } from 'react';
import { useMutation } from '#apollo/react-hooks';
import { gql } from 'apollo-boost';
import Layout from '../components/Layout';
import withData from '../lib/apollo';
const CREATE_USER = gql`
mutation registerUser {
registerUser(input: {$name, $mutation, $email}) {
user {
id
name
}
}
}`;
const Login = () => {
const [email, setEmail] = useState('');
const [name, setName] = useState('');
const mutation = 'reslkj3sd';
const [createUser, { loading, error }] = useMutation(CREATE_USER);
const updateEmail = (value) => {
setEmail(value);
};
const updateName = (value) => {
setName(value);
};
const handleCreateUser = (event) => {
event.preventDefault();
createUser({ variables: { name, mutation, email } });
};
return (
<Layout>
<form onSubmit={handleCreateUser}>
<input type="email" value={email} onChange={(e) => { updateEmail(e.target.value) }} />
<input type="text" value={name} onChange={(e) => { updateName(e.target.value) }} />
<input type="submit"></input>
{error && <p>{error.message}</p>}
</form>
</Layout>
);
};
export default withData(Login);
I appreciate any help I can get to get this to work.
Those GraphQL variables need to be defined before they are passed in. Think of the GraphQL schema as an object. The values injected on the return line are always defined using GraphQL values in the line immediately preceding that. For example
// query DocumentNode
const GET_ALL_AUTHORS = gql`
query GetAllAuthors(
$order: OrderEnum!
$field: PostObjectsConnectionOrderbyEnum!
$first: Int!
) {
AllAuthors(
where: { orderby: { field: $field, order: $order } }
first: $first
) {
edges {
node {
id
name
firstName
lastName
avatar {
url
size
height
width
}
slug
locale
}
}
}
}
`;
// your variables to pass into the query hook
const GetAllAuthorsQueryVars = {
first: 20,
field: PostObjectsConnectionOrderbyEnum.SLUG,
order: OrderEnum.ASC
};
You could, however, pass form-derived data directly into the variables defined by GetAllAuthors so long as said data satisfies their definitions.
Moving on to user registration. This mutation can be absolutely massive. I will try to simplify it, but you do need a password object, and to provide that via WPGraphQL you should install the WPGraphQL JWT plugin and follow the steps to configure a JWT Secret in your wp-config.php file. Once that is configured, you can generate a refresh token via executing a login mutation using your wordpress credentials. It will return an Auth Token as well which is relatively ephemeral and depends on the Refresh token to perpetually regenerate it. Once you configure this plugin, make sure you store the tokens returned in cookies or something. If the super strict EU laws passed in recent years apply to you, be sure to not store the email or any other information that would violate that law in your cookie.
Anyway, using your credentials (plus a password variable):
const REGISTER_USER = gql`mutation registerUser($username: String!, $password: String!, email: String!, $clientMutationId: String!) {
registerUser(input: {username: $username, clientMutationId: $clientMutationId email: $email, password: $password}) {
clientMutationId
user {
id
name
email
}
}
}`
You can inject these values straight into the registerUser definition or you can practice separation of concerns and define a mutation variable object as follows:
const GetMutationInputVars = {
username: inputE1,
clientMutationId: inputE2,
email: inputE3,
password: inputE4
};
This should get you off on the right foot.
Below is an example of how extensive this particular mutation can get, which more than likely isn't necessary beyond the scope of large projects.
import { gql } from '#apollo/client';
const NEW_USER = gql`
mutation RegisterUser(
$locale: String
$refreshJwtUserSecret: Boolean
$nickname: String
$clientMutationId: String!
$username: String!
$password: String!
$email: String!
$firstName: String
$lastName: String
$registered: String!
) {
registerUser(
input: {
locale: $locale
clientMutationId: $clientMutationId
refreshJwtUserSecret: $refreshJwtUserSecret
username: $username
password: $password
email: $email
firstName: $firstName
lastName: $lastName
registered: $registered
nickname: $nickname
}
) {
clientMutationId
user {
id
databaseId
username
email
firstName
lastName
registeredDate
avatar {
foundAvatar
url
}
jwtAuthExpiration
jwtUserSecret
jwtRefreshToken
nickname
locale
slug
uri
roles {
nodes {
capabilities
}
}
}
}
}
`;
export default NEW_USER;

Is there way to improve a functional component using hooks?

So I decided to give React Hooks a try.
I have a component which contains a bunch of logics like form validation and form submission as shown below:
const MyAwesomeComponent: React.FC = ()=> {
const [firstName, setFirstName]=useState('')
const [lastName, setLastName]=useState('')
const [formValidation, setFormValidation] = useState({
firstName: true,
lastName: true
})
const handleSubmitForm = (evt: React.FormEvent)=> {
evt.preventDefault();
handleSubmitButton()
}
const handleSubmitButton = ()=> {
let formBody = {}
if(!firstName) {
setFormValidation({...formValidation, firstName: false})
} else {
formBody = {...formBody, {firstName}}
}
if(!lastName) {
setFormValidation({...formValidation, lastName: false})
} else {
formBody = {...formBody, {lastName}}
}
// Nice proceed and submit the form to the backend
}
return (
<form onSubmit={(evt)=>handleSubmitForm(evt)}>
{/* form inputs go here */}
<button onClick={()=>handleSubmitButton()}></button>
</form>
)
}
export default MyAwesomeComponent
The code above feels a bit bloated and a bit difficult to maintain in my opinion. Is there a way to improve the handleSubmitButton function in order to abstract some of its code into a separate function out of the MyAwesomeComponent component?
For one thing, you could do
const handleSubmitButton = ()=> {
let formBody = {firstName, lastName}
setFormValidation({firstName: !!firstName, lastName: !!lastName});
// Nice proceed and submit the form to the backend
}
In other words, why do it in two separate steps? BTW if you haven't seen it, the !! is "not not" which converts a truthy or falsy value into an actual true or false.

How to use react context with nested mobx stores?

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.

Resources