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 { type SubmitHandler } from 'react-hook-form';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';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 { BuildingIcon, UserIcon } from 'lucide-react';import { type SubmitHandler } from 'react-hook-form';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';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' });