タスク管理ツールをReact + ASP.Coreで作る 012詳細画面(編集機能)

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



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



## 流れ
- クライアント側のフォルダ整理
- 入力フォームをFormikで構築


## クライアント側のフォルダ整理

ファイルが増えてきたので、フォルダ構成を一度整理しました。
Reactの各ページを構成するようなコンポーネント類を「components」、それ以外の処理関連や、共通して使うようなReactコンポーネントを「app」に配置するという構成です。

← Before | After →
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2664470/12c91fa5-08eb-8669-6e3f-cd2541041273.png)

## クライアント側にパッケージのインストール
ここまであまり個々の機能に専用パッケージみたいなものは入れず、極力クリーンアーキテクチャーでやってきましたが、この先全部自前は逆に大変そうなので、入力関連はパッケージを使っていこうと思います。

以下をインストールします。
```
npm install formik react-datepicker @types/react-datepicker yup
```

Reactで入力フォームを使う際に使用事例が多いformik、データ検証を可能にするYup、日付操作機能を簡単に実装するためのreact-datepickerを導入しました。

## コードの変更

コードを変更していきます。
今回は若干変更量多いです。


### サーバーサイド

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

```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);
+        }
    }
}

```

### 入力用の部品

次は入力用の部品たちです。
Formにすべての定義を書くと長くなるので、データの型ごとに入力フォーム用の部品を作っておきます。

```diff_tsx:CheckBoxGeneral.tsx
import { useField } from "formik";
import { Form } from 'react-bootstrap'

interface Props{
    name:string;
    label?: string;
}

export default function CheckBoxGeneral(props: Props){
    const[field, meta] = useField({ name: props.name, type: "checkbox" }
        );
    return (
        <>
            <Form.Check {...field} type='checkbox' label={props.label} />
            {meta.touched && meta.error ? (
                <Form.Label>{meta.error}</Form.Label>
            ) : null}
        </>
    )
}
```


```diff_tsx:DateInputGeneral.tsx

import { useField } from "formik";
import React from "react";
import { Form } from "react-bootstrap";
import DatePicker, {ReactDatePickerProps} from 'react-datepicker';

export default function DateInputGeneral(props: Partial<ReactDatePickerProps>){
    const[field, meta, helpers] = useField(props.name!);
    return (
        <Form.Group>
            <DatePicker
                {...field}
                {...props}
                selected={(field.value && new Date(field.value)) || null}
                onChange={value => helpers.setValue(value)}
            />
            {meta.touched && meta.error ? (
                <Form.Label basic color='red'>{meta.error}</Form.Label>
            ) : null}
        </Form.Group>
    )
}

```

```diff_tsx:TextAreaGeneral.tsx

import { useField } from "formik";
import { Form } from 'react-bootstrap'

interface Props{
    placeholder: string;
    name:string;
    rows:number;
    label?: string;
}

export default function TextAreaGeneral(props: Props){
    const[field, meta] = useField(props.name);
    return (
        <>
        <Form.Group>
            { props.label && <Form.Label>{props.label}</Form.Label> }
            <Form.Control as="textarea" {...field} {...props} />
            {meta.touched && meta.error ? (
                <Form.Label>{meta.error}</Form.Label>
            ) : null}
        </Form.Group>
        </>
    )
}

```


```diff_tsx:TextInputGeneral.tsx

import { useField } from "formik";
import { Form } from 'react-bootstrap'

interface Props{
    placeholder: string;
    name:string;
    type?: string;
    label?: string;
    disabled?: boolean;
}

export default function TextInputGeneral(props: Props){
    const[field, meta] = useField(props.name);
    return (
        <>
        <Form.Group>
            { props.label && <Form.Label>{props.label}</Form.Label> }
            <Form.Control {...field} {...props} />
            {meta.touched && meta.error ? (
                <Form.Label>{meta.error}</Form.Label>
            ) : null}
        </Form.Group>
        </>
    )
}

```

### TaskEditの変更

TaskEditは、前回まではEditといいつつ表示のみでしたが、今回で既存レコードの編集機能を実装した状態になりました。
変更点が多いので差分ではなく変更後の状態を載せてあります。

```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";

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


interface Props {
    id_task: string
}

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

    useEffect(() => {
        id_task !== "" && loadTaskDetails();
    }, [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) => {
        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>
            <h3>Task Detail : {task && 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='Short Description' placeholder='Description' name='short_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>

        

    )
}

```

Index.tsxに、react-datepickerのcssを呼びます。
(これを呼ばないと、日時選択画面がうまく動作しない)

```diff_tsx:Index.tsx
import App from './App';
import reportWebVitals from './reportWebVitals';
import 'bootstrap/dist/css/bootstrap.min.css';
+ import 'react-datepicker/dist/react-datepicker.css';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
```

変更後のフォルダの全体像は以下です

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2664470/d434d7cc-794b-38bd-4a99-8140ded9fd85.png)


## 動作させてみる

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

https://www.youtube.com/watch?v=SrBcz5eXOPg

ブラウザを更新しても入力値が残っているので、各レコードを操作することが可能になっているのが分かります。

一方で日付に対してスタイルが適切に当たっていなかったり、右側の詳細エリアの更新結果が左側のリストには反映されていない(詳細側を更新しても期日やfinishフラグが更新されていない)など、修正を要する点があることも併せて分かりました。
これらも今後引き続き作りこみながら直していこうと思います。

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