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

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.

Related

react-hook-forms's onSubmit function not getting triggered in my code

Why the function onSubmit does not get triggered in my code? (I need to keep it all as it is, or ar least to function in the same manner, the Input tag I have managed by Controller, needs to show a state "price" all the time, which is changed by two functions (handlePriceInputChange, handleSelectedBox)
const schema = yup
.object({
voucherPrice: yup.number().positive().required(),
})
.required();
function PriceSelection(props) {
const {
register,
handleSubmit,
control,
formState: { errors },
} = useForm({
resolver: yupResolver(schema),
});
const onSubmit = (data) => {
console.log("does not work?", data);
};
const classes = useStylesPriceSelection();
const [selected, setSelected] = useState(false);
const [price, setPrice] = useState("");
const handleSelectedBox = (ev) => {
console.log("", price);
setSelected(ev.target.getAttribute("data-boxprice"));
setPrice(parseInt(ev.target.getAttribute("data-price")));
};
const handlePriceInputChange = (ev) => {
console.log("change", price);
setPrice(parseInt(ev.target.value));
};
const priceBox1 = 25;
const priceBox2 = 50;
const priceBox3 = 75;
const priceBox4 = 100;
return (
<div>
<section className={classes.priceBoxWrap}>
<div
data-boxprice="1"
data-price={`${priceBox1}`}
onClick={(ev) => handleSelectedBox(ev)}
className={clsx(
classes.priceBox,
((selected === "1" && price === priceBox1) ||
price === priceBox1) &&
classes.priceBoxSelected
)}
>
{`${priceBox1}€`}
</div>
<div
data-boxprice="2"
data-price={`${priceBox2}`}
onClick={(ev) => handleSelectedBox(ev)}
className={clsx(
classes.priceBox,
((selected === "2" && price === priceBox2) ||
price === priceBox2) &&
classes.priceBoxSelected
)}
>
{`${priceBox2}€`}
</div>
</section>
<section className={classes.priceBoxWrap}>
<div
data-boxprice="3"
data-price={`${priceBox3}`}
onClick={(ev) => handleSelectedBox(ev)}
className={clsx(
classes.priceBox,
((selected === "3" && price === priceBox3) ||
price === priceBox3) &&
classes.priceBoxSelected
)}
>
{`${priceBox3}€`}
</div>
<div
data-boxprice="4"
data-price={`${priceBox4}`}
onClick={(ev) => handleSelectedBox(ev)}
className={clsx(
classes.priceBox,
((selected === "4" && price === priceBox4) ||
price === priceBox4) &&
classes.priceBoxSelected
)}
>
{`${priceBox4}€`}
</div>
</section>
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="voucherPrice"
control={control}
defaultValue={false}
rules={{ required: true }}
render={({ field: { onChange, value, ref, ...field } }) => (
<Input {...field} onChange={(ev) => handlePriceInputChange(ev)} value={price} type="number" innerRef={ref} />
)}
/>
<p>{errors.voucherPrice?.message}</p>
<Button
variant="contained"
sx={{ mt: 1, mr: 1 }}
type="submit"
>
{"Continue"}
</Button>
</form>
</div>
);
}
export default PriceSelection;
You can just use react-hook-form for this situation and can get rid of the additional local state management via useState. I reworked your example and removed the data attributes and just passed the price values for each box via the onClick callback.
const schema = yup
.object({
voucherPrice: yup.number().positive().required()
})
.required();
const priceBox1 = 25;
const priceBox2 = 50;
const priceBox3 = 75;
const priceBox4 = 100;
const selectedStyles = { outline: "2px solid green" };
function PriceSelection(props) {
const {
handleSubmit,
control,
formState: { errors },
setValue,
watch
} = useForm({ resolver: yupResolver(schema) });
const voucherPrice = watch("voucherPrice");
const onSubmit = (data) => {
console.log(data);
};
const setVoucherPrice = (price) => () => setValue("voucherPrice", price);
return (
<div>
<section>
<div
className="pricebox"
style={voucherPrice === priceBox1 ? selectedStyles : null}
onClick={setVoucherPrice(priceBox1)}
>
{`${priceBox1}€`}
</div>
<div
className="pricebox"
style={voucherPrice === priceBox2 ? selectedStyles : null}
onClick={setVoucherPrice(priceBox2)}
>
{`${priceBox2}€`}
</div>
</section>
<section>
<div
className="pricebox"
style={voucherPrice === priceBox3 ? selectedStyles : null}
onClick={setVoucherPrice(priceBox3)}
>
{`${priceBox3}€`}
</div>
<div
className="pricebox"
style={voucherPrice === priceBox4 ? selectedStyles : null}
onClick={setVoucherPrice(priceBox4)}
>
{`${priceBox4}€`}
</div>
</section>
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="voucherPrice"
control={control}
rules={{ required: true }}
defaultValue={0}
render={({ field: { ref, ...field } }) => (
<Input {...field} type="number" innerRef={ref} />
)}
/>
<p>{errors.voucherPrice?.message}</p>
<Button variant="contained" sx={{ mt: 1, mr: 1 }} type="submit">
{"Continue"}
</Button>
</form>
</div>
);
}
If the handleSubmit function does not call e.preventDefault your page will refresh, to fix this you can wrap the handler like this:
...
const _handleSubmit = (e) => {
e.preventDefault()
handleSubmit(e)
}
...
<form onSubmit={_handleSubmit}>
...

Form in React + Formik + Material Ui (dialog + stepper)

My form worked 2 days ago when i didn't implement material UI with dialog and stepper. All the data was sent to the database.
Now i did it, It doesn't want to send at all, i don't even have a mistake in the console or network tab in dev tools. You'll find below at first what that doesnt work. In second where that work.
Thanks a lot in advance.
import { Formik, Form, Field } from 'formik';
import { makeStyles, useTheme } from '#material-ui/core/styles';
import Button from '#material-ui/core/Button';
import Dialog from '#material-ui/core/Dialog';
import DialogContent from '#material-ui/core/DialogContent';
import DialogTitle from '#material-ui/core/DialogTitle';
import MobileStepper from '#material-ui/core/MobileStepper';
import KeyboardArrowLeft from '#material-ui/icons/KeyboardArrowLeft';
import KeyboardArrowRight from '#material-ui/icons/KeyboardArrowRight';
import axios from 'axios';
import Tools from './Tools';
import Technos from './Technos';
import Clients from './Clients';
import Managers from './Managers';
import Collaborators from './Collaborators';
const validate = (values) => {
const errors = {};
if (values.project_name === '') {
errors.project_name = 'Please choose at least a project name';
} else if (values.end_date < values.start_date) {
errors.end_date = 'The end date has to be after the start date';
}
return errors;
};
const useStyles = makeStyles(() => ({
steps: {
maxWidth: 400,
flexGrow: 1,
},
ModalDialog: {
height: '682px',
width: '683px',
},
}));
const CreateProject = () => {
const [tools, setTools] = useState([]);
const [toolsSelected, setToolsSelected] = useState([]);
const [newTool, setNewTool] = useState('');
const [technos, setTechnos] = useState([]);
const [technosSelected, setTechnosSelected] = useState([]);
const [newTechno, setNewTechno] = useState('');
const [clients, setClients] = useState([]);
const [clientSelected, setClientSelected] = useState();
const [newClient, setNewClient] = useState('');
const [collaborators, setCollaborators] = useState([]);
const [collaboratorsSelected, setCollaboratorsSelected] = useState([]);
const [managers, setManagers] = useState([]);
const [manager, setManager] = useState();
const [projectNameExist, setProjectNameExist] = useState(false);
const classes = useStyles();
const theme = useTheme();
const [open, setOpen] = React.useState(false);
const [activeStep, setActiveStep] = React.useState(0);
const submitting = (values, actions) => {
const data = { ...values };
data.ClientId = parseInt(clientSelected, 10);
data.Tools = toolsSelected;
data.Technos = technosSelected;
data.ManagerId = manager;
data.Collaborators = collaboratorsSelected;
axios({
method: 'post',
url: 'http://localhost:5000/api/projects',
data,
})
.then(() => actions.setSubmitting(false))
// .then(() => history.push('/'))
.catch((err) => {
if (err.response.status === 409) {
setProjectNameExist(true);
}
});
};
useEffect(() => {
axios
.get('http://localhost:5000/api/tools')
.then((res) => res.data)
.then((data) => setTools(data));
}, []);
const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<Formik
validate={validate}
validateOnChange={false}
validateOnBlur={false}
onSubmit={submitting}
initialValues={{
project_name: '',
start_date: undefined,
end_date: undefined,
teams_link: '',
number_sprints: undefined,
duration_sprints: undefined,
description: '',
time_budget_type: undefined,
project_type: undefined,
signed: undefined,
chance_winning: 0,
}}
>
{({ errors }) => (
<Form id="formCreateProject">
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
Create Project
</Button>
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog">
<DialogTitle id="form-dialog">Create a project</DialogTitle>
<div className={classes.ModalDialog}>
<DialogContent>
{activeStep === 0 ? (
<div style={{ visibility: activeStep !== 0 ? 'hidden' : '' }}>
<label htmlFor="project_name">Project Name:</label>
<Field
type="text"
id="project_name"
name="project_name"
placeholder="Your project name"
/>
{errors.project_name && <p>{errors.project_name}</p>}
{projectNameExist ? <p style={{ color: 'white' }}>This project name already exist</p> : ''}
<label htmlFor="project_type">Project type:</label>
<Field name="project_type" id="project_type" as="select">
<option value="">--Please choose an option--</option>
<option value="Development">Development</option>
<option value="Design">Design</option>
<option value="Cybersecurity">Cybersecurity</option>
<option value="Cloud">Cloud</option>
</Field>
<Clients
clients={clients}
clientSelected={clientSelected}
newClient={newClient}
setNewClient={setNewClient}
setClientSelected={setClientSelected}
setClients={setClients}
/>
<Managers
managers={managers}
setManagers={setManagers}
setManager={setManager}
/>
<label htmlFor="signed">Is the contract signed?</label>
<Field name="signed" id="signed" as="select">
<option value="">--Please choose an option--</option>
<option value="0">False</option>
<option value="1">True</option>
</Field>
</div>
) : '' }
{activeStep === 1 ? (
<div style={{ visibility: activeStep !== 1 ? 'hidden' : '' }}>
Step 2
<label htmlFor="description">Description:</label>
<Field
type="text"
id="description"
name="description"
placeholder="Short description of the project, what was sold, what are the expectations of the client?"
rows="4"
cols="50"
/>
<label htmlFor="teams_link">Teams link:</label>
<Field
type="text"
id="teams_link"
name="teams_link"
placeholder="Your teams link"
/>
</div>
) : '' }
{activeStep === 2 ? (
<div style={{ visibility: activeStep !== 2 ? 'hidden' : '' }}>
ddsdsds
</div>
) : '' }
{activeStep === 3 ? (
<div style={{ visibility: activeStep !== 3 ? 'hidden' : '' }}>
<Tools
tools={tools}
toolsSelected={toolsSelected}
newTool={newTool}
setNewTool={setNewTool}
setToolsSelected={setToolsSelected}
setTools={setTools}
/>
<Technos
technos={technos}
technosSelected={technosSelected}
newTechno={newTechno}
setNewTechno={setNewTechno}
setTechnosSelected={setTechnosSelected}
setTechnos={setTechnos}
/>
</div>
) : '' }
{activeStep === 4 ? (
<div style={{ visibility: activeStep !== 4 ? 'hidden' : '' }}>
Step 5
<Collaborators
collaborators={collaborators}
setCollaborators={setCollaborators}
collaboratorsSelected={collaboratorsSelected}
setCollaboratorsSelected={setCollaboratorsSelected}
/>
<button type="submit">
Create
</button>
</div>
) : '' }
</DialogContent>
</div>
<MobileStepper
variant="dots"
steps={5}
position="static"
activeStep={activeStep}
className={classes.steps}
nextButton={(
<Button size="small" onClick={handleNext} disabled={activeStep === 4}>
Next
{theme.direction === 'rtl' ? <KeyboardArrowLeft /> : <KeyboardArrowRight />}
</Button>
)}
backButton={(
<Button size="small" onClick={handleBack} disabled={activeStep === 0}>
{theme.direction === 'rtl' ? <KeyboardArrowRight /> : <KeyboardArrowLeft />}
Back
</Button>
)}
/>
</Dialog>
</Form>
)}
</Formik>
);
};
export default CreateProject;```
The previous component where that worked is here:
```import React, { useState, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { Formik, Form, Field } from 'formik';
import axios from 'axios';
import Tools from './Tools';
import Technos from './Technos';
import Clients from './Clients';
import Managers from './Managers';
import Collaborators from './Collaborators';
const validate = (values) => {
const errors = {};
if (values.project_name === '') {
errors.project_name = 'Please choose at least a project name';
} else if (values.end_date < values.start_date) {
errors.end_date = 'The end date has to be after the start date';
}
return errors;
};
const NewProject = () => {
const history = useHistory();
const [tools, setTools] = useState([]);
const [toolsSelected, setToolsSelected] = useState([]);
const [newTool, setNewTool] = useState('');
const [technos, setTechnos] = useState([]);
const [technosSelected, setTechnosSelected] = useState([]);
const [newTechno, setNewTechno] = useState('');
const [clients, setClients] = useState([]);
const [clientSelected, setClientSelected] = useState();
const [newClient, setNewClient] = useState('');
const [collaborators, setCollaborators] = useState([]);
const [collaboratorsSelected, setCollaboratorsSelected] = useState([]);
const [managers, setManagers] = useState([]);
const [manager, setManager] = useState();
const [projectNameExist, setProjectNameExist] = useState(false);
const submitting = (values, actions) => {
const data = { ...values };
data.ClientId = parseInt(clientSelected, 10);
data.Tools = toolsSelected;
data.Technos = technosSelected;
data.ManagerId = manager;
data.Collaborators = collaboratorsSelected;
axios({
method: 'post',
url: 'http://localhost:5000/api/projects',
data,
})
.then(() => actions.setSubmitting(false))
.then(() => history.push('/'))
.catch((err) => {
if (err.response.status === 409) {
setProjectNameExist(true);
}
});
};
useEffect(() => {
axios
.get('http://localhost:5000/api/tools')
.then((res) => res.data)
.then((data) => setTools(data));
}, []);
return (
<div id="newProject">
<Formik
validate={validate}
validateOnChange={false}
validateOnBlur={false}
onSubmit={submitting}
initialValues={{
project_name: '',
start_date: undefined,
end_date: undefined,
teams_link: '',
number_sprints: undefined,
duration_sprints: undefined,
description: '',
time_budget_type: undefined,
project_type: undefined,
signed: undefined,
chance_winning: 0,
}}
>
{({ errors }) => (
<Form id="formCreateProject">
<h1 style={{ color: 'white' }}>Create a project</h1>
<label htmlFor="project_name">Project Name:</label>
<Field
type="text"
id="project_name"
name="project_name"
placeholder="Your project name"
/>
{errors.project_name && <p>{errors.project_name}</p>}
{projectNameExist ? <p style={{ color: 'white' }}>This project name already exist</p> : ''}
<Clients
clients={clients}
clientSelected={clientSelected}
newClient={newClient}
setNewClient={setNewClient}
setClientSelected={setClientSelected}
setClients={setClients}
/>
<Managers managers={managers} setManagers={setManagers} setManager={setManager} />
<label htmlFor="start_date">Start date:</label>
<Field
type="date"
id="start_date"
name="start_date"
min="1000-01-01"
max="3000-12-31"
/>
<label htmlFor="end_date">End date:</label>
<Field
type="date"
id="end_date"
name="end_date"
min="1000-01-01"
max="3000-12-31"
/>
{errors.end_date && <p>{errors.end_date}</p>}
<label htmlFor="teams_link">Teams link:</label>
<Field
type="text"
id="teams_link"
name="teams_link"
placeholder="Your teams link"
/>
<label htmlFor="number_sprints">Number of sprints:</label>
<Field type="number" id="number_sprints" name="number_sprints" />
<label htmlFor="duration_sprints">
Duration of sprints (in weeks):
</label>
<Field
type="number"
id="duration_sprints"
name="duration_sprints"
/>
<label htmlFor="description">Description:</label>
<Field
type="text"
id="description"
name="description"
placeholder="Short description of the project, what was sold, what are the expectations of the client?"
rows="4"
cols="50"
/>
<label htmlFor="time_budget_type">Budget type:</label>
<Field name="time_budget_type" id="time_budget_type" as="select">
<option value="">--Please choose an option--</option>
<option value="Time and material">Time and material</option>
<option value="Time Package">Time Package</option>
</Field>
<label htmlFor="project_type">Project type:</label>
<Field name="project_type" id="project_type" as="select">
<option value="">--Please choose an option--</option>
<option value="Development">Development</option>
<option value="Design">Design</option>
<option value="Cybersecurity">Cybersecurity</option>
<option value="Cloud">Cloud</option>
</Field>
<label htmlFor="signed">Is the contract signed?</label>
<Field name="signed" id="signed" as="select">
<option value="">--Please choose an option--</option>
<option value="0">False</option>
<option value="1">True</option>
</Field>
<label htmlFor="chance_winning">
Chance to win the contract (max 100%):
</label>
<Field
type="number"
id="chance_winning"
name="chance_winning"
min="0"
max="100"
/>
<Tools
tools={tools}
toolsSelected={toolsSelected}
newTool={newTool}
setNewTool={setNewTool}
setToolsSelected={setToolsSelected}
setTools={setTools}
/>
<Technos
technos={technos}
technosSelected={technosSelected}
newTechno={newTechno}
setNewTechno={setNewTechno}
setTechnosSelected={setTechnosSelected}
setTechnos={setTechnos}
/>
<Collaborators
collaborators={collaborators}
setCollaborators={setCollaborators}
collaboratorsSelected={collaboratorsSelected}
setCollaboratorsSelected={setCollaboratorsSelected}
/>
<button type="submit">
Create
</button>
</Form>
)}
</Formik>
</div>
);
};
export default NewProject;```

Jest/Enzyme: Mock function in Functional Component

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"
);

Formik validation on each mui stepper

I am using Material UI stepper and on each step I have different components. I am trying to validate each step using Yup and have used Formik but the instead of validating it moves on to next step. How can I validate this on every step and can get all the data of each step at last.
import React from 'react';
..
import FormStep1 from './FormStep1';
import FormStep2 from './FormStep2';
function getSteps() {
return ['Select general campaign settings', 'Ads setting', 'Upload Ad contents', 'Review and Submit'];
}
function getStepContent(stepIndex, handleStepSubmit, handleNext) {
switch (stepIndex) {
case 0:
return <FormStep1 onSubmit={handleStepSubmit} onNext={handleNext}/>;
case 1:
return <FormStep2 />;
case 2:
return 'No need to worry abt this!';
default:
return 'Unknown stepIndex';
}
}
const IobdCampCreate = () => {
const classes = useStyles();
const [activeStep, setActiveStep] = React.useState(0);
const [state, setState] = React.useState({
steps: [
{ name: 'Select general campaign settings', data: {} },
{ name: 'form2', data: {} }
],
activeStep: 0,
});
const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
const handleReset = () => {
setActiveStep(0);
};
const handleStepSubmit = (stepIndex, data) => {
console.log("-------", stepIndex, data)
setState((prevState) => ({
...prevState,
activeStep: prevState.activeStep + 1,
steps: prevState.steps.map((step, index) => {
if (stepIndex !== index) {
return step;
}
return {
...step,
data
}
})
}))
}
return (
<React.Fragment>
<div className={classes.root}>
<Stepper activeStep={activeStep} alternativeLabel>
{state.steps.map((label) => (
<Step key={label.name}>
<StepLabel>{label.name}</StepLabel>
</Step>
))}
</Stepper>
<div>
{activeStep === state.steps.length ? (
<div>
<Typography className={classes.instructions}>All steps completed</Typography>
<Button onClick={handleReset}>Reset</Button>
</div>
) : (
<div>
<div className={classes.instructions}>
{getStepContent(activeStep, handleStepSubmit,handleNext )}
</div>
<div>
<Button
disabled={activeStep === 0}
onClick={handleBack}
className={classes.backButton}
>
Back
</Button>
<Button variant="contained" color="primary" onClick={handleNext}>
{activeStep === state.steps.length - 1 ? 'Finish' : 'Next'}
</Button>
</div>
</div>
)}
</div>
</div>
</React.Fragment>
);
};
export default IobdCampCreate;
Here is a the formstep1.js
import React from 'react';
import { Formik } from 'formik';
const FormStep1 = (props) => (
<div>
<h3>Form A</h3>
<Formik
initialValues={{ email: '', password: '' }}
validate={values => {
const errors = {};
if (!values.email) {
errors.email = 'Required';
} else if (
!/^[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
) {
errors.email = 'Invalid email address';
}
return errors;
}}
onSubmit={(values) => {
props.onSubmit(0, values);
}}
>
{({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
isSubmitting,
/* and other goodies */
}) => (
<form onSubmit={handleSubmit}>
<input
type="email"
name="email"
onChange={handleChange}
onBlur={handleBlur}
value={values.email}
/>
{errors.email && touched.email && errors.email}
<input
type="password"
name="password"
onChange={handleChange}
onBlur={handleBlur}
value={values.password}
/>
{errors.password && touched.password && errors.password}
<button variant="contained" color="primary" type="submit" >
Next
</button>
</form>
)}
</Formik>
</div>
);
export default FormStep1;

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