Im using Material UI to create a new user..the nonprofit is optional. But I'm having an issue with validating the form then breaking out of the validation so I can send the users data to axios POST request. any help would be appreciated.
Im not sure how to refactor this code to allow me to do this. I tried a custom hook and made a new file but I just got all confused.
const useStyles = makeStyles((theme) => ({
root: {
'& .MuiTextField-root': {
margin: theme.spacing(1),
width: 288,
borderRadius: 8,
},
},
button: {
width: 288,
borderRadius: 8,
},
}));
// eslint-disable-next-line react/prop-types
export default function CreateAccountMuiForm() {
const classes = useStyles();
const [emailError, setEmailError] = useState('');
const [passwordError, setPasswordError] = useState('');
// const [nonProfitError, setNonProfitError] = useState('');
const [showPassword, setShowPassword] = useState(false);
const handleClickShowPassword = () => setShowPassword(!showPassword);
const handleMouseDownPassword = () => setShowPassword(!showPassword);
const [values, setValues] = useState({
email: '',
password: '',
nonProfit: '',
});
const handleChange = (e) => {
const { name, value } = e.target;
setValues({
...values,
[name]: value,
});
};
const handleSubmit = async (e) => {
e.preventDefault();
// sets intial state to false
setEmailError(false);
setPasswordError(false);
// // form validation happens here after submited
// // this checks to make sure a valid email is input
if (values.email === '') {
setEmailError(true);
} else if (!/\S+#\S+\.\S+/.test(values.email)) {
setEmailError(true);
}
// this checks to make sure password is greater than 5
if (values.password === '') {
setPasswordError(true);
} else if (values.password.length < 5) {
setPasswordError(true);
}
// this checks to make sure both validations are true
if (values.email && values.password) {
// eslint-disable-next-line no-console
console.log(values);
}
return
};
return (
<FormControl>
<form className={classes.root} noValidate autoComplete="off" onSubmit={handleSubmit}>
<div>
<TextField
name="email"
id="outlined-password-input"
placeholder="Email"
type="email"
autoComplete="current-email"
variant="outlined"
required
error={emailError}
value={values.email}
onChange={handleChange}
// onChange={(e) => setEmail(e.target.value)}
/>
<TextField
name="password"
id="password"
placeholder="password"
type={showPassword ? 'text' : 'password'}
autoComplete="current-password"
variant="outlined"
required
error={passwordError}
value={values.password}
onChange={handleChange}
// onChange={(e) => setPassword(e.target.value)}
InputProps={{ // <-- This is where the toggle button is added.
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
>
{showPassword ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
),
}}
/>
<TextField
name="nonProfit"
id="nonProfit"
placeholder="Select you nonprofit (optional)"
type="text"
autoComplete="none"
variant="outlined"
// onChange={(e) => setNonProfit(e.target.value)}
value={values.nonProfit}
onChange={handleChange}
// error={nonProfitError}
/>
<Button
variant="contained"
type="submit"
className={classes.button}
>
Create an account
</Button>
<p className="terms-of-use">
By creating an account you agree to our
<Link to="/termsofuse" style={{ textDecoration: 'none', color: '#909090' }}>
<strong> Terms of use</strong>
</Link>
</p>
</div>
</form>
</FormControl>
);
}
TL; DR: Follow low coupling and high cohesion principles: separate the state values and pair them with their error states so you can validate via hooks.
NL; PR: Excellent question, this is often overlooked in tutorials. In this pastebin you can observe a way to do so. These are the relevant snippets:
Don't
High coupling: unnecessary dependency among state values:
const [values, setValues] = useState({
email: '',
password: '',
nonProfit: '',
});
Low cohesion: error state is derived from another value:
const [emailError, setEmailError] = useState('');
Do
Low coupling: separate state handling for each value, high cohesion: email value and error are handled in one place (a hook).
const useEmailValidator = () => {
const [value, setValue] = useState("");
const [emailError, setEmailError] = useState(false);
const handleAndValidateEmail = useCallback((value) => {
setEmailError(!(value && /\S+#\S+\.\S+/.test(value)));
setValue(value);
}, []);
return [value, handleAndValidateEmail, emailError];
};
Try refactoring the other state values and errors using validator hooks.
Finally, if you are feeling adventurous, consider the DRY principle and try form management libraries.
Related
I have a Formik form that is using a progressive stepper and have multiple fields across different components, thus requiring the need to store the values in React Context. However none of the field values are being passed, so when I click submit, all values are empty strings and the validation fails. You can see on each Formik Field i am setting the value as {state.[field]}, which comes from the Context, so I believe something is going wrong here. Can anyone see what I'm doing wrong?
Thanks a lot
Here is my parent component
const AddSongPage = () => {
const { state, dispatch } = useUploadFormContext();
const initialValues = {
name: "",
};
const { mutate: handleCreateTrack } = useCreateSyncTrackMutation(
gqlClient,
{}
);
const handleSubmit = (values: any) => {
handleCreateTrack(
{
...values,
},
{
onSuccess() {
console.log("Track added succesfully");
},
}
);
};
const validate = Yup.object({
name: Yup.string().required("Song name is required"),
description: Yup.string().optional(),
});
return (
<Layout headerBg="brand.blue">
<Formik
onSubmit={(values) => handleSubmit(values)}
initialValues={initialValues}
validationSchema={validate}
>
<Form>
<Box> {state.activeStep === 1 && <Step1 />}</Box>
<Box> {state.activeStep === 2 && <Step2 />}</Box>
<Box> {state.activeStep === 3 && <Step3 />}</Box>
<Button type={"submit"}>Submit</Button>
</Form>
</Formik>
</Layout>
);
};
Here is step 1
const Step1 = () => {
const { state, dispatch } = useUploadFormContext();
const onInputChange = (e: FormEvent<HTMLInputElement>) => {
const inputName = e.currentTarget.name;
dispatch({
type: "SET_UPLOAD_FORM",
payload: {
[inputName]: e.currentTarget.value,
},
});
};
return (
<Stack spacing={4}>
<Field name={"name"}>
{({ field, form }: any) => (
<FormControl isInvalid={form.errors.name && form.touched.name}>
<Input
{...field}
onChange={onInputChange}
value={state.name}
/>
</FormControl>
)}
</Field>
<Field name={"description"}>
{({ field, form }: any) => (
<FormControl isInvalid={form.errors.name && form.touched.name}>
<Input
{...field}
onChange={onInputChange}
value={state.description}
/>
</FormControl>
)}
</Field>
</Stack>
);
};
export default Step1;
i am using react Mui for components ,not getting any errors in chrome inspector or terminal
how can i slove this
I get no errors from either eslint nor Chrome Inspector.
Submitting the form itself works as does the actual input field when it is located either in the render's return or while being imported as a separate component but not in how I have it coded below.
Why is this so?
Here is my code :
import React, { useState } from "react";
import {
Box,
Container,
Stack,
TextField,
Typography,
Button,
Divider,
} from "#mui/material";
import { styled } from "#mui/material/styles";
import { Link, useNavigate } from "react-router-dom";
const Register = () => {
const Curve = styled("div")(({ theme }) => ({
height: "35vh",
position: "absolute",
top: 0,
left: 0,
width: "100%",
background: `linear-gradient(120deg,${theme.palette.secondary.light},${theme.palette.secondary.main})`,
zIndex: -1,
borderBottomLeftRadius: "30px",
borderBottomRightRadius: "30px",
}));
const ProfileBox = styled(Box)(({ theme }) => ({
// margin: theme.spacing(18, 1),
background: theme.palette.primary.dark,
border: `solid 0.8px ${theme.palette.primary.light}`,
borderRadius: "10px",
padding: theme.spacing(3),
}));
const navigate = useNavigate();
const handleRegister = (e) => {
e.preventDefault();
navigate("/");
};
const [values, setValues] = useState({
name: "",
email: "",
password: "",
conpassword: "",
phone: "",
proffesion: "",
});
const [err, setErr] = useState({});
const [valid, setValid] = useState(false);
const handleChange = (e) => {
const { id, value } = e.target;
setValues(() => ({
[id]: value,
}));
setValid(() => true);
};
const validate = () => {
return setErr(checkErr());
function checkErr() {
const error = {};
if (values.name.length === 0) error.name = "Name nedded";
if (values.password.length === 0) error.password = "Password nedded";
if (!values.email.match(/^[^\s#]+#[^\s#]+\.[^\s#]+$/))
error.email = "Invalid email";
if (values.password.length < 6)
error.password = "Password must be longer than 6 charectors";
if (values.password !== values.conpassword)
error.conpassword = "Password doesn't match";
if (values.email.length === 0) error.email = "Email nedded";
if (values.phone.length === 0) error.phone = "Phone number nedded";
if (values.phone.length < 10) error.phone = "Invalid number";
if (values.proffesion.length === 0)
error.proffesion = "Proffesion nedded ex: lawyer ,student";
return error;
}
};
return (
<Container maxWidth="sm">
<Curve></Curve>
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
height: "30vh",
alignItems: "center",
}}
>
<Typography variant="h6" algin="center">
Register
</Typography>
<Typography variant="caption" algin="center">
your data is secured with us
</Typography>
</Box>
<ProfileBox>
<Stack spacing={2}>
<TextField
variant="standard"
label="Name"
color="secondary"
value={values.name}
onChange={handleChange}
onBlur={validate}
id="name"
error={err.name}
helperText={err.name}
/>
<TextField
type="email"
variant="standard"
label="Email"
color="secondary"
id="email"
value={values.email}
onChange={handleChange}
onBlur={validate}
error={err.email}
helperText={err.email}
/>
<TextField
type="password"
variant="standard"
label="Password"
color="secondary"
value={values.password}
onChange={handleChange}
onBlur={validate}
id="password"
error={err.password}
helperText={err.password}
/>
<TextField
type="password"
variant="standard"
label="Conform password"
color="secondary"
value={values.conpassword}
onChange={handleChange}
onBlur={validate}
id="conpassword"
error={err.conpassword}
helperText={err.conpassword}
/>
<TextField
type="tel"
variant="standard"
label="Phone"
color="secondary"
value={values.phone}
onChange={handleChange}
onBlur={validate}
id="phone"
error={err.phone}
helperText={err.phone}
/>
<TextField
variant="standard"
label="Proffestion"
color="secondary"
value={values.proffesion}
onChange={handleChange}
onBlur={validate}
id="proffesion"
error={err.proffesion}
helperText={err.proffesion}
/>
<Button
variant="contained"
color="secondary"
sx={{
color: "primary.main",
}}
onClick={handleRegister}
>
Signup
</Button>
<Divider />
<Typography variant="caption" algin="center">
Allready have account{" "}
<span>
<Link to="/login" style={{ color: "var(--secondary)" }}>
Login
</Link>
</span>
</Typography>
</Stack>
</ProfileBox>
</Container>
);
};
export default Register;
Try to change your handleChange function like this:
const handleChange = (e) => {
const { id, value } = e.target;
setValues(() => (prevState => {
return {
...prevState,
[id]: value,
}
}));
setValid(true);
};
This is happening because you're creating components inside your Register component. This pattern is really bad for performance and prone to bugs exactly like your question.
On every type you're changing the state of the Register component, it re-renders itself, and re-creates Curve and ProfileBox from scratch, including their children (input fields). Which causes them to reset all their and their children's state, including focus.
You need to move Curve and ProfileBox outside of it, it will fix the issue.
const Curve = styled('div')(({ theme }) => ({
... // the same
}));
const ProfileBox = styled(Box)(({ theme }) => ({
... // the same
}));
const Register = () => {
I've tried mutliple approaches, but I cannot seem to clear a Material UI textfield with type="file"
I am limiting the file size, and if a user oversteps the limit, an error message pops up, but the Textfield also needs to be cleared.
Here is my code:
function CreateClaim(props) {
const [supporting_docs, setSupportingDocs] = useState(null);
const handleFileUpload = (event) => {
event.preventDefault()
if(event.target.files[0].size > 10971520) {
setFileError(true)
setSupportingDocs(null)
}
else {
setSupportingDocs(event.target.files)
}
};
return (
<TextField onChange={handleFileUpload}
InputLabelProps={{ shrink: true }}
margin="normal"
required
fullWidth
id="supporting_docs"
label="Bewys van uitgawe"
name="supporting_docs"
type="file"
/>
)
} export default CreateClaim
The error message works well, but cant clear the Textfield, any suggestions?
You can add this line:
if (event.target.files[0].size > 10971520) {
setFileError(true);
setSupportingDocs(null);
e.target.value = null;
}
You can try this. If the file is more than 1MB it will show an Error.
Click to see preview
const Demo = () => {
const [error, setError] = useState(false);
const handleFileUpload = (e) => {
console.log(e.target.files[0]);
const file = e.target.files[0];
if(file.size) return setError(true)
}
return(<>
{error ? <h1 style={{color: 'red'}} >Error</h1> : <TextField
onChange={handleFileUpload}
InputLabelProps={{ shrink: true }}
margin="normal"
required
fullWidth
id="supporting_docs"
label="Bewys van uitgawe"
name="supporting_docs"
type="file"
/>}
</>)
}
I am trying to create an edit form using react and data stored in redux. The form helps a user update their profile but my form seems to be read only. The values i pass to the form are displaying well, but nothing happens when i change the input and try to update the state. Any recommendations/assistance on how to update the previous values stored will be appreciated.
My code :
export default function UpdateProfileSection() {
const dispatch = useDispatch();
const [name, setName] = React.useState("");
const [username, setUserName] = React.useState("");
const [email, setEmail] = React.useState("");
const { user: currentUser } = useSelector((state) => state.auth);
if(!currentUser) {
return <Redirect to="/login-page" />;
}
let nme = currentUser.name;
let em = currentUser.email;
let uname = currentUser.username;
let id = currentUser.id;
const handleProfileUpdate = (e) => {
setshowloader(true)
dispatch(update_profile(name, username, email, role, id))
.then((response) => {
console.log(response);
})
.catch((error) => {
console.log(error)
})
}
return (
<div>
<Label as="a" color="red" size="huge" ribbon style={{ marginBottom: 50 }}>
Update Profile
</Label>
<CustomInput
id="first"
formControlProps={{
fullWidth: true
}}
inputProps={{
value: nme,
type: "text",
onChange: event => {
const value = event.target.value;
setName(value);
}
}}
/>
<CustomInput
labelText="User Name..."
id="user"
formControlProps={{
fullWidth: true
}}
inputProps={{
value: uname,
type: "text",
onChange: event => {
const value = event.target.value;
setUserName(value);
}
}}
/>
<CustomInput
labelText="Email..."
id="Email"
formControlProps={{
fullWidth: true
}}
inputProps={{
value: em,
type: "text",
onChange: event => {
const value = event.target.value;
setEmail(value);
}
}}
/>
<GridItem style={{ textAlign: "center", marginTop: 10 }}>
<Button
color="danger"
size="lg"
onClick={handleProfileUpdate}>
Save Changes
</Button>
</GridItem>
</div>
);
}
The code use nme as value, and this value is the current user in the store. When you write on the input, the value doesn't change, it still being the current user info.
You can try to make an initialization by using useEffect hook, as the following:
I am trying to create an edit form using react and data stored in redux. The form helps a user update their profile but my form seems to be read only. The values i pass to the form are displaying well, but nothing happens when i change the input and try to update the state. Any recommendations/assistance on how to update the previous values stored will be appreciated.
My code :
export default function UpdateProfileSection() {
const dispatch = useDispatch();
const [name, setName] = React.useState("");
const [username, setUserName] = React.useState("");
const [email, setEmail] = React.useState("");
const { user: currentUser } = useSelector((state) => state.auth);
if(!currentUser) {
return <Redirect to="/login-page" />;
}
useEffect( () => {
setName(currentUser.name);
setEmail(currentUser.email);
setUserName(currentUser.username);
}, [])
let id = currentUser.id;
const handleProfileUpdate = (e) => {
setshowloader(true)
dispatch(update_profile(name, username, email, role, id))
.then((response) => {
console.log(response);
})
.catch((error) => {
console.log(error)
})
}
return (
<div>
<Label as="a" color="red" size="huge" ribbon style={{ marginBottom: 50
}}>
Update Profile
</Label>
<CustomInput
id="first"
formControlProps={{
fullWidth: true
}}
inputProps={{
value: name,
type: "text",
onChange: event => {
const value = event.target.value;
setName(value);
}
}}
/>
<CustomInput
labelText="User Name..."
id="user"
formControlProps={{
fullWidth: true
}}
inputProps={{
value: username,
type: "text",
onChange: event => {
const value = event.target.value;
setUserName(value);
}
}}
/>
<CustomInput
labelText="Email..."
id="Email"
formControlProps={{
fullWidth: true
}}
inputProps={{
value: email,
type: "text",
onChange: event => {
const value = event.target.value;
setEmail(value);
}
}}
/>
<GridItem style={{ textAlign: "center", marginTop: 10 }}>
<Button
color="danger"
size="lg"
onClick={handleProfileUpdate}>
Save Changes
</Button>
</GridItem>
</div>
);
}
Note that the useEffect is executed only one time when the component is mounted, and use the state values to put in the inputs. This can fix the error
I am currently messing around with client-side form validation. There is no back end or anything along those lines. I have an on submit function, and I am trying to push the user to the home page once a valid form has been submitted using props.history.push. For some reason, I have to submit the form twice for the on submit function to actually push the user into the home page. I am unsure of why this is happening. Below I am going to provide my useForm hook, my Login page, and my validation function I am using. I feel like the answer might be obvious, but I just can't find the issue.
useForm Hook:
import { useState } from "react"
const INITIAL_STATE = {
email: "",
password: ""
}
const UseForm = (ValidateLogin, props) => {
const [formData, setFormData] = useState(INITIAL_STATE)
const [errors, setErrors] = useState({})
const [user, setUser] = useState(null)
const handleChange = field => e => {
setFormData({ ...formData, [field]: e.target.value })
}
const handleSubmit = event => {
event.preventDefault()
setUser(formData)
setErrors(ValidateLogin(formData))
user && !errors.email && !errors.password && props.history.push("/")
}
return {
handleChange,
handleSubmit,
formData,
user,
errors
}
}
export default UseForm
Login Page:
import React from "react"
import UseForm from "../Login/UseForm"
import ValidateLogin from "../Login/ValidateLogin"
import { makeStyles } from "#material-ui/core/styles"
// Material UI
import TextField from "#material-ui/core/TextField"
import Button from "#material-ui/core/Button"
import Typography from "#material-ui/core/Typography"
import Container from "#material-ui/core/Container"
const useStyles = makeStyles(theme => ({
form: {
textAlign: "center",
width: "100%", // Fix IE 11 issue.
marginTop: theme.spacing(1),
position: "relative"
},
logo: {
width: 80,
margin: "20px auto 20px auto"
},
paper: {
marginTop: theme.spacing(8),
display: "flex",
flexDirection: "column",
alignItems: "center"
},
submit: {
margin: theme.spacing(3, 0, 2),
position: "relative"
},
progress: {
position: "absolute"
},
customError: {
color: "red",
fontSize: "0.8rem",
width: "100%",
position: "absolute"
}
}))
const Login = props => {
const classes = useStyles()
const { handleChange, handleSubmit, formData, user, errors } = UseForm(
ValidateLogin,
props
)
const isInvalid = !formData.email || !formData.password
return (
<div style={{ textAlign: "center" }}>
<Typography variant='h5' gutterBottom>
Welcome, Please Login
</Typography>
<Container component='main' maxWidth='xs'>
<form className={classes.form} onSubmit={handleSubmit}>
<TextField
variant='outlined'
fullWidth
margin='normal'
label='Email'
type='text'
name='email'
value={formData.email}
error={errors.email ? true : false}
helperText={errors.email}
onChange={handleChange("email")}
/>
<TextField
variant='outlined'
margin='normal'
fullWidth
label='Password'
type='password'
name='password'
value={formData.password}
error={errors.password ? true : false}
helperText={errors.password}
onChange={handleChange("password")}
/>
<br />
<Button
variant='outlined'
color='primary'
type='submit'
disabled={isInvalid}
className={classes.submit}
>
Submit
</Button>
</form>
</Container>
<br />
{user &&
!errors.email &&
!errors.password &&
JSON.stringify(user, null, 2)}
</div>
)
}
export default Login
Validate Login:
export default function ValidateLogin(formData) {
let errors = {}
if (!formData.email) {
errors.email = "Email address is required"
} else if (!/\S+#\S+\.\S+/.test(formData.email)) {
errors.email = "Email address is invalid"
}
if (!formData.password) {
errors.password = "Password is required"
} else if (formData.password.length < 6) {
errors.password = "Password must be longer than 6 characters"
}
return errors
}
Any helpful suggestions or feedback would be greatly appreciated!! I apologize for the long-winded question. Thanks for taking the time to read this.
It looks like your issue is the conditional in this block:
setUser(formData)
setErrors(ValidateLogin(formData))
user && !errors.email && !errors.password && props.history.push("/")
user and errors will not be updated by the time this condition is evaluated because state setters are async. Here is a more detailed explanation.
This is why it works on the second submit. The values have then been updated, and the condition passes.
This may not be the desired end solution, but it should solve the problem for you.
const handleSubmit = event => {
event.preventDefault()
setUser(formData)
// Use temp variable so that you can use it immediately below
const tempErrors = ValidateLogin(formData);
setErrors(tempErrors)
// Use the formData directly and the tempErrors
formData && !tempErrors.email && !tempErrors.password && props.history.push("/")
}