Next.jsで編集フォームを作成しました

## 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)開発
```ruby:todos_controller.rb
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  
```

```ruby:routes.rb
post 'todos/:id/edit',    to: 'todos#update'
```

```ruby:モデル
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) => (
 (省略)
))}
```

その他の、






### 編集From１：TodoとTaskを編集するフォーム


```Next.js:frontend\app\guidelines\[id]\page.tsx
// 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側では配列を用いてデータ管理する基本を確認