ui
ui copied to clipboard
Feature: Add image preview in file input
It would be nice to add a parameter in the Input
component, or create a component to have a preview of an image that we upload.
I am also looking for a great solution to preview image(s) before an upload. bump.
I was able to create image display using below approach
"use client";
import { useForm } from "react-hook-form";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { ChangeEvent, useState } from "react";
import { Button } from "@/components/ui/button";
import { zodResolver } from "@hookform/resolvers/zod";
import {
RegisterCircleInputClient,
registerCircleSchemaClient,
} from "@/schema/circle.schema";
function getImageData(event: ChangeEvent<HTMLInputElement>) {
// FileList is immutable, so we need to create a new one
const dataTransfer = new DataTransfer();
// Add newly uploaded images
Array.from(event.target.files!).forEach((image) =>
dataTransfer.items.add(image)
);
const files = dataTransfer.files;
const displayUrl = URL.createObjectURL(event.target.files![0]);
return { files, displayUrl };
}
export function RegisterForm() {
const [preview, setPreview] = useState("");
const form = useForm<RegisterCircleInputClient>({
mode: "onSubmit",
resolver: zodResolver(registerCircleSchemaClient),
});
function submitCircleRegistration(value: RegisterCircleInputClient) {
console.log({ value });
}
return (
<>
<Form {...form}>
<form
className="space-y-8"
onSubmit={form.handleSubmit(submitCircleRegistration)}
>
<Avatar className="w-24 h-24">
<AvatarImage src={preview} />
<AvatarFallback>BU</AvatarFallback>
</Avatar>
<FormField
control={form.control}
name="circle_image"
render={({ field: { onChange, value, ...rest } }) => (
<>
<FormItem>
<FormLabel>Circle Image</FormLabel>
<FormControl>
<Input
type="file"
{...rest}
onChange={(event) => {
const { files, displayUrl} = getImageData(event)
setPreview(displayUrl);
onChange(files);
}}
/>
</FormControl>
<FormDescription>
Choose best image that bring spirits to your circle.
</FormDescription>
<FormMessage />
</FormItem>
</>
)}
/>
<Button type="submit">Register</Button>
</form>
</Form>
</>
);
}
got the inspiration from https://github.com/shadcn-ui/ui/issues/884#issuecomment-1695758803
@muhsalaa Thanks for the solution! Just looking for it
I was able to create image display using below approach
"use client"; import { useForm } from "react-hook-form"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { ChangeEvent, useState } from "react"; import { Button } from "@/components/ui/button"; import { zodResolver } from "@hookform/resolvers/zod"; import { RegisterCircleInputClient, registerCircleSchemaClient, } from "@/schema/circle.schema"; function getImageData(event: ChangeEvent<HTMLInputElement>) { // FileList is immutable, so we need to create a new one const dataTransfer = new DataTransfer(); // Add newly uploaded images Array.from(event.target.files!).forEach((image) => dataTransfer.items.add(image) ); const files = dataTransfer.files; const displayUrl = URL.createObjectURL(event.target.files![0]); return { files, displayUrl }; } export function RegisterForm() { const [preview, setPreview] = useState(""); const form = useForm<RegisterCircleInputClient>({ mode: "onSubmit", resolver: zodResolver(registerCircleSchemaClient), }); function submitCircleRegistration(value: RegisterCircleInputClient) { console.log({ value }); } return ( <> <Form {...form}> <form className="space-y-8" onSubmit={form.handleSubmit(submitCircleRegistration)} > <Avatar className="w-24 h-24"> <AvatarImage src={preview} /> <AvatarFallback>BU</AvatarFallback> </Avatar> <FormField control={form.control} name="circle_image" render={({ field: { onChange, value, ...rest } }) => ( <> <FormItem> <FormLabel>Circle Image</FormLabel> <FormControl> <Input type="file" {...rest} onChange={(event) => { const { files, displayUrl} = getImageData(event) setPreview(displayUrl); onChange(files); }} /> </FormControl> <FormDescription> Choose best image that bring spirits to your circle. </FormDescription> <FormMessage /> </FormItem> </> )} /> <Button type="submit">Register</Button> </form> </Form> </> ); }
got the inspiration from https://github.com/shadcn-ui/ui/issues/884#issuecomment-1695758803
Thank you
up
I was able to create image display using below approach
"use client"; import { useForm } from "react-hook-form"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { ChangeEvent, useState } from "react"; import { Button } from "@/components/ui/button"; import { zodResolver } from "@hookform/resolvers/zod"; import { RegisterCircleInputClient, registerCircleSchemaClient, } from "@/schema/circle.schema"; function getImageData(event: ChangeEvent<HTMLInputElement>) { // FileList is immutable, so we need to create a new one const dataTransfer = new DataTransfer(); // Add newly uploaded images Array.from(event.target.files!).forEach((image) => dataTransfer.items.add(image) ); const files = dataTransfer.files; const displayUrl = URL.createObjectURL(event.target.files![0]); return { files, displayUrl }; } export function RegisterForm() { const [preview, setPreview] = useState(""); const form = useForm<RegisterCircleInputClient>({ mode: "onSubmit", resolver: zodResolver(registerCircleSchemaClient), }); function submitCircleRegistration(value: RegisterCircleInputClient) { console.log({ value }); } return ( <> <Form {...form}> <form className="space-y-8" onSubmit={form.handleSubmit(submitCircleRegistration)} > <Avatar className="w-24 h-24"> <AvatarImage src={preview} /> <AvatarFallback>BU</AvatarFallback> </Avatar> <FormField control={form.control} name="circle_image" render={({ field: { onChange, value, ...rest } }) => ( <> <FormItem> <FormLabel>Circle Image</FormLabel> <FormControl> <Input type="file" {...rest} onChange={(event) => { const { files, displayUrl} = getImageData(event) setPreview(displayUrl); onChange(files); }} /> </FormControl> <FormDescription> Choose best image that bring spirits to your circle. </FormDescription> <FormMessage /> </FormItem> </> )} /> <Button type="submit">Register</Button> </form> </Form> </> ); }
got the inspiration from #884 (comment)
Can you share this part as well?
RegisterCircleInputClient,
registerCircleSchemaClient,
} from '@/schema/circle.schema';
to anyone who wants that avatar upload process where theres just a button to upload, here's how i did it, can basically work with the form
"use client";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { toBase64 } from "@/lib/utils";
import { PencilIcon, User2Icon } from "lucide-react";
import React from "react";
type AvatarUploadProps = {
value?: string;
onChange?: (value?: string) => void;
}
export function AvatarUpload({
value,
onChange
}: AvatarUploadProps) {
const inputRef = React.useRef<HTMLInputElement>(null)
const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
const file = e.target.files[0];
const base64 = await toBase64(file) as string;
onChange?.(base64);
}
}
return (
<div className="relative w-40 h-40">
<Avatar className="w-full h-full">
<AvatarImage src={value} className="object-cover"/>
<AvatarFallback className="bg-secondary">
<User2Icon className="w-16 h-16"/>
</AvatarFallback>
</Avatar>
<Button
variant="ghost"
size="icon"
className="rounded-full p-1 bg-secondary-foreground/90 hover:bg-secondary-foreground absolute bottom-0 right-0"
onClick={e => {
e.preventDefault()
inputRef.current?.click()
}}
>
<PencilIcon className="w-4 h-4 text-black"/>
</Button>
<Input
ref={inputRef}
type="file"
className="hidden"
onChange={handleChange}
accept="image/*"
/>
</div>
)
}
Here is my utility function to convert to base64
export function toBase64(file: File) {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.onload = () => {
resolve(fileReader.result);
};
fileReader.onerror = (error) => {
reject(error);
};
});
}
Then just use it like this
<FormField
control={form.control}
name="image"
render={({field}) => (
<FormItem>
<FormControl>
<AvatarUpload value={field.value} onChange={field.onChange}/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
I was able to create image display using below approach
"use client"; import { useForm } from "react-hook-form"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { ChangeEvent, useState } from "react"; import { Button } from "@/components/ui/button"; import { zodResolver } from "@hookform/resolvers/zod"; import { RegisterCircleInputClient, registerCircleSchemaClient, } from "@/schema/circle.schema"; function getImageData(event: ChangeEvent<HTMLInputElement>) { // FileList is immutable, so we need to create a new one const dataTransfer = new DataTransfer(); // Add newly uploaded images Array.from(event.target.files!).forEach((image) => dataTransfer.items.add(image) ); const files = dataTransfer.files; const displayUrl = URL.createObjectURL(event.target.files![0]); return { files, displayUrl }; } export function RegisterForm() { const [preview, setPreview] = useState(""); const form = useForm<RegisterCircleInputClient>({ mode: "onSubmit", resolver: zodResolver(registerCircleSchemaClient), }); function submitCircleRegistration(value: RegisterCircleInputClient) { console.log({ value }); } return ( <> <Form {...form}> <form className="space-y-8" onSubmit={form.handleSubmit(submitCircleRegistration)} > <Avatar className="w-24 h-24"> <AvatarImage src={preview} /> <AvatarFallback>BU</AvatarFallback> </Avatar> <FormField control={form.control} name="circle_image" render={({ field: { onChange, value, ...rest } }) => ( <> <FormItem> <FormLabel>Circle Image</FormLabel> <FormControl> <Input type="file" {...rest} onChange={(event) => { const { files, displayUrl} = getImageData(event) setPreview(displayUrl); onChange(files); }} /> </FormControl> <FormDescription> Choose best image that bring spirits to your circle. </FormDescription> <FormMessage /> </FormItem> </> )} /> <Button type="submit">Register</Button> </form> </Form> </> ); }
got the inspiration from #884 (comment)
Can you share this part as well?
RegisterCircleInputClient, registerCircleSchemaClient, } from '@/schema/circle.schema';
The RegisterCircleInputClient
type may look like below:
type RegisterCircleInputClient = { circle_image: string; // ...other definations }
I was able to create image display using below approach
"use client"; import { useForm } from "react-hook-form"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { ChangeEvent, useState } from "react"; import { Button } from "@/components/ui/button"; import { zodResolver } from "@hookform/resolvers/zod"; import { RegisterCircleInputClient, registerCircleSchemaClient, } from "@/schema/circle.schema"; function getImageData(event: ChangeEvent<HTMLInputElement>) { // FileList is immutable, so we need to create a new one const dataTransfer = new DataTransfer(); // Add newly uploaded images Array.from(event.target.files!).forEach((image) => dataTransfer.items.add(image) ); const files = dataTransfer.files; const displayUrl = URL.createObjectURL(event.target.files![0]); return { files, displayUrl }; } export function RegisterForm() { const [preview, setPreview] = useState(""); const form = useForm<RegisterCircleInputClient>({ mode: "onSubmit", resolver: zodResolver(registerCircleSchemaClient), }); function submitCircleRegistration(value: RegisterCircleInputClient) { console.log({ value }); } return ( <> <Form {...form}> <form className="space-y-8" onSubmit={form.handleSubmit(submitCircleRegistration)} > <Avatar className="w-24 h-24"> <AvatarImage src={preview} /> <AvatarFallback>BU</AvatarFallback> </Avatar> <FormField control={form.control} name="circle_image" render={({ field: { onChange, value, ...rest } }) => ( <> <FormItem> <FormLabel>Circle Image</FormLabel> <FormControl> <Input type="file" {...rest} onChange={(event) => { const { files, displayUrl} = getImageData(event) setPreview(displayUrl); onChange(files); }} /> </FormControl> <FormDescription> Choose best image that bring spirits to your circle. </FormDescription> <FormMessage /> </FormItem> </> )} /> <Button type="submit">Register</Button> </form> </Form> </> ); }
got the inspiration from #884 (comment)
Can you share this part as well?
RegisterCircleInputClient, registerCircleSchemaClient, } from '@/schema/circle.schema';
The
RegisterCircleInputClient
type may look like below:
type RegisterCircleInputClient = { circle_image: string; // ...other definations }
And the schema:
const registerCircleSchemaClient = z.object({ circle_image: z .any() .refine((file) => file?.length == 1, 'Photo is required.') .refine((file) => file[0]?.size <= 3000000,
Max file size is 3MB.) });
is there any easy way to do this in .js ? Or a setting that we can switch on the component
I was able to create image display using below approach
"use client"; import { useForm } from "react-hook-form"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { ChangeEvent, useState } from "react"; import { Button } from "@/components/ui/button"; import { zodResolver } from "@hookform/resolvers/zod"; import { RegisterCircleInputClient, registerCircleSchemaClient, } from "@/schema/circle.schema"; function getImageData(event: ChangeEvent<HTMLInputElement>) { // FileList is immutable, so we need to create a new one const dataTransfer = new DataTransfer(); // Add newly uploaded images Array.from(event.target.files!).forEach((image) => dataTransfer.items.add(image) ); const files = dataTransfer.files; const displayUrl = URL.createObjectURL(event.target.files![0]); return { files, displayUrl }; } export function RegisterForm() { const [preview, setPreview] = useState(""); const form = useForm<RegisterCircleInputClient>({ mode: "onSubmit", resolver: zodResolver(registerCircleSchemaClient), }); function submitCircleRegistration(value: RegisterCircleInputClient) { console.log({ value }); } return ( <> <Form {...form}> <form className="space-y-8" onSubmit={form.handleSubmit(submitCircleRegistration)} > <Avatar className="w-24 h-24"> <AvatarImage src={preview} /> <AvatarFallback>BU</AvatarFallback> </Avatar> <FormField control={form.control} name="circle_image" render={({ field: { onChange, value, ...rest } }) => ( <> <FormItem> <FormLabel>Circle Image</FormLabel> <FormControl> <Input type="file" {...rest} onChange={(event) => { const { files, displayUrl} = getImageData(event) setPreview(displayUrl); onChange(files); }} /> </FormControl> <FormDescription> Choose best image that bring spirits to your circle. </FormDescription> <FormMessage /> </FormItem> </> )} /> <Button type="submit">Register</Button> </form> </Form> </> ); }
got the inspiration from #884 (comment)
Thank you so much.
This issue has been automatically closed because it received no activity for a while. If you think it was closed by accident, please leave a comment. Thank you.