LoginSignup
7
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-10-11

開発環境

  • 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のインストール方法については解説しません。
知りたい方はこちらをクリックしてください。

以下のコマンドをターミナルに入力し、プロジェクトを作成します。

terminal
vue create プロジェクト名

選択肢が3つ現れるので、一番下の"Manually select features"をクリック。
以降10回選択をします。
以下に従って設定してください。
スクリーンショット 2022-10-11 午後12.45.32.png

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

②不要なファイルを削除

まず不要なファイルを削除していきます。
ファイルを削除する手順は以下の3つです。

  • routerから削除
  • App.vueから削除
  • ファイルの削除

routerから削除

以下のコードを削除してください。

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

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から削除

以下のコードを削除してください。

src/App.vue(削除前)
<template>
  <nav>
    <!-- ここから削除 -->
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link>|
    <!--ここまで削除-->
  </nav>
  <router-view/>
</template>

<style>
</style>

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の設定

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にリンクを作成

以下のコードを追記します。

src/App.vue
<template>
  <na
    <router-link to="/todo">ToDo</router-link> | <!--追記-->
  </nav>
  <router-view/>
</template>

<style>
</style>

TodoListViewファイルを作成

フォーム入力と、入力した内容を反映できるよう、テンプレート部分の記載を行います。
ファイルを作成し、以下のコードを記載してください。

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つです。

  • 入力した内容を双方向バインディング
  • 配列に入力した内容を追加
  • 追加後、入力した内容を削除
  • 要素の削除

ちなみに、要素を追加する際、バリデーションをつけたい場合はこちらをご覧ください。

以下のコードを記載してください。

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 メソッドに渡す。

順に解説していきます。

ちなみにストアファイルが膨らむのが嫌で、モジュール分割して管理したいよーという方は、こちらをご覧ください

型付けされた InjectionKey を定義

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 インスタンスに渡す

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 メソッドに渡す

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で管理

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に実行させたい処理を記載

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も読んであげます。

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>

③ローカルストレージに保存

以下を追記してください。

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対応されていないので、使えるようにする必要があります。
ファイルを作成し、以下を追記してください。

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

tsconfig.jsonに定義を追記

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

tsconfig.json
    "paths": {
      "@/*": [
        "src/*"
      ],
      "vuedraggable": [
        "src/types/vuedraggable"
      ]
    },

package.jsonにvuedraggable.nextを追記

package.json
  "dependencies": {
    "vue": "^3.2.13",
    "vue-router": "^4.0.3",
    "vuedraggable": "^4.1.0",
    "vuex": "^4.0.0"
  },

追記したら、npm installをターミナルに入力してください。

draggableをインポート

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の反映
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を送る

以下のコードを追記します。

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を呼ぶ

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>

終わりに

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

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

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

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2