Skip to content

Commit

Permalink
feat: add transform
Browse files Browse the repository at this point in the history
  • Loading branch information
fourcels committed Jan 9, 2025
1 parent 8779dc6 commit 835798b
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 24 deletions.
55 changes: 38 additions & 17 deletions docs/docs/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,51 @@ sidebar_position: 2

## ImageUploadProps

| Name | Description | Type | Default |
| ------------------ | --------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| children | custom upload zone | React.ReactNode | |
| width | image width | `number` | 100 |
| height | image height | `number` | `width` |
| value | default images | `string` \| `ImageItem` \| `(string \| ImageItem)[]` | [] |
| onChange | callback for when the images changes | (value: `ImageItem[]`) => void | |
| onUpload | callback for when the drop event occurs | (file: `File`) => `string` \| `Promise<string>` | (file: File) => URL.createObjectURL(file) |
| max | max image number | `number` | Infinity |
| readonly | for preview | `boolean` | |
| className | root className | `string` | |
| itemClassName | image className | `string` | |
| dropzoneClassName | dropzone className | `string` | |
| dropzoneOptions | | [DropzoneOptions](https://react-dropzone.js.org/#section-components) | |
| photoProviderProps | | Omit\<[PhotoProviderProps](https://react-photo-view.vercel.app/en-US/docs/api#photoprovider), "children"\> | |

### Types
| Name | Description | Type | Default |
| ------------------ | --------------------------------------- | ------------------------------------------------------- | ---------------- |
| children | custom upload zone | React.ReactNode | |
| width | image width | `number` | 100 |
| height | image height | `number` | `width` |
| value | default images | `ValueType` | |
| onChange | callback for when the images changes | (value: `ValueType`) => void | |
| transform | transform images to value | (images: `ImageItem[]`, max: number) => `ValueType` | defaultTransform |
| onUpload | callback for when the drop event occurs | (file: `File`) => `string` \| `Promise<string>` | defaultUpload |
| max | max image number | `number` | Infinity |
| readonly | for preview | `boolean` | |
| className | root className | `string` | |
| itemClassName | image className | `string` | |
| dropzoneClassName | dropzone className | `string` | |
| dropzoneOptions | | [DropzoneOptions][dropdown-url] | |
| photoProviderProps | | Omit\<[PhotoProviderProps][photoview-url], "children"\> | |

[dropdown-url]: https://react-dropzone.js.org/#section-components
[photoview-url]: https://react-photo-view.vercel.app/en-US/docs/api#photoprovider

### Types & Defaults

```ts
type ValueItem = {
url: string;
name?: string;
};

type ValueType = string | ValueItem | (string | ValueItem)[];

type ImageItem = {
id?: string;
url?: string;
name?: string;
file?: File;
loading?: boolean;
};

const defaultUpload = (file: File) => URL.createObjectURL(file);

const defaultTransform = (images: ImageItem[], max: number): ValueType => {
const value = images.map((item) => item.url);
if (max === 1) {
return value.join(",");
}
return value;
};
```
16 changes: 15 additions & 1 deletion examples/shadcn/components/InputForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ const FormSchema = z.object({
username: z.string().min(2, {
message: "Username must be at least 2 characters.",
}),
images: z.any().array().min(1, {
images: z.string().array().min(1, {
message: "Please upload at least one image.",
}),
avatar: z.string().optional(),
});

export function InputForm() {
Expand Down Expand Up @@ -87,6 +88,19 @@ export function InputForm() {
</FormItem>
)}
/>
<FormField
control={form.control}
name="avatar"
render={({ field }) => (
<FormItem>
<FormLabel>Avatar</FormLabel>
<FormControl>
<ImageUpload max={1} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
Expand Down
6 changes: 6 additions & 0 deletions packages/react-image-upload/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @fourcels/react-image-upload

## 0.6.1

### Patch Changes

- feat: add transform

## 0.6.0

### Minor Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/react-image-upload/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fourcels/react-image-upload",
"version": "0.6.0",
"version": "0.6.1",
"type": "module",
"description": "A image upload component for React",
"main": "./dist/index.js",
Expand Down
43 changes: 38 additions & 5 deletions packages/react-image-upload/src/ImageUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import "react-photo-view/dist/react-photo-view.css";
import "./style.css";
import { PhotoProviderProps } from "react-photo-view/dist/PhotoProvider";
import { Dropzone } from "./Dropzone";
import { use } from "motion/react-client";
import { image, use } from "motion/react-client";

const RemoveIcon = () => (
<svg
Expand Down Expand Up @@ -52,6 +52,13 @@ const PreviewIcon = () => (
</svg>
);

type ValueItem = {
url: string;
name?: string;
};

type ValueType = string | ValueItem | (string | ValueItem)[];

type ImageItem = {
id?: string;
url?: string;
Expand All @@ -60,15 +67,27 @@ type ImageItem = {
loading?: boolean;
};

const checkValue = (a: ValueType, b: ValueType) => {
return a === b || JSON.stringify(a) === JSON.stringify(b);
};

const defaultUpload = (file: File) => URL.createObjectURL(file);
const defaultTransform = (images: ImageItem[], max: number): ValueType => {
const value = images.map((item) => item.url);
if (max === 1) {
return value.join(",");
}
return value;
};

export type ImageUploadProps = {
width?: number;
height?: number;
dropzoneOptions?: DropzoneOptions;
photoProviderProps?: Omit<PhotoProviderProps, "children">;
value?: string | ImageItem | (string | ImageItem)[];
onChange?: (value: ImageItem[]) => void;
value?: ValueType;
onChange?: (value: ValueType) => void;
transform?: (value: ImageItem[], max: number) => ValueType;
max?: number;
onUpload?: (file: File) => string | Promise<string>;
readonly?: boolean;
Expand All @@ -87,6 +106,7 @@ export const ImageUpload = forwardRef<HTMLElement, ImageUploadProps>(
max = Infinity,
onChange,
onUpload = defaultUpload,
transform = defaultTransform,
readonly,
dropzoneOptions,
photoProviderProps,
Expand All @@ -96,13 +116,17 @@ export const ImageUpload = forwardRef<HTMLElement, ImageUploadProps>(
children,
} = props;

const valueRef = useRef<ValueType>();
const dropzoneRef = useRef<HTMLElement>(null);

useImperativeHandle(ref, () => dropzoneRef.current);

const [images, setImages] = useState<ImageItem[]>([]);

useEffect(() => {
if (checkValue(valueRef.current, value)) {
return;
}
let newImages = [];
if (value) {
let innerValue = value;
Expand All @@ -123,9 +147,18 @@ export const ImageUpload = forwardRef<HTMLElement, ImageUploadProps>(
})
.slice(0, max);
}
valueRef.current = value;
setImages(newImages);
}, [value, max]);

const onChangeInner = useCallback(
(images: ImageItem[]) => {
valueRef.current = transform?.(images, max);
onChange?.(valueRef.current);
},
[transform, onChange, max]
);

const onDropAccepted = useCallback(
async (acceptedFiles: File[]) => {
const addImages = acceptedFiles
Expand All @@ -146,15 +179,15 @@ export const ImageUpload = forwardRef<HTMLElement, ImageUploadProps>(
return [...images];
});
})
).then(() => onChange?.(newImages));
).then(() => onChangeInner(newImages));
},
[images, max, onChange]
);

const onRemoveImage = useCallback(
(idx: number) => {
const newImages = images.filter((_, index) => index != idx);
onChange?.(newImages);
onChangeInner(newImages);
setImages(newImages);
},
[images, onChange]
Expand Down

0 comments on commit 835798b

Please sign in to comment.