Chapter 08

フロントエンド(Vue3) - 実装編

is_ryo
is_ryo
2020.10.27に更新

TODOページを作る

もしローカルでVueアプリを起動していない場合は起動しておいてください。

$ cd front-app
$ npm run serve

ファイルを生成する

viewsディレクトリ以下にファイルを生成します。

$ cd views
$ touch Todo.vue

生成したTodo.vueに下記の内容をコピペします。

Todo.vue
<template>
  <div class="todo">
    <h1>TODO APP</h1>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'Todo',
});
</script>

Routeを用意する

Todoページを表示するpathを用意します。
Vueはrouter/index.ts内にルートの情報を書いていきます。
下記のように追記します。(コメントは書かないでください)

router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import Home from '../views/Home.vue';
import Todo from '../views/Todo.vue'; // add

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =>
      import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
  // add start
  {
    path: '/todo',
    name: 'Todo',
    component: Todo,
  },
  // end
];

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
});

export default router;

リンクを追加する

TODOページへのリンクを設置します。
下記のようにApp.vueに追記します。(コメントは書かないでください)

App.vue
<template>
  <div id="nav">
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link> | // add ('|')
    <router-link to="/todo">Todo</router-link> // add
  </div>
  <router-view />
</template>
...

そうすると画面上部にリンクが設置され、クリックするとTODOページに遷移します。

TODOを作成してDBに保存する

ここからは実際にローカル環境に立ち上がっているExpressに対してリクエストを投げてTODOタスクを作成したり取得したりしていきます。
ローカルサーバ環境を立ち上げていない場合は立ち上げておいてください。

$ cd server
$ npm run dev

Vueからローカルサーバにリクエストをするためにaxiosを使うのでインストールしておきます。

$ cd front-app
$ npm i axios

作成するためのフォームを作成する

Todo.vueの内容を下記のように書き換えます。

Todo.vue
<template>
  <div class="todo">
    <h1>TODO APP</h1>
    <div class="create">
      <div class="title">
        <label>title:</label>
        <input v-model="state.title" />
        <button @click="createTodo">Create</button>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive } from 'vue';
import axios from 'axios';

const baseURL = 'http://localhost:3000/';

export default defineComponent({
  name: 'Todo',
  setup() {
    const state = reactive({
      title: '',
    });

    const createTodo = async () => {
      await axios.put(baseURL, { title: state.title });
    };

    return {
      state,
      create,
    };
  },
});
</script>

このときの画面の見た目はこんな感じになっているはずです。
inputにタイトルを入力してCreateボタンをクリックするとTODOタスクが作成されてDBに保存されます。

TODOを取得して表示する

TODOをDBから取得してページに表示します。
Todo.vueに下記の内容を追記します。(コメントは書かないでください)

Todo.vue
<template>
  <div class="todo">
    <h1>TODO APP</h1>
    <div class="create">
      <div class="title">
        <label>title:</label>
        <input v-model="state.title" />
        <button @click="createTodo">Create</button>
      </div>
    </div>
    // add start
    <div v-for="todo in state.todos" :key="todo.uuid">
      <div class="title margin-r1">title: {{ todo.title }}</div>
      <div class="status margin-r1">status: {{ todo.status }}</div>
    </div>
    // end
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive } from 'vue';
import axios from 'axios';

const baseURL = 'http://localhost:3000/';

export default defineComponent({
  name: 'Todo',
  setup() {
    const state = reactive({
      title: '',
      todos: null,
    });

    // add start
    const getTodos = () => {
      axios.get(baseURL).then((res) => {
        if (res && res.data) {
          console.log(res);
          state.todos = res.data.resBody;
        }
      });
    };

    getTodos();
    // end

    const createTodo = async () => {
      await axios.put(baseURL, { title: state.title });
      getTodos(); // add
    };

    return {
      state,
      create,
    };
  },
});
</script>

// add start
<style lang="scss" scoped>
.todo-item {
  display: flex;
  justify-content: center;

  .margin-r1 {
    margin-right: 1rem;
  }
}
</style>
// end

TODOを編集/削除する

TODOを編集したり、削除したりするためにTodo.vueを下記のように編集します。(コメントは書かないでください)

Todo.vue
<template>
  <div class="todo">
    <h1>TODO APP</h1>
    <div class="create">
      <div class="title">
        <label>title:</label>
        <input v-model="state.title" />
        <button @click="createTodo">Create</button>
      </div>
    </div>
    <div class="todo-item" v-for="todo in state.todos" :key="todo.uuid">
      // add start
      <!-- <div class="title margin-r1">title: {{ todo.title }}</div> -->
      <input class="margin-r1" v-model="todo.title" />
      <!-- <div class="status margin-r1">status: {{ todo.status }}</div> -->
      <select class="margin-r1" v-model="todo.status">
        <option value="todo">TODO</option>
        <option value="wip">WIP</option>
        <option value="done">DONE</option>
      </select>
      <button class="update margin-r1" @click="updateTodo(todo)">Update</button>
      <button class="delete margin-r1" @click="deleteTodo(todo)">Delete</button>
      // end
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive } from 'vue';
import axios from 'axios';

const baseURL = 'http://localhost:3000/';

type Todo = {
  uuid: string;
  title: string;
  status: 'todo' | 'wip' | 'done';
};

export default defineComponent({
  name: 'Todo',
  setup() {
    const state = reactive({
      title: '',
      todos: [],
    });

    const getTodos = () => {
      axios.get(baseURL).then((res) => {
        if (res && res.data) {
          console.log(res);
          state.todos = res.data.resBody;
        }
      });
    };

    getTodos();

    const createTodo = async () => {
      await axios.put(baseURL, { title: state.title });
      getTodos();
    };
    
    // add start
    const updateTodo = async (todo: Todo) => {
      await axios.post(baseURL + todo.uuid, {
        title: todo.title,
        status: todo.status,
      });
      getTodos();
    };

    const deleteTodo = async (todo: Todo) => {
      await axios.delete(baseURL + todo.uuid);
      getTodos();
    };
    // end

    return {
      state,
      createTodo,
      updateTodo, // add
      deleteTodo, // add
    };
  },
});
</script>

<style lang="scss" scoped>
.todo-item {
  display: flex;
  justify-content: center;
  margin: 10px 0;

  .margin-r1 {
    margin-right: 1rem;
  }
}
</style>

ここまで書くとこのような動きができると思います。

これでTODOページが完成しました!