タスク管理ツールをReact + ASP.Coreで作る 013詳細画面(新規登録)

[前回](https://qiita.com/mosuma/items/07fa83299f70a548fce6)の続きです

今回から、新規登録機能を作っていこうと思います


### ガイド
全体Index：[タスク管理ツールをReact + ASP.Coreで作る - Index](https://qiita.com/mosuma/items/88e40dc44e7bff1a54d8)


## 流れ
- コントローラーを修正してサーバーサイドに新規追加用のapiを作成
- クライアントに「新規タスク登録モード」を管理するstateと各コンポーネントでstateに沿った動作の変更


## コントローラーを修正してサーバーサイドに新規追加用のapiを作成

まずコントローラーに、更新用の入力を受け取る関数を定義します

```diff_c_sharp:TaskController.cs


+        [HttpPost("create")]
+        public async Task<ActionResult<t_task>> Create([FromBody] t_task task)
+        {
+            var new_task = new t_task
+            {
+                id_task = task.id_task,
+                title = task.title,
+                is_finish = task.is_finish,
+                description = task.description,
+                end_date_scheduled = task.end_date_scheduled,
+                end_date_actual = task.end_date_actual,
+            };
+
+            await _context.t_tasks.AddAsync(new_task);
+            await _context.SaveChangesAsync();
+
+            return await _context.t_tasks.FindAsync(task.id_task);
+        }
    }
```


<details><summary>参考：TaskController.cs全体(開くと表示)</summary><div>



```diff_c_sharp:TaskController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using server_app.Models.EDM;

namespace server_app.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class TaskController : ControllerBase
    {
        private readonly DataContext _context;
        public TaskController(DataContext context)
        {
            _context = context;
        }
        [HttpGet]
        public async Task<ActionResult<List<t_task>>> Get()
        {
            return await _context.t_tasks.ToListAsync();
        }
        [HttpGet("{id}")]
        public async Task<ActionResult<t_task>> Details(Guid id)
        {
            return await _context.t_tasks.FindAsync(id);
        }
        [HttpPost("update")]
        public async Task<ActionResult<t_task>> Update([FromBody] t_task task)
        {
            var temp = await _context.t_tasks.FindAsync(task.id_task);
            temp.title = task.title;
            temp.is_finish = task.is_finish;
            temp.description = task.description;
            temp.end_date_scheduled = task.end_date_scheduled;
            temp.end_date_actual = task.end_date_actual;

            await _context.SaveChangesAsync();

            return await _context.t_tasks.FindAsync(task.id_task);
        }
        [HttpPost("create")]
        public async Task<ActionResult<t_task>> Create([FromBody] t_task task)
        {
            var new_task = new t_task
            {
                id_task = task.id_task,
                title = task.title,
                is_finish = task.is_finish,
                description = task.description,
                end_date_scheduled = task.end_date_scheduled,
                end_date_actual = task.end_date_actual,
            };

            await _context.t_tasks.AddAsync(new_task);
            await _context.SaveChangesAsync();

            return await _context.t_tasks.FindAsync(task.id_task);
        }
    }
}
```


</div></details>





## クライアントの仕様検討

### 状態管理の見直し
これまで、画面の制御では「selectedId_task」という変数を用いて、この変数が設定されていればその変数の値を使って詳細画面表示、選択されていなければ詳細画面表示には何も表示しないという状態管理を基本にしていました。

表に書いてみると以下のような関係性になります。


selectedId_taskの値と表示制御
|値 |動作 |
|---|---|
|　空|何も表示しない|
|　空以外 |selectedId_taskの中身をapiから取得して詳細を表示|

新規登録時は詳細情報編集画面となっているエリアを新規登録画面にしようと思うのですが、その場合「①何も選択されていない」、「②タスク新規登録」、「③登録済みタスクの詳細情報表示/編集」という3つの状態を管理する必要があります。
「何も選択されていなければ新規登録画面を出す」というのもありですが、常に新規登録画面が出ているのもちょっと微妙です。

アプローチ方法は何種類もあると思いますが、今回は「新規登録モードかどうか」を管理する変数を設けて、それを使って状態を管理しようと思います。

それぞれの値と表示制御

|id_mode_addnewの値 |selectedId_taskの値 |動作 |
|---|---|---|
|　true   |　内容問わず |新規登録画面を表示|
|　false　|　空|何も表示しない|
|　false  |　空以外 |selectedId_taskの中身をapiから取得して詳細を表示|

「新規登録モード」なら新規登録画面を表示、そうではないときはid_taskが選択されているならid_taskの詳細を表示、「新規登録モード」ではなく、かつ「idが選択されていない」場合は何も選択しないという動作にしようと思います。


## クライアント側にパッケージのインストール
新規登録時は、タスクIDを取得する必要があります。タスクIDをどこで取得するかは色々アプローチがありますが、guidを採用しているのでクライアント側で取得してサーバーサイドで渡す仕様にしようとおもいます。

uuidを取得するためのパッケージをインストールします

```
>npm install uuid @types/uuid
```





### クライアントサイドのコード変更

コードを変更していきます。


「id_mode_addnew」変数は「TaskOperationMain」に設けて、各コンポーネントに渡していく仕様とします。
また、新規追加モード中にリストで行を選択した場合は登録済みアイテム編集モードに移行できるように、リスト側に「setIsModeAddnew」の参照を渡しています。

```diff_tsx:TaskOperationMain.tsx
import { useState } from "react";
- import { Col, Row } from "react-bootstrap"
+ import { Button, Col, Row } from "react-bootstrap"
import { TaskEdit } from "./TaskEdit"
import { TaskList } from "./TaskList"


export const TaskOperationMain = () => {
    
    
+   const [isModeAddnew, setIsModeAddnew] = useState(false);
    const [selectedId_task, setSelectedId_task] = useState("");

    return (
        <div>
            
+           <Button variant="primary" onClick={()=>{setIsModeAddnew(true); setSelectedId_task("")}}>Add New Task</Button>

            <Row>
                <Col>
-                   <TaskList selectedId_task={selectedId_task} setSelectedId_task={setSelectedId_task}/>
+                   <TaskList setIsModeAddnew={setIsModeAddnew} selectedId_task={selectedId_task} setSelectedId_task={setSelectedId_task}/>
                </Col>
                <Col>
-                   <TaskEdit id_task={selectedId_task} />
+                    {
+                        (isModeAddnew || selectedId_task !== "") &&
+                            <TaskEdit isModeAddnew={isModeAddnew} id_task={selectedId_task} />
+                    }
                </Col>
            </Row>
        </div>
    )
}
```


「TaskEdit」コンポーネントは、「登録済みタスク操作モード」の場合はこれまでと変わらない動作で、「新規追加モード」の際は新たにサーバーサイド側に作ったapiに登録用データを渡す仕様に変更してあります。

```diff_tsx:TaskEdit.tsx
import { Form, Formik } from "formik";
import { useEffect, useState } from "react";
import { Col, Row } from "react-bootstrap";
import * as Yup from 'yup';
import CheckBoxGeneral from "../app/common/CheckBoxGeneral";
import DateInputGeneral from "../app/common/DateInputGeneral";
import TextAreaGeneral from "../app/common/TextAreaGeneral";
import TextInputGeneral from "../app/common/TextInputGeneral";
+ import {v4} from 'uuid';

interface Task {
    id_task: string;
    title: string;
    is_finish: boolean;
    description: string;
+    end_date_scheduled: Date | null;
+    end_date_actual: Date | null;
  }


interface Props {
+    isModeAddnew: boolean;
    id_task: string;
}

+ export const TaskEdit = ({isModeAddnew, id_task}: Props) => {    
    
    const [task, setTask] = useState<Task>();

    useEffect(() => {
+        if(id_task !== ""){
            loadTaskDetails();
+        } else {
+            setTask({id_task : "", title : "", is_finish: false, description:"", +end_date_scheduled : null, end_date_actual : null})
+        }
    }, [id_task]);
  
    const loadTaskDetails = async () => {
        const response = await fetch(`https://localhost:5001/task/${id_task}`);
        const data = await response.json();
        setTask(data);
    };


    const updateTaskDetails = async (value : Task) => {

        if(value.id_task===""){
+            const newTask = value;
+            newTask.id_task=v4();
+
+            const response = await fetch("https://localhost:5001/task/create", { 
+                method: "POST",
+                headers: {
+                    'Content-Type': 'application/json',
+                },
+                body: JSON.stringify(newTask)  
+            });
+            const data = await response.json();
+            setTask(data);
+
        } else {
            const response = await fetch("https://localhost:5001/task/update", { 
                method: "POST",
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(value)  
            });
            const data = await response.json();
            setTask(data);
        }
    };
    
    const validationSchema = Yup.object({
        title: Yup.string().required(),
        is_finish: Yup.bool().required(),
        description: Yup.string().nullable(),
        end_date_scheduled: Yup.date().nullable(),
        end_date_actual: Yup.date().nullable(),
    });



    return (
        <div>
+             {
+                isModeAddnew ?
+                    <h3>Add New Task</h3>
+                    :
+                    <h3>Task Detail : {task?.title}</h3>
+             }
            { task &&
            <div>
                <Formik
                validationSchema={validationSchema}
                enableReinitialize 
                initialValues={task} 
                onSubmit={values => updateTaskDetails(values)}>
                {({ handleSubmit, isValid, isSubmitting, dirty }) => (
                    <Form className="ui form" onSubmit = {handleSubmit} autoComplete='off'>

                        <Row>
                            <Col><TextInputGeneral label='Title' name='title' placeholder='Title' /></Col>
                        </Row>

                        <hr />
                        
                        <Row>
                            <Col ><TextAreaGeneral label='Description' placeholder='Description' name='description' rows={3}   /></Col>
                        </Row>
                        
                        <Row>
                            <Col ><DateInputGeneral placeholderText='Due Date' name = 'end_date_scheduled' dateFormat='MM d, yyyy' /></Col>
                            <Col ><DateInputGeneral placeholderText='Completion Date' name = 'end_date_actual' dateFormat='MM d, yyyy' /></Col>
                        </Row>


                        
                        <Row>
                            <Col xs={4}><CheckBoxGeneral label='Finished' name='is_finish'  /></Col>
                        </Row>
                        
                        <hr />
                        
                        <button disabled={!isValid || !dirty || isSubmitting} type = 'submit' className='btn btn-primary'>
                            {isSubmitting ? "Processing" : "submit"}
                        </button>
                    </Form>
                )}

                </Formik>

            </div>

            }
        </div>

        

    )
}
```








```diff_tsx:TaskList.tsx
import { useEffect, useState } from 'react';
import { Button, Table } from 'react-bootstrap';

interface Task {
  id_task: string;
  title: string;
  is_finish: boolean;
  end_date_scheduled: Date;
}


interface Props {
  selectedId_task: string;
+  setIsModeAddnew: React.Dispatch<React.SetStateAction<boolean>>;
  setSelectedId_task: React.Dispatch<React.SetStateAction<string>>;
}


+export const TaskList = ({setIsModeAddnew, selectedId_task, setSelectedId_task}: Props) => {
    
    
    const [loading, setLoading] = useState(true);
    const [tasks, setTasks] = useState<Task[]>();
  
    useEffect(() => {
        populateWeatherData();
    }, []);
  
    const populateWeatherData = async () => {
        const response = await fetch('https://localhost:5001/task');
        const data = await response.json();
        setTasks(data);
        setLoading(false);
    };
    
    if(loading) return <div>loading....</div>

    return (
        <div>
            <h1 id="tabelLabel">Task List</h1>
            <p>This component demonstrates fetching data from the server.</p>
            <Table >
              <thead>
                <tr>
                  <th>No.</th>
                  <th>Fin.</th>
                  <th>Title</th>
                  <th>Due Date</th>
                </tr>
              </thead>
              <tbody>
                {tasks && tasks.map((task, index) => (
                  <tr 
+                    key={task.id_task} onClick={()=>{setIsModeAddnew(false); setSelectedId_task(task.id_task);}}  
                    className={ selectedId_task === task.id_task ? "table-info" :  ""}                  
                  >
                    <td>{index+1}</td>
                    <td><input type="checkbox" defaultChecked={task.is_finish} disabled /></td>
                    <td>{task.title}</td>
                    <td>{task.end_date_scheduled && (new Date(task.end_date_scheduled).toDateString())}</td>
                  </tr>
                ))}
              </tbody>
            </Table>
        </div>
    )
}
```

## 動作させてみる

上記の状態で、サーバー・クライアント双方を更新すると、以下の様になります。

https://youtu.be/PXHZWaocauw

今回は以上です。
続きは[次回](https://qiita.com/mosuma/items/282e5ca6736cf8291b9c)です