Jest/Enzyme: Mock function in Functional Component - reactjs

I have a functional component in React. Here is the code
import React, { useState, Fragment } from "react";
import { makeStyles } from "#material-ui/core/styles";
import "./K8sIPCalculator.css";
import { initialState, checkBoxLabel } from "./FormMetaData";
import { checkPositiveInteger } from "./FormDataValidation";
const useStyles = makeStyles((theme) => ({
// Styles
}));
const K8sIPCalculator = (props) => {
const classes = useStyles();
let intialStateCopy = JSON.parse(JSON.stringify(initialState));
const [data, setData] = useState(intialStateCopy);
const handleCheckBox = (path, value) => {
const newData = { ...data };
if (path === "nodes" || path === "pods") {
if (!checkPositiveInteger(value)) {
newData[path].value = value;
newData[path].helperText = "It should be a positive integer!";
} else {
newData[path].value = value;
newData[path].helperText = "";
}
} else newData[path] = value;
setData(newData);
};
const calculate = () => {
// Does some calculation and update data state
};
const onSubmit = () => {
if (data.nodes.helperText !== "" || data.pods.helperText !== "") {
alert("Data is not correct");
return;
}
calculate();
};
const onReset = () => {
intialStateCopy = JSON.parse(JSON.stringify(initialState));
setData(intialStateCopy);
};
return (
<Fragment>
<h2 className="name">K8s IP Calculator</h2>
<form className={classes.formRoot}>
<Accordion
defaultExpanded={true}
classes={{ expanded: classes.expanded }}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
id="accordion1"
className={classes.summary}
>
<Typography className={classes.heading}>Results</Typography>
</AccordionSummary>
<AccordionDetails className="container">
<InputLabel className={classes.label}>
Total useable IPs required:
</InputLabel>
<TextField
disabled
className={classes.textDisabledInput}
id="ips-required-output"
variant="outlined"
value={data.total}
/>
<InputLabel className={classes.label} htmlFor="subnet-size-output">
Subnet Size required:
</InputLabel>
<TextField
disabled
className={classes.textDisabledInput}
id="subnet-size-output"
variant="outlined"
value={data.subnet_size}
/>
</AccordionDetails>
</Accordion>
<br />
<Accordion
defaultExpanded={true}
classes={{ expanded: classes.expanded }}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
id="accordion2"
className={classes.summary}
>
<Typography className={classes.heading}>K8s Details</Typography>
</AccordionSummary>
<AccordionDetails className="container">
<InputLabel className={classes.label}>Nodes:</InputLabel>
<TextField
size="small"
type="number"
onChange={(e) => handleCheckBox("nodes", e.target.value)}
className={classes.textInput}
id="nodes-input"
variant="outlined"
value={data.nodes.value}
helperText={data.nodes.helperText}
/>
<InputLabel className={classes.label} htmlFor="pods-input">
Pods:
</InputLabel>
<TextField
size="small"
type="number"
onChange={(e) => handleCheckBox("pods", e.target.value)}
className={classes.textInput}
id="pods-input"
variant="outlined"
value={data.pods.value}
helperText={data.pods.helperText}
/>
<div id="nodes-error"></div>
</AccordionDetails>
</Accordion>
<div className="button-container">
<Button
id="reset-button"
className="button"
variant="outlined"
color="primary"
size="small"
onClick={onReset}
startIcon={<UndoIcon />}
>
Reset
</Button>
<Button
id="submit-button"
className="button"
variant="contained"
color="primary"
size="small"
startIcon={<SaveIcon />}
onClick={onSubmit}
>
Submit
</Button>
</div>
</form>
</Fragment>
);
};
export default K8sIPCalculator;
Things, I am trying to test,
When input changes, I want to check if handleCheckBox function has been called
When submit button is called, I want to check if calculate function has been called
How can I call the setData function to update data through Jest/Enzyme
I tried something like this
const spy = jest.spyOn(K8sIPCalculator, "calculate");
But I got
Cannot spy the calculate property because it is not a function; undefined given instead
PS: I am able to assert data change after submit is called. But I want to check if calculate function is called.
const submit = wrapper.find("button#submit-button");
submit.simulate("click");
expect(wrapper.find("input#ips-required-output").props().value).toEqual(35);
expect(wrapper.find("input#subnet-size-output").props().value).toEqual(
"/26"
);

Related

How to update existing data from DB with react-hook-form

I'm creating an E-commerce like website, we upload products through the admin site.
I have Upload.tsx file like so:
...
import FormDetails from '#components/FormDetails';
const Upload: React.FC = () => {
const { register, handleSubmit, reset, unregister, watch, setValue } =
useForm<UploadFormState>();
const watchPacksOrWholesale = watch('packsOrWholesale');
const [uploadLoading, setUploadLoading] = useState<boolean>(false);
const [file, setFile] = useState<File | null>(null);
const [url, setUrl] = useState<string>('');
const [progress, setProgress] = useState<number>(0);
const [createdAt, setCreatedAt] = useState<Timestamp | null>(null);
const saveProduct = useProductStore((state) => state.saveProduct);
const isLoading = useProductStore((state) => state.isLoadingSave);
const handleProductSubmit: SubmitHandler<UploadFormState> = (data) => {
const newProductUpload = {
drinkName: data.drinkName,
description: data.description,
category: data.category,
price: Number(data.price),
url,
createdAt,
packsOrWholesale: data.packsOrWholesale,
packSize: data.packSize,
};
saveProduct(newProductUpload, reset, setFile);
};
return (
<>
<Text fontSize="xl" mb={5} fontWeight="semibold">
Upload Drinks
</Text>
<form onSubmit={handleSubmit(handleProductSubmit)}>
<FormDetails
register={register}
file={file}
progress={progress}
setFile={setFile}
setProgress={setProgress}
isRequired={true}
unregister={unregister}
watchPacksOrWholesale={watchPacksOrWholesale}
setValue={setValue}
/>
<ButtonGroup mt={4} spacing={2}>
<Button
size="sm"
isLoading={uploadLoading}
isDisabled={!file || progress === 100}
onClick={handleProductImageUpload}
>
Upload Image
</Button>
<Button
size="sm"
type="submit"
variant="solid"
// isDisabled={!file || progress === 0}
colorScheme="success"
isLoading={isLoading}
>
Save Product
</Button>
</ButtonGroup>
</form>
</>
);
};
export default Upload;
.....
FormDetails.tsx
import {
....
} from '#chakra-ui/react';
import { FileInput } from '#components/FormFields';
import { drinkCategoriesArray } from '#data/index';
import { FormDetailsProps } from '#interfaces/index';
import { useEffect, useState } from 'react';
import { Controller } from 'react-hook-form';
const FormDetails: React.FC<FormDetailsProps> = ({
....
}) => {
const [isPacksOrWholesale, setIsPacksOrWholesale] = useState<boolean>(
product?.packsOrWholesale || false
);
useEffect(() => {
if (watchPacksOrWholesale) {
register('packsOrWholesale', { required: true });
} else {
unregister('packsOrWholesale');
}
}, [register, unregister, watchPacksOrWholesale, isPacksOrWholesale]);
return (
<>
<SimpleGrid columns={{ base: 1, sm: 2 }} spacing={6} textAlign="left">
<FormControl>
<FormLabel fontSize="14px">Drink name</FormLabel>
<Input
{...register('drinkName', { required: isRequired })}
type={'text'}
placeholder="Meridian"
defaultValue={(showValue && product?.drinkName) || ''}
/>
</FormControl>
<FormControl>
<FormLabel fontSize="14px">Description</FormLabel>
<Input
{...register('description', { required: isRequired })}
type={'text'}
placeholder="Meridian wine is the best"
defaultValue={(showValue && product?.description) || ''}
/>
</FormControl>
<FormControl>
<FormLabel fontSize="14px">Select Category</FormLabel>
<Select
{...register('category', { required: isRequired })}
placeholder="Select category"
defaultValue={(showValue && product?.category) || ''}
>
{drinkCategoriesArray.map(({ drinkCategory, drink_id }) => (
<option key={drink_id} value={drinkCategory}>
{drinkCategory}
</option>
))}
</Select>
</FormControl>
<FormControl>
<FormLabel>
<FormLabel fontSize="14px">Select Product method</FormLabel>
<HStack>
<Text fontWeight={'semibold'}>Product in wholesale</Text>
<Switch
id="packs switch"
{...register('packsOrWholesale')}
colorScheme={'primary'}
size={'sm'}
fontWeight={'semibold'}
defaultChecked={showValue && product?.packsOrWholesale}
isChecked={isPacksOrWholesale}
onChange={() => setIsPacksOrWholesale(!isPacksOrWholesale)}
/>
<Text fontWeight={'semibold'}>Product in packs</Text>
</HStack>
</FormLabel>
</FormControl>
{isPacksOrWholesale ? (
<FormControl>
<FormLabel>
<FormLabel fontSize="14px">Select pack size</FormLabel>
<Select
{...register('packSize', {
required: true,
})}
placeholder="Select Pack Size"
defaultValue={(showValue && product?.packSize) || ''}
>
<option value="6 Packs">6 Packs</option>
<option value="12 Packs">12 Packs</option>
</Select>
</FormLabel>
</FormControl>
) : null}
<FormControl mt={'3px'}>
<FormLabel fontSize="14px">Price</FormLabel>
<Input
{...register('price', { required: isRequired })}
type="number"
placeholder="500 (in Dollars)"
defaultValue={(showValue && product?.price) || ''}
/>
</FormControl>
</SimpleGrid>
....
</Box>
</>
);
};
export default FormDetails;
.....
ProductEdit.tsx
import FormDetails from '#components/FormDetails';
import { useProductStore } from '#store/useProductStore';
const ProductEdit = forwardRef(({ product }: ProductType, ref) => {
const { id } = useParams<ProductParams>();
const { register, handleSubmit, watch, unregister, reset, setValue } =
useForm<UploadFormState>();
const watchPacksOrWholesale = watch('packsOrWholesale');
const [file, setFile] = useState<File | null>(null);
const [uploadLoading, setUploadLoading] = useState<boolean>(false);
const [url, setUrl] = useState<string>('');
const [progress, setProgress] = useState<number>(0);
const { isOpen, onOpen, onClose } = useDisclosure();
const editProduct = useProductStore((state) => state.editProduct);
const isLoading = useProductStore((state) => state.isLoadingEdit);
const handleProductUpdate: SubmitHandler<UploadFormState> = (data) => {
const newUpdate = {
drinkName: data.drinkName || product?.drinkName,
description: data.description || product?.description,
category: data.category || product?.category,
url: url || product?.url,
price: Number(data.price) || Number(product?.price),
packsOrWholesale: data.packsOrWholesale || product?.packsOrWholesale,
packSize: data.packSize || product?.packSize,
};
// editProduct(id, newUpdate, onClose, setFile);
console.log(newUpdate);
};
return (
<Box>
<IconButton
aria-label="edit-button"
size="sm"
colorScheme="secondary"
variant="ghost"
onClick={onOpen}
icon={<RiPencilLine size="18px" />}
/>
<Modal
isCentered
size="lg"
onClose={onClose}
isOpen={isOpen}
motionPreset="slideInBottom"
closeOnOverlayClick={false}
>
<ModalOverlay backdropFilter={'blur(5px)'} />
<ModalContent>
<ModalHeader>
Edit Product{' '}
<chakra.span color="success.700">{product?.drinkName}</chakra.span>?
</ModalHeader>
<ModalCloseButton size="sm" />
<form onSubmit={handleSubmit(handleProductUpdate)}>
<ModalBody>
<FormDetails
register={register}
file={file}
progress={progress}
setFile={setFile}
setProgress={setProgress}
showValue={true}
isRequired={false}
product={product}
unregister={unregister}
watchPacksOrWholesale={watchPacksOrWholesale}
setValue={setValue}
/>
</ModalBody>
<ModalFooter>
<ButtonGroup mt={4} spacing={2}>
<Button
isLoading={uploadLoading}
onClick={handleProductImageUpload}
isDisabled={!file || progress === 100}
size="sm"
>
Upload Image
</Button>
<Button
size="sm"
type="submit"
variant="solid"
isLoading={isLoading}
colorScheme="success"
>
Update Product
</Button>
</ButtonGroup>
</ModalFooter>
</form>
</ModalContent>
</Modal>
</Box>
);
});
export default ProductEdit;
I have a reusable component, called FormDetails. I use this component to get the data from the admin input and do an upload. Alternatively, I also use it for the ProductEdit component.
Everything works fine, except for the fact that,
in the FormDetails component, I want to use react-hook-form to manage the state of what renders. i.e if watchPacksOrWholesale is true, which is linked to the Switch component, the Select component which holds the packSize should show.
The ProductEdit component picks the existing Product from firebase and then on editing the product using react-hook-form if I disable the Switch component the value for the PackSize should be an empty string, meaning I don't want a packSize. I should be able to update the product on Firebase.
All I'm saying is that the Switch component does not update well.

FormData: The data is not placed in the formData, and an empty object is printed

I have a project in order to run a contracting, building and construction company, and I must create an invoice, and to create an invoice I must send a request to the backend, and the form of the request must be as shown in this picture:
And this is what the interface should look like:
The problem is that when I print "formData" it prints an empty object, even though I passed the data to "formData", how can I solve the problem?
this is the empty Object:
And the data is sent to the backend empty
and when i print:
console.log(formData.get("invoice"));
console.log(formData.get("data"));
the console was:
null
[object Object]
file.js:
import React, { useState } from "react";
import InputAdornment from "#material-ui/core/InputAdornment";
import TextField from "#material-ui/core/TextField";
import Grid from "#material-ui/core/Grid";
import "date-fns";
import DateFnsUtils from "#date-io/date-fns";
// import {
// KeyboardDatePicker,
// MuiPickersUtilsProvider,
// DatePicker,
// } from "#material-ui/pickers";
import { makeStyles } from "#material-ui/core/styles";
import Button from "#material-ui/core/Button";
import CloudUploadIcon from "#material-ui/icons/CloudUpload";
import { addInvoice } from "../../../store/invoiceSlice";
import Icon from "#material-ui/core/Icon";
import { motion } from "framer-motion";
import { useDispatch } from "react-redux";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import { alpha } from "#material-ui/core/styles";
const useStyles = makeStyles((theme) => ({
root: {
"& > *": {
margin: theme.spacing(1),
},
},
input: {
display: "none",
},
button: {
margin: theme.spacing(1),
// padding: theme.spacing(4),
},
}));
function ShippingTab(props) {
const dispatch = useDispatch();
const classes = useStyles();
const [issueDate, setIssueDate] = useState(new Date());
const [dueDate, setDueDate] = useState(new Date());
const [netAmount, setNetAmount] = useState("");
const [taxNumber, setTaxNumber] = useState("");
const [grossAmount, setGrossAmount] = useState("");
const [file, setFile] = useState(null);
let formData = new FormData();
const fileSelectedHandler = (event) => {
console.log(event.target.files[0]);
const file = event.target.files[0];
if (event.target && file) {
formData.append("invoice", file);
setFile(file);
}
};
const uploadHandler = (event) => {
// fd.append("invoice", files);
formData.append("data", {
grossAmount,
taxNumber,
netAmount,
dueDate,
issueDate,
});
console.log("formmmmmmmmmmm Data: ", formData);
// call api
dispatch(addInvoice(formData));
};
const handleissueDateChange = (date) => {
setIssueDate(date);
console.log("date issssssssss: ", date);
console.log("date issssssssss: ", issueDate);
};
const handleDueDateChange = (date) => {
setDueDate(date);
};
const handleNetAmountChange = (event) => {
setNetAmount(event.target.value);
};
const handleTaxAmountChange = (event) => {
setTaxNumber(event.target.value);
};
const handleGrossAmountChange = (event) => {
setGrossAmount(event.target.value);
};
return (
<>
{/* <MuiPickersUtilsProvider utils={DateFnsUtils}>
<div className="flex -mx-4">
<KeyboardDatePicker
inputVariant="outlined"
className="mt-8 mb-16"
margin="normal"
id="date-picker-dialog"
label="issue Date"
format="MM/dd/yyyy"
KeyboardButtonProps={{
"aria-label": "change date",
}}
value={issueDate}
onChange={handleissueDateChange}
/> */}
<div className="flex -mx-4">
<TextField
id="date"
variant="outlined"
label="Choose Issue Date"
className="mt-8 mb-16 mr-5"
type="date"
defaultValue={issueDate}
InputLabelProps={{
shrink: true,
}}
// value={issueDate}
onChange={handleissueDateChange}
fullWidth
/>
<TextField
id="date"
variant="outlined"
className="mt-8 mb-16"
label="Choose Due Date"
type="date"
defaultValue={dueDate}
InputLabelProps={{
shrink: true,
}}
// value={dueDate}
onChange={handleDueDateChange}
fullWidth
/>
</div>
{/* <KeyboardDatePicker
inputVariant="outlined"
className="mt-8 mb-16 ml-6"
margin="normal"
id="date-picker-dialog"
label="Due Date"
format="MM/dd/yyyy"
KeyboardButtonProps={{
"aria-label": "change date",
}}
value={dueDate}
onChange={handleDueDateChange}
/>
</div>
</MuiPickersUtilsProvider> */}
<TextField
className="mt-8 mb-16"
label="Net Amount"
id="extraShippingFee"
variant="outlined"
InputProps={{
startAdornment: <InputAdornment position="start">$</InputAdornment>,
}}
value={netAmount}
onChange={handleNetAmountChange}
fullWidth
/>
<TextField
className="mt-8 mb-16"
label="Tax Number"
id="extraShippingFee"
variant="outlined"
InputProps={{
startAdornment: <InputAdornment position="start">$</InputAdornment>,
}}
value={taxNumber}
onChange={handleTaxAmountChange}
fullWidth
/>
<TextField
className="mt-8 mb-16"
label="Gross Amount"
id="extraShippingFee"
variant="outlined"
InputProps={{
startAdornment: <InputAdornment position="start">$</InputAdornment>,
}}
value={grossAmount}
onChange={handleGrossAmountChange}
fullWidth
/>
<div className={classes.root}>
<input
accept="application/pdf"
// className={classes.input}
// multiple
type="file"
onChange={fileSelectedHandler}
/>
{/* <label htmlFor="contained-button-file"> */}
{/* <Button
variant="contained"
color="primary"
size="large"
className={classes.button}
startIcon={<CloudUploadIcon />}
> */}
{/* Upload */}
{/* </Button> */}
{/* </label> */}
</div>
<motion.div
className="flex"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0, transition: { delay: 0.3 } }}
>
<Button
className="whitespace-nowrap mx-4"
variant="contained"
color="secondary"
// onClick={handleRemoveProduct}
startIcon={<Icon className="hidden sm:flex">delete</Icon>}
>
Cancel
</Button>
<Button
className="whitespace-nowrap mx-4"
variant="contained"
color="secondary"
// disabled={_.isEmpty(dirtyFields) || !isValid}
onClick={uploadHandler}
>
Create
</Button>
</motion.div>
</>
);
}
export default ShippingTab;
#hiba-youssef This is not a problem, console logging formData always gives empty obj but that doesn't mean it is empty, to print the values you can use get() method , kindly refer this official documenation https://developer.mozilla.org/en-US/docs/Web/API/FormData/get.
--EDIT--
As per your postman screenshot you can modify the uploadHandler fn as below
const uploadHandler = (event) => {
const formData=new FormData();
formData.append('grossAmount',grossAmount);
formData.append('taxNumber',taxNumber);
formData.append('dueDate',dueDate);
formData.append('netAmount',netAmount);
formData.append('issueDate',issueDate);
formData.append('issueDate',issueDate);
formData.append('invoice',file);
// now the formData is as per postman screenshot. You can also add all keys to an array and loop it
};
Thanks ,
working fine when i used => Object.fromEntries(formData.entries())
const uploadHandler = (event) => {
const formData=new FormData();
formData.append('grossAmount',grossAmount);
formData.append('taxNumber',taxNumber);
formData.append('dueDate',dueDate);
formData.append('netAmount',netAmount);
formData.append('issueDate',issueDate);
formData.append('issueDate',issueDate);
formData.append('invoice',file);
console.log(Object.fromEntries(formData.entries()))
};
if you send data in api using formData you must remember.
1- if data with text and image you need to use inside header
headers = {
"Content-Type": "multipart/form-data",
};
2- if data only text use
headers = {
"Content-Type": "application/json",
};

Probleme with Reach-hook-form validation on update

I Working with hook-form and MaterialUI, I want to make the validation of the form, the validation working well in adding, but on update (input are ready full) in shows me erros (inputs are empty):
my code:
import Box from "#mui/material/Box";
import Button from "#mui/material/Button";
import Grid from "#mui/material/Grid";
import TextField from "#mui/material/TextField";
import axios from "axios";
import "bootstrap/dist/css/bootstrap.min.css";
import React from "react";
import { useForm } from "react-hook-form";
import { useNavigate, useParams } from "react-router-dom";
function UpdateEmployer(props) {
const { id } = useParams();
const { register, handleSubmit, formState, control } = useForm({
mode: "onChange",
});
// declare states
const [FullName, setFullName] = React.useState("");
const [cin, setCin] = React.useState("");
const [profile, setProfile] = React.useState("");
const [years, setYears] = React.useState("");
const { isSubmitting, errors, isValid } = formState;
const navigate = useNavigate();
// get data from form and post to api
const onSubmit = async (data) => {
// set state from data
const { FullName, cin, profile, years } = data;
console.log(data);
await axios.put(`http://localhost:5000/employers/${id}`, {
fullname: FullName,
cin: cin,
profile: profile,
experience: years,
});
navigate("/");
};
React.useEffect(() => {
axios.get(`http://localhost:5000/employers/${id}`).then((res) => {
setFullName(res.data.fullname);
setCin(res.data.cin);
setProfile(res.data.profile);
setYears(res.data.experience);
});
}, []);
// -------------------- render componenet --------------------
return (
<div className="container mt-4">
<div className="text-center my-3 font-weight-bold">Update Employer</div>
<form onSubmit={handleSubmit(onSubmit)} style={{ width: "100%" }}>
<Grid container spacing={2}>
<Grid item xs={8}>
<TextField
id="FullName"
name="FullName"
label="Full Name"
variant="outlined"
fullWidth
value={FullName}
{...register("FullName", { required: "Please Entre your name" })}
onChange={(e) => setFullName(e.target.value)}
/>
<span className="text-danger">
{errors.FullName && errors.FullName.message}
</span>
</Grid>
<Grid item xs={4}>
<TextField
id="cin"
name="cin"
label="CIN"
variant="outlined"
value={cin}
fullWidth
{...register(
"cin",
{ required: "Please Entre your cin" },
{
minLength: {
value: 6,
message: "Oops! the cin have to ve at least 6 characters",
},
}
)}
onChange={(e) => {
console.log(e.target.value);
setCin(e.target.value);
}}
/>
<span className="text-danger">
{errors.cin && errors.cin.message}
</span>
</Grid>
<Grid item xs={8}>
<TextField
id="profile"
name="profile"
label="Profile"
variant="outlined"
fullWidth
value={profile}
{...register("profile", {
required: "Please Entre your profile",
})}
onChange={(e) => setProfile(e.target.value)}
/>
</Grid>
<Grid item xs={4}>
<TextField
id="years"
name="years"
label="Years of Experience"
variant="outlined"
value={years}
fullWidth
{...register("years", { required: "Please Entre your years" })}
onChange={(e) => setYears(e.target.value)}
/>
</Grid>
<Grid item xs={12}>
<Box textAlign="center">
<Button
id="Update_employer"
variant="contained"
color="primary"
type="submit"
// disabled={!isValid}
>
Update Employee
</Button>
</Box>
</Grid>
</Grid>
</form>
</div>
);
}
export default UpdateEmployer;
The fill he inputs in with useEffect() and it shows me that the input are full with the right information but when I want to submit it shows me that error that the input are empty.

Using a checkbox with Material UI to change the boolean value of an attribute within redux

I have a form that is controlled by redux state and I need to use checkboxes that will change the value of certain attributes of state from true to false and back again. The way that I have it written, I can click the checkbox and it updates the attribute to true but when I try to click on it again, it doesn't uncheck and it doesn't change state.
Here is my form,
import React, { useEffect } from 'react';
import Avatar from '#material-ui/core/Avatar';
import Button from '#material-ui/core/Button';
import CssBaseline from '#material-ui/core/CssBaseline';
import TextField from '#material-ui/core/TextField';
import {Link} from 'react-router-dom'
import Grid from '#material-ui/core/Grid';
import Box from '#material-ui/core/Box';
import EditIcon from '#material-ui/icons/Edit';
import Typography from '#material-ui/core/Typography';
import { makeStyles } from '#material-ui/core/styles';
import Container from '#material-ui/core/Container';
import {connect} from 'react-redux'
import {updateProfileForm, setProfileForm} from '../actions/updateProfileActions'
import { update } from '../actions/currentUserActions'
import FormControl from '#material-ui/core/FormControl';
import Checkbox from '#material-ui/core/Checkbox';
import FormControlLabel from '#material-ui/core/FormControlLabel';
import FormGroup from '#material-ui/core/FormGroup';
import FormLabel from '#material-ui/core/FormLabel';
import { TimePicker } from "#material-ui/pickers"
function Copyright() {
return (
<Typography variant="body2" color="textSecondary" align="center">
{'Copyright © '}
<Link color="inherit" href="https://material-ui.com/">
NTXASN
</Link>{' '}
{new Date().getFullYear()}
{'.'}
</Typography>
);
}
const useStyles = makeStyles((theme) => ({
paper: {
marginTop: theme.spacing(8),
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
avatar: {
margin: theme.spacing(1),
backgroundColor: theme.palette.secondary.main,
},
form: {
width: '100%', // Fix IE 11 issue.
marginTop: theme.spacing(3),
},
submit: {
margin: theme.spacing(3, 0, 2),
},
}));
const UpdateProfileForm =({updateFormData, updateProfileForm, update, history, currentUser, setProfileForm })=> {
const useUpdateProfileForm = () => {
useEffect(()=> {
setProfileForm(currentUser.attributes)
}, [])
}
useUpdateProfileForm()
const classes = useStyles();
const handleChange = event => {
const {name, value } = event.target
const updatedFormInfo = {
...updateFormData,
[name]: value
}
updateProfileForm(updatedFormInfo)
}
const handleBoolean = event => {
const {name, value } = event.target
console.log(event.target.checked)
const updatedFormInfo = {
...updateFormData,
[name]: !value
}
updateProfileForm(updatedFormInfo)
console.log(event.target.checked)
}
const handleSubmit = event =>{
event.preventDefault()
update(updateFormData, history)
}
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<Avatar className={classes.avatar}>
<EditIcon />
</Avatar>
<Typography component="h1" variant="h5">
Update your Driver Profile
</Typography>
<form className={classes.form} noValidate onSubmit={handleSubmit}>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
name="name"
variant="outlined"
required
fullWidth
id="name"
label="Name"
autoFocus
onChange={handleChange}
value={updateFormData.name}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
onChange={handleChange}
value={updateFormData.email}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="phone_number"
label="Phone Number"
name="phone_number"
autoComplete="phone number"
onChange={handleChange}
type="tel"
value={updateFormData.phone_number}
/>
</Grid>
<FormControl component="fieldset" className={classes.formControl}>
<FormLabel component="legend">Select Available Days</FormLabel>
<FormGroup>
<FormControlLabel
control={<Checkbox checked={updateFormData.monday} onChange={handleBoolean} name="monday" />}
label="Monday"
/>
<FormControlLabel
control={<Checkbox checked={updateFormData.tuesday} onChange={handleBoolean} name="tuesday" />}
label="Tuesday"
/>
<FormControlLabel
control={<Checkbox checked={updateFormData.wednesday} onChange={handleBoolean} name="wednesday" />}
label="Wednesday"
/>
</FormGroup>
</FormControl>
<FormControl component="fieldset" className={classes.formControl}>
<FormLabel component="legend">-</FormLabel>
<FormGroup>
<FormControlLabel
control={<Checkbox checked={updateFormData.thursday} onChange={handleBoolean} name="thursday" />}
label="Thursday"
/>
<FormControlLabel
control={<Checkbox checked={updateFormData.friday} onChange={handleBoolean} name="friday" />}
label="Friday"
/>
<FormControlLabel
control={<Checkbox checked={updateFormData.saturday} onChange={handleBoolean} name="saturday" />}
label="Saturday"
/>
</FormGroup>
</FormControl>
<FormControl component="fieldset" className={classes.formControl}>
<FormLabel component="legend">-</FormLabel>
<FormGroup>
<FormControlLabel
control={<Checkbox checked={updateFormData.sunday} onChange={handleBoolean} name="sunday" />}
label="Sunday"
/>
</FormGroup>
</FormControl>
</Grid>
<br/>
<Grid>
<TimePicker autoOk label="Hour Availability Lower Limit" value={updateFormData.availability_hours_lower} name="availability_hours_lower" onChange={handleChange}/>
<TimePicker autoOk label="Hour Availability Upper Limit" value={updateFormData.availability_hours_lower} name="availability_hours_upper" onChange={handleChange}/>
</Grid>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
>
Update Profile
</Button>
</form>
</div>
<Box mt={5}>
<Copyright />
</Box>
</Container>
);
}
const mapStateToProps = state => {
return{
currentUser: state.currentUser,
updateFormData: state.updateProfile
}
}
export default connect(mapStateToProps, {updateProfileForm, update, setProfileForm})(UpdateProfileForm)
here are my actions
export const updateProfileForm = (formData) => {
return {
type: "UPDATE_PROFILE_FORM",
formData: formData
}
}
export const resetProfileForm = () => {
return {
type: "RESET_PROFILE_FORM",
}
}
export const setProfileForm = (formData) => {
return {
type: 'SET_PROFILE_FORM',
formData: formData
}
}
here is my reducer
const initialState = {
name: '',
email: '',
phone_number: '',
monday: false,
tuesday: false,
wednesday: false,
thursday: false,
friday: false,
saturday: false,
sunday: false,
availability_hours_lower: '',
availability_hours_upper: ''
}
const updateProfileReducer = (state = initialState, action) => {
switch(action.type){
case "UPDATE_PROFILE_FORM":
console.log(action.formData)
return action.formData
case "RESET_SIGNUP_FORM":
return initialState
case "SET_PROFILE_FORM":
return action.formData
default:
return state
}
}
export default updateProfileReducer
As I stated, right now when I click the check box it updates redux state from false to true and renders a checked box but when I try to uncheck the box, redux state is not updated and it does not uncheck the box. Any help would be greatly appreciated!
You should use the checked attribute to check if the checkbox is on or not.
const handleBoolean = (event) => {
const { name, checked } = event.target
updateProfileForm({
...updateFormData,
[name]: checked,
})
}
This is unrelated to the question but you can refactor your handleChange and handleBoolean functions by updating the reducer to apply the changes to the existing state.
const updateProfileReducer = (state = initialState, action) => {
switch (action.type) {
case 'UPDATE_PROFILE_FORM':
return { ...state, ...action.formData }
// ...
default:
return state
}
}
const handleChange = (event) => {
const { name, value } = event.target
updateProfileForm({
[name]: value,
})
}
const handleBoolean = (event) => {
const { name, checked } = event.target
updateProfileForm({
[name]: checked,
})
}
Btw, you don't even need to manage the form state in Redux since it's a local state. I would recommend using React Hook Form to simplify the code.

Get values from nested forms using final form in react/typescript

I wasn't able to get the values from my nested forms using final form.
here is my index.tsx
import React, { useState, ChangeEvent } from 'react';
import { Form } from 'react-final-form';
import {
Box, Typography, Button, IconButton, Grid, FormControl, InputLabel, Select,
} from '#material-ui/core';
import AddCircleIcon from '#material-ui/icons/AddCircle';
import FieldInput from 'forms/shared/fields/input';
import FieldFileUploader from 'forms/shared/fields/file-uploader';
import FieldCheckbox from 'forms/shared/fields/checkbox';
import FormRadioQuestions from './partials/form-radio';
import FormExperience from './partials/form-experience';
import FormEducation from './partials/form-education';
import FormPersonalInfo from './partials/form-personal-info';
import FormGovernmentIds from './partials/form-government-id';
import ItemExperience from './partials/experience-item';
import ItemEducation from './partials/education-item';
import useStyles from './styles';
const PublicApplicationForm = () => {
const [state, setState] = useState<{ client: string | number; name: string }>({ client: '', name: 'hai' });
const [showExp, setOpenExp] = useState(false);
const [showEdu, setOpenEdu] = useState(false);
const [data, setData] = useState({
experience: [{}],
});
const [educations, setEducations] = useState([]);
const [experiences, setExperiences] = useState([]);
const classes = useStyles();
const radioValues = [{ value: 'yes', label: 'Yes' }, { value: 'no', label: 'No' }];
const variables = { title: 'How did you hear about us?*', typeOfService: 'Type of Service*' };
const relationOptions = ['Walk-In', 'Employee Referral', 'Job Boards', 'Job Fair', 'Social Media'];
const checkBoxLabel = 'Please be informed that your application to this job offer will trigger some processing of your personal data by the company. For more information on data processing, please refer to the company’s talent acquisition privacy policy.';
const handleChange = ({ target }: ChangeEvent<{ name?: string; value: unknown }>) => {
setState({ ...state, [target.name as keyof typeof state]: target.value });
};
const handleBlur = (event: any) => {
data.experience[0] = { ...data.experience[0], [event.target.name]: event.target.value };
setData({ ...data });
};
const onAddEdu = (edu: any) => { setEducations([...educations, edu]); setOpenEdu(false); };
const onAddExp = (exp: any) => { setExperiences([...experiences, exp]); setOpenExp(false); };
return (
<Grid className={classes.pageContainer} container>
<Grid className={classes.formContainer} item xs={12}>
<Form
onSubmit={(values) => { console.log(values); }} // eslint-disable-line no-console
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<Typography className={classes.formHeading} variant="h5">Personal Information</Typography>
<Box className={classes.marginedBottom}>
<FieldFileUploader
required
fileType="avatars"
accept={['image/*']}
name="resume"
/>
</Box>
<FormPersonalInfo setData={setData} data={data} />
<Box className={classes.spaced}>
<Box className={classes.fieldContainer}>
<Typography className={classes.noMarginBottom} variant="h6">Experience</Typography>
<IconButton color="primary" onClick={() => { setOpenExp(!showExp); }}><AddCircleIcon /></IconButton>
</Box>
{
showExp && (
<FormExperience
onCancel={() => setOpenExp(false)}
onSave={onAddExp}
handleBlur={handleBlur}
/>
)
}
{experiences.map((exp, index) => <ItemExperience key={index} exp={exp} />)}
</Box>
<Box className={classes.spaced}>
<Box className={classes.fieldContainer}>
<Typography className={classes.noMarginBottom} variant="h6">Education</Typography>
<IconButton color="primary" onClick={() => { setOpenEdu(!showEdu); }}><AddCircleIcon /></IconButton>
</Box>
{
showEdu && (
<FormEducation
onCancel={() => setOpenEdu(false)}
onSave={onAddEdu}
setData={setData}
data={data}
/>
)
}
{educations.map((edu, index) => <ItemEducation key={index} edu={edu} />)}
</Box>
<Typography className={classes.formText} variant="h6">On the web</Typography>
<Box className={classes.fieldContainer}>
<FieldInput className={classes.textField} type="text" required name="applicant-linkedin" label="Linkedin" onChange={(event: React.ChangeEvent<HTMLInputElement>) => setData({ ...data, [event.target.name]: event.target.value })} />
</Box>
<Box className={`${classes.fieldContainer} ${classes.marginedBottom}`}>
<FieldInput className={classes.textField} type="text" required name="applicant-facebook" label="Facebook" onChange={(event: React.ChangeEvent<HTMLInputElement>) => setData({ ...data, [event.target.name]: event.target.value })} />
</Box>
<Typography className={classes.formText} variant="h6">Resume</Typography>
<Box className={`${classes.fieldContainer} ${classes.marginedBottom}`}>
<Box className={classes.dropZone}>
<FieldFileUploader
required
fileType="resumes"
fieldLabel="Click or Drag file here to upload resume"
accept={['application/pdf']}
name="resume"
/>
</Box>
</Box>
<Typography className={classes.formText} variant="h6">Check the box of the ones you don&apos;t have.</Typography>
<Box className={classes.marginedBottom}>
<FormGovernmentIds setData={setData} data={data} />
</Box>
<Typography className={classes.formText} variant="h6">Preliminary Questions</Typography>
<Box className={`${classes.fieldContainer} ${classes.marginedBottom}`}>
<FormControl variant="outlined" className={classes.textField}>
<InputLabel htmlFor="outlined-age-native-simple">{variables.title}</InputLabel>
<Select
native
value={state.client}
onChange={handleChange}
label="How did you hear about us?"
inputProps={{ name: 'client', id: 'outlined-age-native-simple' }}
onClick={
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => setData({
...data,
[event.target.name]: event.target.value,
})
}
>
<option aria-label="None" value="" />
{relationOptions.map((item, index) => (
<option key={index} value={item.replace(' ', '-').toLowerCase()}>{item}</option>))}
</Select>
</FormControl>
</Box>
<FormRadioQuestions
fieldClassName={classes.fieldContainer}
textClassName={classes.textField}
values={radioValues}
setData={setData}
data={data}
/>
<Box className={classes.fieldContainer}>
<FieldCheckbox className={classes.textField} name="confirm-registration" label={checkBoxLabel} />
</Box>
<Box className={classes.fieldContainer}>
<Button component="button" type="submit" className={classes.spaced} variant="contained" color="primary">
Submit
</Button>
</Box>
</form>
)}
/>
</Grid>
</Grid>
);
};
export default PublicApplicationForm;
The FormPersonalInfo component consists of fields such as applicant-first-name, last name, email, location, mobile, etc.
When I click on the AddCircleIcon button to show some other fields and to add experiences/education, I got this error.
When I click the submit button I only got this log.
This is the Experience forms same with Education forms
My goal is to add experince and education array to the result set just like this.
The error is clear, there are <form> tags nested.
Quick example to reproduce error
const { useState, useEffect } = React;
const App = () => {
return <form>
<form></form>
</form>
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone#6/babel.min.js"></script>
<div id="root"></div>
In your case showExp evaluates to true after click action, then FormExperience component, which apparently has form tag in it, renders and validateDOMNesting throws error.
showExp && (
<FormExperience
onCancel={() => setOpenExp(false)}
onSave={onAddExp}
handleBlur={handleBlur}
/>)
I had a similar case before I managed to solve it using Material UI Portals
import React from "react";
import Portal from "#material-ui/core/Portal";
export default NestedForms = () => {
const container = React.useRef(null);
return (
<>
<form>
{/*form content */}
<div ref={container} />
{/*form content */}
</form>
<Portal container={container.current}>
<form>
{/* otherform */}
<form/>
</Portal>
</>
)
}

Resources