Skip to content

Commit 65aef19

Browse files
purfectliteraturecysjonathan
authored andcommitted
refactor(ImageCropper): remove react-emitter-factory
1 parent f4a57b5 commit 65aef19

File tree

2 files changed

+84
-75
lines changed

2 files changed

+84
-75
lines changed
Lines changed: 79 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
import { ReactEventHandler, useRef, useState } from 'react';
2-
import useEmitterFactory, { Emits } from 'react-emitter-factory';
1+
import {
2+
forwardRef,
3+
ReactEventHandler,
4+
useImperativeHandle,
5+
useRef,
6+
useState,
7+
} from 'react';
38
import ReactCrop, { PercentCrop, PixelCrop } from 'react-image-crop';
49
import { RotateRight } from '@mui/icons-material';
510
import { Slider } from '@mui/material';
@@ -15,7 +20,7 @@ const DEFAULT_CROP: PercentCrop = {
1520
height: 50,
1621
};
1722

18-
export interface ImageCropperEmitter {
23+
export interface ImageCropperRef {
1924
/**
2025
* Resets the cropper to allow initial crop to be generated on new `src` load.
2126
*/
@@ -27,7 +32,7 @@ export interface ImageCropperEmitter {
2732
getImage?: () => Promise<Blob | undefined>;
2833
}
2934

30-
interface ImageCropperProps extends Emits<ImageCropperEmitter> {
35+
interface ImageCropperProps {
3136
src?: string;
3237
alt?: string;
3338
circular?: boolean;
@@ -37,78 +42,82 @@ interface ImageCropperProps extends Emits<ImageCropperEmitter> {
3742
type?: string;
3843
}
3944

40-
const ImageCropper = (props: ImageCropperProps): JSX.Element => {
41-
const [crop, setCrop] = useState<PercentCrop>();
42-
const [completedCrop, setCompletedCrop] = useState<PixelCrop>();
43-
const [rotation, setRotation] = useState(0);
44-
const imgRef = useRef<HTMLImageElement>(null);
45+
const ImageCropper = forwardRef<ImageCropperRef, ImageCropperProps>(
46+
(props, ref) => {
47+
const [crop, setCrop] = useState<PercentCrop>();
48+
const [completedCrop, setCompletedCrop] = useState<PixelCrop>();
49+
const [rotation, setRotation] = useState(0);
50+
const imgRef = useRef<HTMLImageElement>(null);
4551

46-
const generateImage = async (): Promise<Blob | undefined> => {
47-
if (!imgRef.current || !completedCrop) return undefined;
48-
return getImage(
49-
imgRef.current!,
50-
completedCrop,
51-
rotation,
52-
props.type ?? 'image/jpeg',
53-
);
54-
};
52+
const generateImage = async (): Promise<Blob | undefined> => {
53+
if (!imgRef.current || !completedCrop) return undefined;
54+
return getImage(
55+
imgRef.current!,
56+
completedCrop,
57+
rotation,
58+
props.type ?? 'image/jpeg',
59+
);
60+
};
5561

56-
const handleImageLoad: ReactEventHandler<HTMLImageElement> = (e) => {
57-
if (props.aspect) {
58-
const { width, height } = e.currentTarget;
59-
setCrop(centerAspectCrop(width, height, props.aspect));
60-
} else {
61-
setCrop(DEFAULT_CROP);
62-
}
63-
};
62+
const handleImageLoad: ReactEventHandler<HTMLImageElement> = (e) => {
63+
if (props.aspect) {
64+
const { width, height } = e.currentTarget;
65+
setCrop(centerAspectCrop(width, height, props.aspect));
66+
} else {
67+
setCrop(DEFAULT_CROP);
68+
}
69+
};
6470

65-
// Must set completedCrop and rotation as dependencies because generateImage
66-
// captures them as the state changes, except imgRef which persists throughout.
67-
useEmitterFactory(
68-
props,
69-
{
70-
resetImage: () => setCrop(undefined),
71-
getImage: () => generateImage(),
72-
},
73-
[completedCrop, rotation],
74-
);
71+
// Must set completedCrop and rotation as dependencies because generateImage
72+
// captures them as the state changes, except imgRef which persists throughout.
73+
useImperativeHandle(
74+
ref,
75+
() => ({
76+
resetImage: (): void => setCrop(undefined),
77+
getImage: (): Promise<Blob | undefined> => generateImage(),
78+
}),
79+
[completedCrop, rotation],
80+
);
7581

76-
return (
77-
<div>
78-
<ReactCrop
79-
aspect={props.aspect}
80-
circularCrop={props.circular}
81-
crop={crop}
82-
keepSelection
83-
onChange={(_, percentCrop): void => setCrop(percentCrop)}
84-
onComplete={setCompletedCrop}
85-
ruleOfThirds={props.grids ?? true}
86-
>
87-
<img
88-
ref={imgRef}
89-
alt={props.alt}
90-
className="pointer-events-none select-none"
91-
onError={props.onLoadError}
92-
onLoad={handleImageLoad}
93-
src={props.src}
94-
style={{ transform: `rotate(${rotation}deg)` }}
95-
/>
96-
</ReactCrop>
82+
return (
83+
<div>
84+
<ReactCrop
85+
aspect={props.aspect}
86+
circularCrop={props.circular}
87+
crop={crop}
88+
keepSelection
89+
onChange={(_, percentCrop): void => setCrop(percentCrop)}
90+
onComplete={setCompletedCrop}
91+
ruleOfThirds={props.grids ?? true}
92+
>
93+
<img
94+
ref={imgRef}
95+
alt={props.alt}
96+
className="pointer-events-none select-none"
97+
onError={props.onLoadError}
98+
onLoad={handleImageLoad}
99+
src={props.src}
100+
style={{ transform: `rotate(${rotation}deg)` }}
101+
/>
102+
</ReactCrop>
97103

98-
<div className="flex items-center">
99-
<RotateRight className="mr-8" />
104+
<div className="flex items-center">
105+
<RotateRight className="mr-8" />
100106

101-
<Slider
102-
max={180}
103-
min={0}
104-
onChange={(_, angle): void => setRotation(angle as number)}
105-
value={rotation}
106-
valueLabelDisplay="auto"
107-
valueLabelFormat={(degree): string => `${degree}\u00B0`}
108-
/>
107+
<Slider
108+
max={180}
109+
min={0}
110+
onChange={(_, angle): void => setRotation(angle as number)}
111+
value={rotation}
112+
valueLabelDisplay="auto"
113+
valueLabelFormat={(degree): string => `${degree}\u00B0`}
114+
/>
115+
</div>
109116
</div>
110-
</div>
111-
);
112-
};
117+
);
118+
},
119+
);
120+
121+
ImageCropper.displayName = 'ImageCropper';
113122

114123
export default ImageCropper;

client/app/lib/components/core/dialogs/ImageCropDialog.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { ComponentProps, useState } from 'react';
1+
import { ComponentProps, useRef, useState } from 'react';
22

33
import Prompt from 'lib/components/core/dialogs/Prompt';
44
import ImageCropper, {
5-
ImageCropperEmitter,
5+
ImageCropperRef,
66
} from 'lib/components/core/ImageCropper';
77
import useTranslation from 'lib/hooks/useTranslation';
88
import formTranslations from 'lib/translations/form';
@@ -23,15 +23,15 @@ interface ImageCropDialogProps {
2323
const ImageCropDialog = (props: ImageCropDialogProps): JSX.Element => {
2424
const { t } = useTranslation();
2525
const [disabled, setDisabled] = useState(false);
26-
const [imageCropper, setImageCropper] = useState<ImageCropperEmitter>();
26+
const imageCropperRef = useRef<ImageCropperRef>(null);
2727

2828
const handleLoadError = (): void => {
2929
props.onLoadError?.();
3030
props.onClose?.();
3131
};
3232

3333
const handleConfirmImage = async (): Promise<void> => {
34-
const image = await imageCropper?.getImage?.();
34+
const image = await imageCropperRef.current?.getImage?.();
3535
if (!image) throw new Error(`ImageCropper returned: ${image} image data.`);
3636

3737
props.onConfirmImage?.(image);
@@ -51,10 +51,10 @@ const ImageCropDialog = (props: ImageCropDialogProps): JSX.Element => {
5151
title={props.title}
5252
>
5353
<ImageCropper
54+
ref={imageCropperRef}
5455
alt={props.alt}
5556
aspect={props.aspect}
5657
circular={props.circular}
57-
emitsVia={setImageCropper}
5858
onLoadError={handleLoadError}
5959
src={props.src}
6060
type={props.type}

0 commit comments

Comments
 (0)