Skip to content

Instantly share code, notes, and snippets.

@alettieri
Created July 6, 2023 18:01
Show Gist options
  • Save alettieri/29543a90783c2bc77b06bb933241bf04 to your computer and use it in GitHub Desktop.
Save alettieri/29543a90783c2bc77b06bb933241bf04 to your computer and use it in GitHub Desktop.
React DropZone validation logic
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 };
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 };
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;
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