Go + Nuxt + GraphQLで簡単カンバンアプリチュートリアル on docker-compose（フロントエンド編）

## はじめに
* Go/Vue/GraphQL初学者が学習用にCRUD操作のできるアプリを作ってみた
* サーバーサイド編とフロントエンド編に分けてその手順を紹介
* ざっくりとした構成は
  * サーバー（Go + gqlgen）
  * フロント（Nuxt + Apollo）
  * データベース（MySQL）
  * ORM(gorm)
  * 開発環境（docker-compose）

## 完成イメージ
![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366929/a07236a8-2412-38fa-ade6-f112fc8fa786.gif)

※ タスクの並び順は操作できません

## 関連リンク
* [ソースコード](https://github.com/MrFuku/kanban-go-nuxt-graphql)
* [サーバーサイド編](https://qiita.com/iPzYN2gLSV2HeSS/items/3062b753bde7e93085b1)

## 免責事項
* GoもVueもそんなに知見がありません
* そのため見苦しい書きぶりをしている箇所が多々ありますがご容赦ください🙏

## フロントエンド

### Nuxt起動
`front/Dockerfile`を作成し、`docker-compose.yml`にfrontコンテナを追加します
※ `front`ディレクトリも作成。以後、フロントのコードはこの配下に置きます

```docker:front/Dockerfile
FROM node:14.2-alpine3.11

ENV LANG ja_JP.UTF-8
ENV TZ Asia/Tokyo

WORKDIR /front

RUN apk add python make g++

# Nuxt生成後にコメントを外す
# COPY package.json ./
# COPY yarn.lock ./

# RUN yarn install
```

```yaml:docker-compose.yml
version: '3'
services:
  # frontコンテナを追加
  front:
    build:
      context: ./front
    ports:
      - 3000:3000
    volumes:
      - ./front:/front:cached
      - front_node_modules:/front/node_modules
    tty: true
    stdin_open: true
    command: yarn dev
  server:
    build: ./server
    tty: true
    ports:
      - 8080:8080
    environment:
      LANG: ja_JP.UTF-8
      TZ: Asia/Tokyo
    volumes:
      - ./server/:/go/src/github.com/MrFuku/kanban-go-nuxt-graphql/server
  db:
    image: mysql:5.7.24
    ports:
      - 3306:3306
    environment:
      TZ: 'Asia/Tokyo'
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_USER: localuser
      MYSQL_PASSWORD: localpass
      MYSQL_DATABASE: localdb
    volumes:
      - mysql_data:/var/lib/mysql
    command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci
# ボリュームも追加
volumes:
  front_node_modules:
  mysql_data:
```

Nuxtをインストールします

```bash
# frontコンテナを立ち上げ、shを起動
❯ docker-compose run front sh

# createコマンドでNuxtをインストール
❯ yarn create nuxt-app .
yarn create v1.22.4
[1/4] Resolving packages...
warning create-nuxt-app > sao > micromatch > snapdragon > source-map-resolve > resolve-url@0.2.1: https://github.com/lydell/resolve-url#deprecated
warning create-nuxt-app > sao > micromatch > snapdragon > source-map-resolve > urix@0.1.0: Please see https://github.com/lydell/urix#deprecated
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Installed "create-nuxt-app@2.15.0" with binaries:
      - create-nuxt-app

create-nuxt-app v2.15.0
✨  Generating Nuxt.js project in .
? Project name front
? Project description My wondrous Nuxt.js project
? Author name 
? Choose programming language JavaScript
? Choose the package manager Yarn
? Choose UI framework None
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Choose test framework None
? Choose rendering mode Universal (SSR)
? Choose development tools (Press <space> to select, <a> to toggle all, <i> to invert selection)

🎉  Successfully created project front

  To get started:

	yarn dev

  To build & start for production:

	yarn build
	yarn start

Done in 48.14s.
```

起動時の環境変数を設定
`yarn dev`でNuxtを立ち上げる時に環境変数`HOST=0.0.0.0 PORT=3000`が設定されるようにします

```json:front/package.json
{
  "name": "front",
  "version": "1.0.0",
  "description": "My wondrous Nuxt.js project",
  "author": "",
  "private": true,
  "scripts": {
    "dev": "HOST=0.0.0.0 PORT=3000 nuxt",
    "build": "nuxt build",
    "start": "nuxt start",
    "generate": "nuxt generate"
  },
  "dependencies": {
    "nuxt": "^2.0.0"
  },
  "devDependencies": {}
}
```
コメントアウトしていた箇所を戻す

```docker:front/Dockerfile
FROM node:14.2-alpine3.11

ENV LANG ja_JP.UTF-8
ENV TZ Asia/Tokyo

WORKDIR /front

RUN apk add python make g++

COPY package.json ./
COPY yarn.lock ./

RUN yarn install
```
dockerイメージを再ビルドしてNuxtが起動するか確認

```bash
# dockerイメージを再ビルド
❯ docker-compose build front

# frontコンテナを立ち上げ、Nuxtが起動することを確認
❯ docker-compose up front
```
`http://localhost:3000/`にアクセスし、Nuxtが起動していればOK

![スクリーンショット 2020-05-31 15.55.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366929/d8164def-52a4-1740-0e89-78d586f98625.png)

### Apolloインストール

```bash
# frontコンテナを立ち上げ、shを起動
❯ docker-compose run front sh

# Apolloインストール
❯ yarn add @nuxtjs/apollo
```
`nuxt.config.js`のmodulesにapolloを追加し、apolloプロパティを新しく追加します

```js:front/nuxt.config.js
...
  ** Nuxt.js modules
  */
  modules: [
    '@nuxtjs/apollo'
  ],
  apollo: {
    clientConfigs: {
      default: {
        // GraphQLサーバーのエンドポイント
        httpEndpoint: 'http://server:8080/query',
        browserHttpEndpoint: 'http://localhost:8080/query',
      }
    },
  },
...
```
で、ここでNuxtを起動させると鬼のようなエラーが出てきます

```bash
 ERROR  Failed to compile with 39 errors                                                                                                                                                                                                              friendly-errors 14:59:40

These dependencies were not found:                                                                                                                                                                                                                    friendly-errors 14:59:40
                                                                                                                                                                                                                                                      friendly-errors 14:59:40
* core-js/modules/es6.array.find in ./.nuxt/client.js                                                                                                                                                                                                 friendly-errors 14:59:40
* core-js/modules/es6.array.from in ./.nuxt/client.js, ./.nuxt/components/nuxt-link.client.js                                                                                                                                                         friendly-errors 14:59:40
* core-js/modules/es6.array.iterator in ./.nuxt/client.js                                                                                                                                                                                             friendly-errors 14:59:40
* core-js/modules/es6.date.to-string in ./.nuxt/client.js, ./.nuxt/components/nuxt-link.client.js                                                                                                                                                     friendly-errors 14:59:40
* core-js/modules/es6.function.name in ./.nuxt/client.js, ./.nuxt/components/nuxt-link.client.js                                                                                                                                                      friendly-errors 14:59:40
* core-js/modules/es6.object.assign in ./.nuxt/client.js                                                                                                                                                                                              friendly-errors 14:59:40
* core-js/modules/es6.object.keys in ./.nuxt/client.js                                                                                                                                                                                                friendly-errors 14:59:40
* core-js/modules/es6.object.to-string in ./.nuxt/client.js, ./.nuxt/components/nuxt-link.client.js and 1 other                                                                                                                                       friendly-errors 14:59:40
* core-js/modules/es6.promise in ./.nuxt/client.js                                                                                                                                                                                                    friendly-errors 14:59:40
* core-js/modules/es6.regexp.constructor in ./.nuxt/utils.js                                                                                                                                                                                          friendly-errors 14:59:40
* core-js/modules/es6.regexp.match in ./.nuxt/client.js                                                                                                                                                                                               friendly-errors 14:59:40
* core-js/modules/es6.regexp.replace in ./.nuxt/utils.js, ./.nuxt/components/nuxt.js                                                                                                                                                                  friendly-errors 14:59:40
* core-js/modules/es6.regexp.search in ./.nuxt/utils.js                                                                                                                                                                                               friendly-errors 14:59:40
* core-js/modules/es6.regexp.split in ./.nuxt/utils.js, ./node_modules/babel-loader/lib??ref--2-0!./node_modules/vue-loader/lib??vue-loader-options!./.nuxt/components/nuxt-build-indicator.vue?vue&type=script&lang=js&                              friendly-errors 14:59:40
* core-js/modules/es6.regexp.to-string in ./.nuxt/client.js, ./.nuxt/components/nuxt-link.client.js                                                                                                                                                   friendly-errors 14:59:40
* core-js/modules/es6.string.includes in ./.nuxt/client.js, ./.nuxt/components/nuxt-link.client.js                                                                                                                                                    friendly-errors 14:59:40
* core-js/modules/es6.string.iterator in ./.nuxt/client.js, ./.nuxt/components/nuxt-link.client.js                                                                                                                                                    friendly-errors 14:59:40
* core-js/modules/es6.string.repeat in ./.nuxt/utils.js                                                                                                                                                                                               friendly-errors 14:59:40
* core-js/modules/es6.string.starts-with in ./.nuxt/utils.js                                                                                                                                                                                          friendly-errors 14:59:40
* core-js/modules/es6.symbol in ./.nuxt/client.js, ./.nuxt/components/nuxt-link.client.js                                                                                                                                                             friendly-errors 14:59:40
* core-js/modules/es7.array.includes in ./.nuxt/client.js, ./.nuxt/components/nuxt-link.client.js                                                                                                                                                     friendly-errors 14:59:40
* core-js/modules/es7.object.get-own-property-descriptors in ./.nuxt/utils.js                                                                                                                                                                         friendly-errors 14:59:40
* core-js/modules/es7.promise.finally in ./.nuxt/client.js                                                                                                                                                                                            friendly-errors 14:59:40
* core-js/modules/es7.symbol.async-iterator in ./.nuxt/client.js, ./.nuxt/components/nuxt-link.client.js                                                                                                                                              friendly-errors 14:59:40
* core-js/modules/web.dom.iterable in ./.nuxt/client.js, ./.nuxt/components/nuxt-link.client.js                                                                                                                                                       friendly-errors 14:59:40
                                                                                                                                                                                                                                                      friendly-errors 14:59:40
To install them, you can run: npm install --save core-js/modules/es6.array.find core-js/modules/es6.array.from core-js/modules/es6.array.iterator core-js/modules/es6.date.to-string core-js/modules/es6.function.name core-js/modules/es6.object.assign core-js/modules/es6.object.keys core-js/modules/es6.object.to-string core-js/modules/es6.promise core-js/modules/es6.regexp.constructor core-js/modules/es6.regexp.match core-js/modules/es6.regexp.replace core-js/modules/es6.regexp.search core-js/modules/es6.regexp.split core-js/modules/es6.regexp.to-string core-js/modules/es6.string.includes core-js/modules/es6.string.iterator core-js/modules/es6.string.repeat core-js/modules/es6.string.starts-with core-js/modules/es6.symbol core-js/modules/es7.array.includes core-js/modules/es7.object.get-own-property-descriptors core-js/modules/es7.promise.finally core-js/modules/es7.symbol.async-iterator core-js/modules/web.dom.iterable
```
結論から言うと`core-js`をダウングレードさせないといけないようです（詳しい理由を知っている方いましたら、ぜひ教えていただきたく🙏）。
https://qiita.com/shunk-py/items/27bd33ee8df6d3e505f4

```bash
# frontコンテナ内でcore-jsをダウングレードする
❯ yarn add core-js@2.6.9
```

## クエリ定義

サーバーにリクエストを送る際に使うクエリを定義します

```gql:front/apollo/mutations/createTodo.gql
mutation($id: String!, $text: String!, $done: Boolean!, $userId: String!) {
  updateTodo(input: { id: $id, text: $text, done: $done, userId: $userId }) {
    id
    text
    done
    user {
      id
      name
    }
  }
}
```

```gql:front/apollo/mutations/createUser.gql
mutation($name: String!) {
  createUser(input: { name: $name }) {
    id
    name
  }
}
```

```gql:front/apollo/mutations/deleteTodo.gql
mutation($id: String!) {
  deleteTodo(input: $id){
    id
    text
    done
    user{
      id
      name
    }
  }
}
```

```gql:front/apollo/mutations/updateTodo.gql
mutation($id: String!, $text: String!, $done: Boolean!, $userId: String!) {
  updateTodo(input: { id: $id, text: $text, done: $done, userId: $userId }) {
    id
    text
    done
    user {
      id
      name
    }
  }
}
```

```gql:front/apollo/queries/todos.gql
query todos {
  todos {
    id
    text
    done
    userId
    user {
      id
      name
    }
  }
}
```

```gql:front/apollo/queries/users.gql
query users {
  users {
    id
    name
  }
}
```

## Vuetifyインストール
デザイン付けを簡単にするため、デザインフレームワーク(Vuetify)を使います

```bash
# frontコンテナを立ち上げ、shを起動
❯ docker-compose run front sh

❯ yarn add @nuxtjs/vuetify
```

`front/nuxt.config.js`を修正し、moduleを追加します

```js:front/nuxt.config.js
...
  /*
  ** Nuxt.js modules
  */
  modules: [
    '@nuxtjs/apollo',
    '@nuxtjs/vuetify'
  ],
...
```

### 画面側の実装
詳細な説明は省きますが、以下の通り画面を実装します
正直なところイケテナイ書きぶりになっています、悪しからず（こんな書きぶりがいいよ！などありましたら是非フィードバックいただきたく！🙏）

<details>
<summary>`front/pages/index.vue`</summary><div>


```vue:front/pages/index.vue
<template>
  <v-app id="inspire">
    <Header
      :drawer="drawer"
      @change="drawer = !drawer"
    >
      <SideNav />
    </Header>
    <v-content>
      <v-container
        fluid
        fill-height
      >
        <v-row style="height: 90%;">
          <v-col cols="6">
            <Board
              :todos="unfinishedTodos"
              :status="false"
              @updateTodo="updateTodo"
            >
              <h3>未完了</h3>
            </Board>
          </v-col>
          <v-col cols="6">
            <Board
              :todos="finishedTodos"
              :status="true"
              @updateTodo="updateTodo"
            >
              <h3>完了</h3>
            </Board>
          </v-col>
        </v-row>
      </v-container>
    </v-content>
  </v-app>
</template>

<script>
import Header from "~/components/Header";
import SideNav from "~/components/SideNav";
import Board from "~/components/Board";
import todos from "~/apollo/queries/todos.gql";
import users from "~/apollo/queries/users.gql";
import updateTodo from "~/apollo/mutations/updateTodo.gql";

export default {
  components: {
    Header,
    SideNav,
    Board
  },
  data() {
    return {
      drawer: true
    };
  },
  computed: {
    unfinishedTodos() {
      return this.todos.filter(t => !t.done);
    },
    finishedTodos() {
      return this.todos.filter(t => t.done);
    }
  },
  methods: {
    updateTodo(todoId, status) {
      let todo = this.todos.find(t => t.id === todoId);
      console.log(todoId, status)
      if (!todo || todo.done === status) return;

      const { id, text, userId } = todo;
      const done = status;
      this.$apollo
        .mutate({
          mutation: updateTodo,
          variables: {
            id,
            text,
            done,
            userId
          },
          refetchQueries: [
            {
              query: todos
            }
          ]
        })
        .catch(err => {
          console.log(err);
        });
    }
  },
  apollo: {
    todos: {
      prefetch: true,
      query: todos
    },
    users: {
      prefetch: true,
      query: users
    }
  }
};
</script>
```
</div></details>

<details>
<summary>`front/components/Board.vue`</summary><div>


```vue:front/components/Board.vue
<template>
  <v-card
    class="blue-grey lighten-4"
    height="100%"
    @drop="putTodo($event)"
    @dragover.prevent
    @dragenter.prevent
  >
    <v-container>
      <slot></slot>
      <v-row dense>
        <v-col
          v-for="(todo, i) in todos"
          :key="i"
          cols="12"
        >
          <TaskCard :todo="todo" />
        </v-col>
      </v-row>
    </v-container>
  </v-card>
</template>

<script>
import TaskCard from "~/components/TaskCard";

export default {
  components: {
    TaskCard
  },
  props: {
    todos: {
      type: Array,
      required: true
    },
    status: {
      type: Boolean,
      required: true
    }
  },
  methods: {
    putTodo(event) {
      const todoId = event.dataTransfer.getData("todoId");
      this.$emit("updateTodo", todoId, this.status);
    }
  }
};
</script>
```

</div></details>

<details>
<summary>`front/components/Header.vue`</summary><div>


```vue:front/components/Header.vue
<template>
  <div>
    <v-navigation-drawer
      :value="drawer"
      :clipped="$vuetify.breakpoint.lgAndUp"
      app
    >
      <slot></slot>
    </v-navigation-drawer>
    <v-app-bar
      :clipped-left="$vuetify.breakpoint.lgAndUp"
      app
      color="blue darken-3"
      dark
    >
      <v-app-bar-nav-icon @click.prevent="$emit('change')"></v-app-bar-nav-icon>
      <v-toolbar-title class="ml-0 pl-4">
        <span class="hidden-sm-and-down">Kanban App</span>
      </v-toolbar-title>
    </v-app-bar>
  </div>
</template>

<script>
export default {
  props: {
    drawer: {
      type: Boolean,
      required: true
    }
  }
};
</script>
```
</div></details>

<details>
<summary>`front/components/SideNav.vue`</summary><div>


```vue:front/components/SideNav.vue
<template>
  <div>
    <v-list dense>
      <v-list-item @click="taskDialog = true">
        <v-icon>mdi-calendar-check</v-icon>
        タスク作成
      </v-list-item>
      <v-list-item @click="userDialog = true">
        <v-icon>mdi-account</v-icon>
        ユーザー作成
      </v-list-item>
    </v-list>
    <TaskForm
      :dialog="taskDialog"
      mode="new"
      @close="taskDialog = false"
    />
    <UserForm
      :dialog="userDialog"
      @close="userDialog = false"
    />
  </div>
</template>

<script>
import TaskForm from "~/components/TaskForm";
import UserForm from "~/components/UserForm";

export default {
  components: {
    TaskForm,
    UserForm
  },
  data() {
    return {
      taskDialog: false,
      userDialog: false
    };
  }
};
</script>
```
</div></details>


<details>
<summary>`front/components/TaskCard.vue`</summary><div>


```vue:front/components/TaskCard.vue
<template>
  <div>
    <v-card
      draggable
      @dragstart="stashTodo($event, todo)"
    >
      <v-card-text class="font-weight-bold">{{ todo.text }}</v-card-text>
      <v-card-subtitle class="d-flex">
        {{ todo.user.name }}
        <v-spacer></v-spacer>
        <v-icon @click="taskDialog = true">mdi-square-edit-outline</v-icon>
        <TaskDeleteBtn :todo-id="todo.id" />
      </v-card-subtitle>
    </v-card>
    <TaskForm
      :dialog="taskDialog"
      :todo="todo"
      mode="edit"
      @close="taskDialog = false"
    />
  </div>
</template>

<script>
import TaskForm from "~/components/TaskForm";
import TaskDeleteBtn from "~/components/TaskDeleteBtn";

export default {
  components: {
    TaskForm,
    TaskDeleteBtn
  },
  props: {
    todo: {
      type: Object,
      required: true
    },
  },
  data() {
    return {
      taskDialog: false
    };
  },
  methods: {
    stashTodo(event, todo) {
      event.dataTransfer.effectAllowed = "move";
      event.dataTransfer.dropEffect = "move";
      event.dataTransfer.setData("todoId", todo.id);
    }
  }
};
</script>
```
</div></details>

<details>
<summary>`front/components/TaskDeleteBtn.vue`</summary><div>


```vue:front/components/TaskDeleteBtn.vue
<template>
  <v-dialog
    v-model="dialog"
    max-width="290"
  >
    <template v-slot:activator="{ on }">
      <v-icon v-on="on">mdi-delete</v-icon>
    </template>
    <v-card>
      <v-card-title class="headline">タスクの削除</v-card-title>
      <v-card-text>本当に削除しますか？</v-card-text>
      <v-card-actions>
        <v-spacer></v-spacer>
        <v-btn @click="dialog = false">キャンセル</v-btn>
        <v-btn
          color="error"
          @click="deleteTodo"
        >削除</v-btn>
      </v-card-actions>
    </v-card>
  </v-dialog>
</template>
<script>
import todos from "~/apollo/queries/todos.gql";
import deleteTodo from "~/apollo/mutations/deleteTodo.gql";

export default {
  props: {
    todoId: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      dialog: false
    };
  },
  methods: {
    deleteTodo() {
      this.$apollo
        .mutate({
          mutation: deleteTodo,
          variables: {
            id: this.todoId
          },
          refetchQueries: [
            {
              query: todos
            }
          ]
        })
        .then(res => {
          this.dialog = false;
        })
        .catch(err => {
          console.log(err)
        });
    }
  }
};
</script>
```
</div></details>

<details>
<summary>`front/components/TaskForm.vue`</summary><div>

```vue:front/components/TaskForm.vue
<template>
  <v-dialog
    :value="dialog"
    @input="$emit('close')"
    width="500"
  >
    <v-card>
      <v-card-title
        class="headline grey lighten-2"
        primary-title
      >
        {{ title }}
      </v-card-title>
      <v-container>
        <v-form
          ref="form"
          v-model="valid"
        >
          <v-textarea
            v-model="editTodo.text"
            outlined
            label="タスクの内容"
            :rules="textRules"
          />
          <v-row>
            <v-col cols="6">
              <v-select
                :items="todoStatuses"
                v-model="editTodo.done"
                outlined
                dense
                label="完了ステータス"
              />
            </v-col>
            <v-col cols="6">
              <v-select
                :items="users"
                item-text="name"
                item-value="id"
                v-model="editTodo.userId"
                outlined
                dense
                label="担当者"
                :rules="userIdRulues"
              />
            </v-col>
          </v-row>
        </v-form>
      </v-container>
      <v-divider />
      <v-card-actions>
        <v-spacer />
        <v-btn @click="$emit('close')">
          キャンセル
        </v-btn>
        <v-btn
          color="primary"
          @click="exec"
          :disabled="!valid"
        >
          {{ submitLabel }}
        </v-btn>
      </v-card-actions>
    </v-card>
  </v-dialog>
</template>

<script>
import todos from "~/apollo/queries/todos.gql";
import users from "~/apollo/queries/users.gql";
import createTodo from "~/apollo/mutations/createTodo.gql";
import updateTodo from "~/apollo/mutations/updateTodo.gql";

export default {
  props: {
    dialog: {
      type: Boolean,
      required: true
    },
    mode: {
      type: String,
      required: true
    },
    todo: {
      type: Object,
      default: () => {
        return {
          text: "",
          userId: "",
          done: false
        };
      }
    }
  },
  data() {
    return {
      editTodo: {},
      todoStatuses: [
        { text: "未完了", value: false },
        { text: "完了", value: true }
      ],
      textRules: [
        v => !!v || "タスクの内容は必須です",
        v => (v && v.length <= 1000) || "1000文字以内で入力してください"
      ],
      userIdRulues: [v => !!v || "担当者は必須です"],
      valid: false
    };
  },
  methods: {
    createTodo() {
      const { text, done, userId } = this.editTodo;
      this.$apollo
        .mutate({
          mutation: createTodo,
          variables: {
            text,
            done,
            userId
          },
          refetchQueries: [
            {
              query: todos
            }
          ]
        })
        .then(res => {
          this.$emit("close");
        })
        .catch(err => {
          console.log(err);
        });
    },
    updateTodo() {
      const { id, text, done, userId } = this.editTodo;
      this.$apollo
        .mutate({
          mutation: updateTodo,
          variables: {
            id,
            text,
            done,
            userId
          },
          refetchQueries: [
            {
              query: todos
            }
          ]
        })
        .then(res => {
          this.$emit("close");
        })
        .catch(err => {
          console.log(err);
        });
    }
  },
  computed: {
    title() {
      if (this.mode === "new") return "新規タスク作成";
      if (this.mode === "edit") return "タスク編集";
    },
    submitLabel() {
      if (this.mode === "new") return "作成";
      if (this.mode === "edit") return "更新";
    },
    exec() {
      if (this.mode === "new") return this.createTodo;
      if (this.mode === "edit") return this.updateTodo;
    }
  },
  watch: {
    // フォームの入力内容を初期化する
    // 雑なやり方しか思い浮かばず。他に良いやり方があれば教えてください
    dialog(newValue) {
      if (newValue) {
        this.editTodo = Object.assign({}, this.todo);
      } else {
        this.editTodo = Object.assign({}, this.todo);
        this.$refs.form.resetValidation();
      }
    }
  },
  apollo: {
    users: {
      query: users
    }
  }
};
</script>
```
</div></details>

<details>
<summary>`front/components/UserForm.vue`</summary><div>

```vue:front/components/UserForm.vue
<template>
  <v-dialog
    :value="dialog"
    @input="$emit('close')"
    width="500"
  >
    <v-card>
      <v-card-title
        class="headline grey lighten-2"
        primary-title
      >
        新規ユーザー作成
      </v-card-title>
      <v-form
        ref="form"
        v-model="valid"
      >
        <v-container>
          <v-text-field
            v-model="name"
            :rules="nameRulues"
            outlined
            label="ユーザー名"
          />
        </v-container>
        <v-divider />
        <v-card-actions>
          <v-spacer />
          <v-btn @click="$emit('close')">
            キャンセル
          </v-btn>
          <v-btn
            color="primary"
            @click="createUser"
            :disabled="!valid"
          >
            作成
          </v-btn>
        </v-card-actions>
      </v-form>
    </v-card>
  </v-dialog>
</template>

<script>
import users from "~/apollo/queries/users.gql";
import createUser from "~/apollo/mutations/createUser.gql";

export default {
  props: {
    dialog: {
      type: Boolean,
      required: true
    }
  },
  data() {
    return {
      name: "",
      nameRulues: [
        v => !!v || "ユーザー名は必須です",
        v => (v && v.length <= 1000) || "1000文字以内で入力してください"
      ],
      valid: false
    };
  },
  methods: {
    createUser() {
      const name = this.name;
      this.$apollo
        .mutate({
          mutation: createUser,
          variables: {
            name
          },
          refetchQueries: [
            {
              query: users
            }
          ]
        })
        .then(res => {
          this.$emit('close');
        })
        .catch(err => {
          console.log(err)
        });
    }
  },
  watch: {
    // フォームの入力内容を初期化する
    // 雑なやり方しか思い浮かばず。他に良いやり方があれば教えてください
    dialog(newValue) {
      if (!newValue) {
        this.name = "";
        this.$refs.form.resetValidation();
      }
    }
  }
};
</script>
```
</div></details>

以上で完成です！（お疲れ様でした）