Next.jsで編集フォームを作成
こんにちは、jjj0331です。今回はNext.jsを用いて編集フォームを作成したのですが、
苦労したところ、学んだところがたくさんありましたので、
紹介させてください。
作成した編集フォームの概要
Todoアプリを考えてもらって、3つのテーブルが存在します。
これらのテーブルを編集するためのフォームを作成していきます。
1:Todoのタイトル、概要を保持するテーブル
2:Todoリストの各todoの情報を格納するテーブル
3:各todoの詳細のdetail_todoの情報を格納するテーブル
1:Todoテーブル
id | タイトル | 概要 |
---|---|---|
1 | Pythonを勉強する | アルゴリズムを学習するための準備としてPythonを学習する |
2 | Railsを学ぶ | Webアプリの構築のために |
2:Taskテーブル
id | Task | Todo_id |
---|---|---|
1 | Pythonの基本文法を学ぶ | 1 |
2 | Pythonでclassを作成する | 1 |
3 | Rails環境を整える | 2 |
3:Detail_taskテーブル
id | detail_task | task_id |
---|---|---|
1 | printで出力を行う | 1 |
2 | if文を学ぶ | 1 |
3 | loopを学ぶ | 1 |
バックエンド(Rials)開発
class TodosController < ApplicationController
def update
#編集対象のTodoをidで検索
@todo = Todo.find_by(id: params[:id])
if @todo.update(todo_params)
render json: @todo, status: :ok # ステータスコードを適切に変更
else
render json: { message: '更新に失敗しました', errors: @todo.errors.full_messages }, status: :unprocessable_entity
end
end
#パラメータ制御
private
#Postで送信されてくるに不要なものがないのか制御する
def todo_params
params.require(:todo).permit(:title, :description, tasks_attributes: [
:id, :title, :_destroy, detail_tasks_attributes: [
:id, :title, :_destroy
]
])
end
post 'todos/:id/edit', to: 'todos#update'
class Todo < ApplicationRecord
has_many :tasks, dependent: :destroy
#子テーブルに当たるtasksモデルの更新とTodo.updateだけで行えるような設定
accepts_nested_attributes_for :tasks, allow_destroy: true
end
class Task < ApplicationRecord
belongs_to :todo
has_many :detail_tasks, dependent: :destroy
accepts_nested_attributes_for :detail_tasks, allow_destroy: true
end
class DetailTask < ApplicationRecord
belongs_to :task
has_many :user_statuses, dependent: :destroy
end
編集Fromの概要
編集Fromは大きく2つに分割されており、「TodoとTaskを編集するフォーム」、「DetailTaskを編集するフォーム」がある。
今回の記事では、「TodoとTaskを編集するフォーム」についてのみ解説する
1:Todoテーブル
id | タイトル | 概要 |
---|---|---|
1 | Pythonを勉強する | アルゴリズムを学習するための準備としてPythonを学習する |
2 | Railsを学ぶ | Webアプリの構築のために |
2:Taskテーブル
id | Task | Todo_id |
---|---|---|
1 | Pythonの基本文法を学ぶ | 1 |
2 | Pythonでclassを作成する | 1 |
3 | Rails環境を整える | 2 |
以上のテーブルのデータを編集するフォームになる
まず、最初にページをレンダリングすると
const { id } = useParams();
によって、idの値が更新されることで
useEffect(() => {
(省略 ※下に全コードを載せています)
catchdatas();
}, [id]);
useEffectの中のcatchdatas関数が実行され、
バックエンドから更新対象のデータを抽出する
それぞれのデータは
「todoTitle」、「todoDescription」「todos」に格納され、それぞれのuseStateで管理される
const [todos, setTodos] = useState([]);
const [todoTitle, setTodoTitle] = useState('');
const [todoDescription, setTodoDescription] = useState('');
Todoテーブルのデータ[todoTitle][todoDescription]は
<input onChange={(e) => setTodoTitle(e.target.value)}/>
でinputに入力すれば即データが更新されるようになっている
Taskテーブルのデータは、
addTodos、removeItem関数で管理し
addTodosは「追加」ボタンをクリックすると実行され
からデータがtodosに追加される
removeItem関数は
特定のindexの_destroyをtrueにし、バックエンドに送ることで削除
ちなみに、送信しないと削除されないため、送信前に削除したように見せるため以下のように工夫
{todos.filter(todo => !todo._destroy).map((todo, index) => (
(省略)
))}
その他の、
編集From1:TodoとTaskを編集するフォーム
// useStateなどCSRを行うため記述
'use client';
// のちに解説するdetail_taskを編集するフォーム
import Detailsform from '@/app/Components/Form/Detailtasks/page';
// useStateでデータを管理
import { useState,useEffect } from 'react';
// axiosでバックエンドにHTTP送信を行うため
import axios from "axios";
// Next.js内でのページ遷移を実行するため
import { useRouter } from 'next/navigation';
// URLの[id]の値を取得するため
import { useParams } from 'next/navigation';
// TodoとTaskを編集フォームを作成
const Form = () => {
//URLの[id]の値をidという変数に格納
const { id } = useParams();
//「isDetailVisible」という変数でdetail_taskを編集するためのフォームを表示・非表示を切り替える
const [isDetailVisible, setDetailVisible] = useState(false);
// 現在、編集対象のTodoのid
const [currentTodoIndex, setCurrentTodoIndex] = useState(null);
//Todo,Taskテーブルのデータを管理
const [todos, setTodos] = useState([]);
//Todoテーブルの[タイトル]のデータを管理
const [todoTitle, setTodoTitle] = useState('');
//Todoテーブルの[概要]のデータを管理
const [todoDescription, setTodoDescription] = useState('');
//ページ遷移を行うため、useRouterをインスタンス化
const router = useRouter();
//「todos」に空データを挿入するための関数
const addTodos = () => {
const newTodo = { title: '', detail_tasks: [], _destroy: false };
setTodos([...todos, newTodo]);
};
//「todos」に対してindexにも基づいてデータを削除するための関数
const removeItem = (index) => {
const newTodos = [...todos];
if (newTodos[index].id) {
newTodos[index]._destroy = true;
} else {
newTodos.splice(index, 1);
}
setTodos(newTodos);
};
// indexを更新し、DetailVisibleをTrueにしサブフォームを表示させるための関数
const showDetail = (index) => {
setCurrentTodoIndex(index);
setDetailVisible(true);
};
// サブフォームを非表示にするための関数
const closeDetail = () => {
setDetailVisible(false);
setCurrentTodoIndex(null);
};
// Todoテーブルのタイトルを更新するための関数
const handleTitleChange = (index, newTitle) => {
const newTodos = [...todos];
newTodos[index].title = newTitle;
setTodos(newTodos);
};
// サブフォームで編集した内容を更新する関数
const updateTaskDetails = (index, details) => {
const newTodos = [...todos];
newTodos[index].detail_tasks = details;
setTodos(newTodos);
};
// 「todo」を作成して、バックエンドに送信
const Submit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
const todo = {
title: todoTitle,
description: todoDescription,
tasks_attributes: todos.map(todo => ({
id: todo.id,
title: todo.title,
_destroy: todo._destroy,
detail_tasks_attributes: todo.detail_tasks.map(detail => ({
id: detail.id,
title: detail.detailtitle,
_destroy: detail._destroy
}))
}))
};
const response = await axios.post(`http://127.0.0.1:3001/todos/${id}/edit`,
{ todo }
);
alert("更新できました");
router.push('/');
} catch (error) {
console.error('投稿に失敗しました', error);
}
};
//idが変更するたびにバックエンドからデータを再取得
useEffect(() => {
const catchdatas = async () => {
try {
const response = await axios.get(`http://127.0.0.1:3001/todos/${id}`);
const todo = response.data;
setTodoTitle(todo.title);
seTodooDescription(todo.description);
setTodos(todo.tasks.map(task => ({
id: task.id,
title: task.title,
_destroy: false,
detail_tasks: task.detail_tasks.map(detail => ({
id: detail.id,
detailtitle: detail.title,
_destroy: false // Initialize _destroy flag
}))
})));
} catch (error) {
console.error('データの取得に失敗しました', error);
}
};
catchdatas();
}, [id]);
//対象のTodoを削除
const handleDelete = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (window.confirm("本当にこのガイドラインを削除しますか?")) {
try {
const response = await axios.delete(`http://127.0.0.1:3001/todos/${id}`);
if (response.status === 200) {
alert("ガイドラインが正常に削除されました");
router.push('/');
} else {
alert("ガイドラインの削除に失敗しました");
}
} catch (error) {
console.error('データの削除に失敗しました', error);
alert("ガイドラインの削除中にエラーが発生しました");
}
}
};
return (
<div className='mx-auto w-6/12 mt-6'>
//
{isDetailVisible && currentTodoIndex !== null ? (
<Detailsform
//サブフォームを切り替えるための関数
closeDetail={closeDetail}
//サブフォームで更新するデータ
todo={todos[currentTodoIndex]}
//更新対象のindex
index={currentTodoIndex}
//detailtaskの内容を反映させるための関数
updateTodoDetails={updateTodoDetails}
/>
) : (
<form >
<button type="button" onClick={handleDelete} className='px-4 py-2 bg-red-500 border rounded-lg'>削除</button>
<div id="open">
<div className='w-full'>
<label className='mt-4 font-bold mr-4'>タイトル</label>
<input
type="text"
className="w-full border px-4 py-2"
value={guidelineTitle}
??
onChange={(e) => setTodoTitle(e.target.value)}
/>
</div>
<div className='w-full mt-4'>
<label className='mt-4 font-bold mr-4'>概要</label>
<input
type="text"
className="w-full border px-4 py-2"
value={guidelineDescription}
onChange={(e) => setTodoDescription(e.target.value)}
/>
</div>
<div className='w-full mt-4'>
<label className='mt-4 font-bold mr-4'>Todolist</label>
{todos.filter(todo => !todo._destroy).map((todo, index) => (
<div className="w-full" id="target" key={index}>
<div className="flex items-center gap-1 mb-2 mt-1">
<input
type="text"
className="w-10/12 border py-2 rounded-lg"
value={todo.title}
onChange={(e) => handleTitleChange(index, e.target.value)}
/>
<button type="button" onClick={() => showDetail(index)} className='px-2 py-2 border-2 rounded-lg bg-blue-700 text-white font-bold'>
詳細
</button>
<button type="button" onClick={() => removeItem(index)} className='px-2 py-2 border-2 rounded-lg bg-red-700 text-white font-bold'>
削除
</button>
</div>
</div>
))}
</div>
<button type="button" onClick={addTodos} className='mt-1 font-bold px-2 py-2 rounded-md bg-blue-700 text-white'>
追加
</button>
<button onClick={Submit} type="submit" className='px-2 py-2 border-2 rounded-lg bg-blue-700 text-white font-bold mt-1'>
登録
</button>
</div>
</form>
)}
</div>
);
}
export default Form;
まとめ
Next.jsで編集フォームを作成する方法をまとめました。
useEffectを用いてデータ取得することや、useStateでデータを管理するを再確認することができてよかったです。
また、データ削除のために
「_destroy」をRialsに送信することが必要で、
Next.js側では配列を用いてデータ管理する基本を確認