Web3.0検証(15)-MeteorでTODO管理アプリの開発(タスクの更新/削除)

##### [前回] [Web3.0検証(14)-MeteorでTODO管理アプリの開発(新しいタスクの追加機能)](https://qiita.com/mingchun_zhao/items/7b9dcf05b166f3e2fe04)

## はじめに

今回は、TODO管理アプリにタスク更新/削除の機能を追加します。

## 理論武装: Reactでフォーム要素の制御

### フォーム要素の状態保持の違い

- HTMLの場合
    - 状態保持は、フォーム要素自身で行う
        - フォーム要素は、`<input>`、`<textarea>`、`<select>`など
    - 状態更新は、ユーザ入力に基づく
- Reactの場合
    - 状態保持は、コンポーネントの`state`プロパティにて
    - 状態更新は、setState()関数でのみ

### Reactでフォーム要素のハンドリング方法2点

- 制御コンポーネント(controlled component)
    - Reactコンポーネントが、フォーム要素のユーザ入力をハンドリング
        - `state`の更新に対し、イベントハンドラを書く
        - Reactの`state`を`信頼できる唯一情報源(single source of truth)`とみなす
    - メリット
        - `state`値を、他のUI要素に渡したり、他のイベントハンドラからリセット可能に
- 非制御コンポーネント(uncontrolled component)
    - DOMが、フォーム要素のユーザ入力をハンドリング
        - `ref`を使用し、DOMからフォーム要素値を取得
        - DOM(Document Object Model)とは
            - マークアップされたリソース(Document)をリソース要素(Object)の木構造(Model)で表現し操作可能にする仕組み
            - HTMLやXMLなどマークアップされたドキュメントを、オブジェクトの木構造モデルで表現することで、プログラムから操作・利用可能にする
            - Documentの種類、操作に用いるプログラミング言語の種類に依存しない仕様

### フォーム要素ハンドリングのサンプルコード

- 制御コンポーネントで`Name`値を受け取る例
    - フォーム要素にvalue属性を設定し、常に`this.state.value`を表示
    - `handleChange`はユーザ入力により実行され、Reactの`state`を更新

```
class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('The name is: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}
```

- 非制御コンポーネントで`Name`値を受け取る例
    - フォーム要素に`ref`属性を設定
    - `ref`の値`this.input`を使用し、フォーム要素値を取得

```
class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.input = React.createRef();
  }

  handleSubmit(event) {
    alert('The name is: ' + this.input.current.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" ref={this.input} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}
```

## ここから、開発続行

### 端末1から、Meteorアプリを実行
```
$ cd simple-todos-react
$ meteor run
[[[[[ ~/simple-todos-react ]]]]]

=> Started proxy.
=> Started HMR server.
=> Started MongoDB.
=> Started your app.

=> App running at: http://localhost:3000/
```

### 端末2から、フォームにチェックボックスを追加

- `Task`コンポーネントにcheckbox要素を追加
    - `checkbox`の状態更新に`onChange`イベントを使用しないため、`readOnly`属性指定
    - `checked`属性を`boolean`値に指定し、非制御コンポーネントから制御コンポーネントに切り替える
    - コールバック`onCheckboxClick`を受け取る
        - チェックボックスがクリックされたときに呼び出される関数

```imports/ui/Task.jsx
import React from 'react';

export const Task = ({ task, onCheckboxClick }) => {
  return (
    <li>
      <input
        type="checkbox"
        checked={!!task.isChecked}
        onClick={() => onCheckboxClick(task)}
        readOnly
      />
      <span>{task.text}</span>
    </li>
  );
};
```

- チェックボックスの切り替え
    - `isChecked`フィールドを切り替えることで、タスクのドキュメントを更新可能に
    - ドキュメントを変更する関数を作成し、`Task`コンポーネントに渡す

```imports/ui/App.jsx
import React from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import { Task } from './Task';
import { TasksCollection } from '/imports/api/TasksCollection';
import { TaskForm } from './TaskForm';

const toggleChecked = ({ _id, isChecked }) => {
          TasksCollection.update(_id, {
                      $set: {
                                    isChecked: !isChecked
                                  }
                    })
};

export const App = () => {
          const tasks = useTracker(() => TasksCollection.find({}, { sort: { createdAt: -1 } }).fetch());

          return (
                      <div>
                        <h1>Welcome to Meteor!</h1>

                        <TaskForm/>
                        <ul>
                          { tasks.map(task => <Task key={ task._id } task={ task } onCheckboxClick={toggleChecked} />) }
                        </ul>
                      </div>
                    );
};
```

### ブラウザでアプリを確認

ブラウザから、`http://localhost:3000/`にアクセス。
タスク毎に、チェックボックスが追加されています。

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/b380c158-c221-0a87-1508-2e5f73bb915e.png)|
|--:|

### タスクの削除機能

- まず、`Task`コンポーネントで、削除ボタンを追加し、コールバック関数を受け取る

```imports/ui/Task.jsx
import React from 'react';

export const Task = ({ task, onCheckboxClick, onDeleteClick }) => {
          return (
                      <li>
                        <input
                          type="checkbox"
                          checked={!!task.isChecked}
                          onClick={() => onCheckboxClick(task)}
                          readOnly
                        />
                        <span>{task.text}</span>
                        <button onClick={ () => onDeleteClick(task) }>&times;</button>
                      </li>
                    );
};
```

- つぎ、アプリに削除ロジックを追加
    - タスク削除用関数を作成
    - `Task`コンポーネントのコールバックプロパティ`onDeleteClick`に、この関数を指定

```imports/ui/App.jsx
const deleteTask = ({ _id }) => TasksCollection.remove(_id);

export const App = () => {
  ..
  <ul>
    { tasks.map(task => <Task
      key={ task._id }
      task={ task }
      onCheckboxClick={toggleChecked}
      onDeleteClick={deleteTask}
    />) }
  </ul>
  ..
}
```

## 再度、ブラウザからアプリを確認

タスク別、末尾に削除ボタンが追加されました。

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/b10e67ce-56e4-c4d5-ad5e-f633e4b1b304.png)|
|--|

試しに、先頭`My new task`の`x`ボタンをクリックしてみます。
すると、タスク消えました。やったー。

## おわりに

タスクの更新/削除機能を追加しました。
次回もアプリ開発続きます。お楽しみに。

##### [次回] [Web3.0検証(16)-MeteorでTODO管理アプリの開発(タスク絞込み機能とUI改善)](https://qiita.com/mingchun_zhao/items/c857e5313b6eb547f440)