LoginSignup
1
2

More than 3 years have passed since last update.

Vue3 todoアプリ作成② ~ Vuex vue-routerを触ってみる ~

Last updated at Posted at 2020-12-28

はじめに

謎todoアプリ作成 第2弾です

Vue3 todoアプリ作成① ~ compositionAPI ~
Vue3 todoアプリ作成② ~ Vuex vue-routerを触ってみる ~ :point_left:今ここ
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が完了した時の処理を行います。

  1. 完了ボタンコンポーネント作成
  2. 登録同様 カスタムイベント作成
  3. spliceメソッドを使用し、配列のなかからindexが同じ要素を削除。
terminal
// 完了ボタンコンポーネント作成
$ touch ./src/components/CompleteButton.vue
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>
TodoList.vue
<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>

反映

Image from Gyazo

vue-router Vuex 設定

TODO詳細を実装するにあたって、
vue-router, Vuexを導入していく(使いたかったから。:pray:

インストール/ファイル作成

terminal
$ 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

ファイル設定

router.js
import { createRouter, createWebHistory } from 'vue-router';

export const router =  createRouter({
  history: createWebHistory(),
  routes: []
})

export default router

注意点

  • new Router() => createRouter()
index.ts
import { createStore } from "vuex";

export const store = createStore({
  state: {},
  mutations: {},
  actions: {},
  modules: {}
});

export default store

注意点

  • new Vuex.Store() => createStore()
main.js
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のデータをストアへ

ページの遷移先でもデータを使用したいので、
ストアでデータを保持し、ページ遷移先でも取得可能な状態にする。

index.ts
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

TodoList.vue
<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>
TodoInput.vue
<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>

components/CompleteButton.vue
<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を使用して、実装できました。
次は詳細画面を実装してきます。

詳細画面作成

詳細画面と詳細画面までのルーティング作成。

terminal
$ cd src
$ mkdir views
$ touch ./views/Detail.vue
main.js
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')
Detail.vue
//詳細ページ
<template>
</template>
<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'Detail',

  setup() {
  }
});
</script>


router.js
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作成コンポーネント達をそこで読み込むように設定

terminal
$ cd src
$ touch ./views/Home.vue
App.vue
<template>
  <router-view></router-view>
</template>

<script>
import { defineComponent } from 'vue'
export default defineComponent({
  name: 'App',
})
</script>

views/Home.vue
<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>


components/TodoList.vue
//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>
router.js
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がどの画面へ遷移しても登録できる状態になります。

詳細ボタン作成/遷移

後少しで完成です 頑張りましょう。。

components/TodoList.vue
<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>

これで遷移は完了。
次は遷移先でデータの表示を行う。

Detail.vue
<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で条件分岐させ表示。

反映

Image from Gyazo

おわり

今回も長くなってしまいました。
vue-router vuexの初級をクリアした感覚です。
次回はtailwindcssをぶっ込んでいこうと思います。

参考

vue-router
Vuex
既存のVue.jsプロジェクトをVue 3へ移行したときに必要だった修正まとめ

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2