Redux ExampleのTodo ListをはじめからていねいにVue.jsで(2)

## 概要
1回目では、TodoをTodo Listに追加する「Add Todo」を作った。
今回は、Todoの完了・未完了を切り替える「Toggle Todo」の機能を作っていく。

[Redux ExampleのTodo ListをはじめからていねいにVue.jsで(1)][*2-1]

## ディレクトリ構成（Add Todo時）

```console
.
├── bin
│   ├── bash.sh
│   ├── build.sh
│   ├── container_build.sh
│   ├── fix.sh
│   └── up.sh
├── docker
│   ├── docker-compose.yml
│   ├── config
│   │   ├── cypress.json
│   │   ├── jest.config.js
│   │   ├── package.json
│   │   ├── postcss.config.js
│   │   └── vue.config.js
│   └── vue
│       └── Dockerfile
├── docs
│   ├── readme.md
│   └── todo.md
├── public
│   ├── favicon.ico
│   └── index.html
├── src
│   ├── main.ts
│   ├── App.vue
│   ├── assets
│   │   └── logo.png
│   ├── components
│   │   ├── AddTodo.vue
│   │   ├── HelloWorld.vue
│   │   ├── Todo.vue
│   │   └── TodoList.vue
│   ├── models
│   │   └── TodoItem.ts
│   ├── store
│   │   ├── index.ts
│   │   ├── todo
│   │   │   ├── index.ts
│   │   │   └── types.ts
│   │   └── types.ts
│   └── types
│       ├── shims-tsx.d.ts
│       ├── shims-vue.d.ts
│       ├── shims-vuex.d.ts
│       └── types.d.ts
├── tests
└── tsconfig.json
```

## Toggle Todo
### 1. 完了・未完了を表すcompletedによってスタイルを変える

#### todoにcompleted要素を追加して、とりあえず取り消し線を表示する

まず、todoごとに完了・未完了を区別するために、completedという要素を追加。

```diff:src/models/TodoItem.ts
export type TodoItem = {
  id: number;
  text: string;
+  completed: boolean;
};
```

前回つくったmutationを修正。
todo作成時は、未完了なので、デフォルトでfalseとする。


```diff:src/store/todo/index.ts
// Vuexのストアの状態を変更できる唯一の方法
const mutations: Mutations<State, IMutations> = {
  addTodoText(state, text) {
    const todo = {
      id: 0,
      text,
+      completed: false,
    };
    if (state.todos.length !== 0) {
      todo.id = state.todos[state.todos.length - 1].id + 1;
    }
    state.todos.push(todo);
  },
};
```

completedによってviewを変えるので、Todoコンポーネントを修正する。
completedがtrueだったらtextDecorationをline-throughとする。

```diff:src/components/Todo.vue
<template>
-  <li>{{text}}</li>
+  <li v-bind:style="{ textDecoration: textDecoration }" >{{text}}</li>
</template>

<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { TodoItem } from "../models/TodoItem";

@Component
export default class Todo extends Vue {
  @Prop()
  public todo: TodoItem;

  get text() {
    return this.todo.text;
  }

+  get textDecoration() {
+    return this.todo.completed ? "line-through" : "none";
+  }
}
</script>

```

これで、stateで保持されるtodoのcompletedがtrueのとき取り消し線がつく。
一時的にsrc/store/todo/index.tsのcompleted: falseをtrueに変えて動作確認をする。

[この時点のソース](https://github.com/hibohiboo/wasureta/tree/16fe695bd270ef99a84778ed8a75f67a8d0a74a4/takuan/plugins/room)  

#### actionからcompleted要素を操作する
次に、action経由で取り消し線のON/OFFを行うために、actionとmutationの作成を行う。
必要となるのはidだけ。
addTodoTextとasyncSetTodoTextが長く感じてきたので、addTodoにリネームを行った。


mutationではpayloadで受け取ったidのcompletedを反転させる。

```diff:src/store/todo/types.ts
export interface IMutations {
  addTodo: string;
+  toggleTodo: number;
}
export interface RootMutations {
  'todo/addTodo': IMutations['addTodo'];
+  'todo/toggleTodo': IMutations['toggleTodo'];
}
```

```diff:src/store/todo/index.ts
const mutations: Mutations<State, IMutations> = {
  addTodo(state, text) {
    const todo = {
      id: 0,
      text,
      completed: false,
    };
    if (state.todos.length !== 0) {
      todo.id = state.todos[state.todos.length - 1].id + 1;
    }
    state.todos.push(todo);
  },
+  toggleTodo(state, id) {
+    const target = state.todos.find(todo => todo.id === id);
+    if (target === undefined) {
+      throw new Error(`not found id:${id}`);
+    }
+    target.completed = !target.completed;
+  }
};
```

actionはmutationを呼び出すだけ。

```diff:src/store/todo/types.ts
export interface IActions {
  addTodo: string;
+  toggleTodo: number;
}

export interface RootActions {
  'todo/addTodo': IActions['addTodo'];
+  'todo/toggleTodo': IActions['toggleTodo'];
}
```

```diff:src/store/todo/index.ts
const actions: Actions<
  State,
  IActions,
  IGetters,
  IMutations
> = {
  async addTodo({ commit }, text) {
    commit('addTodo', text);
  },
+  async toggleTodo({ commit }, id) {
+    commit('toggleTodo', id);
+  },
};
```

main.tsからtoggleTodoを使って動作確認。

```diff:src/main.ts
store.dispatch('todo/addTodo', 'Hello World!');
store.dispatch('todo/addTodo', 'Hello World!!');
+ store.dispatch('todo/toggleTodo', 0);

new Vue({
  render: (h: (app: any) => Vue.VNode) => h(App),
  store,
}).$mount('#app');
```

[この時点のソース](https://github.com/hibohiboo/wasureta/tree/16fe695bd270ef99a84778ed8a75f67a8d0a74a4/takuan/plugins/room)  

### 2. クリックしてcompletedの値を変える

コンポーネントでdispatchが使えるように型を拡張。

```diff:src/types/shims-vuex.d.ts
  type RootGetters = Todo.RootGetter; // 増えたら、& Hoge.RootGetter のように、Intersection Typesで連結していく。
+  type RootMutations = Todo.RootMutations;
+  type RootActions = Todo.RootActions;
  interface ExStore extends Store<{}> {
    // ここに拡張型を追加していく。
    getters: RootGetters,
+    commit: ExCommit<RootMutations>,
+    dispatch: ExDispatch<RootActions>
  }
```

```diff:src/components/Todo.vue
<template>
-  <li v-bind:style="{ textDecoration: textDecoration }" >{{text}}</li>
+  <li v-bind:style="{ textDecoration: textDecoration }" v-on:click="onClick">{{text}}</li>
</template>

<script lang="ts">
+ import * as Vuex from 'vuex';
import { Component, Prop, Vue } from 'vue-property-decorator';
import { TodoItem } from '../models/TodoItem';

@Component
export default class Todo extends Vue {
+   $store!: Vuex.ExStore;

  @Prop()
  public todo: TodoItem;

  get text() {
    return this.todo.text;
  }

  get textDecoration() {
    return this.todo.completed ? 'line-through' : 'none';
  }

+   onClick() {
+     this.$store.dispatch('todo/toggleTodo', this.todo.id);
+   }
}
</script>
```

「Toggle Todo」機能が完成。

[この時点のソース](https://github.com/hibohiboo/wasureta/tree/da36c0fb1087bc311c886f4f19c918c26726e587/takuan/plugins/room)  

## Filter
### 概要

以下のフィルターによって表示を変更する

* SHOW_ALL: 全部表示
* SHOW_COMPLETED: 完了しているtodoのみ
* SHOW_ACTIVE: 完了していないtodoのみ

#### 実装の順番

1. フィルターの値をstore(state)に格納
1. フィルターの値によってviewを変更（手動でフィルターを操作して動作確認）
1. リンクをクリックしてフィルターを操作してviewを変更 

### 1. フィルターの値をstore(state)に格納

まずは定義から作成。

```diff:src/store/todo/types.ts
+ // String Lieral Typesでとりうる値を限定
+ export type VisibilityFilter = 'SHOW_ALL' | 'SHOW_COMPLETED' | 'SHOW_ACTIVE';

export interface State {
  todos: TodoItem[];
+  visibilityFilter: VisibilityFilter;
}
```

```diff:src/store/todo/index.ts
- const todoState: State = ({ todos: []} as State);
+ const todoState: State = ({ todos: [], visibilityFilter: 'SHOW_ALL' } as State);
```

visibilityStateを取得するgetterを追加する。

```diff:src/store/todo/types.ts
export interface IGetters {
  todos: TodoItem[];
  todosCount: number;
+  visibilityFilter: VisibilityFilter;
}

export interface RootGetter {
  'todo/todos': IGetters['todos'];
  'todo/todosCount': IGetters['todosCount'];
+  'todo/visibilityFilter': IGetters['visibilityFilter'];
}
```

```diff:src/store/todo/index.ts
const getters: Getters<State, IGetters> = {
  todos: state => state.todos,
  todosCount: state => state.todos.length,
+  visibilityFilter: state => state.visibilityFilter
};
```

mutationはfilterを更新する。

```diff:src/store/todo/types.ts
export interface IMutations {
  addTodo: string;
  toggleTodo: number;
+  setVisibilityFilter: VisibilityFilter;
}

export interface RootMutations {
  'todo/addTodo': IMutations['addTodo'];
  'todo/toggleTodo': IMutations['toggleTodo'];
+  'todo/setVisibilityFilter': IMutations['setVisibilityFilter'];
}
```

```diff:src/store/todo/index.ts
const mutations: Mutations<State, IMutations> = {
  
  ...

+  setVisibilityFilter(state, filter) {
+    state.visibilityFilter = filter;
+  }
};
```

actionはmutationを呼び出す。

```diff:src/store/todo/types.ts
export interface IActions {
  addTodo: string;
  toggleTodo: number;
+  setVisibilityFilter: string;
}

export interface RootActions {
  'todo/addTodo': IActions['addTodo'];
  'todo/toggleTodo': IActions['toggleTodo'];
+  'todo/setVisibilityFilter': IActions['setVisibilityFilter'];
}
```

```diff:src/store/todo/index.ts
const actions: Actions<

  ...

+  async setVisibilityFilter({ commit }, filter) {
+    switch (filter) {
+      case 'SHOW_ALL':
+      case 'SHOW_COMPLETED':
+      case 'SHOW_ACTIVE':
+        commit('setVisibilityFilter', filter);
+        return;
+      default:
+        throw new Error(`cannot set filter:${filter}`)
+    }
+  }
};
```



#### 手動で動作確認
正しく格納できるか確認。

```ts:src/main.ts
console.log('before', store.getters['todo/visibilityFilter']);
store.dispatch('todo/setVisibilityFilter', 'SHOW_COMPLETED');
console.log('after', store.getters['todo/visibilityFilter']);
```

storeの初期値としてSHOW_ALLが格納されており、setVisibilityFilterによって、
値が変更されていることを確認する。

[この時点のソース](https://github.com/hibohiboo/wasureta/tree/3361cd303f4422743b202f58421ff297f68be205/takuan/plugins/room)  

確認するときに、storeのコード補完が効いていなかったので修正。

```diff:src/store/index.ts
import Vue from 'vue';
import * as Vuex from 'vuex';
import todo from './todo';
// ルートコンポーネントに store オプションを指定することですべての子コンポーネントにストアを注入。
Vue.use(Vuex);

export default new Vuex.Store({
  modules: { todo },
- });
+ }) as Vuex.ExStore;
```

### 2. フィルターの値によってviewを変更（手動でフィルターを操作して動作確認）

```diff
const getters: Getters<State, IGetters> = {
-  todos: state => state.todos,
+  todos: state => {
+    switch (state.visibilityFilter) {
+      case 'SHOW_ALL':
+        return state.todos;
+      case 'SHOW_ACTIVE':
+        return state.todos.filter(todo => !todo.completed);
+      case 'SHOW_COMPLETED':
+        return state.todos.filter(todo => todo.completed);
+    }
+  },
  todosCount: state => state.todos.length,
  visibilityFilter: state => state.visibilityFilter,
};
```

[この時点のソース](https://github.com/hibohiboo/wasureta/tree/e20f9e940edfdaa575b2b4ffb0ae1580413527ce/takuan/plugins/room)  

### 3. リンクをクリックしてフィルターを操作してviewを変更
フィルターの値を格納することと、その値によって表示を変更することができた。
手動ではなくリンクをクリックしてフィルターの値を変え、viewに反映するとこを作っていく。

#### とりあえず、リンクを表示させる

<slot></slot>はコンポーネントの中身を取得。
Linkコンポーネントを使うときの、<Link>xxx</Link>のxxx。

```vue:src/components/Link.vue
<template>
  <a href="#">
    <slot></slot>
  </a>
</template>
<script lang="ts">
import { Prop, Component, Vue } from "vue-property-decorator";
@Component
export default class Link extends Vue {}
</script>
```

LinkコンポーネントはFooterコンポーネントで表示。
FooterコンポーネントはAppコンポーネントに表示。

```vue:src/components/Footer.vue
<template>
  <p>
    Show:
    <Link>All</Link>,
    <Link>Active</Link>,
    <Link>Completed</Link>
  </p>
</template>
<script lang="ts">
import { Prop, Component, Vue } from "vue-property-decorator";
import Link from "./Link.vue";
@Component({ components: { Link } })
export default class Footer extends Vue {}
</script>
```

```diff:src/App.vue
<template>
  <div id="app">
    <AddTodo />
    <TodoList :todos="todos" />
+     <Footer />
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import * as Vuex from "vuex";
import TodoList from "./components/TodoList.vue";
import AddTodo from "./components/AddTodo.vue";
+ import Footer from "./components/Footer.vue";

@Component({
  components: {
    TodoList,
    AddTodo,
+    Footer
  }
})
export default class App extends Vue {
```

これで、とりあえずリンクが表示された。
[この時点のソース](https://github.com/hibohiboo/wasureta/tree/85c88effc93f007449a3b0859e9789a568f44136/takuan/plugins/room)  


#### Linkコンテナをクリックしたときにdispatchを呼び出す。

```diff
<template>
-  <a href="#">
+  <a href="#" v-on:click="onClick">
    <slot></slot>
  </a>
</template>
<script lang="ts">
import { Prop, Component, Vue } from "vue-property-decorator";
import { VisibilityFilter } from "../store/todo/types";
+ import * as Vuex from "vuex";

@Component
export default class Link extends Vue {
+   $store!: Vuex.ExStore;

  @Prop()
  public filter: VisibilityFilter;

+   onClick() {
+     this.$store.dispatch("todo/setVisibilityFilter", this.filter);
+   }
}
</script>
```

```diff:src/components/Footer.vue
<template>
  <p>
    Show:
-    <Link>All</Link>,
-    <Link>Active</Link>,
-    <Link>Completed</Link>
+    <Link filter="SHOW_ALL">All</Link>,
+    <Link filter="SHOW_ACTIVE">Active</Link>,
+    <Link filter="SHOW_COMPLETED">Completed</Link>
  </p>
</template>
```

これで一連の流れが実装できた。

* リンクを押すとonClick()が呼び出される
* その中でdispatchが呼び出される
* storeに格納されているフィルターの値が更新される
* その値にしたがってviewが書き換わる

[この時点のソース](https://github.com/hibohiboo/wasureta/tree/6d11ac1635ac37c082fbae027b8a8f2ec85a5b30/takuan/plugins/room)  

## activeな状態のリンクを押せないようにする

```diff:src/componetns/Link.vue
<template>
+  <span v-if="active">
+    <slot></slot>
+  </span>
-  <a href="#" v-on:click="onClick">
+  <a href="#" v-on:click="onClick" v-else>
    <slot></slot>
  </a>
</template>

<script lang="ts">
import { Prop, Component, Vue } from "vue-property-decorator";
import { VisibilityFilter } from "../store/todo/types";
import * as Vuex from "vuex";

@Component
export default class Link extends Vue {
  ...

+  get active() {
+    return this.$store.getters["todo/visibilityFilter"] === this.filter;
+  }
   ...
}
</script>
```

[この時点のソース](https://github.com/hibohiboo/wasureta/tree/6a0e2242ca2fc040c1a39e4ab20a9de4f5c8400e/takuan/plugins/room)  


### 参考
[Redux ExampleのTodo ListをはじめからていねいにVue.jsで(1)][*2-1]
[Redux ExampleのTodo Listをはじめからていねいに(2)][*2-2]
[Redux ExampleのTodo Listをはじめからていねいに(3)][*2-3]
[ToDoリストを作りながら学習しよう！][*2-4]

[*2-1]:https://qiita.com/hibohiboo/items/e3030350ecc83cb2c3bc
[*2-2]:https://qiita.com/xkumiyu/items/e7e1e8ed6a5d6a6e20dd
[*2-3]:https://qiita.com/xkumiyu/items/1ba476b8043b71561f52
[*2-4]:https://cr-vue.mio3io.com/tutorials/#%E3%81%93%E3%81%AE%E3%83%81%E3%83%A5%E3%83%BC%E3%83%88%E3%83%AA%E3%82%A2%E3%83%AB%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6