【Next.js】ToDoアプリの新規作成をモーダル化する
概要
下記の記事を参考にNext.jsで簡易なToDoアプリを作成しました。
この記事ではタスクの新規作成は別ページへルーティングしていますが、別ページへ遷移せず、新規タスク作成のモーダルが表示されるように修正してみたのでその備忘録です。
モーダルはshadcnのDialogコンポーネントを使用しました。
以降の説明では、上記の記事に記載のToDoアプリが作成済みの前提で進めます。
修正後のイメージはこんな感じです。

ヘッダーのボタンにダイアログのトリガーを設定する
Dialogコンポーネントでは、DialogTriggerコンポーネントにasChild属性をつけると、子コンポーネントをトリガーにダイアログが表示されます。
例えば、公式のサンプルは以下のように、"Share"ボタンを押下するとダイアログが表示されます(公式のサンプルを一度見ると動きがイメージできると思います)。
export function DialogCloseButton() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Share</Button>
</DialogTrigger>
(中略)
<Dialog />
)
}
参考記事では、ヘッダーに"New Task"ボタンがあり、そのボタンを押すと新規タスク作成のページへ遷移していましたが、今回はそのボタンを押下すると新規作成用のモーダル(ダイアログ)が出現するようにします。
まずはdialog周りのコンポーネントを追加してください。
npx shadcn@latest add dialog
続いて、ヘッダーを修正します。
CreateDialogコンポーネントはまだ作成していませんが、この後作成します(IDEでimportのパスを自動補完したい場合は、先にsrc/components/layouts/CreateDialog/index.tsxを作成してCreateDialogコンポーネントをexportしておいてください)。
import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
import Link from "next/link";
+import { CreateDialog } from "../CreateDialog";
export const Header = () => {
return (
<header className="flex fixed w-[100vw] items-center h-[60px] px-4 border-b bg-white">
<div className="flex-1 min-w-0">
<h1 className="font-bold text-xl">
<Link href="/">Todo List App</Link>
</h1>
</div>
+ <Dialog>
+ <DialogTrigger asChild>
- <Button size="sm"><Link href="create/">New Task</Link></Button>
+ <Button size="sm">New Task</Button>
+ </DialogTrigger>
+ <CreateDialog />
+ </Dialog>
</header>
)
}
これで、ヘッダーの"New Task"ボタンを押すとCreateDialogコンポーネントが表示されるようになります(もちろん今の段階ではまだ何も表示されません)。
新規タスク作成用ダイアログの作成
ではCreateDialogコンポーネントを作成していきます。
といっても、参考サイトのCreateTaskコンポーネントを少し書き換えてやるだけでOKです。
まずは、src/app/create/page.tsxの内容をsrc/components/layouts/CreateDialog/index.tsxにコピーして、コンポーネント名をCreateDialogに変更してください。
あとは基本的に、returnの中身のCardをDialogに変更すればいいです。
ただ、両者で一部構成が異なる部分があるので気をつけてください。
(具体的には、Cardの場合、CardHeader, CardContent, CardFooterは同じ階層ですが、Dialogの場合、DialogContentの中にDialogHeaderとDialogFooterを入れるような構成です)
具体的な修正後のコードを載せておきます。
'use client'
(中略)
+import {
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogClose,
+} from "@/components/ui/dialog"
(中略)
+export const CreateDialog = () => {
(中略)
return (
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>Create Todo</DialogTitle>
+ <DialogDescription>Create your new Todo in one-click.</DialogDescription>
+ </DialogHeader>
<Form {...form} >
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8" >
<FormField
control={form.control}
name="title"
render={({field}) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input placeholder="workout" {...field}></Input>
</FormControl>
<FormMessage />
<FormDescription>This is your Todo title.</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({field}) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea placeholder="1 hour workout at 4:00 p.m." {...field}></Textarea>
</FormControl>
<FormMessage />
<FormDescription>This is your Todo description.</FormDescription>
</FormItem>
)}
/>
+ <DialogFooter className="flex justify-between">
+ <DialogClose asChild>
+ <Button variant="outline">Cancel</Button>
+ </DialogClose>
+ <DialogClose asChild>
+ <Button type="submit">Create</Button>
+ </DialogClose>
+ </DialogFooter>
</form>
</Form>
+ </DialogContent>
)
}
あとは、DialogFooterのButton部分も少し修正しています。
元々の記事では、"Cancel"ボタンを押したらホームディレクトリへ遷移するようにすることでキャンセル機能を実現していましたが、DialogCloseの子コンポーネントとすることでダイアログがそのまま閉じるようにしています。
また、"Create"ボタンは、type="submit"とすることでformを送信してからダイアログが閉じるようにしています。
これでヘッダーの"New Task"ボタンを押すと、以下のようにダイアログが表示されるかと思います。

ちなみに、ページ遷移がないので、onSubmit関数でのrouter.push("/");の部分は削除可能です(そのままでも特に影響はない?)。
useRouterについては勉強不足なので、この辺調べておきたいと思います。
Discussion