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.
Form with upload dropzone
Installation
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.
Set up upload route
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 {
generateObjectKey: ({ file }) => `form/${file.name}`,
};
},
}),
},
};
export const { POST } = createUploadRouteHandler(router);
Define the form schema
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),
});
Define the form
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);
}
}
Build the form UI
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>
<UploadDropzone control={uploadControl} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
);
}
Optional: Hide dropzone after upload
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() {
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>
);
}
Advanced: Upload on form submit
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>
);
}