Closed3

vue3 vuetify3 Provide/Injectとcomuptedで作成した状態管理の挙動がうまく動かない、、、

masamasa

vue3 vuetify3 Provide/Injectを使ったTodoアプリを開発。
propsの受け渡しやprovideやinjectの挙動を確認するなどメモしていきます。
下記の画像のようなシンプルなTodoリストです。
作る機能は

  • Todo一覧機能
  • Todo追加機能
  • Todo絞り込み機能
  • Todo削除機能(モーダル表示)
masamasa

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>

masamasa

一旦解決

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にクローズされました