【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