Formik Validation Schema preventing onSubmit method to be called - javascript

It's my first time using Formik and I'm trying to use it to validate a user registration form. Here is my code:
interface Props { }
export interface RegisterData {
name: string;
email: string;
password: string;
status?: boolean
}
const validationSchema: SchemaOf<RegisterData> = yup.object().shape({
name: yup
.string()
.max(20, "Name cannot be more than 20 characters")
.required("name is required"),
email: yup
.string()
.email("Enter a valid email")
.required("Email is required"),
password: yup
.string()
.min(8, "Password should be of minimum 8 characters")
.required("Password is required"),
status: yup
.boolean()
.notRequired()
});
const Create: React.FC<Props> = () => {
const dispatch = useAppDispatch();
const theme = useTheme();
const navigate = useNavigate();
const [checked, setChecked] = useState<boolean>(false);
const initialFormValues: RegisterData = {
name: "",
email: "",
password: ""
}
const handleChecked = (event: any) => {
console.log(event.target.checked);
setChecked(event.target.checked);
}
const handleSubmit = async (values: any) => {
console.log("hello");
console.log(values);
}
const formik = useFormik({
initialValues: initialFormValues,
validationSchema: validationSchema,
onSubmit: handleSubmit
})
return (
<Box
...
<Box component="form" onSubmit={formik.handleSubmit} sx={{ mt: 2 }}>
<Grid container spacing={1}>
<Grid item xs={12}>
<Typography
variant="subtitle1"
sx={{
color: theme.palette.text.secondary
}}
>
{Constants.Create.NAME_HEADER}
</Typography>
</Grid>
<Grid item xs={12}>
<TextField
margin="normal"
required
fullWidth
id="name"
name="name"
label="Required"
variant="filled"
size="small"
sx={{
marginTop: "0px"
}}
/>
</Grid>
<Grid item xs={12}>
<Typography
variant="subtitle1"
sx={{
color: theme.palette.text.secondary
}}
>
{Constants.Create.EMAIL_HEADER}
</Typography>
</Grid>
<Grid item xs={12}>
<TextField
margin="normal"
required
fullWidth
id="email"
name="email"
label="Required"
variant="filled"
size="small"
sx={{
marginTop: "0px"
}}
/>
</Grid>
<Grid item xs={12}>
<Typography
variant="subtitle1"
sx={{
color: theme.palette.text.secondary
}}
>
{Constants.Create.PASSWORD_HEADER}
</Typography>
</Grid>
<Grid item xs={12}>
<TextField
type="password"
margin="normal"
required
fullWidth
id="password"
name="password"
label="Required"
variant="filled"
size="small"
sx={{
marginTop: "0px"
}}
/>
</Grid>
</Grid>
<Button
type="submit"
fullWidth
variant="contained"
size="large"
sx={{ mt: 2, mb: 2 }}
>
{Constants.Create.CREATE_ACCOUNT}
</Button>
</Box>
</Box >
)
}
When I click on my submit button and handleSubmit is called, nothing happens and I dont see hello or values being printed to the console. However when I remove validationSchema from useFormik, I can get the outputs.
Why is this happening? How can I add the schemaValidation to Formik?

I suppose that the problem could be that you are not controlling the state of your fields. When you click the submit button nothing happens because the input elements are updated with uncontrolled values.
The solution is to add on each field the value and the onChange attributes pointing to the formik values state and the handleChange method.
You can also add the disabled attribute to the button passing to it disabled={formik.isValid || formik.isSubmitting} to have a dynamic view of the state of the form. Here more infos about formik APIs https://formik.org/docs/api/formik#handlechange-e-reactchangeeventany--void
Example:
<TextField
value={formik.values.password}
onChange={formik.handleChange}
...your other props
/>
In addition, just for suggestion, put outside the function component body the initialFormValues object, you don't need to recreate it at every re-render.

Related

How to send/receive props to BasicLayout (#devexpress/dx-react-scheduler)

I'm from Angular and new to React. Im doing well but here is a problem I'm stuck at. As you can see I have BasicLayout and AppointmentForm, both are in one file. BasicLayout is being used inside AppointmentForm but not like an element i.e <BasicLayout/> so I'm not able to understand how to pass props or its even possible now. I want to trigger commitChanges(inside AppointmentForm) function when onSubmit(inside Basic Layout) function is triggered. How can I pass props between these components?
const BasicLayout = (props) => {
const formik = useFormik({
initialValues: {
title: '',
agenda: '',
description: '',
participants: [],
host: user?.id,
guest: '',
location: '',
},
validationSchema,
onSubmit: async (values) => {
values.startDate = props.appointmentData.startDate;
values.endDate = props.appointmentData.endDate;
values.guest = values.guest?._id;
createAppointment(values);
console.log(values);
},
});
return (
<Container>
<Typography sx={{ fontSize: 24, fontWeight: 'bold' }} color="text.primary" gutterBottom>
Create Appointment
</Typography>
<Box sx={{ flexGrow: 1 }}>
<FormikProvider value={formik}>
<Form autoComplete="off" onSubmit={handleSubmit}>
<Grid container spacing={2}>
<Grid item xs={6} md={6}>
<TextField
label="Title"
color="secondary"
id="title"
type="text"
key="title"
value={formik.values.title}
onChange={formik.handleChange}
{...getFieldProps('title')}
error={Boolean(touched.title && errors.title)}
helperText={touched.title && errors.title}
fullWidth
/>
</Grid>
<Grid item container xs={12} md={12} direction="row" justifyContent="center" alignItems="center">
<LoadingButton size="medium" type="submit" variant="contained" loading={isSubmitting}>
Create
</LoadingButton>
</Grid>
</Grid>
</Form>
</FormikProvider>
</Box>
<ToastContainer />
</Container>
);
};
const AppointmentsDashboard = (props) => {
const commitChanges = ({ added, changed, deleted }) => {
console.log(props);
console.log({ added, changed, deleted });
if (added) {
if (!isValidate) {
notify('Please fill all required fields', 'error');
return;
}
const startingAddedId = data.length > 0 ? data[data.length - 1].id + 1 : 0;
setData([...data, { id: startingAddedId, ...added }]);
}
if (changed) {
setData(
data.map((appointment) =>
changed[appointment.id] ? { ...appointment, ...changed[appointment.id] } : appointment
)
);
}
if (deleted !== undefined) {
setData(data.filter((appointment) => appointment.id !== deleted));
}
return data;
};
return (
<>
<Paper>
<Scheduler data={data} height={660}>
<ViewState currentDate={currentDate} />
<EditingState
onCommitChanges={commitChanges}
addedAppointment={addedAppointment}
onAddedAppointmentChange={changeAddedAppointment}
appointmentChanges={appointmentChanges}
onAppointmentChangesChange={changeAppointmentChanges}
editingAppointment={editingAppointment}
onEditingAppointmentChange={changeEditingAppointment}
onAppointmentFormClosing={() => {
console.log('asdasd');
}}
allowAdding={true}
/>
<WeekView startDayHour={9} endDayHour={17} />
<AllDayPanel />
<EditRecurrenceMenu />
<ConfirmationDialog />
<Appointments />
<AppointmentTooltip showOpenButton showDeleteButton />
<AppointmentForm basicLayoutComponent={BasicLayout} />
</Scheduler>
</Paper>
</>
);
};
export default AppointmentsDashboard;

Validation is not working with useState hook

I am trying to validate my form so that it does not send if there is no name, or email input. I am using emailjs for the form functionality and everything is working. I've used this code before for validating forms when using material UI. Not sure why it's not working.
const form = useRef();
// Validations
const [name, setName] = useState("");
const [error, setError] = useState("");
const handleError = (e) => {
setName(e.target.value);
if (e.target.value.length < 1) {
setError("Enter name");
}
}
// send email
const sendEmail = (e) => {
e.preventDefault();
emailjs.sendForm('sID', 'tID', form.current, 'XXXX')
.then((result) => {
console.log(result.text);
}, (error) => {
console.log(error.text);
});
e.target.reset()
};
<form ref={form} onSubmit={sendEmail}>
<Grid container>
{error ?
<p style={{ color: '#D35E3C' }}>{error}</p> : ''}
<Grid item>
<TextField onChange={handleError} id="standard-basic" label="Name" name="name" variant="standard" />
</Grid>
<Grid item>
<Button type="submit" />
</Grid>
</Grid>
</form>
I was able to fix it by integrating the react-hook-form.
Here is the full code if anyone is interested.
import Grid from '#mui/material/Grid';
import Box from '#mui/material/Box';
import React, { useRef, useState } from 'react';
import { useForm } from "react-hook-form";
import TextField from '#mui/material/TextField';
import { Typography } from '#mui/material';
import Button from '#mui/material/Button';
import "../css/contact.css"
import { motion } from 'framer-motion';
import emailjs from '#emailjs/browser'
const Contact = ({ pageTransitions }) => {
const form = useRef();
const [successMessage, setSuccessMessage] = useState("");
const { register, handleSubmit, formState: { errors }, } = useForm();
const serviceID = "";
const templateID = "";
const userID = "";
const onSubmit = (data, r) => {
sendEmail(
serviceID,
templateID,
{
name: data.name,
email: data.email,
subject: data.subject,
message: data.message,
},
userID
)
r.target.reset();
}
// send email
const sendEmail = (serviceID, templateID, variables, userID) => {
emailjs.send(serviceID, templateID, variables, userID)
.then(() => {
setSuccessMessage("Sent successfully! I will contact you as soon as possible!");
}).catch(err => console.error(`Something went wrong ${err}`));
};
return (
<motion.div
initial='out'
animate='in'
exit='out'
variants={pageTransitions}>
<form ref={form} onSubmit={handleSubmit(onSubmit)}>
<Grid container padding={2} sx={{
boxShadow: 5
}} className="element center-form" >
<Grid item xs={12} sm={12} md={12}>
<Typography sx={{
textShadow: '2px 3px 5px rgb(0 0 0 / 50%)'
}} variant="h3">
Contact Me
</Typography>
<span className="error-message">
<p> {errors.name && errors.name.message} </p>
<p> {errors.email && errors.email.message} </p>
<p> {errors.subject && errors.subject.message} </p>
<p> {errors.message && errors.message.message} </p>
</span>
</Grid>
<Grid item xs={12} md={6}>
<Box>
<TextField sx={{
marginBottom: 2,
}} {
...register("name", {
required: "* Please enter your name",
maxLength: {
value: 20,
message: "Name must not be longer than 20 characters",
}
})
} fullWidth={true} id="standard-basic" label="Name" name="name" variant="standard" />
<br />
<TextField sx={{
marginBottom: 2
}} {
...register("email", {
required: "* Please enter a valid email",
pattern: {
value: /^[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "* Invalid Email"
}
})
} fullWidth={true} id="standard-basic" label="Email" name="email" variant="standard" />
<br />
<TextField sx={{
marginBottom: 2
}} {
...register("subject", {
required: "* Please enter a subject",
maxLength: {
value: 20,
message: "* Subject must not be longer than 20 characters",
}
})
} fullWidth={true} id="standard-basic" label="Subject" name='subject' variant="standard" />
</Box>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{
marginTop: 13.2,
}}>
<TextField multiline
minRows={2}
maxRows={4} sx={{
marginBottom: 2,
width: '85%'
}} {
...register("message", {
required: "* Please enter a message",
})
} id="standard-basic" label="Message" name='message' variant="standard"
/>
</Box>
</Grid>
<Grid item xs={12} sm={12} md={12}>
<Button type="submit" className='button-shadow' sx={{
border: 1,
width: '85%',
}} variant="outlined">Send</Button>
</Grid>
</Grid>
</form>
</motion.div>
)
}
export default Contact

How to fix my password validation rule during confirmation of re-type during sign-up?

I'm using react-hook-form and Material-UI for my sign-up form. I'm trying to create a second password field that acts as the validation confirmation to check if the user typed in their correct password in the first field. Following my rule below:
rules={{
pattern: {
value: field.password2 == field.password,
message:
<span className={classes.warningText}>
<WarningIcon color="secondary" />
<p style={{ marginLeft: "1vw" }}>
Invalid use of characters
</p>
</span>,
}
}}
I'm trying to keep it simple and do something like this: value: field.password2 == field.password. However field is just an imaginary name, how can I reference my first password field in order to replicate the example? They are both using <TextField> and are wrapped around a <Controller>. I can provide more information about my form but I think this will do below:
<Grid item xs={12}>
<Controller
name="password"
as={
<TextField
variant="outlined"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
error={Boolean(fieldsErrors.password)}
onChange={
(evt) =>
{
let key = evt.currentTarget.name;
let value = evt.currentTarget.value;
handleChange({ [key]: value });
}
}
helperText={
<Container component="span">
<List disablePadding component="span">
<ListItem disableGutters={true} ContainerComponent="div">- Password is case-sensitive</ListItem>
<ListItem disableGutters={true} ContainerComponent="div">- Minimum eight characters</ListItem>
<ListItem disableGutters={true} ContainerComponent="div">- At least one letter</ListItem>
<ListItem disableGutters={true} ContainerComponent="div">- At least one number</ListItem>
</List>
</Container>
}
/>
}
control={control}
defaultValue=""
rules={{
required:
<Container className={classes.warningText}>
<WarningIcon color="secondary" />
</Container>,
pattern: {
value: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,50}$/,
message:
<Container className={classes.warningText}>
<WarningIcon color="secondary" />
<span style={{ marginLeft: "1vw" }}>
Please try another password
</span>
</Container>,
}
}}
/>
</Grid>
<Grid item xs={12}>
<Controller
name="password2"
as={
<TextField
variant="outlined"
required
fullWidth
name="password2"
label="Re-type Password"
type="password"
id="password2"
error={Boolean(fieldsErrors.password)}
onChange={
(evt) =>
{
let key = evt.currentTarget.name;
let value = evt.currentTarget.value;
handleChange({ [key]: value });
}
}
/>
}
control={control}
defaultValue=""
rules={{
required:
<span className={classes.warningText}>
<WarningIcon color="secondary" />
<p style={{ marginLeft: "1vw" }}>
Required
</p>
</span>,
pattern: {
value: field.password2 == field.password,
message:
<span className={classes.warningText}>
<WarningIcon color="secondary" />
<p style={{ marginLeft: "1vw" }}>
Invalid use of characters
</p>
</span>,
}
}}
/>
{fieldsErrors.password?.type && <p>{fieldsErrors.password?.message}</p>}
</Grid>
<Grid item xs={12}>
How can I reference the users input from field password and check if its equal to password2 field and add it to my validation rule?
Thank you for the help!
In react hook form you can watch both password fields using the watch method returned from useForm.
const { watch, ...etc } = useForm();
const password = watch("password");
const password2 = watch("password2");
And your validation code can just check password === password2 somewhere.
See https://react-hook-form.com/api#watch for information on how/when this function runs because your onBlur/onChange strategy may make a difference.
I'll also reference getValues as an option if you want to manually set an error in a useEffect. https://react-hook-form.com/api#getValues
useEffect(() => {
const { password, password2 } = getValues(['password', 'password2'])
if (password !== password2) setError(...)
else clearErrors(...)
}, [formState])
In React, to validate the values in the form which is constantly changing on "onChange" event it is better to have those in the local component state.
For that, it is advisable to use React Hook "useState".
Here is what you can do:
1: Initialize the state at the beginning of the component:
const [formFields, setFormFields] = React.useState({
password: '',
retypePassword: ''
})
2: Handle the "onChange" event this way:
const handlePasswordChange = (event) => {
setFormFields({ ...formFields, password: event.target.value})
}
and for retype password
const handleRetypePasswordChange = (event) => {
setFormFields({ ...formFields, retypePassword: event.target.value })
}
or pass an argument to specify the field and dynamically change the value
3: Now you can validate for the equivalency this way:
formFields.password === formFields.retypePassword
Also, feel free to checkout the popular library that can handle the advanced validations for you:
https://www.npmjs.com/package/yup

React - How to return Axios catch errors inline inside my form?

During user-registration, my backend informs the client if an email and/or username are currently in use by someone else. Below is the response logged into a webconsole thanks to Axios catch error.
I would like to map each email and username to the appropriate field.
My form is based off of Material-UI and react-hook-form
Here is the example of the error response provided by my Axios Instance.
{
"email":["This email is already in use"],
"username":["This username is already in use"]
}
Here is my complete react form, I cut some things out to make it easier to read:
export default function SignUp()
{
const { register, control, errors: fieldsErrors, handleSubmit } = useForm()
const onSubmit = (data, e) => {
console.log(data);
axiosInstance
.post(`api/user/register/`, {
email: data.email,
username: data.username,
password: data.password,
})
.then((res) => {
history.push('/login');
console.log(res);
console.log(res.data);
})
.catch(err => {
console.error(err.response.data);
}
)
};
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<Avatar className={classes.avatar}></Avatar>
<Typography component="h1" variant="h5">
Sign up
</Typography>
<form className={classes.form} noValidate onSubmit={handleSubmit(onSubmit)}>
<FormControl fullWidth variant="outlined">
<Grid container spacing={2}>
<Grid item xs={12}>
<Controller
name="email"
as={
<TextField
variant="outlined"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
error={Boolean(fieldsErrors.email)}
onChange={
(evt) =>
{
let key = evt.currentTarget.name;
let value = evt.currentTarget.value;
handleChange({ [key]: value });
}
}
/>
}
control={control}
defaultValue=""
rules={{
required: 'Required',
pattern: {
value: /^[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
message: 'invalid email address'
}
}}
/>
</Grid>
<Grid item xs={12}>
<Controller
name="username"
as={
<TextField
variant="outlined"
required
fullWidth
id="username"
label="Username"
name="username"
onChange={
(evt) => {
let key = evt.currentTarget.name;
let value = evt.currentTarget.value;
handleChange({[key]: value});
}
}
/>
}
control={control}
defaultValue=""
rules={{
required: 'Required',
pattern: {
value: /^(?!.*\.\.)(?!.*\.$)[^\W][\w.]{0,29}$/i,
message: 'Invalid use of characters'
}
}}
/>
{fieldsErrors.username?.type && <p>{fieldsErrors.username?.message}</p>}
</Grid>
<Grid item xs={12}>
<Controller
name="password"
as={
<TextField
variant="outlined"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
onChange={
(evt) =>
{
let key = evt.currentTarget.name;
let value = evt.currentTarget.value;
handleChange({ [key]: value });
}
}
/>
}
control={control}
defaultValue=""
/>
</Grid>
</Grid>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={handleSubmit}
>
Sign Up
</Button>
</FormControl>
</form>
</div>
</Container>
);
}
I've managed to create the error into a webalert, but that did not look nice. Is there any simple way to implement my catch error into my form?
I use this field error :
{fieldsErrors.username?.type && <p>{fieldsErrors.username?.message}</p>}
As my Regex error to warn users of illegal characters in their username. I was thinking maybe I could add the errors there? But I do not know how to do that.
You can leverage the useState hook to store errors from the API. Then you can use your template to render those errors in the same format that you display your regex errors.
On a side note, for security's sake it's not a great idea to tell a user that an email is already taken. But if your application doesn't have sensitive data then it's not a huge deal.
EDIT: example
export default function SignUp()
{
const [ apiError, setApiError ] = useState(null)
const onSubmit = (data, e) => {
setApiError(null)
axiosInstance
//...
.catch(err => {
setApiError(err.response.data.message)
console.error(err.response.data);
}
)
};
return (
//....
{apiError && <p>{apiError}</p>}
//....
)

Resetting a form when using onBlur as opposed to onChange

I had a form that has a lot of lag due to a large amount of state being handled for user's with a large number of job posts etc. I am trying to subdue this lag my switching my onChange to onBlur, this works great. The only problem is that my form no longer gets set to InitialState( empty string). I also have a submit button that I am keeping invalid until all inputs are filled. due to the onblur it remains invalid until I click away from the form. Is there a way I can still reset a form when using onBlur?? and does anyone have a solution to the issue of my button remaining invalid until I click away from the form. My inputs code are as follows:
the handleSubmit function:
const handleSubmit = async e => {
e.preventDefault()
setIsLoading(true)
const fireToken = await localStorage.FBIdToken
await axios
.post(`/job`, formData, {
headers: {
Authorization: `${fireToken}`
}
})
.then(res => {
setOpen(true)
setMessage(res.data)
fetchUser()
setIsLoading(false)
setIsModalOpen(false)
setFormData(INITIAL_STATE)
})
.catch(err => {
setErrors(err.response.data)
console.log(err)
setIsLoading(false)
})
}
The form code:
import React from 'react'
// components
import SelectStatus from './SelectStatus'
// Material UI Stuff
import CircularProgress from '#material-ui/core/CircularProgress'
import Typography from '#material-ui/core/Typography'
import TextField from '#material-ui/core/TextField'
import CardContent from '#material-ui/core/CardContent'
import Button from '#material-ui/core/Button'
import Card from '#material-ui/core/Card'
import Grid from '#material-ui/core/Grid'
// JobCardStyles
import useJobCardStyles from '../styles/JobCardStyles'
const NewJobForm = React.forwardRef(
({ handleSubmit, formData, handleInputChange, isloading }, ref) => {
const { company, position, status, link } = formData
const isInvalid = !company || !position || !link || !status || isloading
const classes = useJobCardStyles()
return (
<Card className={classes.card}>
<CardContent className={classes.content}>
<form noValidate onSubmit={handleSubmit} className={classes.form}>
<Grid
container
spacing={2}
alignItems="center"
justify="space-between"
>
<Grid item sm="auto" xs={12} className={classes.grid}>
<Typography>New</Typography>
<Typography>Job</Typography>
</Grid>
<Grid item sm={3} xs={12} className={classes.grid}>
<TextField
className={classes.jobField}
margin="normal"
fullWidth
id="company"
type="company"
label="Company"
name="company"
autoComplete="company"
defaultValue={company}
onBlur={handleInputChange('company')}
/>
</Grid>
<Grid item sm={3} xs={12} className={classes.grid}>
<TextField
className={classes.jobField}
margin="normal"
fullWidth
id="position"
type="position"
label="Position"
name="position"
autoComplete="position"
defaultValue={position}
onBlur={handleInputChange('position')}
/>
</Grid>
<Grid item sm={2} xs={12} className={classes.grid}>
<SelectStatus
status={status}
handleInputChange={handleInputChange}
/>
</Grid>
<Grid item sm={2} xs={12} className={classes.grid}>
<TextField
className={classes.jobField}
margin="normal"
fullWidth
id="link"
type="text"
label="Link"
name="link"
autoComplete="link"
defaultValue={link}
onBlur={handleInputChange('link')}
/>
</Grid>
<Grid item sm={1} xs={12} className={classes.grid}>
<Button
fullWidth
type="submit"
variant="contained"
color="primary"
disabled={isInvalid}
className={classes.submit}
disableElevation
>
Submit
{isloading && (
<CircularProgress size={30} className={classes.progress} />
)}
</Button>
</Grid>
</Grid>
</form>
</CardContent>
</Card>
)
}
)
export default NewJobForm
Try making another function to wrap several functions.
const NewJobForm = React.forwardRef(
//other logic
const reset = () => {//your reset function logic}
//ver 1
const handleOnBlur = (fn, relatedParam) => {
reset();
fn(relatedParam);
}
//ver 2
const handleOnBlur = (relatedParam) => {
reset();
handleInputChange(relatedParam);
}
return (
<TextField
//other props
onBlur={() => handleOnBlur('company')}
/>
)

Categories

Resources