āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā š shadcn/directory/nic13gamer/better-upload/guides/form ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā
It's common for file uploads to be part of a form. In this guide, we'll take a look at how to add file uploads to a form built using shadcn/ui. This guide will assume you have a basic understanding of:
This guide is for uploading multiple files, but the same principles apply for single file uploads.
To follow along with this guide, install the following shadcn/ui components:
npx shadcn@latest add form input button
This will also automatically install all required dependencies.
</Step> <Step>Set up your upload route. Use your preferred framework, but for this example, we'll use Next.js.
import { S3Client } from '@aws-sdk/client-s3';
import {
createUploadRouteHandler,
route,
type Router,
} from 'better-upload/server';
const s3 = new S3Client();
const router: Router = {
client: s3,
bucketName: 'my-bucket',
routes: {
form: route({
multipleFiles: true,
maxFiles: 5,
onBeforeUpload() {
return {
generateObjectInfo: ({ file }) => ({ key: `form/${file.name}` }),
};
},
}),
},
};
export const { POST } = createUploadRouteHandler(router);
</Step>
<Step>
We'll now create the form. The form uses the Upload Dropzone component to allow users to upload files.
Define the form schema using zod. The schema contains two fields:
folderName: For an arbitrary text input.objectKeys: For the uploaded files, stores the S3 object keys.'use client'; // For Next.js
import { z } from 'zod';
const formSchema = z.object({
folderName: z.string().min(1),
objectKeys: z.array(z.string()).min(1),
});
</Step>
<Step>
Use the useForm hook from react-hook-form to create the form.
Also use the useUploadFiles hook to handle the uploads.
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useUploadFiles } from 'better-upload/client';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const formSchema = z.object({
folderName: z.string().min(1),
objectKeys: z.array(z.string()).min(1),
});
export function MyForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
folderName: '',
objectKeys: [],
},
});
const { control: uploadControl } = useUploadFiles({
route: 'form',
onUploadComplete: ({ files }) => {
form.setValue(
'objectKeys',
files.map((file) => file.objectKey)
);
},
onError: (error) => {
form.setError('objectKeys', {
message: error.message || 'An error occurred',
});
},
});
function onSubmit(data: z.infer<typeof formSchema>) {
// call your API here
console.log(data);
}
}
</Step>
<Step>
We can now use the <Form /> component from shadcn/ui to build our form UI.
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useUploadFiles } from 'better-upload/client';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { UploadDropzone } from '@/components/ui/upload-dropzone';
const formSchema = z.object({
folderName: z.string().min(1),
objectKeys: z.array(z.string()).min(1),
});
export function MyForm() {
// ...
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="folderName"
render={({ field }) => (
<FormItem>
<FormLabel>Folder name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="objectKeys"
render={({ field }) => (
<FormItem>
<FormLabel>Files</FormLabel>
<FormControl>
{/* [!code highlight] */}
<UploadDropzone control={uploadControl} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
);
}
</Step>
<Step>
Now let's hide the dropzone after the user has uploaded files. We can do this by using the uploadedFiles array returned by the useUploadFiles hook.
'use client';
export function MyForm() {
// [!code highlight]
const { control: uploadControl, uploadedFiles } = useUploadFiles({
route: 'form',
onUploadComplete: ({ files }) => {
form.setValue(
'objectKeys',
files.map((file) => file.objectKey)
);
},
onError: (error) => {
form.setError('objectKeys', {
message: error.message || 'An error occurred',
});
},
});
// ...
return (
<Form>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
{/* ... */}
{uploadedFiles.length > 0 ? (
<div className="flex flex-col">
{uploadedFiles.map((file) => (
<p key={file.objectKey}>{file.name}</p>
))}
</div>
) : (
<FormField
control={form.control}
name="objectKeys"
render={({ field }) => (
<FormItem>
<FormLabel>Files</FormLabel>
<FormControl>
<UploadDropzone control={uploadControl} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<Button type="submit">Submit</Button>
</form>
</Form>
);
}
</Step>
</Steps>
In this example, we only upload the files after the user clicks on the submit button. We use the uploadOverride prop to override the default behavior of the <UploadDropzone />.
The full code example for the form is below.
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useUploadFiles } from 'better-upload/client';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { UploadDropzone } from './ui/upload-dropzone';
const formSchema = z.object({
folderName: z.string().min(1),
files: z.array(z.instanceof(File)).min(1), // for Zod v4: z.array(z.file()).min(1),
});
export function FormUploader() {
const {
upload,
control: uploadControl,
isPending: isUploading,
} = useUploadFiles({
route: 'form',
onError: (error) => {
form.setError('files', {
message: error.message || 'An error occurred',
});
},
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
folderName: '',
files: [],
},
});
async function onSubmit(data: z.infer<typeof formSchema>) {
const { files } = await upload(data.files);
// call your API here
console.log({
folderName: data.folderName,
objectKeys: files.map((file) => file.objectKey),
});
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="folderName"
render={({ field }) => (
<FormItem>
<FormLabel>Folder name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{form.watch('files').length > 0 ? (
<div className="flex flex-col">
{form.watch('files').map((file, i) => (
<p key={i}>{file.name}</p>
))}
</div>
) : (
<FormField
control={form.control}
name="files"
render={() => (
<FormItem>
<FormLabel>Files</FormLabel>
<FormControl>
<UploadDropzone
control={uploadControl}
uploadOverride={(files) => {
form.setValue('files', Array.from(files));
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<Button type="submit" disabled={isUploading}>
Submit
</Button>
</form>
</Form>
);
}
ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā