【Composition API】Kanban-Board作成してみた。【Script SetUp】【Typescript】【Vuex】

# 開発環境
* MacBook Air (M1, 2020)
* npm 8.18.0
* "vue": "^3.2.13"
* "vuedraggable": "^4.1.0"


# 本記事の内容
本記事では、以下の3項目でKanban-Boardを作成することを目指します。
* 環境構築
* TodoLists作成
* TodoCardsの作成

# 環境構築
この章では以下を行います。
①vue cliでプロジェクトの作成
②不要なファイルを削除
③新たなファイルを作成


# ①Vue CLIでプロジェクトの作成
ここではVue CLIのインストール方法については解説しません。
知りたい方は[こちら](https://qiita.com/ITmanbow/items/9ea7d3f5a9219d760d0c)をクリックしてください。

以下のコマンドをターミナルに入力し、プロジェクトを作成します。
```:terminal
vue create プロジェクト名
```
選択肢が3つ現れるので、一番下の"Manually select features"をクリック。
以降10回選択をします。
以下に従って設定してください。
![スクリーンショット 2022-10-11 午後12.45.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1973643/bb48c1a9-8113-6149-212d-4505c6faad29.png)

これでプロジェクトの作成ができました。

# ②不要なファイルを削除
まず不要なファイルを削除していきます。
ファイルを削除する手順は以下の3つです。
* routerから削除
* App.vueから削除
* ファイルの削除

## routerから削除
以下のコードを削除してください。
```typescript:src/router/index.ts(削除前)
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
//ここから削除
import HomeView from '../views/HomeView.vue'
import AboutView from '../views/AboutView.vue'
//ここまで削除

const routes: Array<RouteRecordRaw> = [
//ここから削除
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/about',
    name: 'about',
    component: AboutView
  },
//ここまで削除
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

```
```typescript:src/router/index.ts(削除後)
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'

const routes: Array<RouteRecordRaw> = [
    //新しいrouterを作成するときのために取っておく
  {
    path: '/',
    name: '',
    component: 
  },
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

```
## App.vueから削除
以下のコードを削除してください。
```vuejs:src/App.vue(削除前)
<template>
  <nav>
    <!-- ここから削除 -->
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link>|
    <!--ここまで削除-->
  </nav>
  <router-view/>
</template>

<style>
</style>

```
```vuejs:src/App.vue(削除後)
<template>
  <nav>
    <router-link to="/"></router-link> |
  </nav>
  <router-view/>
</template>

<style>
</style>

```
## ファイルを削除
以下のファイルを削除してください。
* src/views/HomeView.vue
* src/views/AboutView.vue
* src/components/HelloWorld.vue

# TodoListの作成
この項目では以下の3工程を行います。
①TodoListViewファイルの作成とメソッドの定義
② Vuexで管理
③ ローカルストレージに保存
④ draggableの実装


# ①TodoListViewファイルの作成とメソッドの定義
以下の手順で行います。
* ファイルの作成
* メソッドの定義

順に実装していきましょう。

## ファイルの作成
* routerの設定
* App.vueにリンクを作成
* TodoListView.vueファイルの作成

### routerの設定

```typescript:src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import TodoView from '../views/TodoListView.vue'　// 追記

const routes: Array<RouteRecordRaw> = [
   //ここから追記
  {
    path: '/todo',
    name: 'todo',
    component: TodoListView
  },
    //ここまで追記

]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

```
### App.vueにリンクを作成
以下のコードを追記します。
```vuejs:src/App.vue
<template>
  <na
    <router-link to="/todo">ToDo</router-link> |　<!--追記-->
  </nav>
  <router-view/>
</template>

<style>
</style>

```
### TodoListViewファイルを作成
フォーム入力と、入力した内容を反映できるよう、テンプレート部分の記載を行います。
ファイルを作成し、以下のコードを記載してください。
```vuejs:src/views/TodoListView.vue
<script setup lang="ts"> 
</script>

<template>
    <div>
        <form @submit.prevent="addList">
            <input type="text" id="description" v-model="todoList.description" />
            <input type="submit" value="submit" />
        </form>
        <hr/>
        <div v-for="todoList in todoLists" :key="todoList.id">
            <p>
                {{ todoList.description }}
                <span><button @click="removeList(element.id)">X</button></span>
            </p>
        </div>
    </div>
    </div>
</template>
```

## メソッドの定義
実行させたい処理は以下の3つです。
* 入力した内容を双方向バインディング
* 配列に入力した内容を追加
* 追加後、入力した内容を削除
* 要素の削除

ちなみに、要素を追加する際、バリデーションをつけたい場合は[こちら](https://qiita.com/tak8_al/items/933411d830b1ff089ae5)をご覧ください。

以下のコードを記載してください。
```vuejs:TodoView.vue
<!--typescriptを使用-->
<script setup lang="ts"> 
    //リアクティブにするためにref, reactiveを使用
    import { ref,reactive} from 'vue';
    //Todoリストの型を定義しておく。実装したい要素に対応する型を定義しておく。
    type TodoList {
        id: number;
        description: string;
        //後々この配列に要素を追加するため、記載しておく。
        todoCards: TodoCard[];
    };
    //todoCardsに対応する型を定義しておく。
    type TodoCard ={
      id: number;
      description: string;
    };
    //要素を配列で管理。変更に対応できるようリアクティブにする。
    const todoLists = ref<TodoList[]>([])
    //オブジェクトにはリアクティブを使用。使用できるときはreactiveを使用する。
    const todoList = reactive<TodoList>({
        id: 0,
        description: "",
        todoCards: []
    });
    //追加後、入力した内容を削除する処理
    const clearForm = () => {
        todoList.description = "";
    };
    //入力した内容を追加する処理
    const addList = () =>{
        //refの場合は取得するためにはvalueを記載する必要がある。
        todoLists.value.push({
            //idが追加されるたびに数が増えていく処理
            id: ++todoList.id,
            description:todoList.description
        })
        clearForm();
    }
    //要素を削除する処理
    const removeList = (list_id: number) =>{
        todoLists.value = todoLists.value.filter(list=> list.id !== list_id)
    }
    
</script>

<template>
    <div>
        <form　@submit.prevent="addList">
            <input type="text" id="description" v-model="todoList.description" />
            <input type="submit" value="submit"/>
        </form>
        <hr/>
        <div v-for="todoList in todoLists" :key="todo.id">
            <p>
                {{ todoList.description }}
                <span><button @click="removeList(element.id)">X</button></span>
            </p>
        </div>
    </div>
</template>
```
# ②Vuexで管理
* Typescriptを用いたVuex管理
* TodoListをVuexで管理
* ローカルストレージに保存

## Typescriptを用いたVuex管理
手順は以下の3つです。
* 型付けされた InjectionKey を定義 
* ストアをインストールする際に、型付けされた InjectionKey を Vue App インスタンスに渡す。 
* 型付けされた InjectionKey を useStore メソッドに渡す。

順に解説していきます。

ちなみにストアファイルが膨らむのが嫌で、モジュール分割して管理したいよーという方は、[こちら](https://qiita.com/tak8_al/items/17013a3e4288581cfc79)をご覧ください

### 型付けされた InjectionKey を定義 
```typescript:src/store/index.ts
import { InjectionKey } from 'vue';
import { createStore, Store, useStore as baseUseStore } from "vuex";

type TodoList = {
  id: number;
  description: string;
};

// storeのstateの型を定義
type State = {
  todoItems: TodoList[];
};

// storeをprovide/injectするためのキー
export const key: InjectionKey<Store<State>> = Symbol();

// store本体
export const store = createStore<State>({
  getters: {
  },
  state: {
  },
  mutations: {
  },
  actions: {
  }
});

// 独自の `useStore` 関数を定義
export const useStore = () => {
  return baseUseStore(key);
}

```
### InjectionKey を Vue App インスタンスに渡す
```typescript:src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
//追記
import { store, key } from './store'

createApp(App).use(store, key).use(router).mount('#app')

```
### InjectionKey を useStore メソッドに渡す

```vuejs:TodoListView.vue
<script setup lang="ts"> 
    import { ref,reactive, computed } from 'vue';
    //追記
    import { useStore } from '../store'
    //追記
    const store = useStore()
    
</script>
```
## TodoリストをVuexで管理
* stateで配列を管理
* mutationsに実行させたい処理を記載

### 配列をStateで管理
```typescript: src/store/index.ts
import { InjectionKey } from 'vue';
import { createStore, Store, useStore as baseUseStore } from "vuex";
//この型定義は他でも使用するため、exportと記載し、他のコンポーネントでも使えるようにする。
export type TodoList = {
  id: number;
  description: string;
  todoCards: TodoCard[];
};

export type TodoCard ={
  id: number;
  description: string;
}

// stateの型定義
type State = {
  todoLists: TodoList[];
};

export const key: InjectionKey<Store<State>> = Symbol();

export const store = createStore<State>({
  getters: {
  },
  state: {
    todoLists: [],
  },
  mutations: {
  },
  actions: {
  }
});

export const useStore = () => {
  return baseUseStore(key);
}
```

### mutationsに実行させたい処理を記載

```typescript: src/store/index.ts
import { InjectionKey } from 'vue';
import { createStore, Store, useStore as baseUseStore } from "vuex";

type TodoList = {
  id: number;
  description: string;
};

type State = {
  todoLists: TodoList[];
};

export const key: InjectionKey<Store<State>> = Symbol();

export const store = createStore<State>({
//stateを取得する際は、gettersを通じて取得するものとするため、gettersも定義。
  getters: {
    todoLists: state => state.todoLists,
  },
  state: {
    todoLists: [],
  },
//実行したいメソッドを定義
  mutations: {
    addList(state, todoList: TodoList){
      state.todoLists.push(todoList)
    },
    removeList(state, list_id: number){
      state.todoLists = state.todoLists.filter(list=> list.id !== list_id)
    },
  },
//mutationsを呼ぶ際は、actionsを経由して呼ぶこととするため、actionsも定義。
  actions: {
    addList({commit}, todoList: TodoList) {
      commit('addList', todoList)
    },
    removeList({commit}, list_id: number) {
      commit('addList', number)
    },
  }
});

export const useStore = () => {
  return baseUseStore(key);
}
```
テンプレート側でactionsも読んであげます。
```vuejs: src/views/TodoListView.vue
<script setup lang="ts"> 
    import { reactive, computed } from 'vue';
    //先ほどexportと記載した型定義もインポート
    import { useStore, TodoList } from '../store'

    const todoList = reactive<TodoList>({
        id: 0,
        description: "",
        todoCards: []
    });

    const clearForm = () => {
        todoList.description = "";
    };

    const store = useStore()
    //Vuexのactionsを呼ぶ処理
    const addList = () =>{
        store.dispatch('addList', {
            id: ++todoList.id,
            description:todoList.description,
            todoCards: todoList.todoCards
        });
        clearForm();
    }
    const removeList = (list_id: number) =>{
        store.dispatch('removeList',list_id)
    }
    //stateで管理している配列をgettersを使用して取得する。
//stateで取得しても良いが、ここでは、stateから取得する際はgettersを使うものとする。
    const todoLists = computed(()=>{
    return store.getters.todoLists
    })
</script>

<template>
    <div>
        <form @submit.prevent="addList">
            <input type="text" id="description" v-model="todoList.description" />
            <input type="submit" value="submit"/>
        </form>
        <div v-for="todoList in todoLists" :key="todoList.id">
            <p>
                {{ todoList.description }}
                <span><button @click="removeList(todoList.id)">X</button></span>
            </p>
        </div>
    </div>
</template>

```
# ③ローカルストレージに保存
以下を追記してください。

```typescript:src/store/index.ts
import { InjectionKey } from 'vue';
import { createStore, Store, useStore as baseUseStore } from "vuex";

export type todoList = {
  id: number;
  description: string;
};

export type State = {
  todoLists: todoList[];
};

export const key: InjectionKey<Store<State>> = Symbol();
//追記
const savedTodos = localStorage.getItem('todos')

export const store = createStore<State>({
  getters: {
  },
  state: {
    //追記
    todoLists: savedTodos ? JSON.parse(savedTodos): [],
  },
  mutations: {
  },
  actions: {
  }
});
//追記
store.subscribe((mutation, state) => {
//保存名と保存するものを設定。保存する際はJSON形式に置き換える必要がある。
  localStorage.setItem('todos', JSON.stringify(state.todoLists))
})
export const useStore = () => {
  return baseUseStore(key);
}
```

# ④draggableの実装
手順は以下の4工程です。
* ファイルの作成
* tsconfig.jsonに定義を追記
* package.jsonにvuedraggable.nextを追記
* draggableをインポート

## ファイルの作成
Vue-Draggableは TypeScript対応されていないので、使えるようにする必要があります。
ファイルを作成し、以下を追記してください。

```ts:types/vuedraggable.d.ts
declare module 'vuedraggable'
```

## tsconfig.jsonに定義を追記
tsconfig.jsonのcompilerOptionsのpathsに定義を以下を追加します

```json:tsconfig.json
    "paths": {
      "@/*": [
        "src/*"
      ],
      "vuedraggable": [
        "src/types/vuedraggable"
      ]
    },
```
## package.jsonにvuedraggable.nextを追記
```json:package.json
  "dependencies": {
    "vue": "^3.2.13",
    "vue-router": "^4.0.3",
    "vuedraggable": "^4.1.0",
    "vuex": "^4.0.0"
  },
```
追記したら、npm installをターミナルに入力してください。

## draggableをインポート
```vuejs:src/views/TodoListView.vue
<script setup lang="ts"> 
    import { reactive, computed } from 'vue';
    import { useStore, TodoList } from '../store'
    //追記
    import draggable from 'vuedraggable';

    //省略

    //vuexにおいてv-modelの変更をstateに反映させたい場合は、getとsetを用いる。
    const todoLists = computed({
        //変更を反映させたい配列を取得
        get: (): TodoList[] => store.getters.todoLists,
        //変更された状態がvalとなる。
        set: (val: TodoList[]) => {
            store.dispatch('dragList', val)
        }
    })
</script>

<template>
    <div>
        <form　 @submit.prevent="addList">
            <input type="text" id="description" v-model="todoList.description" />
            <input type="submit" value="submit" />
        </form>
        <p>{{todoList.description}}</p>
        <hr/>
        <!--v-modelに配列を入れることで、順番の変更を反映させる。groupに名前を記載することで、同じgroupのみ入れ替えを可能にする。-->
        <draggable v-model="todoLists" group="list" item-key="id">
            <template #item="{element}">
                <div>
                    {{element.description}}
                    <button @click="removeList(element.id)">X</button>
                </div>
            </template>
        </draggable>
    </div>
</template>
```
# todoCardsの作成

todoCardsの作成の工程は以下の3つです。
①実行したい処理をstore/index.tsに追記
②propsでtodoListのidを送る
③actionsを呼ぶ

# ①実行したい処理をstore/index.tsに追記
実行させたい処理は以下の3つです。
* 配列への要素の追加
* 要素の削除
* Drag&Dropの反映
```typescript:src/store/index.ts
import { InjectionKey } from 'vue';
import { createStore, Store, useStore as baseUseStore } from "vuex";

export type TodoList = {
  id: number;
  description: string;
  todoCards: TodoCard[]
};

export type TodoCard ={
  id: number;
  description: string;
}

type State = {
  todoLists: TodoList[];
};

export const key: InjectionKey<Store<State>> = Symbol();

const savedTodos = localStorage.getItem('todos')

export const store = createStore<State>({
  getters: {
    todoLists: state => state.todoLists,
    //idからそのidに該当するオブジェクトの配列を取得する処理
    todoCard: state => (list_id:number) => {
      const todoList = state.todoLists.find(todoList=>todoList.id === list_id)
      //Objectがundefineの可能性があるため、undefineの場合は何も返さない。todoListがある場合は、todoCardsを返すよう設定。
      if (!todoList) {
        return
      }
      return todoList.todoCards
    }
  },
  state: {
    todoLists: savedTodos ? JSON.parse(savedTodos): [],
  },
  mutations: {
    addList(state, todoList: TodoList){
      state.todoLists.push(todoList)
    },
    removeList(state, list_id: number){
      state.todoLists = state.todoLists.filter(list=> list.id !== list_id)
    },

    dragList(state, newTodoLists: TodoList[]) {
      state.todoLists = newTodoLists
    },

    //受け取ったidのtodoListのcardsに要素を追加する処理
    addCard(state, payload: {list_id: number, todoCard: TodoCard}){
      const todoList = state.todoLists.find(list=>list.id === payload.list_id)
      if (!todoList) {
        return
      }
      return todoList.todoCards.push(payload.todoCard)
    },
    //todoListとtodoCardのidを受け取り、受け取ったidのtodoCardを削除する処理
    removeCard(state, payload: {card_id: number, list_id: number}){
      const todoList = state.todoLists.find(list=>list.id === payload.list_id)
      if (!todoList) {
        return
      }
      return todoList.todoCards = todoList.todoCards.filter(card => card.id !== payload.card_id)
    },
    //Drag&Dropを反映
    dragCard(state, payload: {val: TodoCard[], list_id: number}){
      const todoList = state.todoLists.find(list=>list.id === payload.list_id)
      if (!todoList) {
        return
      }
      return todoList.todoCards = payload.val
    },
  },
  actions: {
    addList({commit}, todoList:TodoList) {
      commit('addList', todoList)
    },
    removeList({commit}, list_id: number) {
      commit('removeList', list_id)
    },

    dragList({commit}, newTodoLists: TodoList[]) {
      commit('dragList', newTodoLists)
    },

    addCard({commit}, payload: {list_id:number, todoCard: TodoCard}) {
      commit('addCard', payload)
    },
    removeCard({commit}, card_id: number) {
      commit('removeCard', card_id)
    },

    dragCard({commit}, payload: {val: TodoCard[], list_id: number}) {
      commit('dragCard', payload)
    },
  }
});

store.subscribe((mutation, state) => {
  localStorage.setItem('todos', JSON.stringify(state.todoLists))
})
export const useStore = () => {
  return baseUseStore(key);
}
```
# ②propsでtodoListのidを送る
以下のコードを追記します。
```vuejs:src/views/TodoListView.vue
<script setup lang="ts"> 
    import { reactive, computed } from 'vue';
    import { useStore, TodoList } from '../store'
    import draggable from 'vuedraggable';
    //追記
    import TodoCard from '../components/TodoCard.vue';
    
    //以下省略

</script>
<template>
    <div>
        <form @submit.prevent="addList">
            <input type="text" id="description" v-model="todoList.description" />
            <input type="submit" value="submit" />
        </form>
        <p>{{todoList.description}}</p>
        <hr/>
        <draggable v-model="todoLists" group="list" item-key="id">
            <template #item="{element}">
                <div>
                    {{element.description}}
                    <button @click="removeList(element.id)">X</button>
                    <!--追記-->
                    <!--TodoCardコンポーネントにlist_idという名前でtodoListのidを渡す処理-->
                    <todo-card :list_id="element.id" />
                </div>
            </template>
        </draggable>
        <p>{{todoLists}}</p>
    </div>
</template>

```
# ③actionsを呼ぶ
```vuejs:src/views/TodoListView.vue
<script setup lang="ts">   
    import { reactive, computed } from 'vue';
    import { useStore, TodoCard } from "../store";
    import draggable from 'vuedraggable';
    //propsの型を定義
    interface Props {
        list_id: number
    }

    const props = defineProps<Props>()
    //store/index.tsで定義したTodoCardの型をインポート
    const todoCard = reactive<TodoCard>({
        id: 0,
        description:''
    })

    const clearForm = () => {
        todoCard.description = "";
    };

    const store = useStore()
    //受け取ったidのcardsにtodoCardを追加する処理
    const addCard = () => {
        store.dispatch('addCard', {
            todoCard:{
            //100000の整数をランダムに返す処理
            id: Math.floor(Math.random() * 100000), 
            description: todoCard.description
            },
            list_id: props.list_id
        });
        clearForm();
    }
    //list_idとcard_idを受け取り、todoCardを削除する処理
    const removeCard = (card_id: number) => {
        store.dispatch('removeCard', {
            card_id, 
            list_id: props.list_id
        })
    }
    todoListのidを受け取り、受け取ったtodoListのtodoCardの順番を反映させる処理
    const todoCards = computed<TodoCard[]>({
        get: () => store.getters.todoCard(props.list_id),
        set: val => {
            store.dispatch('dragCard', {
                val, 
                list_id: props.list_id
            })
        }
    })


</script>

<template>
    <div>
        <draggable v-model="todoCards" group="card" item-key="id">
            <template #item="{element}">
                <div>
                    {{element.description}}
                    <button @click="removeCard(element.id)">X</button>
                </div>
            </template>
        </draggable>
        <form @submit.prevent="addCard">
            <input type="text" id="description" v-model="todoCard.description" />
            <input type="submit" value="submit" />
        </form>
        <p>{{todoCards}}</p>
        <p>{{props.list_id}}</p>
        <hr/>
    </div>
</template>
```

# 終わりに
学ぶことがあって非常に苦労しました。
でもその分楽しかったです！
間違いがあれば、ご指摘のほどお願いします！

より詳しく見たい方は以下のリンクから飛べます。

* [FireBase Authenticationでログイン機能実装](https://qiita.com/tak8_al/items/545cb10e00f52541f1c7)
* [編集用モーダルの作成](https://qiita.com/tak8_al/items/d868026a0dbc3f96372c)
* [検索機能の実装](https://qiita.com/tak8_al/items/94cd5d3db9605fd86baa)
* [当日のタスクのみ表示](https://qiita.com/tak8_al/items/105226eb7988f55c2bef)

関心がある方は、ぜひご覧ください。