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;
Related
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.
I am creating an interactive React app. I want it to be as optimal and modular as possible, so I created ONE authentication form, and I switch the various sub-forms (SignIn, SignUp, ForgotPassword) in the App component.
How do I define different validation schemas (yup), and initial values for each separate form, while nested in one physical Form component?
This is what my form looks like right now:
(Authentication.jsx)
const Authentication = ({ title }) => {
return (
<Container id='auth'>
<center>
<Formik
initialValues={{ name: '' }}
validationSchema={validationSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
actions.setSubmitting(false);
}, 1000);
}}>
{(props) => (
/* Authentication Form */
<Form autoComplete='off' className='auth-form' noValidate>
<h2 className='auth-title'>{title}</h2>
{title === 'Sign In' ? (
<SignIn />
) : title === 'Sign Up' ? (
<SignUp />
) : (
<ForgotPassword />
)}
</Form>
)}
</Formik>
</center>
</Container>
);
};
and an example sub-form:
(SignIn.jsx)
const validationSchema = yup.object().shape({
email: yup.string().email('Invalid email').required('Required'),
password: yup.string().password('Invalid password').required('Required'),
});
const SignIn = ({ name, errors, touched, ...props }) => {
const { values, submitForm } = useFormikContext();
const [input, setInputs] = useState({
email: '',
password: '',
});
const handleSetValues = (name) => (e) => {
let formikValue = 'formik.values.' + name;
setInputs({
...input,
[name]: e.target.value,
[formikValue]: e.target.value,
});
};
const [signInWithEmailAndPassword, user, loading, error] = useSignInWithEmailAndPassword(auth);
const [showPassword, setShow] = useState(false);
const navigate = useNavigate();
useEffect(() => {
if (loading || error) {
return;
}
if (user) {
navigate('/workshop');
}
}, [user, loading, error]);
const handleSubmit = (e) => {
e.preventDefault();
if (submitForm) {
signInWithEmailAndPassword(input.email, input.password).catch((err) => {
console.log(err);
});
}
};
return (
<div id='login'>
<Row>
{/* Email Input */}
<TextField
id='email'
label='Email'
variant='standard'
required
type='email'
value={input.email}
onChange={(e) => handleSetValues('email')}
error={values.touched.email && Boolean(values.errors.email)}
helperText={values.touched.email && values.errors.email}
/>
</Row>
<Row>
{/* Password Input */}
<FormControl variant='standard'>
<InputLabel htmlFor='password'>Password</InputLabel>
<Input
id='password'
label='Password'
variant='standard'
required
type={showPassword ? 'text' : 'password'}
value={input.password}
onChange={(e) => handleSetValues('password')}
error={values.touched.password && Boolean(values.errors.password)}
helperText={values.touched.password && values.errors.password}
// {...(error && { error: true, helperText: getError(error) })}
endAdornment={
<InputAdornment position='end'>
<IconButton
aria-label='toggle password visibility'
onClick={() => setShow(!showPassword)}
onMouseDown={(e) => e.preventDefault}
edge='end'>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
}
/>
</FormControl>
</Row>
{/* Display Error */}
<div className='error'>{error ? getError(error) : ' '}</div>
{/* Submit Button */}
<Button className='btn-auth' type='submit' onClick={handleSubmit}>
Sign In
</Button>
<Row>
{/* Link to Register */}
<Link link='/register' pretext='New to AME?' text='Sign Up' posttext='now.' />
{/* Link to Reset Password */}
<Link link='/forgot' text='Forgot password?' />
</Row>
</div>
);
};
i have a problem.
I use a form to edit my data, then when i want to see edited data, i get an ×
TypeError: Cannot read properties of undefined (reading 'id')
Pointing at my
{users &&
users.map((user) => {
return (
<div key={user.id}>
<Link to={`users/${user.id}`}> {user.name} </Link>
</div>
);
})}
Which is used to display data.
After refreshing the site (F5) it works, so i assume that the redux has problem with reading edited data for the first time, altough it do work with adding new data. anyone know what i can do?
My UserEditForm:
const UserEditForm = () => {
let { id } = useParams();
const { user } = useSelector((state) => state.user);
const [state, setState] = useState({
name: "",
birthday: "",
img: "",
});
const [error, setError] = useState("");
console.log(id);
let history = useHistory();
let dispatch = useDispatch();
const { name, birthday, img } = state;
useEffect(() => {
dispatch(getSingleUser(id));
}, []);
useEffect(() => {
if (user) {
setState({ ...user });
}
}, [user]);
const handleInputChange = (e) => {
let { name, value } = e.target;
setState({ ...state, [name]: value });
};
const handleSubmit = (e) => {
dispatch(updateUser(state, id));
history.push("/");
setError("");
};
return (
<div>
<Button
style={{ width: "100px", marginTop: "20px" }}
variant="contained"
color="secondary"
onClick={() => history.push("/")}
>
Go Back
</Button>
<h2>Edit User</h2>
{error && <h3 style={{ color: "red" }}>{error}</h3>}
<form noValidate autoComplete="off" onSubmit={handleSubmit}>
<TextField
id="standard-basic"
label="Name"
value={name || ""}
name="name"
type="text"
onChange={handleInputChange}
/>
<br />
<TextField
id="standard-basic"
label="birthday"
name="birthday"
value={birthday || ""}
type="birthday"
onChange={handleInputChange}
/>
<br />
<TextField
id="standard-basic"
label="img"
value={img || ""}
name="img"
type="number"
onChange={handleInputChange}
/>
<Button
style={{ width: "100px" }}
variant="contained"
color="primary"
type="submit"
onChange={handleInputChange}
>
Update
</Button>
</form>
</div>
);
};
export default UserEditForm;
My UserList component:
const UserList = ({ users, history }) => {
const dispatch = useDispatch();
const fetchUsers = async () => {
const response = await axios
.get("http://localhost:3000/characters")
.catch((err) => {
console.log("Err: ", err);
});
dispatch(setUsers(response.data));
};
useEffect(() => {
fetchUsers();
}, []);
console.log(users);
return (
<div>
<button onClick={() => history.goBack()}>...back</button>
<li>
<Link to="/user/add">Add Users</Link>
</li>
{users &&
users.map((user) => {
return (
<div key={user.id}>
<Link to={`users/${user.id}`}> {user.name} </Link>
</div>
);
})}
</div>
);
};
const mapStateToProps = (state) => {
return {
users: state.allUsers.users,
};
};
export default connect(mapStateToProps, null)(UserList);
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}>
...
How to use react-dropzone with react-hook-form so that the form returns proper file - not just file name?
Here is a work from a react-hook-form Github discussion:
export const DropzoneField = ({
name,
multiple,
...rest
}) => {
const { control } = useFormContext()
return (
<Controller
render={({ onChange }) => (
<Dropzone
multiple={multiple}
onChange={e =>
onChange(multiple ? e.target.files : e.target.files[0])
}
{...rest}
/>
)}
name={name}
control={control}
defaultValue=''
/>
)
}
const Dropzone = ({
multiple,
onChange,
...rest
}) => {
const {
getRootProps,
getInputProps,
} = useDropzone({
multiple,
...rest,
})
return (
<div {...getRootProps()}>
<input {...getInputProps({ onChange })} />
</div>
)
}
You should check out Controller API as it was made to make integration with external controlled input easier. There are quite a few examples therein as well.
i did this way #Bill
const FileUpload = (props) => {
const {
control,
label,
labelClassName,
name,
isRequired,
rules,
error,
multiple,
maxFiles,
setValue,
accept,
maxSize,
setError,
clearErrors,
formGroupClassName,
watch,
} = props;
const [files, setFiles] = useState(watch(name));
const onDrop = useCallback(
(acceptedFiles, rejectedFiles) => {
if (rejectedFiles && rejectedFiles.length > 0) {
setValue(name, []);
setFiles([]);
setError(name, {
type: 'manual',
message: rejectedFiles && rejectedFiles[0].errors[0].message,
});
} else {
setFiles(
acceptedFiles.map((file) =>
Object.assign(file, {
preview: URL.createObjectURL(file),
}),
),
);
clearErrors(name);
acceptedFiles.forEach((file) => {
const reader = new FileReader();
reader.onabort = () => toastError('File reading was aborted');
reader.onerror = () => toastError('file reading has failed');
reader.readAsDataURL(file);
reader.onloadend = () => {
setValue(name, file, { shouldValidate: true });
};
});
}
},
[name, setValue, setError, clearErrors],
);
const deleteFile = (e, file) => {
e.preventDefault();
const newFiles = [...files];
newFiles.splice(newFiles.indexOf(file), 1);
if (newFiles.length > 0) {
setFiles(newFiles);
} else {
setFiles(null);
setValue(name, null);
}
};
const thumbs =
files &&
files !== null &&
files.map((file) => {
const ext = file.name && file.name.substr(file.name.lastIndexOf('.') + 1);
return ext === 'pdf' ? (
<ul key={file.name} className="mt-2">
<li>{file.name}</li>
</ul>
) : (
<div className="thumb position-relative" key={file.name}>
<img src={file.preview ? file.preview : file} alt={file.name} />
<Button
className="trash-icon"
color="danger"
size="sm"
onClick={(e) => deleteFile(e, file)}
>
<FontAwesomeIcon icon={faTrashAlt} size="sm" />
</Button>
</div>
);
});
useEffect(() => {
if (
watch(name) !== '' &&
typeof watch(name) === 'string' &&
watch(name).startsWith('/')
) {
setFiles([
{
preview: getFileStorageBaseUrl() + watch(name),
name: watch(name)
.substr(watch(name).lastIndexOf('/') + 1)
.substr(0, watch(name).lastIndexOf('/')),
},
]);
}
}, [watch, name]);
useEffect(
() => () => {
if (files && files.length > 0) {
files.forEach((file) => URL.revokeObjectURL(file.preview));
}
},
[files],
);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
maxFiles: multiple ? maxFiles : 0,
accept,
onDrop,
minSize: 0,
maxSize,
multiple,
});
return (
<div className={formGroupClassName || 'file-input my-2 form-group'}>
{label && (
<label className={labelClassName} htmlFor={name}>
{label}
{isRequired && <span style={{ color: 'red' }}> * </span>}
</label>
)}
<Controller
control={control}
name={name}
rules={rules}
render={(controllerProps) => (
<div
{...getRootProps({
className: 'dropzone w-100 fs-20 d-flex align-items-center',
})}
{...controllerProps}
>
<input {...getInputProps()} />
<FontAwesomeIcon
icon={faCloudUploadAlt}
size="sm"
className="mr-1"
/>
{isDragActive ? (
<span className="fs-16">Drop the files here ... </span>
) : (
<span className="fs-16">Select files </span>
)}
</div>
)}
/>
<aside className="thumbs-container">{thumbs}</aside>
{error && <p className="form-error mb-0">{error.message}</p>}
</div>
);
};
here is the solution with v7
const DropzoneField = ({
name,
control,
...rest
}: {
name: string;
control: Control<FieldValues>;
}) => {
// const { control } = useFormContext();
return (
<Controller
render={({ field: { onChange } }) => (
<Dropzone onChange={(e: any) => onChange(e.target.files[0])} {...rest} />
)}
name={name}
control={control}
defaultValue=""
/>
);
};
const Dropzone = ({ onChange, ...rest }: { onChange: (...event: any[]) => void }) => {
const onDrop = useCallback((acceptedFiles) => {
// Do something with the files
console.log({ acceptedFiles });
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
return (
<div {...getRootProps()}>
<input {...getInputProps({ onChange })} />
{isDragActive ? (
<p>Drop the files here ...</p>
) : (
<p>Drag 'n' drop some files here, or click to select files</p>
)}
</div>
);
};
I got it working properly including with both drop and click to add a file using the following code:
FileInput.js
import React, { useCallback, useEffect } from "react"
import { useDropzone } from "react-dropzone"
import { useFormContext } from "react-hook-form"
const FileInput = props => {
const { name, label = name } = props
const { register, unregister, setValue, watch } = useFormContext()
const files = watch(name)
const onDrop = useCallback(
droppedFiles => {
setValue(name, droppedFiles, { shouldValidate: true })
},
[setValue, name]
)
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: props.accept,
})
useEffect(() => {
register(name)
return () => {
unregister(name)
}
}, [register, unregister, name])
return (
<>
<label className=" " htmlFor={name}>
{label}
</label>
<div
{...getRootProps()}
type="file"
role="button"
aria-label="File Upload"
id={name}
>
<input {...props} {...getInputProps()} />
<div
style={{ width: "500px", border: "black solid 2px" }}
className={" " + (isDragActive ? " " : " ")}
>
<p className=" ">Drop the files here ...</p>
{!!files?.length && (
<div className=" ">
{files.map(file => {
return (
<div key={file.name}>
<img
src={URL.createObjectURL(file)}
alt={file.name}
style={{
height: "200px",
}}
/>
</div>
)
})}
</div>
)}
</div>
</div>
</>
)
}
export default FileInput
Form
import React from "react"
import { FormProvider, useForm } from "react-hook-form"
import FileInput from "./FileInput"
const Form = () => {
const methods = useForm({
mode: "onBlur",
})
const onSubmit = methods.handleSubmit(values => {
console.log("values", values)
})
return (
<FormProvider {...methods}>
<form onSubmit={onSubmit}>
<div className="">
<FileInput
accept="image/png, image/jpg, image/jpeg, image/gif"
name="file alt text"
label="File Upload"
/>
</div>
</form>
</FormProvider>
)
}
export default Form