Modals
Learn how to use modals in the starter kit.
The starter kit uses NiceModal as a modal manager. This includes dialogs, sheets, drawers and so on.
Why NiceModal? Simply because we don't need to have the dialog in the tree and handle open/close state anymore.
1. Create modal component
Here, we will create the AddItemModal component using NiceModal to manage modal visibility, react-hook-form for form handling, and Shadcn UI for the UI elements.
'use client';
import NiceModal from '@ebay/nice-modal-react';
import { Button } from '@workspace/ui/components/button';
import { FormProvider } from '@workspace/ui/components/form';
import { cn } from '@workspace/ui/lib/utils';
import { addItem } from '~/actions/items/add-item';
import { useEnhancedModal } from '~/hooks/use-enhanced-modal';
import { useZodForm } from '~/hooks/use-zod-form';
import {
addItemSchema,
type AddItemSchema
} from '~/schemas/items/add-item-schema';
import { type SubmitHandler } from 'react-hook-form';
export const AddItemModal = NiceModal.create(() => {
const modal = useEnhancedModal();
const methods = useZodForm({
schema: addItemSchema,
mode: 'onSubmit',
defaultValues: {
name: ''
}
});
const title = 'Add Item';
const description = 'Create a new item by filling out the form below.';
const canSubmit =
!methods.formState.isSubmitting &&
(!methods.formState.isSubmitted || methods.formState.isDirty);
const onSubmit: SubmitHandler<AddItemSchema> = async (values) => {
// handle submission
};
return (
<FormProvider {...methods}>
<Dialog open={modal.visible}>
<DialogContent
className="max-w-sm"
onClose={modal.handleClose}
onAnimationEndCapture={modal.handleAnimationEndCapture}
>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription className="sr-only">
{description}
</DialogDescription>
</DialogHeader>
<form
className="space-y-4"
onSubmit={methods.handleSubmit(onSubmit)}
>
{/* Implementation */}
</form>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={modal.handleClose}
>
Cancel
</Button>
<Button
type="button"
variant="default"
disabled={!canSubmit}
loading={methods.formState.isSubmitting}
onClick={methods.handleSubmit(onSubmit)}
>
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</FormProvider>
);
});2. Make the modal responsive
A common tactic is to render a mobile drawer (also sometimes called a bottom sheet) instead of a dialog for modals.
Putting it together:
'use client';
import NiceModal, { type NiceModalHocProps } from '@ebay/nice-modal-react';
import { ItemRecord } from '@workspace/database';
import { Button } from '@workspace/ui/components/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@workspace/ui/components/dialog';
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle
} from '@workspace/ui/components/drawer';
import { FormProvider } from '@workspace/ui/components/form';
import { Input } from '@workspace/ui/components/input';
import { useMediaQuery } from '@workspace/ui/hooks/use-media-query';
import { MediaQueries } from '@workspace/ui/lib/media-queries';
import { cn } from '@workspace/ui/lib/utils';
import { addItem } from '~/actions/items/add-item';
import { useEnhancedModal } from '~/hooks/use-enhanced-modal';
import { useZodForm } from '~/hooks/use-zod-form';
import { itemRecordLabel } from '~/lib/labels';
import {
addItemSchema,
type AddItemSchema
} from '~/schemas/items/add-item-schema';
import { BuildingIcon, UserIcon } from 'lucide-react';
import { type SubmitHandler } from 'react-hook-form';
export const AddItemModal = NiceModal.create(() => {
const modal = useEnhancedModal();
const mdUp = useMediaQuery(MediaQueries.MdUp, { ssr: false });
const methods = useZodForm({
schema: addItemSchema,
mode: 'onSubmit',
defaultValues: {
record: ItemRecord.PERSON,
name: '',
description: '',
price: ''
}
});
const title = 'Add Item';
const description = 'Create a new item by filling out the form below.';
const canSubmit =
!methods.formState.isSubmitting &&
(!methods.formState.isSubmitted || methods.formState.isDirty);
const onSubmit: SubmitHandler<AddItemSchema> = async (values) => {
// handle submission
};
const renderForm = (
<form
className={cn('space-y-4', !mdUp && 'p-4')}
onSubmit={methods.handleSubmit(onSubmit)}
>
{/* Implementation */}
</form>
);
const renderButtons = (
<>
<Button
type="button"
variant="outline"
onClick={modal.handleClose}
>
Cancel
</Button>
<Button
type="button"
variant="default"
disabled={!canSubmit}
loading={methods.formState.isSubmitting}
onClick={methods.handleSubmit(onSubmit)}
>
Save
</Button>
</>
);
return (
<FormProvider {...methods}>
{mdUp ? (
<Dialog open={modal.visible}>
<DialogContent
className="max-w-sm"
onClose={modal.handleClose}
onAnimationEndCapture={modal.handleAnimationEndCapture}
>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription className="sr-only">
{description}
</DialogDescription>
</DialogHeader>
{renderForm}
<DialogFooter>{renderButtons}</DialogFooter>
</DialogContent>
</Dialog>
) : (
<Drawer
open={modal.visible}
onOpenChange={modal.handleOpenChange}
>
<DrawerContent>
<DrawerHeader className="text-left">
<DrawerTitle>{title}</DrawerTitle>
<DrawerDescription className="sr-only">
{description}
</DrawerDescription>
</DrawerHeader>
{renderForm}
<DrawerFooter className="flex-col-reverse pt-4">
{renderButtons}
</DrawerFooter>
</DrawerContent>
</Drawer>
)}
</FormProvider>
);
});3. Calling the modal from client component
This is quite easy, we just can pass the modal component as argument:
const handleShowAddItemModal = (): void => {
NiceModal.show(AddItemModal);
};4. Adding modal props
The NiceModal.create function takes a generic so we can define expected props.
export type AddItemModalProps = NiceModalHocProps & {
name: string;
};
export const AddItemModal = NiceModal.create<AddItemModalProps>(({ name }) => {
// ...
});Good to know: The property id is reserved by NiceModal and shouldn't be
passed on as property.
And calling it from the client component:
NiceModal.show<AddItemModalProps>(AddItemModal, { name: 'John Doe' });