はじめに
謎todoアプリ作成 第2弾です
Vue3 todoアプリ作成① ~ compositionAPI ~
Vue3 todoアプリ作成② ~ Vuex vue-routerを触ってみる ~ 今ここ
Vue3 todoアプリ作成③ ~ tailwindcss 使ってみた ~
今回は 実際に作成した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が完了した時の処理を行います。
- 完了ボタンコンポーネント作成
- 登録同様 カスタムイベント作成
- spliceメソッドを使用し、配列のなかからindexが同じ要素を削除。
// 完了ボタンコンポーネント作成
$ touch ./src/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>
<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>
反映
vue-router Vuex 設定
TODO詳細を実装するにあたって、
vue-router, Vuexを導入していく(使いたかったから。)
インストール/ファイル作成
$ 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
ファイル設定
import { createRouter, createWebHistory } from 'vue-router';
export const router = createRouter({
history: createWebHistory(),
routes: []
})
export default router
注意点
-
new Router()
=>createRouter()
へ
import { createStore } from "vuex";
export const store = createStore({
state: {},
mutations: {},
actions: {},
modules: {}
});
export default store
注意点
-
new Vuex.Store()
=>createStore()
へ
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のデータをストアへ
ページの遷移先でもデータを使用したいので、
ストアでデータを保持し、ページ遷移先でも取得可能な状態にする。
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
<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>
<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>
<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を使用して、実装できました。
次は詳細画面を実装してきます。
詳細画面作成
詳細画面と詳細画面までのルーティング作成。
$ cd src
$ mkdir views
$ touch ./views/Detail.vue
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')
//詳細ページ
<template>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Detail',
setup() {
}
});
</script>
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作成コンポーネント達をそこで読み込むように設定
$ cd src
$ touch ./views/Home.vue
<template>
<router-view></router-view>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'App',
})
</script>
<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>
//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>
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がどの画面へ遷移しても登録できる状態になります。
詳細ボタン作成/遷移
後少しで完成です 頑張りましょう。。
<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>
これで遷移は完了。
次は遷移先でデータの表示を行う。
<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で条件分岐させ表示。
反映
おわり
今回も長くなってしまいました。
vue-router vuexの初級をクリアした感覚です。
次回はtailwindcssをぶっ込んでいこうと思います。