How to use react-dropzone with react-hook-form so that the form returns proper file - not just file name?
How to use react-dropzone with react-hook-form?
Asked Answered
there is a full article here: dev.to/vibhanshu909/… –
Franky
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.
onChange wasn't fired when I tried this out. I've resolved in using onDrop, see the discussion: github.com/react-dropzone/react-dropzone/pull/623 –
Mcclure
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>
);
};
How do you manage errors on the dropzone itself with a form submit and no files added? –
Wilcher
Thanks for the example. A small aside is
useCallback
is unnecessary since you're not providing anything in the dependancy array –
Valet 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
© 2022 - 2024 — McMap. All rights reserved.