Vue3 todoアプリ作成② ~ Vuex vue-routerを触ってみる ~

## はじめに
謎todoアプリ作成 第2弾です

[Vue3 todoアプリ作成① ~ compositionAPI ~](https://qiita.com/nakamo-03/items/5f1a8283648f87ab0230) 
[Vue3 todoアプリ作成② ~ Vuex vue-routerを触ってみる ~](https://qiita.com/nakamo-03/items/f5a559d89264a6a2b011)  :point_left:今ここ
[Vue3 todoアプリ作成③ ~ tailwindcss 使ってみた ~ ](https://qiita.com/nakamo-03/items/23320776e7a80b4038e6)

今回は 実際に作成したTODOを完了、TODOの詳細に遷移させるようにします。

遷移にあたって vue-router
詳細画面にてデータを表示させるために Vuexを使用いたします。（使いたかっただけ）

### 環境
vite: 1.0.0-rc.1
vue: 3.0.0-rc.2
vue-router: 4
vuex: 4.0.0-rc.2

## 完了ボタン実装
todoが完了した時の処理を行います。

1. 完了ボタンコンポーネント作成
2. 登録同様　カスタムイベント作成
3. spliceメソッドを使用し、配列のなかからindexが同じ要素を削除。

```terminal:terminal
// 完了ボタンコンポーネント作成
$ touch ./src/components/CompleteButton.vue
```

```vue:components/CompleteButton.vue
<template>
  <button @click="completed">完了</button>
</template>

<script lang="ts">
import { defineComponent, SetupContext } from 'vue'

export default defineComponent({
  name: 'CompleteButton',
  props: {
    index: {
      type: Number
    }
  },

  setup(props, context: SetupContext) {

    const completed = (e) =>{
      context.emit('complete-todo', props.index);
    };

    return { completed, props };
  }
});
</script>
```

```vuejs:TodoList.vue
<template>
  <div>
    <ul>
      <li v-for="(todo, index) in state.todoList" :key="todo.todo">
          {{ todo.todo }}
        <complete-button :index="index" @complete-todo="completeTodoAction" />
      </li>
    </ul>
    <Todo-input @add-todo="addTodoAction"/>
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive} from 'vue'
import TodoInput from '../components/TodoInput.vue'
import CompleteButton from '../components/CompleteButton.vue'

export default defineComponent({
  name: 'TodoList',
  components: {
    TodoInput,
    CompleteButton
  },

  setup() {
    const state = reactive<{todoList: Array<{todo: string}>}>({
      todoList: []
    });

    const addTodoAction = (value: string) => {
      state.todoList.push({todo: value})
    };

    const completeTodoAction = (targetIndex) => {
      state.todoList.splice(targetIndex, 1)
    }

    return { state, completeTodoAction, addTodoAction };
  }
});
</script>
```

#### 反映
<a href="https://gyazo.com/89d87e50f215accecd42785de8f61154"><img src="https://i.gyazo.com/89d87e50f215accecd42785de8f61154.gif" alt="Image from Gyazo" width="318"/></a>

## vue-router Vuex 設定
TODO詳細を実装するにあたって、
vue-router, Vuexを導入していく（使いたかったから。:pray:）

### インストール/ファイル作成

```terminal:terminal
$ npm i vue-router@4.0.0-rc.2 vuex@4.0.0-rc.2
//ルーティング設定ファイル作成
$ touch router.js
//ディレクトリ移動
$ cd src
$ mkdir store
// ストア設定ファイル
$ touch ./store/index.ts
```

### ファイル設定

```js:router.js
import { createRouter, createWebHistory } from 'vue-router';

export const router =  createRouter({
  history: createWebHistory(),
  routes: []
})

export default router

```

#### 注意点
- `new Router()` => `createRouter()`へ

```typescript:index.ts
import { createStore } from "vuex";

export const store = createStore({
  state: {},
  mutations: {},
  actions: {},
  modules: {}
});

export default store
```

#### 注意点
- `new Vuex.Store()` => `createStore()`へ

```js:main.js
import { createApp } from 'vue'
import App from './App.vue'
// import router from './router'  後で追加
import store from './store/index'

import './index.css'

const app = createApp(App)
app.use(store)
// app.use(router); 後で追加
app.mount('#app')
```

## TODOのデータをストアへ
ページの遷移先でもデータを使用したいので、
ストアでデータを保持し、ページ遷移先でも取得可能な状態にする。

```typescript:index.ts
import { createStore } from "vuex";

export interface State {
  todoList: Array<{ id: number, todo: string }>
}
export const store = createStore<State>({
  state: {
    todoList: []
  },
  mutations: {
    increment(state, { id, value }) {
      state.todoList.push({ id: id, todo: value, comment: [] })
    },
    complete(state, targetIndex) {
      state.todoList.splice(targetIndex, 1)
    },

  },
  actions: {},
  modules: {}
});

export default store

```


```vuejs:TodoList.vue
<template>
  <div>
    <ul>
      <li v-for="(todo, index) in state.todoList" :key="todo.todo">
          {{ todo.todo }}
        <complete-button :index="index" />
      </li>
    </ul>
    <Todo-input/>
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive} from 'vue'
import { useStore } from 'vuex' //追加
import TodoInput from '../components/TodoInput.vue'
import CompleteButton from '../components/CompleteButton.vue'

export default defineComponent({
  name: 'TodoList',
  components: {
    TodoInput,
    CompleteButton
  },

  setup() {
    const store = useStore() //追加
    const state = reactive<{todoList: Array<{todo: string}>}>({
      todoList: store.state.todoList //追加
    });
    // 削除
     //const addTodoAction = (value: string) => {
      // state.todoList.push({todo: value})
     // };

     // const completeTodoAction = (targetIndex) => {
       // state.todoList.splice(targetIndex, 1)
     // }

    return { state };
  }
});
</script>
```

```vuejs:TodoInput.vue
<template>
  <div>
    <input v-model="todoRef" type="text">
    <button @click="add">登録</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, SetupContext, ref } from 'vue'
import { useStore } from 'vuex' //追加


export default defineComponent({
  name: 'TodoInput',
  setup(props, context: SetupContext) {
    const store = useStore()
    const todoRef = ref<string>('')

     //追加 storeの中の一番最後のidを取得し、 + 1
     const lastId = () => {
      if(store.state.todoList.length === 0){
        return 1
      } else {
        const lastItem = store.state.todoList.slice(-1)[0]
        return lastItem.id + 1
      }
    }

    const add = (e) =>{
      const id = lastId() //追加
      const value = todoRef.value //追加
      store.commit('increment', {id, value}) //追加
      todoRef.value = ''
    };

    return { add, todoRef};
  }
});
</script>

```


```vue:components/CompleteButton.vue
<template>
  <button @click="completed">完了</button>
</template>

<script lang="ts">
import { defineComponent, SetupContext } from 'vue'
import { useStore } from 'vuex'

export default defineComponent({
  name: 'CompleteButton',
  props: {
    index: {
      type: Number
    }
  },

  setup(props, context: SetupContext) {
    const store = useStore()
    const completed = (e) =>{
      const targetIndex = props.index
      store.commit('complete', targetIndex)
    };

    return { completed, props };
  }
});
</script>
```

#### 注意点
- `import { useStore } from 'vuex'`をimportし `const store = useStore()`このように変数などにいれてstoreを扱えるようになっている。


これでtodo作成から完成まで　storeを使用して、実装できました。
次は詳細画面を実装してきます。

## 詳細画面作成

詳細画面と詳細画面までのルーティング作成。

```terminal:terminal
$ cd src
$ mkdir views
$ touch ./views/Detail.vue
```

```js:main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store/index'

import './index.css'

const app = createApp(App)
app.use(store)
app.use(router)
app.mount('#app')
```

```vue:Detail.vue
//詳細ページ
<template>
</template>
<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'Detail',

  setup() {
  }
});
</script>


```

```js:router.js
import { createRouter, createWebHistory } from 'vue-router';

import Detail from './views/Detail.vue'

export const router =  createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/detail/:id',
      name: 'detail',
      component: Detail
    }
  ]
})

export default router
```

詳細画面作成と詳細画面へのルーティングを作成できた。
次はホーム画面と、そのルーティングを作成していく。

## ホーム画面作成
ホーム画面はホーム用ファイルを作成し、
todo作成コンポーネント達をそこで読み込むように設定

```terminal:terminal
$ cd src
$ touch ./views/Home.vue
```

```vuejs:App.vue
<template>
  <router-view></router-view>
</template>

<script>
import { defineComponent } from 'vue'
export default defineComponent({
  name: 'App',
})
</script>

```


```vuejs:views/Home.vue
<template>
  <div>
    <TodoList />
    <TodoInput />
  </div>
</template>
<script>
import TodoList from '../components/TodoList.vue'
import TodoInput from '../components/TodoInput.vue'

export default {
  name: "Home",
  components: {
    TodoList,
    TodoInput
  }
};
</script>


```

```vuejs:components/TodoList.vue
//TodoInputコンポーネント削除
<template>
  <ul>
    <li v-for="(todo, index) in state.todoList" :key="todo.todo">
        {{ todo.todo }}
      <complete-button :index="index" />
    </li>
  </ul>
</template>

<script lang="ts">
import { defineComponent, reactive} from 'vue'
import { useStore } from 'vuex'
import CompleteButton from '../components/CompleteButton.vue'

export default defineComponent({
  name: 'TodoList',
  components: {
    CompleteButton
  },

  setup() {
    const store = useStore()
    const state = reactive<{todoList: Array<{todo: string}>}>({
      todoList: store.state.todoList
    });
    return { state };
  }
});
</script>
```

```js:router.js
import { createRouter, createWebHistory } from 'vue-router';

import Home from './views/Home.vue'
import Detail from './views/Detail.vue'

export const router =  createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/detail/:id',
      name: 'detail',
      component: Detail
    }
  ]
})

export default router
```
これでホーム画面作成は完了です。
見た目は何も変わっていないと思われますが。

この設定をしない場合。todoがどの画面へ遷移しても登録できる状態になります。

## 詳細ボタン作成/遷移
後少しで完成です　頑張りましょう。。

```vuejs:components/TodoList.vue
<template>
  <ul>
    <li v-for="(todo, index) in state.todoList" :key="todo.todo">
        {{ todo.todo }}
      //詳細画面へのリンク追加 toの中で遷移先 params判断。
      <router-link :to="{name: 'detail', params: {id: todo.id}}">詳細</router-link>
      <complete-button :index="index" />
    </li>
  </ul>
</template>

<script lang="ts">
import { defineComponent, reactive} from 'vue'
import { useStore } from 'vuex'
import CompleteButton from '../components/CompleteButton.vue'

export default defineComponent({
  name: 'TodoList',
  components: {
    CompleteButton
  },

  setup() {
    const store = useStore()
    const state = reactive<{todoList: Array<{todo: string}>}>({
      todoList: store.state.todoList
    });
    return { state };
  }
});
</script>
```

これで遷移は完了。
次は遷移先でデータの表示を行う。

```vue:Detail.vue
<template>
  <div>
    <template v-if="detailItem">
      <div>
        <h2>
          <p>タイトル</p>
          <p>{{ detailItem.todo }}</p>
        </h2>
      </div>
    </template>
    <template v-else>
      <div>
        <h2>データが存在しません</h2>
        <router-link to="/">TOPへ</router-link>
      </div>
    </template>
  </div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { useStore } from 'vuex'
import { useRoute} from 'vue-router'
import todoInput from '../components/todoInput.vue'

export default defineComponent({
  name: 'Detail',

  components: {
    todoInput
  },
  setup() {
    const store = useStore()
    const route = useRoute()
    const routerId = route.params.id
    const detailItem = store.state.todoList.find(e => String(e.id) === routerId)
    return { detailItem }
  }
});
</script>
```

#### 注意点
- `this.$route`について。`vue3`では`import { useRoute} from 'vue-router'` インポートを行い`const route = useRoute()`とすると `route.params`のようにrouteに関する情報にアクセスできます。
- リロードなどを行うとstoreの情報は初期化するため、詳細画面でリロードを行なった場合データは無くなります。なのでデータがなかった時ようのviewをv-ifで条件分岐させ表示。

### 反映
<a href="https://gyazo.com/598a101de592c22126c2ae4221a771b0"><img src="https://i.gyazo.com/598a101de592c22126c2ae4221a771b0.gif" alt="Image from Gyazo" width="316"/></a>

## おわり
今回も長くなってしまいました。
vue-router vuexの初級をクリアした感覚です。
次回はtailwindcssをぶっ込んでいこうと思います。

## 参考
[vue-router](https://next.router.vuejs.org/)
[Vuex](https://next.vuex.vuejs.org/)
[既存のVue.jsプロジェクトをVue 3へ移行したときに必要だった修正まとめ](https://qiita.com/laineus/items/d1f1f7972f521556a788#vuex)