はじめに
プロフィール入力フォームをReactで実装するにあたってreact-image-crop
を利用しました。
「NoImageの写真をクリックすることで登録する写真を選択できるようにする」という部分に少々詰まったので、あわせて記事にまとめることにしました。
実装
どのような実装を行ったのか、登録ステップごとに記載します。
写真選択
NoImageの三列表示についてはReact Bootstrapを使用しました。
Reactで置き換える前の画面にBootstrapを使っていたので、デザインを近づけるために今回も使用しました。
export const InputProfilePicture = () => {
const types = [1, 2, 3];
return (
<Container>
<Row>
{types.map((type) => {
return (
<Col xs={4} md={4} key={type}>
<SelectPicture type={type} />
</Col>
);
})}
</Row>
</Container>
);
};
NoImageをクリックして写真を選択できるようにするために、img(Image)
要素とinput
要素をlabel
要素でラップします。
この際、input
が表示されないように、styleにdisplay: 'none'
を設定します。
また、マウスホバーで写真がホップして表示されるように、styleにopacity
boxShadow
transform
を追加します。
ホバーの有無の状態を管理するためにconst [isHover, setIsHover] = useState(false)
を作成し、onMouseEnter()
とonMouseLeave()
内で状態を切り替えるようにします。
export const SelectPicture = ({ type }: { type: number }) => {
const [isHover, setIsHover] = useState(false);
return (
<>
<label>
<Image
src={result}
roundedCircle
style={{
maxWidth: 350,
border: 'solid 1px',
cursor: 'pointer',
opacity: isHover ? 0.8 : 1,
boxShadow: isHover ? '1px 1px 5px 1px #000' : 'none',
transform: isHover ? 'translateY(-2px)' : 'none',
}}
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
/>
<input
type="file"
accept="image/jpeg,image/png"
onChange={handleFileChange}
style={{ display: 'none' }}
/>
</label>
);
};
トリミング・登録
写真のトリミングと登録についてはreact-image-crop
というライブラリを使用しました。
写真を選択するとモーダルTrimmingModal
が出現し、ReactCrop
で写真のトリミングができるようになります。
export const SelectPicture = ({ type }: { type: number }) => {
const [show, setShow] = useState(false);
const [src, setSrc] = useState<string>('');
const [result, setResult] = useState<any>(NoImage);
const [imageInput, setImageInput] = useState<any>({ type });
const [isHover, setIsHover] = useState(false);
const [crop, setCrop, setImage] = useImageCrop(
type,
imageInput,
setImageInput,
setResult
);
const [saveCroppedImg] = useImageSave(setShow, imageInput);
const handleFileChange = (e: any) => {
setSrc(URL.createObjectURL(e.target.files[0]));
setShow(true);
};
return (
<>
<label>
<Image
src={result}
roundedCircle
style={{
maxWidth: 350,
border: 'solid 1px',
cursor: 'pointer',
opacity: isHover ? 0.8 : 1,
boxShadow: isHover ? '1px 1px 5px 1px #000' : 'none',
transform: isHover ? 'translateY(-2px)' : 'none',
}}
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
/>
<input
type="file"
accept="image/jpeg,image/png"
onChange={handleFileChange}
style={{ display: 'none' }}
/>
</label>
<TrimmingModal
show={show}
setShow={setShow}
saveCroppedImg={saveCroppedImg}
>
<ReactCrop
src={src}
onImageLoaded={setImage}
crop={crop}
onChange={setCrop}
/>
</TrimmingModal>
</>
);
};
export const TrimmingModal = ({
show,
setShow,
saveCroppedImg,
children,
}: any) => {
const handleClose = () => setShow(false);
return (
<Modal
show={show}
onHide={handleClose}
backdrop="static"
keyboard={false}
size="lg"
aria-labelledby="contained-modal-title-vcenter"
centered
>
<Modal.Header closeButton>
<Modal.Title>トリミング</Modal.Title>
</Modal.Header>
<Modal.Body className="text-center">{children}</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={handleClose}>
キャンセル
</Button>
<Button variant="primary" onClick={saveCroppedImg}>
登録
</Button>
</Modal.Footer>
</Modal>
);
};
トリミングおよび登録のロジック部分については、カスタムフックuseImageCrop
とuseImageSave
に切りわけました。
トリミングする写真のアスペクト比aspect
、幅初期値width
、高さ初期値height
についてはcrop
のuseState
の初期値で設定します。
トリミングサイズや場所が変更されるごとにresult
とimageInput
の値が更新されます。
export const useImageCrop = (
type: number,
imageInput: any,
setImageInput: Dispatch<SetStateAction<any>>,
setResult: Dispatch<SetStateAction<any>>
) => {
const [crop, setCrop] = useState<any>({
aspect: 1 / 1,
width: 300,
height: 300,
});
const [image, setImage] = useState<any>(null);
useEffect(() => {
if (image) {
const canvas = document.createElement('canvas');
const scaleX = image.naturalWidth / image.width;
const scaleY = image.naturalHeight / image.height;
canvas.width = crop.width;
canvas.height = crop.height;
const ctx: any = canvas.getContext('2d');
ctx.drawImage(
image,
crop.x * scaleX,
crop.y * scaleY,
crop.width * scaleX,
crop.height * scaleY,
0,
0,
crop.width,
crop.height
);
const base64Image = canvas.toDataURL('image/jpeg');
const base64 = base64Image.split(',')[1];
setResult(base64Image);
setImageInput({ ...imageInput, type, base64 });
}
}, [crop]);
return [crop, setCrop, setImage];
};
useImageSave
では、useImageCrop
が返したimageInput
とモーダル表示を更新するsetShow
を受け取り、写真を登録してモーダルを閉じる関数saveCroppedImg
を返すようにします。
モーダルの登録ボタンを押すとsaveCroppedImg
が発火し、登録が完了します。
export const useImageSave = (
setShow: Dispatch<SetStateAction<boolean>>,
imageInput: any
) => {
const saveCroppedImg = async () => {
try {
console.log('imageInputを登録');
} catch (err) {
console.log(err);
}
setShow(false);
};
return [saveCroppedImg];
};
参考資料