Closed3
vue3 vuetify3 Provide/Injectとcomuptedで作成した状態管理の挙動がうまく動かない、、、
![masa](https://res.cloudinary.com/zenn/image/fetch/s--JHNzo2Ns--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/2ef0a10add.jpeg)
vue3 vuetify3 Provide/Injectを使ったTodoアプリを開発。
propsの受け渡しやprovideやinjectの挙動を確認するなどメモしていきます。
下記の画像のようなシンプルなTodoリストです。
作る機能は
- Todo一覧機能
- Todo追加機能
- Todo絞り込み機能
- Todo削除機能(モーダル表示)
![masa](https://res.cloudinary.com/zenn/image/fetch/s--JHNzo2Ns--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/2ef0a10add.jpeg)
Provide/Injectに書き換えたときにキーワード検索の挙動がうまく動かない...
ProvideとInjectを使っての状態管理を実装しました。
ProvideをfilteredTodoList
にしてInjectでデータを取得するようにしましたが、キーワード検索の挙動がうまくいきません。
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))
}
})
ディレクトリ設計としては
- App.vue(親)
- BaseSection.vue(子)
- TodoList.vue(孫)
下記がコードになります。
分かる人がいましたら教えて頂けると幸いです🙇
App.vue
<template>
<BaseSection />
</template>
<script lang="ts">
import { defineComponent, ref, watch, reactive, computed, provide, toRefs } from 'vue'
import BaseSection from '@/components/BaseSection.vue'
import { INIT_TODO_LIST } from '@/constants/todo'
import type { TodoType } from '@/interfaces/Todo'
export default defineComponent({
components: { BaseSection },
setup() {
const searchKeyword = ref('')
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))
}
})
// filteredTodoListをProvide
provide('filteredTodoList', filteredTodoList)
return {
searchKeyword,
todoList,
filteredTodoList
}
}
})
</script>
<style>
/* アイコンを右端に配置するスタイル */
.v-list-item-action {
justify-content: flex-end;
}
</style>
./constants/Todo
BaseSection.vue
<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>
</v-main>
</v-app>
</template>
<script lang="ts">
import { defineComponent, ref, computed, inject } from 'vue'
import AddTodo from '@/components/AddTodo.vue'
import SearchTodo from '@/components/SearchTodo.vue'
import TodoList from '@/components/TodoList.vue'
import { INIT_TODO_LIST } from '@/constants/todo'
import type { TodoType } from '@/interfaces/Todo'
export default defineComponent({
components: { AddTodo, SearchTodo, TodoList },
setup() {
const title = ref('')
const searchKeyword = ref('')
const nextTodoId = ref(4)
const showDeleteDialog = ref(false)
const selectedTodos = ref<TodoType[]>([]) // 選択されたTodoを格納するためのリアクティブ変数
const todoList = ref<TodoType[]>(INIT_TODO_LIST)
// filteredTodoListをInject
const filteredTodoList = inject('filteredTodoList') as TodoType[]
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 = (id: number, targetTitle: string) => {
if (window.confirm(`「${targetTitle}」のtodoを削除しますか?`)) {
todoList.value = todoList.value.filter((todo) => todo.id !== id)
}
}
const cancel = () => {
showDeleteDialog.value = false
}
const confirmDeleteTodo = () => {
const selectedIds = selectedTodos.value.map((todo) => todo.id)
todoList.value = todoList.value.filter((todo) => !selectedIds.includes(todo.id))
selectedTodos.value = [] // 選択された Todo をリセット
showDeleteDialog.value = false
}
return {
title,
searchKeyword,
todoList,
showDeleteDialog,
selectedTodos,
filteredTodoList,
handleSubmit,
handleDelete,
cancel,
confirmDeleteTodo
}
}
})
</script>
<style>
/* アイコンを右端に配置するスタイル */
.v-list-item-action {
justify-content: flex-end;
}
</style>
./constants/Todo
TodoList.vue
<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, inject } from 'vue'
import type { TodoType } from '@/interfaces/Todo'
export default defineComponent({
setup(_, context) {
const filteredTodoList = inject('filteredTodoList') as TodoType[]
const handleDelete = (targetId: Number, targetTitle: String) => {
context.emit('handleDelete', targetId, targetTitle)
}
return {
filteredTodoList,
handleDelete
}
}
})
</script>
<style scoped></style>
![masa](https://res.cloudinary.com/zenn/image/fetch/s--JHNzo2Ns--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/2ef0a10add.jpeg)
一旦解決
filteredTodoListの中にsearchKeywordをrefでリアクティブに扱っているので、同じファイルに書かないとうまく動かないことが分かった。
const searchKeyword = ref('')
結局は、BaseSection.vueにprovideを書くことで解決した。
=> 本来、ProvideとInjectを使うまでもないですが、検証のためprovideとinjectで実装してみました笑
修正したソースコードは下記になります。
BaseSection.vue
<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 @handleDelete="handleDelete" />
</v-form>
</v-card>
</v-col>
</v-row>
</v-container>
</v-main>
</v-app>
</template>
<script lang="ts">
import { defineComponent, ref, computed, inject, type ComputedRef, provide } from 'vue'
import AddTodo from '@/components/AddTodo.vue'
import SearchTodo from '@/components/SearchTodo.vue'
import TodoList from '@/components/TodoList.vue'
import { INIT_TODO_LIST } from '@/constants/todo'
import type { TodoType } from '@/interfaces/Todo'
export default defineComponent({
components: { AddTodo, SearchTodo, TodoList },
setup() {
const title = ref('')
const searchKeyword = ref('')
const nextTodoId = ref(4)
const showDeleteDialog = ref(false)
const selectedTodos = ref<TodoType[]>([]) // 選択されたTodoを格納するためのリアクティブ変数
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))
}
})
// filteredTodoListをProvide
provide('filteredTodoList', filteredTodoList)
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 = (id: number, targetTitle: string) => {
if (window.confirm(`「${targetTitle}」のtodoを削除しますか?`)) {
todoList.value = todoList.value.filter((todo) => todo.id !== id)
}
}
const cancel = () => {
showDeleteDialog.value = false
}
const confirmDeleteTodo = () => {
const selectedIds = selectedTodos.value.map((todo) => todo.id)
todoList.value = todoList.value.filter((todo) => !selectedIds.includes(todo.id))
selectedTodos.value = [] // 選択された Todo をリセット
showDeleteDialog.value = false
}
return {
title,
searchKeyword,
todoList,
showDeleteDialog,
selectedTodos,
filteredTodoList,
handleSubmit,
handleDelete,
cancel,
confirmDeleteTodo
}
}
})
</script>
<style>
/* アイコンを右端に配置するスタイル */
.v-list-item-action {
justify-content: flex-end;
}
</style>
./constants/Todo
TodoList.vue
<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, inject } from 'vue'
import type { TodoType } from '@/interfaces/Todo'
export default defineComponent({
setup(_, context) {
// InjectでfilteredTodoListを取得
const filteredTodoList = inject('filteredTodoList') as TodoType[]
const handleDelete = (targetId: Number, targetTitle: String) => {
context.emit('handleDelete', targetId, targetTitle)
}
return {
filteredTodoList,
handleDelete
}
}
})
</script>
<style scoped></style>
BaseSection.vueにあるTodoListコンポーネントのpropsの値は削除しました。
このスクラップは2023/08/08にクローズされました