Vue3 Composition API vuetify3を使って TodoListを作ってみた!!
はじめに
Vue3のCompofition APIとvuetifyを使ってTodoListアプリを共有します。
ネットの色んな記事を見るとvue2系のoptionsAPIの書き方だったり、vuetify2の書き方だったりなど書き方がバラバラなので、vue3やvuetify3のの書き方があればいいなと思って今回この記事を作成しました!
成果物
対象者
- これからフロントエンド領域でも開発してみたい
- 手軽にアウトプットできるものないかを探している方
- vue.jsをこれから触り始めたい、まだ触り始めて初心者の方
- vuetifyを使った開発をしてみたい(Cssを書くのが面倒くさいと思っている方)
上記当てはまりましたら、ぜひ読んでみてください!
また、サンプルのソースコードも下記に置いてありますので参考にしてみてください!
作る機能
- TodoList一覧機能
- Todo追加機能
- Todoの絞り込み機能
- Todoの削除機能
- モーダル画面(v-dialog)
環境構築
vueプロジェクト作成コマンド
$ npm init vue@latest
色々と質問されますが、
- プロジェクト名を記入
- Add TypeScript => Yes
- Add ESlint => Yes
- Add Prettier => Yes
それ以外はNoを選択して大丈夫です!
$ cd 作成したプロジェクト名
$ npm install
$ npm run dev
上記にアクセスして初期画面が表示れます。
ディレクトリ構成
ディレクトリ構成は下記のとおりです。
|--src
| |--App.vue
| |--assets
| | |--main.css
| |--components
| | |--AddTodo.vue
| | |--BaseSection.vue
| | |--SearchTodo.vue
| | |--TodoList.vue
| | |--modal
| | | |--ConfirmDialog.vue
| |--constants
| | |--todo.ts
| |--interfaces
| | |--Todo.ts
| |--main.ts
|--tsconfig.app.json
|--tsconfig.json
|--tsconfig.node.json
|--vite.config.ts
vuetifyの環境設定
main.ts
vuefityで使うcomponentやstyleなど、main.tsファイルで設定します。
詳細な設定方法はvuetifyのManual Stepsにあります!
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import 'vuetify/styles'
import '@mdi/font/css/materialdesignicons.css'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
const vuetify = createVuetify({
components,
directives
})
createApp(App).use(vuetify).mount('#app')
TodoListの画面構成
コンポーネントの設計としては下記の構成になります。BaseSectionコンポーネントをApp.vueに読み込んでいます。
BaseSection
コンポーネントの中身
- AddTodo.vue
- SearchTodo.vue
- TodoList.vue
- ConfirmDialog.vue
ソースコード
<template>
<BaseSection />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import BaseSection from '@/components/BaseSection.vue'
export default defineComponent({
components: { BaseSection },
setup() {
return {}
}
})
</script>
<style></style>
<template>
<v-app>
<v-main>
<v-container class="mt-12">
<v-row justify="center">
<v-col cols="12" sm="8" md="6">
<h1 class="text-center">Todo List</h1>
<v-card class="pa-10">
<v-form>
<!-- Todo新規追加 -->
<AddTodo v-model:title="title" @handleSubmit="handleSubmit" />
<!-- Todoキーワード検索 -->
<SearchTodo v-model:searchKeyword="searchKeyword" />
<!-- TodoList一覧 -->
<TodoList :filtered-todo-list="filteredTodoList" @handleDelete="handleDelete" />
</v-form>
</v-card>
</v-col>
</v-row>
</v-container>
<ConfirmDialog
card-title="Todoを削除"
:card-text="cardText"
:modelValue="showDeleteDialog"
@cancel="cancel"
@confirmDeleteTodo="confirmDeleteTodo(cardToDeleteId)"
/>
</v-main>
</v-app>
</template>
<script lang="ts">
import { defineComponent, ref, computed } from 'vue'
import AddTodo from '@/components/AddTodo.vue'
import SearchTodo from '@/components/SearchTodo.vue'
import TodoList from '@/components/TodoList.vue'
import ConfirmDialog from './modal/ConfirmDialog.vue'
import { INIT_TODO_LIST } from '@/constants/todo'
import type { TodoType } from '@/interfaces/Todo'
export default defineComponent({
components: { AddTodo, SearchTodo, TodoList, ConfirmDialog },
setup() {
const title = ref('')
const searchKeyword = ref('')
const nextTodoId = ref(4)
const cardText = ref('')
const cardToDeleteId = ref(0)
const showDeleteDialog = ref(false)
const todoList = ref<TodoType[]>(INIT_TODO_LIST)
const filteredTodoList = computed(() => {
const keyword = searchKeyword.value.trim().toLowerCase()
if (!keyword) {
return todoList.value
} else {
return todoList.value.filter((item) => item.title.toLowerCase().startsWith(keyword))
}
})
const handleSubmit = () => {
if (title.value === '') return
const newTodo: TodoType = {
id: nextTodoId.value,
title: title.value.trim() // 入力値をトリムしてから使用
}
todoList.value.push(newTodo) // 新しいTodoをtodosに追加
title.value = '' // 入力フィールドをクリア
nextTodoId.value++ // 次のTodoのIDを更新
}
const handleDelete = (targetId: number, targetTitle: string) => {
cardText.value = targetTitle
cardToDeleteId.value = targetId
showDeleteDialog.value = true
}
const cancel = () => {
showDeleteDialog.value = false
}
const confirmDeleteTodo = (targetId: number) => {
todoList.value = todoList.value.filter((todo) => todo.id !== targetId)
showDeleteDialog.value = false
}
return {
title,
searchKeyword,
todoList,
cardText,
cardToDeleteId,
showDeleteDialog,
filteredTodoList,
handleSubmit,
handleDelete,
cancel,
confirmDeleteTodo
}
}
})
</script>
<style></style>
<template>
<v-text-field
type="text"
:value="title"
@input="onInput"
label="Todoを追加"
class="mb-4"
variant="outlined"
@keypress.enter="handleSubmit"
></v-text-field>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
title: {
type: String,
default: ''
}
},
setup(_, context) {
const onInput = (e: Event) => {
const target = e.target as HTMLInputElement
context.emit('update:title', target.value)
}
const handleSubmit = () => {
context.emit('handleSubmit')
}
return {
onInput,
handleSubmit
}
}
})
</script>
<style scoped></style>
<template>
<v-text-field
:value="searchKeyword"
@input="onInput"
label="キーワード検索"
variant="outlined"
></v-text-field>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
searchKeyword: {
type: String,
default: ''
}
},
setup(_, context) {
const onInput = (e: Event) => {
const target = e.target as HTMLInputElement
context.emit('update:searchKeyword', target.value)
}
return {
onInput
}
}
})
</script>
<style scoped></style>
<template>
<v-list>
<v-list-item
class="mb-6"
border
height="60"
lines="two"
rounded
v-for="(item, index) in filteredTodoList"
:key="index"
>
<v-list-item-title>
{{ item.title }}
</v-list-item-title>
<template v-slot:append>
<v-list-item-action>
<v-icon @click="handleDelete(item.id, item.title)">mdi-delete</v-icon>
</v-list-item-action>
</template>
</v-list-item>
</v-list>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import type { TodoType } from '@/interfaces/Todo'
import type { PropType } from 'vue'
export default defineComponent({
props: {
filteredTodoList: {
type: Array as PropType<TodoType[]>
}
},
setup(_, context) {
const handleDelete = (targetId: Number, targetTitle: String) => {
context.emit('handleDelete', targetId, targetTitle)
}
return {
// filteredTodoList,
handleDelete
}
}
})
</script>
<style scoped></style>
<template>
<v-dialog v-model="showDeleteDialog" max-width="600">
<v-card>
<v-card-title class="headline">{{ cardTitle }}</v-card-title>
<v-card-text>
{{ `${cardText}を削除しますか?` }}
</v-card-text>
<v-card-actions>
<v-btn @click="cancel">キャンセル</v-btn>
<v-btn color="primary" @click="confirmDeleteTodo">削除</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
name: 'ConfirmDeleteTodo',
props: {
cardTitle: {
type: String,
default: ''
},
cardText: {
type: String,
default: ''
}
},
setup(_, context) {
const showDeleteDialog = ref(false)
const cancel = () => {
context.emit('cancel')
}
const confirmDeleteTodo = () => {
context.emit('confirmDeleteTodo')
}
return {
showDeleteDialog,
cancel,
confirmDeleteTodo
}
}
})
</script>
<style scoped></style>
型定義と定数
export interface TodoType {
id: number
title: string
}
import type { TodoType } from '@/interfaces/Todo'
export const INIT_TODO_LIST: TodoType[] = [
{ id: 1, title: 'Todo1' },
{ id: 2, title: 'Todo2' },
{ id: 3, title: 'Todo3' }
]
最後に
今回はvue3のcomposition APIとvuetifyを使ってTodoListアプリを作ってみました!
propsやemit、vuetifyのUIのライブラリを使って開発はいかがだったでしょうか?
v-modelやpropsやemit、vuetifyの流れが少しでも分かって頂けると嬉しいです!
vue3.2からsetup構文を使った書き方などがありますが、基礎を押さえていればsetup構文に移行しても問題なく対応できると思います。
参考文献
vuetify公式ドキュメント
コンポーネントのv-model
propsの使い方
Vue3/TypeScript対応 emitで理解するVue.jsの本質
書籍
Vue 3 フロントエンド開発の教科書
Discussion