開発環境
- 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のインストール方法については解説しません。
知りたい方はこちらをクリックしてください。
以下のコマンドをターミナルに入力し、プロジェクトを作成します。
vue create プロジェクト名
選択肢が3つ現れるので、一番下の"Manually select features"をクリック。
以降10回選択をします。
以下に従って設定してください。
これでプロジェクトの作成ができました。
②不要なファイルを削除
まず不要なファイルを削除していきます。
ファイルを削除する手順は以下の3つです。
- routerから削除
- App.vueから削除
- ファイルの削除
routerから削除
以下のコードを削除してください。
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
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から削除
以下のコードを削除してください。
<template>
<nav>
<!-- ここから削除 -->
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>|
<!--ここまで削除-->
</nav>
<router-view/>
</template>
<style>
</style>
<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の設定
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にリンクを作成
以下のコードを追記します。
<template>
<na
<router-link to="/todo">ToDo</router-link> | <!--追記-->
</nav>
<router-view/>
</template>
<style>
</style>
TodoListViewファイルを作成
フォーム入力と、入力した内容を反映できるよう、テンプレート部分の記載を行います。
ファイルを作成し、以下のコードを記載してください。
<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つです。
- 入力した内容を双方向バインディング
- 配列に入力した内容を追加
- 追加後、入力した内容を削除
- 要素の削除
ちなみに、要素を追加する際、バリデーションをつけたい場合はこちらをご覧ください。
以下のコードを記載してください。
<!--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 を定義
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 インスタンスに渡す
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 メソッドに渡す
<script setup lang="ts">
import { ref,reactive, computed } from 'vue';
//追記
import { useStore } from '../store'
//追記
const store = useStore()
</script>
TodoリストをVuexで管理
- stateで配列を管理
- mutationsに実行させたい処理を記載
配列をStateで管理
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に実行させたい処理を記載
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も読んであげます。
<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>
③ローカルストレージに保存
以下を追記してください。
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対応されていないので、使えるようにする必要があります。
ファイルを作成し、以下を追記してください。
declare module 'vuedraggable'
tsconfig.jsonに定義を追記
tsconfig.jsonのcompilerOptionsのpathsに定義を以下を追加します
"paths": {
"@/*": [
"src/*"
],
"vuedraggable": [
"src/types/vuedraggable"
]
},
package.jsonにvuedraggable.nextを追記
"dependencies": {
"vue": "^3.2.13",
"vue-router": "^4.0.3",
"vuedraggable": "^4.1.0",
"vuex": "^4.0.0"
},
追記したら、npm installをターミナルに入力してください。
draggableをインポート
<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の反映
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を送る
以下のコードを追記します。
<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を呼ぶ
<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>
終わりに
学ぶことがあって非常に苦労しました。
でもその分楽しかったです!
間違いがあれば、ご指摘のほどお願いします!
より詳しく見たい方は以下のリンクから飛べます。
関心がある方は、ぜひご覧ください。