完成品
- React Hook Form ^7.47.0
- Yup ^1.3.2
通常時
マウスオーバー時
ファイルアップロード後
ファイルアップロードコンポーネント
import { ChangeEvent, DragEvent, useState } from 'react';
import { Button, IconButton, Paper, Typography } from '@mui/material';
import { styled } from '@mui/material/styles';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import { bytesToSize } from '@/utils/fileSize';
import CancelIcon from '@mui/icons-material/Cancel';
import CheckCicleIcon from '@mui/icons-material/CheckCircle';
import { Control, useController } from 'react-hook-form';
const VisuallyHiddenInput = styled('input')({
clip: 'rect(0 0 0 0)',
clipPath: 'inset(50%)',
height: 1,
overflow: 'hidden',
position: 'absolute',
bottom: 0,
left: 0,
whiteSpace: 'nowrap',
width: 1,
});
type props = {
files: File[]
setFiles: Function
name: string
control: Control
}
export default function FileUpload({ files, setFiles, name, control }: props) {
const {
field: { ref, onChange, ...rest },
fieldState: { invalid, error, }
} = useController({ name, control })
const [isOver, setIsOver] = useState(false);
// Define the event handlers
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsOver(true);
};
const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsOver(false);
};
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsOver(false);
// Fetch the files
const droppedFiles = Array.from(event.dataTransfer.files);
if(droppedFiles.length > 1) {
console.log('ファイルは1つだけ選択してください。')
return
}
readFiles(droppedFiles)
};
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const selectedFiles = Array.from(event.target.files)
readFiles(selectedFiles)
}
const readFiles = async (files) => {
console.log(files)
setFiles(files)
await files.forEach((file) => {
const reader = new FileReader()
reader.onloadend = () => {
console.log(reader.result);
};
reader.onerror = () => {
console.error('There was an issue reading the file.');
};
reader.readAsDataURL(file);
return reader;
})
}
return (
<>
<Paper
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
sx={{
padding: 2,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
minWidth: 300,
backgroundColor: isOver ? (theme) => theme.palette.grey[300] : undefined,
transition: '.5s all'
}}
>
<Typography fontSize={54} color="grey" lineHeight={1}>
{ error || files.length === 0
? <CloudUploadIcon
fontSize="inherit"
sx={{
transition: '.5s all',
transform: isOver && 'translateY(-10%)'
}}
/>
: <CheckCicleIcon fontSize="inherit" color="success" />
}
</Typography>
{files.length === 0
? <Button component="label" variant="contained">
Upload file
<VisuallyHiddenInput type="file" onChange={handleChange} name={name} />
</Button>
: <>
{files.map(file => (<Typography key={file.name} fontSize="small">{file.name} / {bytesToSize(file.size)}</Typography>))}
<IconButton color="error" onClick={() => setFiles([])}><CancelIcon /></IconButton>
</>
}
</Paper>
{ error && <Typography fontSize="small" color="error">{error.message}</Typography>}
</>
);
}
- VisuallyHiddenInput設置
- useControllerを使用して、onChangeやerrorを参照できるように
- isOverで、ドラッグ&ドロップのイベントを検知
- onDropとonChangeイベントでファイル読み込みを走らせる
コンポーネントの呼び出し
const {
handleSubmit,
control,
setValue,
} = useForm<FormValues>({
resolver: yupResolver(schema),
mode: 'onBlur',
});
const [ files, setFiles ] = useState<File[]>([]);
const handleSetFiles = async (fileList: File[]) => {
setFiles(fileList)
setValue('files', fileList, { shouldValidate: true })
}
<FileUpload
files={files}
setFiles={handleSetFiles}
name="files"
control={control}
/>
注意すべきポイントは、自作関数handleSetFiles。input[type=hidden]は、reactが変更を検知しないため、react hook formsのsetValueを使って更新する。
バリデーション
今回はYupを使用。
const schema = yup.object({
files: .array()
.length(1, 'ファイルを選択してください。')
.test(
'is-pdf',
'CSVファイルを選択してください。',
(value: [File]) => {
return value[0] && value[0].type === 'text/csv'
}
)
.test(
'is-size',
`ファイルサイズは、${bytesToSize(maxSize)}以下にしてください。`,,
(value: [File]) => value[0] && value[0].size <= maxSize
),
});
ファイルサイズ関数
const units = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
type unit = (typeof units)[number]
export const bytesToSize = (bytes: number): string => {
if (bytes === 0) return 'n/a';
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
if (i === 0) return `${bytes} ${units[i]}`;
return `${(bytes / (1024 ** i)).toFixed(1)} ${units[i]}`;
}
export const SizeToBytes = (size: number, unit: unit): number => {
return size * 1024 ** units.indexOf(unit)
}