Dynamically changing (i18n) UI language after Yup validation errors show in Formik form, using hooks -> unexpected behaviour - reactjs

I am showing a login screen to the user. This login screen (or the whole app, to be fair) can be shown in different languages.
The language switching works fine for all UI components except validation errors.
Currently, if a user leaves the email / password field blank, or enters an invalid email address, gets an error message and then switches languages, the error message won't change to the new language.
After a lot of guesswork and experimentation I have arrived at a point where the language changes after clicking either the 'de' or 'en' buttons twice, followed by typing into the input.
I can't for the life of me work out the right combination of useState and useEffect hooks to make the validation messages translate immediately upon hitting either of the language buttons.
Following code:
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { Formik, Field, Form, ErrorMessage } from 'formik'
import * as Yup from 'yup'
import { useTranslation } from 'react-i18next'
import { accountService, alertService } from '#/_services'
function Login({ history, location }) {
//'common' is defined in i18n.js under init's namespace ('ns:') option
//if that's not set, use useTranslation('common'), or whatever the name
//of the json files you use in the language-specific folders is
const { t, i18n } = useTranslation()
const [emailRequired, setEmailRequired] = useState(t('login.email_error_required'))
const [emailInvalid, setEmailInvalid] = useState(t('login.email_error_invalid'))
const [passwordRequired, setPasswordRequired] = useState(t('login.password_error_required'))
const [validationSchema, setvalidationSchema] = useState(
Yup.object().shape({
email: Yup.string().email(emailInvalid).required(emailRequired),
password: Yup.string().required(passwordRequired),
}),
)
useEffect(() => {})
const initialValues = {
email: '',
password: '',
}
const onSubmit = ({ email, password }, { setSubmitting }) => {
alertService.clear()
accountService
.login(email, password)
.then(() => {
const { from } = location.state || { from: { pathname: '/' } }
history.push(from)
})
.catch((error) => {
setSubmitting(false)
alertService.error(error)
})
}
return (
<Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={onSubmit}>
{({ errors, touched, isSubmitting }) => (
<Form>
<h3 className="card-header">{t('login.page_title')}</h3>
<div className="card-body">
<div className="form-group">
<label>{t('login.email_input_label')}</label>
<Field name="email" type="text" className={'form-control' + (errors.email && touched.email ? ' is-invalid' : '')} />
<ErrorMessage name="email" component="div" className="invalid-feedback" />
</div>
<div className="form-group">
<label>{t('login.password_input_label')}</label>
<Field name="password" type="password" className={'form-control' + (errors.password && touched.password ? ' is-invalid' : '')} />
<ErrorMessage name="password" component="div" className="invalid-feedback" />
</div>
<div className="form-row">
<div className="form-group col">
<button type="submit" disabled={isSubmitting} className="btn btn-primary">
{isSubmitting && <span className="spinner-border spinner-border-sm mr-1"></span>}
{t('login.login_btn_label')}
</button>
<Link to="register" className="btn btn-link">
{t('login.register_btn_label')}
</Link>
<button
type="button"
onClick={() => {
i18n.changeLanguage('de')
setEmailRequired(t('login.email_error_required'))
setEmailInvalid(t('login.email_error_invalid'))
setPasswordRequired(t('login.password_error_required'))
setvalidationSchema(
Yup.object().shape({
email: Yup.string().email(emailInvalid).required(emailRequired),
password: Yup.string().required(passwordRequired),
}),
)
}}
>
de
</button>
<button
type="button"
onClick={() => {
i18n.changeLanguage('en')
setEmailRequired(t('login.email_error_required'))
setEmailInvalid(t('login.email_error_invalid'))
setPasswordRequired(t('login.password_error_required'))
setvalidationSchema(
Yup.object().shape({
email: Yup.string().email(emailInvalid).required(emailRequired),
password: Yup.string().required(passwordRequired),
}),
)
}}
>
en
</button>
</div>
<div className="form-group col text-right">
<Link to="forgot-password" className="btn btn-link pr-0">
{t('login.forgot_password_label')}
</Link>
</div>
</div>
</div>
</Form>
)}
</Formik>
)
}
export { Login }
Things I have tried and their results:
1.)
I once didn't use useState at all, so the validationSchema was composed this way:
const emailRequired = t('login.email_error_required');
const emailInvalid = t('login.email_error_invalid');
const passwordRequired = t('login.password_error_required');
const validationSchema = Yup.object().shape({
email: Yup.string().email(emailInvalid).required(emailRequired),
password: Yup.string().required(passwordRequired),
})
This lead to only one click of the 'de' or 'en' buttons being required before typing into one of the inputs would change the error message to the new translation.
2.)
I made the language change buttons into submit-type buttons:
<button
type="button"
onClick={() => {
i18n.changeLanguage('en')
}}
>
but that just felt wrong. I can't imagine that would be the correct solution. It worked, however, as it simply re-ran the 'OnSubmit' function...
3.)
Various combinations of adding 'validationSchema' and the error variables into useState and useEffect hooks, in the hopes that any of them worked, but it simply didn't.
What I would like to understand is why the Formik object appears to 'insulate' its inner components from updates brought about by useEffect. Why is React not seeing the change in language for the validation errors, but for everything else it does?

I haven't had any responses, but figured it out myself in the meantime.
Turns out the translation keys need to go into the schema:
const validationSchema = Yup.object().shape({
email: Yup.string().email('login.email_error_invalid').required('login.email_error_required'),
password: Yup.string().required('login.password_error_required'),
})
And then the error messages need to be custom components. So they need to change from this:
<ErrorMessage name="password" component="div" className="invalid-feedback" />
To this:
{errors.password && <div className="invalid-feedback">{t(errors.password)}</div>}
So, for email, the whole thing looks like:
<div className="form-group">
<label>{t('login.password_input_label')}</label>
<Field name="password" type="password" className={'form-control' + (errors.password && touched.password ? ' is-invalid' : '')} />
{errors.password && <div className="invalid-feedback">{t(errors.password)}</div>}
</div>
I have no idea why this made a difference, but it's hopefully going to help someone else in the future.

Thanks op, it helped me figure out how to make translations work.
The approach it worked for me is:
const LoginSchema = Yup.object().shape({
username: Yup.string().required('loginForm.required'),
password: Yup.string()
.min(6, 'loginForm.passwordShort')
.required('loginForm.required'),
});
But now extract does not work so you have to manually add these keys inside your .po files.
{errors.username && touched.username ? (
<IonItem
lines="none"
className={`${classes.Error} ion-margin-top ion-justify-content-center`}
>
{t({
id: errors.username,
})}
</IonItem>
) : null}
This approach works for Yup + Lingui.js

Related

ERROR in src\containers\Login.js Line 35:5: Parsing error: Unexpected token (35:5)

Hello guys I'm new in React while watching a tutorial I'm getting this error and I checked it multiple times and compared it with the one in tutorial and can't find any difference. I'm trying to create a user authentication with Django and React. I'm using sublime text 3 as an editor and I have also Visual Studio Code installed. Should I install any extension to handle this problem? Can you help me with this parsing error?
import React, { useState } from 'react';
import { Link, Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
const Login = () => {
const [formData, setFormData] = useState({
email: '',
password: ''
});
const { email, password } = formData;
const onChange = e => setFormData({ ...formData, [e.target.name]: e.target.value });
const onSubmit = e => {
e.preventDefault();
// login(email, password)
};
// Is the user Authenticated?
// Redirect them to the home page
return (
<div className='container mt-5'>
<h1>
Sign in
</h1>
<p>
Sign into your account
</p>
<form onSubmit={e => onSubmit(e)}>
<div className='form-group'
<input
className='form-control'
type='email'
placeholder='Email'
name='email'
value={email}
onChange={e => onChange(e)}
required
/>
</div>
<div className='form-group'
<input
className='form-control'
type='password'
placeholder='Password'
name='password'
value={password}
onChange={e => onChange(e)}
minLength='6'
required
/>
</div>
<button className='btn btn-primary' type='submit'>Login</button>
</form>
<p className='mt-3'>
Don't have an account? <Link to='/signup'>Sign Up</Link>
</p>
<p className='mt-3'>
Forgot your Password? <Link to='/reset-password'>Reset Password</Link>
</p>
</div)
);
};
// const mapStateToProps = state => ({
// is authenticated?
// });
export default connect(null, { })(Login);
You didn t close the div tag before the input

Yup required validation not working and not giving error for empty field

I'm working on a form using formik and yup. I have added required schema, but it is not working. I can easily save having empty input fields. I have tried and googled but nothing worked.
I want to make it mandatory and it should give error if field is empty.
snippet of yup schema validation
opening_time: Yup.string().required("Opening time is Requried"),
closing_time: Yup.string().required("Closing time is Requried"),
address: Yup.string().required("Address is Requried"),
about: Yup.string().required("About is Required"),
Input field snippet
<div class="form-group mb-0">
<label>
About<span className="text-danger">*</span>
</label>
<textarea
name="about"
onChange={formik.handleChange}
value={formik.values.about}
class="form-control"
rows="5"
required
/>
{formik.touched.about && formik.errors.about ? (
<div className="err">
{formik.errors.about}
{console.log(formik.errors.about)}
</div>
) : null}
</div>
Try the following:
import React from 'react';
import { Formik, Form, Field } from 'formik';
import * as Yup from 'yup';
const SignupSchema = Yup.object().shape({
opening_time: Yup.string().required("Opening time is Requried"),
closing_time: Yup.string().required("Closing time is Requried"),
address: Yup.string().required("Address is Requried"),
about: Yup.string().required("About is Required"),
});
function ValidationSchemaExample() {
function updateDoctorProfile(e, values) {
console.log(`e: ${e}`);
console.log(`values: ${values}`)
}
return (
<div>
<h1>Signup</h1>
<Formik
initialValues={{
opening_time: "",
closing_time: "",
address: "",
about: "",
}}
validationSchema={SignupSchema}
onSubmit={values => {
// same shape as initial values
console.log(values);
}}
>
{({ values, errors, touched, handleChange, handleSubmit, isSubmitting }) => (
< div className="form-group mb-0">
<label>
About<span className="text-danger">*</span>
</label>
<textarea
name="about"
onChange={handleChange}
value={values.about}
className="form-control"
required
/>
<button type="submit" onClick={(e) => {
handleSubmit();
updateDoctorProfile(e, values);
}} disabled={isSubmitting}>
Submit
</button>
{touched.about && errors.about ? (
<div className="err">
{errors.about}
{console.log(errors.about)}
</div>
) : null}
</div>
)}
</Formik>
</div >
);
}
export default ValidationSchemaExample;
The only change is that the button tag's onClick attribute is passed the handleSubmit function along with your updateProfile function.

In React with Formik how can I build a search bar that will detect input value to render the buttons?

New to Formik and React I've built a search component that I'm having issues with the passing of the input value and rendering the buttons based on input length.
Given the component:
const SearchForm = ({ index, store }) => {
const [input, setInput] = useState('')
const [disable, setDisable] = useState(true)
const [query, setQuery] = useState(null)
const results = useLunr(query, index, store)
const renderResult = results.length > 0 || query !== null ? true : false
useEffect(() => {
if (input.length >= 3) setDisable(false)
console.log('input detected', input)
}, [input])
const onReset = e => {
setInput('')
setDisable(true)
}
return (
<>
<Formik
initialValues={{ query: '' }}
onSubmit={(values, { setSubmitting }) => {
setInput('')
setDisable(true)
setQuery(values.query)
setSubmitting(false)
}}
>
<Form className="mb-5">
<div className="form-group has-feedback has-clear">
<Field
className="form-control"
name="query"
placeholder="Search . . . . ."
onChange={e => setInput(e.currentTarget.value)}
value={input}
/>
</div>
<div className="row">
<div className="col-12">
<div className="text-right">
<button type="submit" className="btn btn-primary mr-1" disabled={disable}>
Submit
</button>
<button
type="reset"
className="btn btn-primary"
value="Reset"
disabled={disable}
onClick={onReset}
>
<IoClose />
</button>
</div>
</div>
</div>
</Form>
</Formik>
{renderResult && <SearchResults query={query} posts={results} />}
</>
)
}
I've isolated where my issue is but having difficulty trying to resolve:
<Field
className="form-control"
name="query"
placeholder="Search . . . . ."
onChange={e => setInput(e.currentTarget.value)}
value={input}
/>
From within the Field's onChange and value are my problem. If I have everything as posted on submit the passed query doesn't exist. If I remove both and hard code a true for the submit button my query works.
Research
Custom change handlers with inputs inside Formik
Issue with values Formik
Why is OnChange not working when used in Formik?
In Formik how can I build a search bar that will detect input value to render the buttons?
You need to tap into the props that are available as part of the Formik component. Their docs show a simple example that is similar to what you'll need:
<Formik
initialValues={{ query: '' }}
onSubmit={(values, { setSubmitting }) => {
setInput('')
otherStuff()
}}
>
{formikProps => (
<Form className="mb-5">
<div className="form-group has-feedback has-clear">
<Field
name="query"
onChange={formikProps.handleChange}
value={formikProps.values.query}
/>
</div>
<button
type="submit"
disabled={!formikProps.values.query}
>
Submit
</button>
<button
type="reset"
disabled={!formikProps.values.query}
onClick={formikProps.resetForm}
>
</Form>
{/* ... more stuff ... */}
)}
</Formik>
You use this render props pattern to pull formiks props out (I usually call them formikProps, but you can call them anything you want), which then has access to everything you need. Rather than having your input, setInput, disable, and setDisable variables, you can just reference what is in your formikProps. For example, if you want to disable the submit button, you can just say disable={!formikProps.values.query}, meaning if the query value in the form is an empty string, you can't submit the form.
As far as onChange, as long as you give a field the correct name as it corresponds to the property in your initialValues object, formikProps.handleChange will know how to properly update that value for you. Use formikProps.values.whatever for the value of the field, an your component will read those updates automatically. The combo of name, value, and onChange, all handled through formikProps, makes form handing easy.
Formik has tons of very useful prebuilt functionality to handle this for you. I recommend hanging out on their docs site and you'll see how little of your own code you have to write to handle these common form behaviors.

How to validate control from material-ui in react?

I using material-ui with react.
I want to do validations such as require and maxlength and minlength. according to material-ui docs I have to use the error prop and helperText to display the error. which mean I have to trigger a function myself that check the control and display the error. :(
I wounder if this is the right way to handle validation with react, the Textfield itself can't display require message (for example)? I have to specify each error myself? for example in angular I just add require or minlength and the control display the correct error.
Or maybe there is an easy way to do it?
got it :) here my ex :
import { Link } from 'react-router-dom';
import useForm from "react-hook-form";
import * as yup from 'yup';
const LoginFormSchema = yup.object().shape({
password: yup.string().required().min(4),
username: yup.string().required().min(4)
});
export function LoginForm(props) {
const { register, handleSubmit, watch, errors } = useForm({ defaultValues, validationSchema: LoginFormSchema });
const onSubmit = data => { props.onSubmit(data); }
<div className="form-container">
<form className="form" onSubmit={handleSubmit(onSubmit)}>
<div className="form-header">
<i className="material-icons">
account_circle
</i>
<h2>Login Form</h2>
</div>
<TextField name="username" label="username" inputRef={register} />
<span className="error-message">
{errors.username && errors.username.type === "required" && "username is required"}
{errors.username && errors.username.type === "min" && "username required to be more than 4 characters"}
</span>
<TextField type="password" name="password" label="password" inputRef={register} />
<span className="error-message">
{errors.password && errors.password.type === "required" && "password is required"}
{errors.password && errors.password.type === "min" && "password required to be more than 4 characters"}
</span>
</form>
You need to install yup and formik: npm i -s yup formik
Here is a working sample with formik yup and material-ui:
import React from "react";
import { Formik, Form, useField } from "formik";
import { TextField } from "#material-ui/core";
import * as yup from "yup";
//Reusable Textbox
const MyTextField = ({
placeholder,
type = "normal",
...props
}) => {
const [field, meta] = useField<{}>(props);
const errorText = meta.error && meta.touched ? meta.error : "";
return (
<TextField
variant="outlined"
margin="normal"
type={type}
placeholder={placeholder}
{...field}
helperText={errorText}
error={!!errorText}
/>
);
};
const validationSchema = yup.object({
username: yup
.string()
.required()
.max(30)
.min(2)
.label("Username"),
password: yup
.string()
.required()
.max(30)
.min(2)
.label("Password")
});
const Signin = ({ history }) => {
return (
<div className="SignupOuter">
<Formik
validateOnChange={true}
initialValues={{
username: "",
password: "",
loading: false
}}
validationSchema={validationSchema}
onSubmit={async (data1, { setSubmitting }) => {
setSubmitting(true);
//Call API here
}}
>
{({ values, errors, isSubmitting }) => (
<Form className="Signup">
<MyTextField placeholder="Username" name="username" />
<MyTextField
placeholder="Password"
name="password"
type="password"
/>
</Form>
)}
</Formik>
</div>
);
};
export default Signin;

REACT.JS + FORMIK input field proble,

this is my first ever stack overflow question. I am trying to build a log-in page (making a full stack app with node express mongodb mongoose) and front end being REACT. I am running into an issue where the input field css does not register until I click into the input field and click out. I am using Formik to develop my form. I have two questions essentially: How can I fix this issue and how can I do an API call to my backend server which is running on localhost:3003. Here is the code:
import React from "react";
import "../App.css";
import { Formik } from "formik";
import * as EmailValidator from "email-validator";
import * as Yup from "yup";
const ValidatedLoginForm = () => (
<Formik
initialValues={{ email: "", password: "" }}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
console.log("Logging in", values);
setSubmitting(false);
}, 500);
}}
validationSchema={Yup.object().shape({
email: Yup.string()
.email()
.required("Required"),
password: Yup.string()
.required("No password provided.")
.min(8, "Password is too short - should be 8 chars minimum.")
.matches(/(?=.*[0-9])/, "Password must contain a number.")
})}
>
{props => {
const {
values,
touched,
errors,
isSubmitting,
handleChange,
handleBlur,
handleSubmit
} = props;
return (
<div className="FormCenter">
<form onSubmit={handleSubmit} className="FormFields">
<div className="FormField">
<label htmlFor="email" className="FormField__Label">
Email
</label>
<input
name="email"
type="text"
placeholder="Enter your email"
value={values.email}
onBlur={handleBlur}
onChange={handleChange}
className={
errors.email && touched.email && "error" && "FormField__Input"
}
/>
{errors.email && touched.email && (
<div className="input-feedback">{errors.email}</div>
)}
</div>
<div className="FormField">
<label htmlFor="email" className="FormField__Label">
Password
</label>
<input
name="password"
type="password"
placeholder="Enter your password"
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
className={
errors.password &&
touched.password &&
"error" &&
"FormField__Input"
}
/>
{errors.password && touched.password && (
<div className="input-feedback">{errors.password}</div>
)}
</div>
<div className="FormField">
<button
type="submit"
className="FormField__Button mr-20"
onChange
>
Login
</button>
</div>
</form>
</div>
);
}}
</Formik>
);
export default ValidatedLoginForm;
This looks like a normal behaviour to me. The validation in the form will get triggered when ,
when you submit the form
when the field is touched . Becoz of this you are seeing the error message being displayed when you switch between the fields without entering the values .
You should be firing the api call from the onSubmit, were you will get to access all the form values . I would suggest you to use fetch api . https://developer.mozilla.org/en/docs/Web/API/Fetch_API
Just make your onSubmit as a asyn method and fire your api call inside it .

Resources