Validation is not working with useState hook - javascript

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

Related

A component is changing an uncontrolled input to be controlled in React.js

Warning: A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component.
I am creating a Social Media App and facing this problems, what is wrong in my code?
Following is my code:
import { useState } from "react";
import {
Box,
Button,
TextField,
useMediaQuery,
Typography,
useTheme,
} from "#mui/material";
import EditOutlinedIcon from "#mui/icons-material/EditOutlined";
import { Formik } from "formik";
import * as yup from "yup";
import { useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import { setLogin } from "state";
import Dropzone from "react-dropzone";
import FlexBetween from "components/FlexBetween";
const registerSchema = yup.object().shape({
firstName: yup.string().required("required"),
lastName: yup.string().required("required"),
email: yup.string().email("invalid email").required("required"),
password: yup.string().required("required"),
location: yup.string().required("required"),
occupation: yup.string().required("required"),
picture: yup.string().required("required"),
});
const loginSchema = yup.object().shape({
email: yup.string().email("invalid email").required("required"),
password: yup.string().required("required"),
});
const initialValuesRegister = {
firstName: "",
lastName: "",
email: "",
password: "",
location: "",
occupation: "",
picture: "",
};
const initialValuesLogin = {
email: "",
password: "",
};
const Form = () => {
const [pageType, setPageType] = useState("login");
const { palette } = useTheme();
const dispatch = useDispatch();
const navigate = useNavigate();
const isNonMobile = useMediaQuery("(min-width:600px)");
const isLogin = pageType === "login";
const isRegister = pageType === "register";
const register = async (values, onSubmitProps) => {
// this allows us to send form info with image
const formData = new FormData();
for (let value in values) {
formData.append(value, values[value]);
}
formData.append("picturePath", values.picture.name);
const savedUserResponse = await fetch(
"http://localhost:3001/auth/register",
{
method: "POST",
body: formData,
}
);
const savedUser = await savedUserResponse.json();
onSubmitProps.resetForm();
if (savedUser) {
setPageType("login");
}
};
const login = async (values, onSubmitProps) => {
const loggedInResponse = await fetch("http://localhost:3001/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
});
const loggedIn = await loggedInResponse.json();
onSubmitProps.resetForm();
if (loggedIn) {
dispatch(
setLogin({
user: loggedIn.user,
token: loggedIn.token,
})
);
navigate("/home");
}
};
const handleFormSubmit = async (values, onSubmitProps) => {
if (isLogin) await login(values, onSubmitProps);
if (isRegister) await register(values, onSubmitProps);
};
return (
<Formik
onSubmit={handleFormSubmit}
initialValues={isLogin ? initialValuesLogin : initialValuesRegister}
validationSchema={isLogin ? loginSchema : registerSchema}
>
{({
values,
errors,
touched,
handleBlur,
handleChange,
handleSubmit,
setFieldValue,
resetForm,
}) => (
<form onSubmit={handleSubmit}>
<Box
display="grid"
gap="30px"
gridTemplateColumns="repeat(4, minmax(0, 1fr))"
sx={{
"& > div": { gridColumn: isNonMobile ? undefined : "span 4" },
}}
>
{isRegister && (
<>
<TextField
label="First Name"
onBlur={handleBlur}
onChange={handleChange}
value={values.firstName}
name="firstName"
error={
Boolean(touched.firstName) && Boolean(errors.firstName)
}
helperText={touched.firstName && errors.firstName}
sx={{ gridColumn: "span 2" }}
/>
<TextField
label="Last Name"
onBlur={handleBlur}
onChange={handleChange}
value={values.lastName}
name="lastName"
error={Boolean(touched.lastName) && Boolean(errors.lastName)}
helperText={touched.lastName && errors.lastName}
sx={{ gridColumn: "span 2" }}
/>
<TextField
label="Location"
onBlur={handleBlur}
onChange={handleChange}
value={values.location}
name="location"
error={Boolean(touched.location) && Boolean(errors.location)}
helperText={touched.location && errors.location}
sx={{ gridColumn: "span 4" }}
/>
<TextField
label="Occupation"
onBlur={handleBlur}
onChange={handleChange}
value={values.occupation}
name="occupation"
error={
Boolean(touched.occupation) && Boolean(errors.occupation)
}
helperText={touched.occupation && errors.occupation}
sx={{ gridColumn: "span 4" }}
/>
<Box
gridColumn="span 4"
border={`1px solid ${palette.neutral.medium}`}
borderRadius="5px"
p="1rem"
>
<Dropzone
acceptedFiles=".jpg,.jpeg,.png"
multiple={false}
onDrop={(acceptedFiles) =>
setFieldValue("picture", acceptedFiles[0])
}
>
{({ getRootProps, getInputProps }) => (
<Box
{...getRootProps()}
border={`2px dashed ${palette.primary.main}`}
p="1rem"
sx={{ "&:hover": { cursor: "pointer" } }}
>
<input {...getInputProps()} />
{!values.picture ? (
<p>Add Picture Here</p>
) : (
<FlexBetween>
<Typography>{values.picture.name}</Typography>
<EditOutlinedIcon />
</FlexBetween>
)}
</Box>
)}
</Dropzone>
</Box>
</>
)}
<TextField
label="Email"
onBlur={handleBlur}
onChange={handleChange}
value={values.email}
name="email"
error={Boolean(touched.email) && Boolean(errors.email)}
helperText={touched.email && errors.email}
sx={{ gridColumn: "span 4" }}
/>
<TextField
label="Password"
type="password"
onBlur={handleBlur}
onChange={handleChange}
value={values.password}
name="password"
error={Boolean(touched.password) && Boolean(errors.password)}
helperText={touched.password && errors.password}
sx={{ gridColumn: "span 4" }}
/>
</Box>
{/* BUTTONS */}
<Box>
<Button
fullWidth
type="submit"
sx={{
m: "2rem 0",
p: "1rem",
backgroundColor: palette.primary.main,
color: palette.background.alt,
"&:hover": { color: palette.primary.main },
}}
>
{isLogin ? "LOGIN" : "REGISTER"}
</Button>
<Typography
onClick={() => {
setPageType(isLogin ? "register" : "login");
resetForm();
}}
sx={{
textDecoration: "underline",
color: palette.primary.main,
"&:hover": {
cursor: "pointer",
color: palette.primary.light,
},
}}
>
{isLogin
? "Don't have an account? Sign Up here."
: "Already have an account? Login here."}
</Typography>
</Box>
</form>
)}
</Formik>
);
};
export default Form;
The file upload input is changing from undefined to defined (thus changing it from uncontrolled to controlled which is not good practice in react) since it has no initial value. I haven't used formik alot but you should read through the docs and check how to set initial value for a dropzone.

In React , why does the input field lose focus after typing a character?

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 = () => {

Having an issue with shipping options

I have a problem fetching the shipping options, the error: 'Unhandled Rejection (TypeError): undefined is not an object (evaluating 'options[0].id')'. I have checked everything I can think of to debug this issue. I am using check.io as for my e-commerce backend. I have added different shipping option on that site however they are not showing up. I have a hard time understanding why there is a type error
import React, { useState, useEffect } from "react";
import {
InputLabel,
Select,
MenuItem,
Button,
Grid,
Typography,
} from "#material-ui/core";
import { useForm, FormProvider } from "react-hook-form";
import { Link } from "react-router-dom";
import FormInput from "../CheckoutForm/CustomTxtField";
import { commerce } from "../../lib/Commerce";
const AddressForm = ({ checkoutToken, test }) => {
const [shippingCountries, setShippingCountries] = useState([]);
const [shippingCountry, setShippingCountry] = useState("");
const [shippingSubdivisions, setShippingSubdivisions] = useState([]);
const [shippingSubdivision, setShippingSubdivision] = useState("");
const [shippingOptions, setShippingOptions] = useState([]);
const [shippingOption, setShippingOption] = useState("");
const methods = useForm();
const fetchShippingCountries = async (checkoutTokenId) => {
const { countries } = await commerce.services.localeListShippingCountries(
checkoutTokenId
);
setShippingCountries(countries);
setShippingCountry(Object.keys(countries)[0]);
};
const fetchSubdivisions = async (countryCode) => {
const { subdivisions } = await commerce.services.localeListSubdivisions(
countryCode
);
setShippingSubdivisions(subdivisions);
setShippingSubdivision(Object.keys(subdivisions)[0]);
};
const fetchShippingOptions = async (
checkoutTokenId,
country,
stateProvince = null
) => {
const options = await commerce.checkout.getShippingOptions(
checkoutTokenId,
{ country, region: stateProvince }
);
setShippingOptions(options);
setShippingOptions(options[0].id);
};
useEffect(() => {
if (checkoutToken && checkoutToken.id) {
fetchShippingCountries(checkoutToken.id);
console.log(checkoutToken.id);
}
}, [checkoutToken]);
useEffect(() => {
if (shippingCountry) fetchSubdivisions(shippingCountry);
}, [shippingCountry]);
useEffect(() => {
if (shippingSubdivision)
fetchShippingOptions(
checkoutToken.id,
shippingCountry,
shippingSubdivision
);
}, [shippingSubdivision]);
return (
<div>
<Typography variant="h6" gutterBottom>
Shipping address
</Typography>
<FormProvider {...methods}>
<form
onSubmit={methods.handleSubmit((data) =>
test({
...data,
shippingCountry,
shippingSubdivision,
shippingOption,
})
)}
>
<Grid container spacing={3}>
<FormInput name="firstName" label="First name" />
<FormInput name="lastName" label="Last name" />
<FormInput name="address1" label="Address line 1" />
<FormInput name="email" label="Email" />
<FormInput name="city" label="City" />
<FormInput name="zip" label="Zip / Postal code" />
<Grid item xs={12} sm={6}>
<InputLabel>Shipping Country</InputLabel>
<Select
value={shippingCountry}
fullWidth
onChange={(e) => setShippingCountry(e.target.value)}
>
{Object.entries(shippingCountries)
.map(([code, name]) => ({ id: code, label: name }))
.map((item) => (
<MenuItem key={item.id} value={item.id}>
{item.label}
</MenuItem>
))}
</Select>
</Grid>
<Grid item xs={12} sm={6}>
<InputLabel>Shipping Subdivision</InputLabel>
<Select
value={shippingSubdivision}
fullWidth
onChange={(e) => setShippingSubdivision(e.target.value)}
>
{Object.entries(shippingSubdivisions)
.map(([code, name]) => ({ id: code, label: name }))
.map((item) => (
<MenuItem key={item.id} value={item.id}>
{item.label}
</MenuItem>
))}
</Select>
</Grid>
<Grid item xs={12} sm={6}>
<InputLabel>Shipping Options</InputLabel>
<Select
value={shippingOption}
fullWidth
onChange={(e) => setShippingOption(e.target.value)}
>
{shippingOptions
.map((sO) => ({
id: sO.id,
label: `${sO.description} - (${sO.price.formatted_with_symbol})`,
}))
.map((item) => (
<MenuItem key={item.id} value={item.id}>
{item.label}
</MenuItem>
))}
</Select>
</Grid>
</Grid>
<br />
<div style={{ display: "flex", justifyContent: "space-between" }}>
<Button component={Link} variant="outlined" to="/cart">
Back to Cart
</Button>
<Button type="submit" variant="contained" color="primary">
Next
</Button>
</div>
</form>
</FormProvider>
</div>
);
};
export default AddressForm;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
This was a commercejs.com issue not a coding issue

Disable submit input field until all required fields and checkboxes are empty

I have a simple web page to get input data:
Input form:
const Step1 = (props) => {
const { register, handleSubmit } = useForm();
const classes = useStyles();
const { actions, state } = useStateMachine({ updateAction });
const onSubmit = (data) => {
actions.updateAction(data);
props.history.push("./step2");
};
var grid_data=data.map(function(row){return FormRow(row);})
return (
<form onSubmit={handleSubmit(onSubmit)}>
<h2>Enter your data</h2>
{grid_data}
<FormControlLabel
control={<WhiteCheckbox name="Usage_terms" />}
label="I accept Terms of Usage"
style={{ margin: "auto", marginLeft:-10}}
/>
<input type="submit" style={{width: "460px", backgroundColor: "green", marginTop:5 }} />
</form>
);
};
export default withRouter(Step1);
Where grid data is created from a special function and json-file "data":
function FormRow( {row_label, row_info, row_mes_unit} ) {
const row_classes=useStyles()
const { register, handleSubmit, formState: { errors } } = useForm();
return ( <React.Fragment>
<Grid item container xs alignItems="flex-end" direction="row">
<Grid item xs>
<input type="number" style={{width: "410px", height:"35px"}} placeholder={row_label} {...register(row_label, {required: true, max: 100, min: 0, maxLength: 100})} />
</Grid>
<Grid item xs={2} justify="flex-end" >
<HtmlTooltip
title={
<React.Fragment>
<Typography color="inherit">Help</Typography>
<p><b>{"Info:"}</b> {row_info} </p>
<p><b>{"Mes. Unit:"}</b> {row_mes_unit}</p>
</React.Fragment>
}
>
<Button className={row_classes.button} style={{
backgroundColor: "inherit",
borderRadius: 5,
}}>
<HelpOutlineIcon className={classNames(row_classes.rightIcon, row_classes.iconSmall)} />
</Button>
</HtmlTooltip>
</Grid>
</Grid>
</React.Fragment>
);
The question is how to disable submit input until all requested fields and checkbox are filled in?

React, Formik Field Arrays - mapping over repeatable fields

I'm trying to figure out how to use Formik field arrays in a react project.
I have one form (glossary) that has 3 Field Arrays within it (one for each of relatedTerms, templates and referenceMaterials).
Each of the field arrays is set out in a separate component. When I only used one of them, I had this working. Adding the next one has caused a problem that I can't solve.
My form has:
import React, { useState } from "react";
import ReactDOM from "react-dom";
import {render} from 'react-dom';
import { Link } from 'react-router-dom';
import firebase, {firestore} from '../../../../firebase';
import { withStyles } from '#material-ui/core/styles';
import {
Button,
LinearProgress,
MenuItem,
FormControl,
Divider,
InputLabel,
FormControlLabel,
TextField,
Typography,
Box,
Grid,
Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
} from '#material-ui/core';
import MuiTextField from '#material-ui/core/TextField';
import {
Formik, Form, Field, ErrorMessage, FieldArray,
} from 'formik';
import * as Yup from 'yup';
import {
Autocomplete,
ToggleButtonGroup,
AutocompleteRenderInputParams,
} from 'formik-material-ui-lab';
import {
fieldToTextField,
TextFieldProps,
Select,
Switch,
} from 'formik-material-ui';
import RelatedTerms from "./RelatedTerms";
import ReferenceMaterials from "./ReferenceMaterials";
import Templates from "./Templates";
const allCategories = [
{value: 'one', label: 'One'},
{value: 'two', label: 'Two'},
];
function UpperCasingTextField(props: TextFieldProps) {
const {
form: {setFieldValue},
field: {name},
} = props;
const onChange = React.useCallback(
event => {
const {value} = event.target;
setFieldValue(name, value ? value.toUpperCase() : '');
},
[setFieldValue, name]
);
return <MuiTextField {...fieldToTextField(props)} onChange={onChange} />;
}
function Glossary(props) {
const { classes } = props;
const [open, setOpen] = useState(false);
const [isSubmitionCompleted, setSubmitionCompleted] = useState(false);
function handleClose() {
setOpen(false);
}
function handleClickOpen() {
setSubmitionCompleted(false);
setOpen(true);
}
return (
<React.Fragment>
<Button
// component="button"
color="primary"
onClick={handleClickOpen}
style={{ float: "right"}}
variant="outlined"
>
Create Term
</Button>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="form-dialog-title"
>
{!isSubmitionCompleted &&
<React.Fragment>
<DialogTitle id="form-dialog-title">Create a defined term</DialogTitle>
<DialogContent>
<DialogContentText>
</DialogContentText>
<Formik
initialValues={{ term: "", definition: "", category: [], context: "", relatedTerms: [], templates: [], referenceMaterials: [] }}
onSubmit={(values, { setSubmitting }) => {
setSubmitting(true);
firestore.collection("glossary").doc().set({
...values,
createdAt: firebase.firestore.FieldValue.serverTimestamp()
})
.then(() => {
setSubmitionCompleted(true);
});
}}
validationSchema={Yup.object().shape({
term: Yup.string()
.required('Required'),
definition: Yup.string()
.required('Required'),
category: Yup.string()
.required('Required'),
context: Yup.string()
.required("Required"),
// relatedTerms: Yup.string()
// .required("Required"),
// templates: Yup.string()
// .required("Required"),
// referenceMaterials: Yup.string()
// .required("Required"),
})}
>
{(props) => {
const {
values,
touched,
errors,
dirty,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
handleReset,
} = props;
return (
<form onSubmit={handleSubmit}>
<TextField
label="Term"
name="term"
// className={classes.textField}
value={values.term}
onChange={handleChange}
onBlur={handleBlur}
helperText={(errors.term && touched.term) && errors.term}
margin="normal"
style={{ width: "100%"}}
/>
<TextField
label="Meaning"
name="definition"
multiline
rows={4}
// className={classes.textField}
value={values.definition}
onChange={handleChange}
onBlur={handleBlur}
helperText={(errors.definition && touched.definition) && errors.definition}
margin="normal"
style={{ width: "100%"}}
/>
<TextField
label="In what context is this term used?"
name="context"
// className={classes.textField}
multiline
rows={4}
value={values.context}
onChange={handleChange}
onBlur={handleBlur}
helperText={(errors.context && touched.context) && errors.context}
margin="normal"
style={{ width: "100%"}}
/>
<Box margin={1}>
<Field
name="category"
multiple
component={Autocomplete}
options={allCategories}
getOptionLabel={(option: any) => option.label}
style={{width: '100%'}}
renderInput={(params: AutocompleteRenderInputParams) => (
<MuiTextField
{...params}
error={touched['autocomplete'] && !!errors['autocomplete']}
helperText={touched['autocomplete'] && errors['autocomplete']}
label="Category"
variant="outlined"
/>
)}
/>
</Box>
<Divider style={{marginTop: "20px", marginBottom: "20px"}}></Divider>
<Box>
<Typography variant="subtitle2">
Add a related term
</Typography>
<FieldArray name="relatedTerms" component={RelatedTerms} />
</Box>
<Box>
<Typography variant="subtitle2">
Add a reference document
</Typography>
<FieldArray name="referenceMaterials" component={ReferenceMaterials} />
</Box>
<Box>
<Typography variant="subtitle2">
Add a template
</Typography>
<FieldArray name="templates" component={Templates} />
</Box>
<DialogActions>
<Button
type="button"
className="outline"
onClick={handleReset}
disabled={!dirty || isSubmitting}
>
Reset
</Button>
<Button type="submit" disabled={isSubmitting}>
Submit
</Button>
{/* <DisplayFormikState {...props} /> */}
</DialogActions>
</form>
);
}}
</Formik>
</DialogContent>
</React.Fragment>
}
{isSubmitionCompleted &&
<React.Fragment>
<DialogTitle id="form-dialog-title">Thanks!</DialogTitle>
<DialogContent>
<DialogContentText>
We appreciate your contribution.
</DialogContentText>
<DialogActions>
<Button
type="button"
className="outline"
onClick={handleClose}
>
Close
</Button>
{/* <DisplayFormikState {...props} /> */}
</DialogActions>
</DialogContent>
</React.Fragment>}
</Dialog>
</React.Fragment>
);
}
export default Glossary;
Then, each subform is as follows (but replacing relatedTerms for templates or referenceMaterials).
import React from "react";
import { Formik, Field } from "formik";
import { withStyles } from '#material-ui/core/styles';
import {
Button,
LinearProgress,
MenuItem,
FormControl,
InputLabel,
FormControlLabel,
TextField,
Typography,
Box,
Grid,
Checkbox,
} from '#material-ui/core';
import MuiTextField from '#material-ui/core/TextField';
import {
fieldToTextField,
TextFieldProps,
Select,
Switch,
} from 'formik-material-ui';
const initialValues = {
title: "",
description: "",
source: ""
};
class RelatedTerms extends React.Component {
render() {
const {form: parentForm, ...parentProps} = this.props;
return (
<Formik
initialValues={initialValues}
render={({ values, setFieldTouched }) => {
return (
<div>
{parentForm.values.relatedTerms.map((_notneeded, index) => {
return (
<div key={index}>
<TextField
label="Title"
name={`relatedTerms.${index}.title`}
placeholder=""
// className="form-control"
// value={values.title}
margin="normal"
style={{ width: "100%"}}
onChange={e => {
parentForm.setFieldValue(
`relatedTerms.${index}.title`,
e.target.value
);
}}
>
</TextField>
<TextField
label="Description"
name={`relatedTerms.${index}.description`}
placeholder="Describe the relationship"
// value={values.description}
onChange={e => {
parentForm.setFieldValue(
`relatedTerms.${index}.description`,
e.target.value
);
}}
// onBlur={handleBlur}
// helperText={(errors.definition && touched.definition) && errors.definition}
margin="normal"
style={{ width: "100%"}}
/>
<Button
variant="outlined"
color="secondary"
size="small"
onClick={() => parentProps.remove(index)}
>
Remove this term
</Button>
</div>
);
})}
<Button
variant="contained"
color="secondary"
size="small"
style={{ marginTop: "5vh"}}
onClick={() => parentProps.push(initialValues)}
>
Add a related term
</Button>
</div>
);
}}
/>
);
}
}
export default RelatedTerms;
Then when I try to render the data submitted in the form, I have:
import React, { useState, useEffect } from 'react';
import {Link } from 'react-router-dom';
import Typography from '#material-ui/core/Typography';
import ImpactMetricsForm from "./Form";
import firebase, { firestore } from "../../../../firebase.js";
import { makeStyles } from '#material-ui/core/styles';
import clsx from 'clsx';
import ExpansionPanel from '#material-ui/core/ExpansionPanel';
import ExpansionPanelDetails from '#material-ui/core/ExpansionPanelDetails';
import ExpansionPanelSummary from '#material-ui/core/ExpansionPanelSummary';
import ExpansionPanelActions from '#material-ui/core/ExpansionPanelActions';
import ExpandMoreIcon from '#material-ui/icons/ExpandMore';
import Chip from '#material-ui/core/Chip';
import Button from '#material-ui/core/Button';
import Divider from '#material-ui/core/Divider';
const useStyles = makeStyles((theme) => ({
root: {
width: '100%',
marginTop: '8vh',
marginBottom: '5vh'
},
heading: {
fontSize: theme.typography.pxToRem(15),
},
heading2: {
fontSize: theme.typography.pxToRem(15),
fontWeight: "500",
marginTop: '3vh',
marginBottom: '1vh',
},
secondaryHeading: {
fontSize: theme.typography.pxToRem(15),
color: theme.palette.text.secondary,
textTransform: 'capitalize'
},
icon: {
verticalAlign: 'bottom',
height: 20,
width: 20,
},
details: {
alignItems: 'center',
},
column: {
flexBasis: '20%',
},
columnBody: {
flexBasis: '70%',
},
helper: {
borderLeft: `2px solid ${theme.palette.divider}`,
padding: theme.spacing(1, 2),
},
link: {
color: theme.palette.primary.main,
textDecoration: 'none',
'&:hover': {
textDecoration: 'underline',
},
},
}));
const Title = {
fontFamily: "'Montserrat', sans-serif",
fontSize: "4vw",
marginBottom: '2vh'
};
const Subhead = {
fontFamily: "'Montserrat', sans-serif",
fontSize: "calc(2vw + 1vh + .5vmin)",
marginBottom: '2vh',
marginTop: '8vh',
width: "100%"
};
function useGlossaryTerms() {
const [glossaryTerms, setGlossaryTerms] = useState([])
useEffect(() => {
firebase
.firestore()
.collection("glossary")
.orderBy('term')
.onSnapshot(snapshot => {
const glossaryTerms = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data(),
}))
setGlossaryTerms(glossaryTerms)
})
}, [])
return glossaryTerms
}
const GlossaryTerms = () => {
const glossaryTerms = useGlossaryTerms()
const classes = useStyles();
return (
<div style={{ marginLeft: "3vw"}}>
<div className={classes.root}>
{glossaryTerms.map(glossaryTerm => {
return (
<ExpansionPanel defaultcollapsed>
<ExpansionPanelSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1c-content"
id="panel1c-header"
>
<div className={classes.column}>
<Typography className={classes.heading}>{glossaryTerm.term}</Typography>
</div>
<div className={classes.column}>
{glossaryTerm.category.map(category => (
<Typography className={classes.secondaryHeading}>
{category.label}
</Typography>
)
)}
</div>
</ExpansionPanelSummary>
<ExpansionPanelDetails className={classes.details}>
<div className={clsx(classes.columnBody)}>
<div>
<Typography variant="subtitle2" className={classes.heading2}>Meaning</Typography>
<Typography>{glossaryTerm.definition}</Typography>
</div>
<div>
<Typography variant="subtitle2" className={classes.heading2}>Context</Typography>
<div>
<Typography>{glossaryTerm.context}</Typography>
</div>
<div className={clsx(classes.helper)}>
<div>
<Typography variant="caption">Related Terms</Typography>
{glossaryTerm.relatedTerms.map(relatedTerm => (
<Typography variant="body2" className="blogParagraph" key={relatedTerm.id}>
{relatedTerm.title}
</Typography>
))}
</div>
<div>
<Typography variant="caption" >Related Templates</Typography>
{glossaryTerm.templates.map(template => (
<Typography variant="body2" className="blogParagraph" key={template.id}>
{template.title}
</Typography>
))}
</div>
<div>
<Typography variant="caption">Related Reference Materials</Typography>
{glossaryTerm.referenceMaterials.map(referenceMaterial => (
<Typography variant="body2" className="blogParagraph" key={referenceMaterial.id}>
{referenceMaterial.title}
</Typography>
))}
</div>
</div>
</ExpansionPanelDetails>
<Divider />
<ExpansionPanelActions>
{glossaryTerm.attribution}
</ExpansionPanelActions>
</ExpansionPanel>
)
})}
</div>
</div>
);
}
export default GlossaryTerms;
When I try this using only the relatedTerms field array, I can submit data in the form and render the list.
When I add in the next two Field Array components for Templates and ReferenceMaterials, I get an error that says:
TypeError: glossaryTerm.referenceMaterials.map is not a function
Each of the 3 field arrays is a duplicate, where I've only changed the name of the value in the main form. You can see from the screen shot attached that the data within each map from the form fields is the same for each of relatedTerms, templates and referenceMaterials. When I comment out templates and referenceMaterials from the rendered output, everything renders properly. When I comment out relatedTerms and try to render either templates or referenceMaterials, I get the error I reported.
If I remove the templates and referenceMaterials map statements from the rendered output, I can use the form with all 3 field arrays in it. They save properly in firebase. I just can't display them using the method that works for relatedTerms.
Everything seems ok with your code. I suspect that the problem is in the data coming from firebase in useGlossaryTerms, some entries in the glossary collection may not have referenceMaterials or templates fields (maybe from a previous form submit that did not have those yet).
You could :
Run a migration script on the collection to add defaults for those fields if they don't exist.
Add defaults on client side :
firebase
.firestore()
.collection("glossary")
.orderBy('term')
.onSnapshot(snapshot => {
const glossaryTerms = snapshot.docs.map(doc => {
const data = doc.data();
return {
id: doc.id,
...data,
referenceMaterials: data.referenceMaterials || [],
templates: data.templates || []
};
}
setGlossaryTerms(glossaryTerms)
})
On the client side, check if those fields exists before rendering :
{
glossaryTerm.templates ? (
<div>
<Typography variant="caption" >Related Templates</Typography>
{glossaryTerm.templates.map(template => (
<Typography variant="body2" className="blogParagraph" key={template.id}>
{template.title}
</Typography>
))}
</div>
) : null
}

Categories

Resources