Nuxt.jsとNestJSでTypeScriptなTodoリストを作ってみる（フロント編）

(バックエンド編: https://qiita.com/daitai-daidai/items/a5e744120492bcc2c591 )

Vue.jsとTypeScriptの練習のために、Nuxt.jsとNestJSでTodoリストを作ってみました。使用技術は以下の通り

- フロントエンド： Nuxt.js (UIフレームワークはVuetify、言語はTypeScript）
- バックエンド： NestJS + TypeORM(mySQL)

「よくわかんないけどNuxt.jsとTypeScriptって今アツいんでしょ？」 っていう駆け出しエンジニアの安易な発想で選びました。（つよつよのみなさまコメントお待ちしております）

今回はフロント編。完成形はこんな感じ。（https://dai65527.github.io/nuxttodo/ ）
![スクリーンショット 2020-09-29 17.14.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677011/8125539e-836d-ffce-71d6-8b02262b5643.png)

### 環境
- MacOS Catalina v10.15.6
- Node.JS v14.10.1
- Nuxt.js v2.14.5
- create-nuxt-app v3.3.0

## create-nuxt-appでプロジェクト作成
Nuxt.jsのプロジェクトを作成する際に便利な`create-nuxt-app`というツールが用意されています。これを使えば、超カンタンにNuxt.jsのプロジェクトが作成できます。
下のように実行。

```zsh
% npx create-nuxt-app tstodo-client
```

### 質問に負けない
`create-nuxt-app`を実行すると、いろいろ聞かれます。初心者なので面食らいますが、[こちらの記事](https://qiita.com/cheez921/items/fdfd224099f686e3173d)に助けながら完答。

```
create-nuxt-app v3.3.0
✨  Generating Nuxt.js project in tstodo-client
? Project name: tstodo-client
? Programming language: TypeScript
? Package manager: Npm
? UI framework: Vuetify.js
? Nuxt.js modules: Axios
? Linting tools: ESLint, Prettier
? Testing framework: None
? Rendering mode: Single Page App
? Deployment target: Server (Node.js hosting)
? Development tools: 
? Version control system: Git
```

- 言語はTypeScript。なんか人気らしいという安易な思想で選んであとで後悔する。
- UIフレームワークはVuetifyを選択。なんか人気らしいという理由で選択したが、こちらは後悔しなかった。
- サーバサイドとの通信は`axios`を使うため、`Nuxt.js modules`で忘れずに選択します。（あとから追加も簡単ですが）
- その他はいい感じに選んだ。たぶんいい感じ。

### 実行してみる
`create-nuxt-app`が終了すると、既に雛形が実行できる状況で用意されています。

```zsh
% ls
tstodo-client
```

立ち上げてみます。

```zsh
% cd tstodo-client
% npm run dev
```

![スクリーンショット 2020-09-29 14.55.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677011/6c8f6ea6-2276-7784-ee47-fda3656cf8b6.png)
表示されました！ （デフォルトではlocalhost:3000で起動します。）
初心者にも超カンタン。何もしてないのにここまでやってくれるなんて、、、至れり尽せりです。

## ディレクトリ構造
### デフォルト
Nuxt.jsのプロジェクトは下記のディレクトリで構成されています。適切な場所に`.vue`やら`.ts`やらをすれば、あとはNuxt.js側で良い感じにルーティングしてバンドルしてくれます。

```
% tree tstodo-client -L 1 
tstodo-client
├── README.md
├── assets
├── components
├── layouts
├── middleware
├── node_modules
├── nuxt.config.js
├── package-lock.json
├── package.json
├── pages
├── plugins
├── static
├── store
└── tsconfig.json
```

各ディレクトリの意味は[ドキュメント参照](https://ja.nuxtjs.org/guide/directory-structure/)。（ちなみに.gitignore等のファイルも同時に生成されます）
ちなみに私は半分くらい意味わからんって感じでしたが、今回はケースでは以下のディレクトリにファイルを作成orファイルを編集していけばOK。

- layouts: レイアウトを記述するためのファイルを配置。
- pages: アプリケーションのビュー及びルーティングを記述したファイルを配置。このディレクトリの構造を元に勝手にNuxtがルーティングしてくれます。（が、今回はルートURLのみ）
- components: アプリケーションを構成するコンポーネントファイルを配置。
- static: faviconなど変更されないで使われるファイルを配置。（今回はデフォルトのfavicon.ico以外使わない）
- assets: スタイルシートや画像等を配置。（今回はいじらない）
- nuxt.config.js: Nuxt.jsの設定ファイル
- tsconfig.json: TypeScriptの設定ファイル
- package.json: 略。
- package-lock.json: 略。
- node_modules: 略。

`middleware`、`plugin`、`store`フォルダと余計なREADME等は削除します。

### 最終的な構造
不要なもの消して、いろいろファイル作ったらこうなりました。

```
% tree tstodo-client
tstodo-client
├── README.md
├── assets
│   └── variables.scss
├── components
│   ├── ItemCard.vue
│   └── List.vue
├── layouts
│   └── default.vue
├── models
│   └── Item.ts
├── node_modules
│   └── (略)
├── nuxt.config.js
├── package-lock.json
├── package.json
├── pages
│   └── index.vue
├── static
│   └── favicon.ico
└── tsconfig.json
```

## ソースコード
各ソースについて見ていきます。
なお、パス中の`@`はプロジェクトのルートディレクトリを指します。

### .vueファイルはTypeScriptでこう書く
今回はTypeScriptを採用しているので、`script`タグ内の様子がJavaScriptの`.vue`ファイルと異なります。
まず、`script`タグのプロパティとして、`lang="ts"`を含めることでTypeScriptを使えるようにします。タグ内の基本構造は以下のようにするのがよいっぽい。

```vue
<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  // script
})
</script>
```

`export default Vue.extend`の部分はいろいろ方法はあるようですが、これが一番JSの場合と差が小さいように思います。（[公式のドキュメント](https://typescript.nuxtjs.org/ja/cookbook/components/#template)でもこの方法となっています。）

### modelsディレクトリ
todoの各アイテムの構造を記述する`Item.ts`を作成しました。新しく`models`ディレクトリを作成して、その配下に配置。

```typescript:@/models/Item.ts
export interface IItem {
  id: number
  name: string
  done: boolean
}

export default class Item {
  private _props: IItem

  constructor(props: IItem) {
    this._props = props
  }

  get id(): number {
    return this._props.id
  }

  get name(): string {
    return this._props.name
  }

  set name(value: string) {
    this._props.name = value
  }

  get done(): boolean {
    return this._props.done
  }

  set done(value: boolean) {
    this._props.done = value
  }

}
```

せっかく勉強したのでinterfaceとかsetterとかgetterとか使ってみました。

### layoutsディレクトリ
ページ全体のレイアウト（ヘッダー、フッター、コンテンツなど）はここで記述します。今回は、コンテンツ（Todoリスト）だけなので、こんな感じに。

```vue:@/layouts/default.vue
<template>
  <v-app>
    <nuxt />
  </v-app>
</template>
```

`nuxt`タグ内に`pages`ディレクトリの内容がレンダリングされます。
また、vuetifyのクラス等を使う際は、`v-app`タグで囲わないと効きませんので注意してください。（僕はこれで半日悩みました。） 何も考えずに全体を囲んでしまいます。

### pagesディレクトリ
`@/layouts/default.vue`内の`nuxt`タグの内容はこの`pages`ディレクトリの内容になります。ページのルートにアクセスした場合、表示されるのが以下の`@/index.vue`になります。

```vue:@/pages/index.vue
<template>
  <v-container class="pt-10">
    <List
      :items="items"
      @add-item="addItem"
      @change-done="changeDone"
      @delete-item="deleteItem"
      @delete-done="deleteDone"
    />
  </v-container>
</template>

<script lang="ts">
import Vue from 'vue'
import Item from '@/models/Item'
import List from '@/components/List.vue'

export default Vue.extend({
  components: {
    List,
  },
  data: () => ({
    items: [
      new Item({
        id: 1,
        name: 'task1',
        done: false,
      }),
      new Item({
        id: 2,
        name: 'task2',
        done: true,
      }),
      new Item({
        id: 3,
        name: 'task3',
        done: false,
      }),
    ],
    newItemId: 4,
  }),
  methods: {
    addItem(itemName: string) {
      this.items.push(
        new Item({
          id: this.newItemId++,
          name: itemName,
          done: false,
        })
      )
    },
    changeDone(id: number) {
      // will access to database
    },
    deleteItem(id: number) {
      this.items = this.items.filter((item) => item.id !== id)
    },
    deleteDone() {
      this.items = this.items.filter((item) => !item.done)
    },
  },
})
</script>
```

Todoリストのアイテムのデータはここに配置します。現状データはベタ打ちしていますが、次回サーバから引っ張ってくるように改造します。　

Todoアイテムを操作する関数もここに書いていきます。基本機能として、

- addItem: アイテム追加
- changeDone: アイテムのステータス変更（実行済/未実行）
- deleteItem: アイテム削除
- deleteDone: 実行済みアイテム削除

を用意しました。
関数内はとりあえず見た目だけ動作するようになってますが、こちらも次回サーバー側のデータベースとやりとりをするように改造します。

（なお、データの場所については、Vuexを使ってもよいのでしょうが、今回はコンポーネント間データの受け渡しの練習としてこうしました。）

Todoリスト自体の中身は`List`コンポーネントに記述します。
各関数を実行するボタン類もこの中に配置します。そのため、`List`にカスタムイベントを作成して、コンポーネント内で`$emit`するようにします。

### componentsディレクトリ
リストのコンポーネント`List.vue`とリスト内の各アイテムを収納する`ItemCard.vue`を作成しました。

```vue:@/components/List.vue
<template>
  <div>
    <v-card class="mx-auto px-3 py-2" width="95%" max-width="500px">
      <v-card-title class="d-flex justify-space-between">
        <p class="ma-0">Things to do...</p>
        <v-btn small outlined color="error" class="pl-2" @click="deleteDone"
          ><v-icon>mdi-delete-outline</v-icon>Done</v-btn
        >
      </v-card-title>
      <v-form @submit.prevent="addItem">
        <v-text-field
          v-model="newItemName"
          placeholder="Add items..."
        ></v-text-field>
      </v-form>
      <div v-for="item in items" :key="item.id">
        <ItemCard
          :item="item"
          @delete-item="deleteItem"
          @change-done="changeDone"
        />
      </div>
    </v-card>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import ItemCard from '@/components/ItemCard.vue'

export default Vue.extend({
  components: {
    ItemCard,
  },
  props: {
    items: {
      required: true,
      type: Array,
    },
  },
  data: () => ({
    newItemName: '',
  }),
  methods: {
    addItem() {
      if (this.newItemName.length !== 0) {
        this.$emit('add-item', this.newItemName)
        this.newItemName = ''
      }
    },
    changeDone(id: number) {
      this.$emit('change-done', id)
    },
    deleteItem(id: number) {
      this.$emit('delete-item', id)
    },
    deleteDone() {
      this.$emit('delete-done')
    },
  },
})
</script>
```

Listにはvuetifyの`v-card`コンポーネントを使いました。簡単シンプルな見た目にできるので、デザイン音痴な自分としてはとてもありがたいですね。propsやclassでいろいろ指定できてcssゼロで作れるっていうのもありがたや（駆け出し並感）。
今回は他にボタンとかチェックボックスとかしか使えなかったですが、今後他のコンポーネントもどんどん使っていきたいです。

各アイテムの中身は`ItemCard`コンポーネントに切り出してみました。

```vue:@/components/ItemCard.vue
<template>
  <v-card class="my-1">
    <v-card-title class="py-0 d-flex justify-space-between">
      <v-checkbox
        v-model="item.done"
        :label="item.name"
        @change="changeDone"
      ></v-checkbox>
      <v-btn icon color="error" @click="deleteItem">
        <v-icon>mdi-close</v-icon>
      </v-btn>
    </v-card-title>
  </v-card>
</template>

<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
  props: {
    item: {
      required: true,
      type: Object,
    },
  },
  methods: {
    changeDone() {
      this.$emit('change-done', this.item.id)
    },
    deleteItem() {
      this.$emit('delete-item', this.item.id)
    },
  },
})
</script>

```

コンポーネント名の通りここでも`v-card`を使いました。汎用性高し。

気になるのは`changeDone`と`deleteItem`をここで発火する。ってところです。
`ItemCard`→`List`→`index`って順に発火していくので、この階層がもっと深くなったら...めんどくさそうですね。　今後はvuexを活用することにします。

## nuxt.config.jsの修正
必要なファイルは全て作成したので、もう一度`npm run dev`してみましょう。

![スクリーンショット 2020-09-29 19.55.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677011/5aa5a103-0f89-4264-a918-63886d988b94.png)

あれ、なんか思ってたんと違う。黒い。

### nuxt.config.js内のvuetifyの設定を変更
`create-nuxt-app`で作成したvuetifyのプロジェクトの雛形はデフォルトでダークモードがオンになってたので、`vuetify.theme.dark`を`false`にしてダークモードを解除します。

```js:nuxt.config.js
import colors from 'vuetify/es5/util/colors'

export default {
  // (略)
  vuetify: {
    customVariables: ['~/assets/variables.scss'],
    theme: {
      dark: false,  // default: true
      themes: {
        dark: {
          primary: colors.blue.darken2,
          accent: colors.grey.darken3,
          secondary: colors.amber.darken3,
          info: colors.teal.lighten1,
          warning: colors.amber.base,
          error: colors.deepOrange.accent4,
          success: colors.green.accent3,
        },
      },
    },
  },
　// (略)
}
```

白くなりました。
![スクリーンショット 2020-09-29 20.20.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677011/a16b6b08-810d-f51d-f25f-72f8ca6afa58.png)


`theme`ごと消しちゃってもよいです。が、変更したくなったときのために取っておきます。
また、`@/assets/variables.scss`を編集することでcssのカスタムもできます。（デザイン音痴なのでやめておきます）

（参考：https://go.nuxtjs.dev/config-vuetify ）

### htmlの`head`タグの設定
htmlの`head`の内容の設定なんかも`nuxt.config.js`でやります。せっかくなので、`title`を`tstodo`に修正します。



```javascript:nuxt.config.js
export default {
  // (中略)

  head: {
    titleTemplate: '%s - tstodo',
    title: 'tstodo',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: '' },
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
  },
  // (中略)
}
```

（参考：https://go.nuxtjs.dev/config-head ）

他にもmoduleの追加のときなどいじる場面が出てきます。

## tsconfig.jsonの修正
デフォルトの`target`が`ES2018`となっている（なぜ？）ので、`ES5`に修正します。

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "ES5",
    "module": "ESNext",
    "moduleResolution": "Node",
    "lib": [
      "ESNext",
      "ESNext.AsyncIterable",
      "DOM"
    ],
    (略)
}
```

また、axiosを使うのにも型宣言を追加する必要などもあるのですが、それは次にします。

## 完成
これで、フロントの見た目が完成しました！！ `npm run build`すれば`dist`ディレクトリにリソースが生成されます。

完成形：https://dai65527.github.io/nuxttodo/
ソース最終版：https://github.com/dai65527/tstodo-client
（githubページへのデプロイも簡単でした：https://ja.nuxtjs.org/faq/github-pages/ ）

初心者でも割と簡単（？）にそれっぽいものが作ることができました。Nuxt.jsとVueはすごいですね。今後はもっと多機能なアプリを作ってみたいと思います。あとNuxt.jsとVue内部どうなってんねん、ってところも勉強したい。

[バックエンド編](https://qiita.com/daitai-daidai/items/a5e744120492bcc2c591)へ続く...

### あとがき
誰かお仕事ください。