Created
July 6, 2023 18:01
-
-
Save alettieri/29543a90783c2bc77b06bb933241bf04 to your computer and use it in GitHub Desktop.
React DropZone validation logic
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import { Box, Input } from '@mui/material'; | |
import { clsx } from 'clsx'; | |
import { useDropzone, DropzoneOptions } from 'react-dropzone'; | |
import { DropZoneProvider } from './DropZoneProvider'; | |
type IDropZoneProps = DropzoneOptions & { | |
children: React.ReactNode; | |
}; | |
const DropZone = ({ children, ...options }: IDropZoneProps) => { | |
const dropzone = useDropzone({ | |
...options, | |
}); | |
return ( | |
<Box | |
{...dropzone.getRootProps({ | |
className: clsx('DropZone-root', { | |
'DropZone-active': dropzone.isDragActive, | |
'DropZone-inactive': !dropzone.isDragActive, | |
'DropZone-accept': dropzone.isDragAccept, | |
'DropZone-reject': dropzone.isDragReject, | |
'DropZone-focused': dropzone.isFocused, | |
'DropZone-disabled': options.disabled, | |
}), | |
})} | |
> | |
<Input {...dropzone.getInputProps({ color: undefined })} /> | |
<DropZoneProvider | |
isDragActive={dropzone.isDragActive} | |
isDragAccept={dropzone.isDragAccept} | |
isDragReject={dropzone.isDragReject} | |
fileRejections={dropzone.fileRejections} | |
acceptedFiles={dropzone.acceptedFiles} | |
open={dropzone.open} | |
> | |
{children} | |
</DropZoneProvider> | |
</Box> | |
); | |
}; | |
export type { IDropZoneProps }; | |
export { DropZone }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import { Box, Typography } from '@mui/material'; | |
import { DropZone, IDropZoneProps } from '../DropZone'; | |
import { ImageUploadErrors } from './ImageUploadErrors'; | |
import { ImageUploadImage } from './ImageUploadImage'; | |
import { ImageUploadLabel } from './ImageUploadLabel'; | |
import { styles } from './styles'; | |
import { getFilesFromEvent, FileWithDimensions } from './utils'; | |
const accept: IDropZoneProps['accept'] = { | |
'image/png': ['.png'], | |
'image/svg': ['.svg'], | |
}; | |
type IImageUploadProps = { | |
heading: React.ReactNode; | |
footer: React.ReactNode; | |
onChange: (file: File) => void; | |
isLoading: boolean; | |
validator?: ( | |
file: FileWithDimensions | |
) => ReturnType<IDropZoneProps['validator']>; | |
}; | |
const ImageUpload = (props: IImageUploadProps) => { | |
const handleDrop = React.useCallback<IDropZoneProps['onDrop']>( | |
(acceptedFiles) => { | |
if (acceptedFiles && acceptedFiles.length > 0) { | |
props.onChange(acceptedFiles[0]); | |
} | |
}, | |
[props] | |
); | |
const handleValidator = React.useCallback( | |
function handleValidator<T = FileWithDimensions | DataTransferItem>( | |
file: T | |
) { | |
if (props.validator && file instanceof FileWithDimensions) { | |
return props.validator(file); | |
} | |
return null; | |
}, | |
[props] | |
); | |
return ( | |
<Box className="ImageUpload-root" sx={styles}> | |
<DropZone | |
onDrop={handleDrop} | |
accept={accept} | |
maxFiles={1} | |
disabled={props.isLoading} | |
validator={handleValidator} | |
getFilesFromEvent={getFilesFromEvent} | |
> | |
<Typography | |
variant="h4" | |
component="h4" | |
paragraph | |
className="ImageUpload-heading" | |
> | |
{props.heading} | |
</Typography> | |
<Box className="ImageUpload-container"> | |
<ImageUploadImage isLoading={props.isLoading} /> | |
<ImageUploadLabel /> | |
</Box> | |
<ImageUploadErrors /> | |
<Box className="ImageUpload-footer">{props.footer}</Box> | |
</DropZone> | |
</Box> | |
); | |
}; | |
export { ImageUpload }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import { Box, Typography } from '@mui/material'; | |
import { ImageUpload } from '../../../components/file/ImageUpload'; | |
const Page = () => { | |
return ( | |
<Box> | |
<Box display="flex" flexDirection="column" gap={6}> | |
<ImageUpload | |
heading="Standard Logo" | |
onChange={(file) => console.log(file)} | |
footer={ | |
<LogoUploadFooter label="Minimum width: 250px" /> | |
} | |
isLoading={false} | |
validator={(file) => { | |
if (file.dimensions.width < 250) { | |
return { | |
code: 'file-invalid-dimensions', | |
message: 'Minimum width: 250px', | |
}; | |
} | |
return null; | |
}} | |
/> | |
</Box> | |
); | |
}; | |
export default Page; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { fromEvent } from 'file-selector'; | |
import { IDropZoneProps } from '../DropZone'; | |
type ImageDimensions = { | |
width: number; | |
height: number; | |
}; | |
interface FileWithDimensions extends File { | |
dimensions: ImageDimensions; | |
initializeFileWithDimensions(file: File): Promise<FileWithDimensions>; | |
} | |
class FileWithDimensions extends File implements FileWithDimensions { | |
private constructor(file: File, public dimensions: ImageDimensions) { | |
super([file], file.name, { | |
type: file.type, | |
lastModified: file.lastModified, | |
}); | |
} | |
static async initializeFileWithDimensions(file: File) { | |
const dimensions = await getImageDimensions(file); | |
return new FileWithDimensions(file, dimensions); | |
} | |
} | |
const getImageFromFile = (file: File) => { | |
return new Promise<HTMLImageElement>((resolve, reject) => { | |
const reader = new FileReader(); | |
reader.onload = (event) => { | |
const image = new Image(); | |
image.src = event.target.result as string; | |
image.onload = () => { | |
resolve(image); | |
}; | |
image.onerror = reject; | |
}; | |
reader.readAsDataURL(file); | |
}); | |
}; | |
const getImageDimensions = async (file: File): Promise<ImageDimensions> => { | |
const image = await getImageFromFile(file); | |
return { | |
width: image.width, | |
height: image.height, | |
}; | |
}; | |
const getFilesFromEvent: IDropZoneProps['getFilesFromEvent'] = async ( | |
event | |
) => { | |
const files = await fromEvent(event); | |
return Promise.all<DataTransferItem | FileWithDimensions>( | |
files.map((file) => { | |
if (file instanceof File) { | |
return FileWithDimensions.initializeFileWithDimensions(file); | |
} | |
if (file instanceof DataTransferItem) { | |
const dataTransferFile = file.getAsFile(); | |
if (dataTransferFile) { | |
return FileWithDimensions.initializeFileWithDimensions( | |
dataTransferFile | |
); | |
} | |
} | |
return Promise.resolve(file); | |
}) | |
); | |
}; | |
export { getFilesFromEvent, FileWithDimensions }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment