Web3.0検証(16)-MeteorでTODO管理アプリの開発(タスク絞込み機能とUI改善)

##### [前回] [Web3.0検証(15)-MeteorでTODO管理アプリの開発(タスクの更新/削除)](https://qiita.com/mingchun_zhao/items/fc699120220ea5c0d323)

## はじめに

今回は、TODO管理アプリにタスク絞込み機能を追加します。

## まずは、アプリUIを改善

### スタイルを修正

- CSS(カスケーディングスタイルシート)とは
    - Webページの文字の色や大きさ、背景、配置といったスタイル(見た目)を設定する言語
    - ※ Flexboxツールを使用し、UI内要素を分散および整列可能
        - 水平/垂直に要素を配置し、柔軟なレイアウトを実現できるCSSレイアウトモジュール

- 修正内容
    - 上部にアプリバーを配置
    - スクロール可能なコンテンツを配置
        - 新タスクを追加するフォーム
        - タスクリスト
```
$ cd simple-todos-react
$ vi client/main.css
```
```client/main.css
body {
  font-family: sans-serif;
  background-color: #315481;
  background-image: linear-gradient(to bottom, #315481, #918e82 100%);
  background-attachment: fixed;

  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;

  padding: 0;
  margin: 0;

  font-size: 14px;
}

button {
  font-weight: bold;
  font-size: 1em;
  border: none;
  color: white;
  box-shadow: 0 3px 3px rgba(34, 25, 25, 0.4);
  padding: 5px;
  cursor: pointer;
}

button:focus {
  outline: 0;
}

.app {
  display: flex;
  flex-direction: column;
  height: 100vh;
}

.app-header {
  flex-grow: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.main {
  display: flex;
  flex-direction: column;
  flex-grow: 1;
  overflow: auto;
  background: white;
}

.main::-webkit-scrollbar {
  width: 0;
  height: 0;
  background: inherit;
}

header {
  background: #d2edf4;
  background-image: linear-gradient(to bottom, #d0edf5, #e1e5f0 100%);
  padding: 20px 15px 15px 15px;
  position: relative;
  box-shadow: 0 3px 3px rgba(34, 25, 25, 0.4);
}

.app-bar {
  display: flex;
  justify-content: space-between;
}

.app-bar h1 {
  font-size: 1.5em;
  margin: 0;
  display: inline-block;
  margin-right: 1em;
}

.task-form {
  display: flex;
  margin: 16px;
}

.task-form > input {
  flex-grow: 1;
  box-sizing: border-box;
  padding: 10px 6px;
  background: transparent;
  border: 1px solid #aaa;
  width: 100%;
  font-size: 1em;
  margin-right: 16px;
}

.task-form > input:focus {
  outline: 0;
}

.task-form > button {
  min-width: 100px;
  height: 95%;
  background-color: #315481;
}

.tasks {
  list-style-type: none;
  padding-inline-start: 0;
  padding-left: 16px;
  padding-right: 16px;
  margin-block-start: 0;
  margin-block-end: 0;
}

.tasks > li {
  display: flex;
  padding: 16px;
  border-bottom: #eee solid 1px;
}

.tasks > li > span {
  flex-grow: 1;
}

.tasks > li > button {
  justify-self: flex-end;
  background-color: #ff3046;
}
```

### スタイルを適用

- 修正内容
    - コンポーネント周囲に要素を追加
        - `App`のメイン`div`に`className`属性を追加
        - `h1`周りにいくつか`div`を含む`header`要素を追加
        - フォームとリスト周りにメイン`div`を追加
        - ※ `className`に注意
            - `className`はCSSで定義したものを指定
            - Reactでは`class`の代わりに`className`使用
                - ReactはJavaScriptを使用しUIを定義
                - `class`がJavaScriptの予約語であるため
    - アプリのタイトルを変更

```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
                  }
    })
};
const deleteTask = ({ _id }) => TasksCollection.remove(_id);

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

  return (
    <div className="app">
      <header>
        <div className="app-bar">
          <div className="app-header">
            <h1>📝️ To Do List</h1>
          </div>
        </div>
      </header>

      <div className="main">
        <TaskForm />

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

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

### Meteorアプリを実行
```
$ cd simple-todos-react
$ meteor run
```

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

- ブラウザから、`http://localhost:3000/`にアクセス
- デベロッパーツールを開く(F12)
- `デバイスのツールバーを切り替え`アイコンをクリック
- デバイスを選択

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/a2b4c867-1225-22fd-8a9e-5977a1f88b0f.png)|
|--|

見た目がアプリぽっくなりました。

## タスクの絞込み機能

### 作業内容

- タスクをフィルタリング可能にし、アプリをもっとインタラクティブに
- タスクをステータスでフィルタリングし、保留中タスクの数を表示

### 完了タスクをリストから表示/非表示にするボタンを追加

#### 作業内容

- Reactのフック`useState`を使って、ボタン状態を保持
    - `useState`フックを`import`
        - フックは常にコンポーネントの上部に追加することをお勧め
            - 常に同じ順序で実行可能
    - `useState`は、2つの項目を含む配列を返す
        - 1番目の要素は、`state`値
        - 2番目の要素は、状態更新用`setter`関数
        - `Array destructuring`(配列の分割代入で複数値を同時代入)を使って、2つの戻り値を同時に`const`変数で受け取る
- タスクフォームの下に、現在状態に基づき異なる内容を表示する`button`を追加
``` imports/ui/App.jsx
import React, { useState } 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
                  }
    })
};
const deleteTask = ({ _id }) => TasksCollection.remove(_id);

export const App = () => {
  const tasks = useTracker(() => TasksCollection.find({}, { sort: { createdAt: -1 } }).fetch());
  const [hideCompleted, setHideCompleted] = useState(false);

  return (
    <div className="app">
      <header>
        <div className="app-bar">
          <div className="app-header">
            <h1>📝️ To Do List</h1>
          </div>
        </div>
      </header>

      <div className="main">
        <TaskForm />
        <div className="filter">
          <button onClick={() => setHideCompleted(!hideCompleted)}>
            {hideCompleted ? 'Show All' : 'Hide Completed'}
          </button>
        </div>
        <ul className="tasks">
          {tasks.map(task => (
            <Task
              key={task._id}
              task={task}
              onCheckboxClick={toggleChecked}
              onDeleteClick={deleteTask}
            />
          ))}
        </ul>
      </div>
    </div>
  );
};
```

### ボタンスタイルを修正

- `client/main.css`の末尾に以下を追記

```client/main.css
.filter {
  display: flex;
  justify-content: center;
}

.filter > button {
  background-color: #62807e;
}
```

### タスクのフィルタリング

- 保留中タスクのみを表示
    - `Mini Mongo`クエリの`selector`に`filter`を追加
        - `isChecked`が`true`でないすべてのタスクを取得
```imports/ui/App.jsx
import React, { useState } 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
                  }
    })
};
const deleteTask = ({ _id }) => TasksCollection.remove(_id);

export const App = () => {
  const [hideCompleted, setHideCompleted] = useState(false);
  const hideCompletedFilter = { isChecked: { $ne: true } };

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

  return (
    <div className="app">
      <header>
        <div className="app-bar">
          <div className="app-header">
            <h1>📝️ To Do List</h1>
          </div>
        </div>
      </header>

      <div className="main">
        <TaskForm />
        <div className="filter">
          <button onClick={() => setHideCompleted(!hideCompleted)}>
            {hideCompleted ? 'Show All' : 'Hide Completed'}
          </button>
        </div>
        <ul className="tasks">
          {tasks.map(task => (
            <Task
              key={task._id}
              task={task}
              onCheckboxClick={toggleChecked}
              onDeleteClick={deleteTask}
            />
          ))}
        </ul>
      </div>
    </div>
  );
};
```

### `Meteor DevTools`拡張機能

- `Meteor DevTools Evolved`拡張機能を使用
    - Mini Mongoデータを視覚化
    - アプリのデバッグ可能
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/585f3d11-d01c-3d95-1014-410e6cb1825f.png)
    - Meteorとサーバー間の送受信メッセージから、Meteor動作を確認可能
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/dd880332-5c71-facf-648b-8e6e8d13f11e.png)

### アプリバーに保留中タスクの数を表示

- 保留中タスクがない場合は、アプリバーにゼロを追加しない
- 保留中タスクの数を取得するトラッカーをもう一つ追加
    - 1つの`useTracker`のみ使用し、二つの検索結果のプロパティを持つオブジェクトを返すことも可能
    - ここでは、コード理解のため2つのトラッカーに分けて作成

```imports/ui/App.jsx
import React, { useState } 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
                  }
    })
};
const deleteTask = ({ _id }) => TasksCollection.remove(_id);

export const App = () => {
  const [hideCompleted, setHideCompleted] = useState(false);
  const hideCompletedFilter = { isChecked: { $ne: true } };

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

  const pendingTasksCount = useTracker(() =>
    TasksCollection.find(hideCompletedFilter).count()
  );

  const pendingTasksTitle = `${
    pendingTasksCount ? ` (${pendingTasksCount})` : ''
  }`;

  return (
    <div className="app">
      <header>
        <div className="app-bar">
          <div className="app-header">
            <h1>
              📝️ To Do List
              {pendingTasksTitle}
            </h1>
          </div>
        </div>
      </header>

      <div className="main">
        <TaskForm />
        <div className="filter">
          <button onClick={() => setHideCompleted(!hideCompleted)}>
            {hideCompleted ? 'Show All' : 'Hide Completed'}
          </button>
        </div>
        <ul className="tasks">
          {tasks.map(task => (
            <Task
              key={task._id}
              task={task}
              onCheckboxClick={toggleChecked}
              onDeleteClick={deleteTask}
            />
          ))}
        </ul>
      </div>
    </div>
  );
};
```

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

|絞込み前|絞込み後|
|:--:|:--:|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/c666e9d1-43b0-e8fc-5cab-2e1769d325ef.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/895ce343-ff2d-c87c-c2eb-831a161562e0.png)|

## おわりに

タスクのフィルタリング機能を追加しました。
次回も続きます。お楽しみに。

##### [次回] [Web3.0検証(17)-MeteorでTODO管理アプリの開発(ユーザーアカウント機能)](https://qiita.com/mingchun_zhao/items/9b3b32ae18845439d478)