Nuxt.js でtodoアプリを作成するまで

NuxtどころかVueもJsも、プログラミングも超絶初心者な私が頑張ってNuxt.jsを使ってtodoアプリを模写？してみました。

参考にしたのはこちらのサイト。ほんまにありがとう…感謝しかありません。インターネット万歳。
https://www.boel.co.jp/tips/vol107/

初心者ゆえ、言い回しや誤ったやり方などあると思いますが、お気づきの際は教えていただけますと幸いです！

さて以下備忘録。

## まずは情報設計
iPadを使ってラフに描いてみました。
使用したのはConceptというアプリ。ベクターデータなのでズームしても線がボケないのが気に入っています。

職場では[backlog](https://backlog.com/ja/)を使っているので、それに習ってカテゴリを追加したり、ステータスの文言を変えてみることにしました。多分このくらいのカスタマイズが私の限界と思われ…
![ia_rough.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97385/64eefd3b-2daa-2719-c99e-033f8113919f.png)

## 雛形作成

pagesディレクトリにtodo.vueを作成して、雛形を作る。
Vuetifyを使っているのでこのようにしました。
headerやfooterはコンポーネント化して別のファイル（Header.vue/Footer.vue）にて編集しています。

![スクリーンショット 2021-06-17 9.00.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97385/313b0fe9-2d79-a5dc-fb36-0599dfde6dad.png)


```vuejs:todo.vue
<template>
  <div>
    <Header />
    <v-container>
      <h2>TO DO List</h2>
    </v-container>
    <v-container>
      <v-form>
        <v-row align="center">
          <v-col class="d-flex" cols="12" sm="6">
            <v-text-field label="the thing to do" required></v-text-field>
          </v-col>
          <v-col class="d-flex" cols="12" sm="6">
            <v-btn class="primary">ADD</v-btn>
            <!-- <Button :small="false" color="primary" :click="deleteItem(index)">
              Add
            </Button> -->
          </v-col>
        </v-row>
        <v-select :items="items" label="Category" style="width: 250px">
        </v-select>
      </v-form>
    </v-container>

    <v-container>
      <div class="filter">
        <v-row align="center" class="mb-6">
          <v-spacer></v-spacer>
          <v-col
            v-for="state in status"
            :key="state.name"
            class="pa-2"
            cols="auto"
          >
            <!-- <v-col
              v-for="state in status"
              :key="state.name"
              class="d-flex justify-space-between"
              sm="1"
            > -->
            <v-btn text elevation="0">{{ state }}</v-btn>
          </v-col>
          <v-spacer></v-spacer>
        </v-row>
      </div>
    </v-container>
    <v-container>
      <v-simple-table fixed-header height="400px">
        <template>
          <thead>
            <tr>
              <th class="text-left">TO DO</th>
              <th class="text-left">REGISTERE</th>
              <th class="text-left">STATUS</th>
              <th>BUTTON</th>
            </tr>
          </thead>
          <tbody>
            <!-- <tr v-for="value in values" :key="value.name"> -->
            <tr>
              <td>TEST</td>
              <td>2021-05-30 17:00</td>
              <td><v-btn outlined>OPEN</v-btn></td>
              <td><v-btn outlined color="error">DELETE</v-btn></td>
            </tr>
          </tbody>
        </template>
      </v-simple-table>
    </v-container>
  </div>
</template>

<script>
export default {
  data() {
    return {
      items: ['TASK', 'BUG', 'ORDER', 'OTHER'],
      status: ['All', 'Open', 'In progress', 'Resolved', 'Closed'],
    }
  },
}
</script>
```

## Vuexストアを作成

まずVuexストアを作成…とあるのだが、まずここでつまずいた。
そもそもVuexってなんやねん！
調べてみると、共通データを保管できる仕組み？らしい？
参考サイトではクラシックモードを使っているとかなんとかで、ESLintでは
>Classic mode for store/ is deprecated and will be removed in Nuxt 3

「そのモード、Nuxt3ではなくなるで、カス」
と言われる（被害妄想）。

どうやらStoreもハブ（index.js）と、モジュールに分けて管理するようです。んで、Nuxt3ではわざわざ取ってこい！ってちまちま書かんでもStore内のモジュールは簡単な書き方で勝手に取ってきてくれるっぽい？

>Vuexストアのステートはアプリケーション全体の状態を保持するオブジェクトです。アプリケーション全体で使用される情報をステートで管理すると保守性が高まります。たとえば、ログイン中のユーザー情報、ECサイトにおける商品情報、ユーザーの端末情報など複数の場所で使用される可能性のあるデータはステートで管理するとよいです。

参考サイト：　[Vuexのステートの使い方を覚えて状態を管理しよう｜Vuex](https://dev83.com/vue-vuex03/#:~:text=Vuex%E3%82%B9%E3%83%88%E3%82%A2%E3%81%AE%E3%82%B9%E3%83%86%E3%83%BC%E3%83%88%E3%81%AF,%E3%81%A7%E7%AE%A1%E7%90%86%E3%81%99%E3%82%8B%E3%81%A8%E3%82%88%E3%81%84%E3%81%A7%E3%81%99%E3%80%82)

そんなわけで
[Nuxt公式](https://ja.nuxtjs.org/docs/2.x/directory-structure/store/)見つつ[Vuex公式](https://vuex.vuejs.org/ja/guide/state.html)見つつ、エイヤっと見よう見真似で書いてみる。

1. Store/index.ｊｓにstateを関数で、mutationをオブジェクトとしてexport

>はじめに、ステートを関数で、ミューテーションとアクションをオブジェクトでエクスポートしましょう。
by 公式

クラシックモードでは

```javascript:index.js
const store = new Vuex.Store({
  // ステートを定義
  state: {
    unco: 1
  }
})
```
と書かなければいけなかったところは、モジュールモードでは

```javascript:index.js
export const state = () => ({
  unco: 1,
})
```
でOKとなった模様。
はてmutationとはなんぞや？と[公式](https://vuex.vuejs.org/ja/guide/mutations.html)を読んでいたのですが、storeの状態を変更する関数？みたいなもので、イベントに近いものらしい。ん？ってことは今は配列を格納したいだけだから、mutationはいらないのか？

というわけで、
store/index.js

```javascript:index.js
export const state = () => ({
  todos: [],
})
```

store/todo.js

```javascript:todo.js
export const state = () => ({
  todos: [
    {
      content: 'テスト',
      created: '2021-06-15 17:00',
      category: 'task',
      status: '作業前',
    },
    {
      content: 'テスト',
      created: '2021-06-15 17:00',
      category: 'task',
      status: '作業前',
    },
    {
      content: 'テスト',
      created: '2021-06-15 17:00',
      category: 'task',
      status: '作業前',
    },
  ],
})

```
これでどうやろか？


## storeの情報はページに表示されるのか…？

次はストアの情報がページに表示されるように頑張ってみる。えいえいおー

pagesディレクトリのtodo.vueに、仮で入力したタスクのリスト部分を以下のように変更。
 

```vuejs:todo.vue
        <template>
          <thead>
            <tr>
              <th class="text-left">TO DO</th>
              <th class="text-left">REGISTERED</th>
              <th class="text-left">CATEGORY</th>
              <th class="text-left">STATUS</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="todo in todos" :key="todo.name">
              <td>{{ todo.content }}</td>
              <td>{{ todo.created }}</td>
              <td>
                <v-btn outlined>{{ todo.category }}</v-btn>
              </td>
              <td>
                <v-btn outlined>{{ todo.status }}</v-btn>
              </td>
              <td>
                <v-btn outlined color="error">DELETE</v-btn>
              </td>
            </tr>
          </tbody>
        </template>
```

んでもって$storeからtodosのデータを取って来ます。

```vuejs:todo.vue
<script>
export default {
  data() {
    return {
      items: ['TASK', 'BUG', 'ORDER', 'OTHER'],
      status: ['All', 'Open', 'In progress', 'Resolved', 'Closed'],
    }
  },
  computed: {
    todos() {
      return this.$store.state.todo2.todos
    },
  },
}

// console.log(JSON.stringify(this.$store.state.todo2.todos))
</script>

```

やったー！！
![スクリーンショット 2021-06-21 9.43.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97385/0c4ee75c-5335-3f22-28a1-0b6f18090509.png)

## 「タスクの追加」機能の実装

登録された日時を取得して整形した上でcreatedに代入し、入力されたタスクをcontentに入れる処理をstore/todo.jsに記述。

```vuejs;store/todo.js

export const mutations = {
  insert(state, obj) {
    // Mon Jun 21 2021 11:32:57 GMT+0900 (日本標準時)
    const d = new Date()
    const fmt =
      d.getFullYear() +
      '-' +
      ('00' + (d.getMonth() + 1)).slice(-2) +
      '-' +
      ('00' + d.getDate()).slice(-2) +
      '-' +
      ('00' + d.getHours()).slice(-2) +
      '-' +
      ('00' + d.getMinutes()).slice(-2)
    state.todos.unshift({
      content: obj.content,
      created: fmt,
      cats: obj.cats,
      status: 'OPEN',
    })
  },
} 
```
input（v-text-field）とselect(v-select),button(v-btn)にはv-modelを追加。

```pages/todo.vue
      <v-form>
        <v-row align="center">
          <v-col class="d-flex" cols="12" sm="6">
            <v-text-field v-model="content" label="the thing to do" required>
            </v-text-field>
          </v-col>
        </v-row>
        <v-row align="center">
          <v-col class="d-flex" cols="12" sm="6">
            <v-select
              v-model="selectCat"
              :items="cats"
              label="Category"
              style="width: 250px"
              required
            >
            </v-select>
          </v-col>
          <v-col class="d-flex" cols="12" sm="6">
            <v-btn class="primary" @click="insert">ADD</v-btn>
          </v-col>
        </v-row>
      </v-form>

```
v-model属性で、inputに入力されたタスクをcontentと紐づけ(データバインディング)。

select(カテゴリー選択)については、v-modelには「選択したセレクトボックスの値を格納する変数」を指定しています。
データの初期値も追加。

``` vuejs:todo.vue
<script>
  data() {
    return {
      content: '',
      selectCat: '',
      cats: ['TASK', 'BUG', 'ORDER', 'OTHER'],
      status: ['ALL', 'OPEN', 'IN PROGRESS', 'RESOLVED', 'CLOSED'],
    }
</script>

```

button(v-btn)については、v-on属性で要素にイベントを設定しています。クリックイベントの設定には、 ```v-on:click``` を使います。v-onは「@」と省略できるため、ここでは ```v-on:click``` を ```@click``` と書いています。

このv-on属性で、ボタンにクリックイベントを設定し、methodsのinsertを呼び出し。

私の場合、storeのmutations呼び出しにはstoreに名前空間（todo）を設定しているので、呼び出し先にも名前空間の指定が必要です。storeの名前がindex.jsであればこの名前空間の指定は不要。

```vuejs:pages/todo.vue
<script>

  methods: {
    insert() {
      // 空のタスクが入力されないように
      if (this.content !== '') {
        this.$store.commit('todo/insert', {
          content: this.content,
          cats: this.selectCat,
        })
        // 入力欄を空に
        this.content = ''
        this.selectCat = ''
      }
    },
  },
</script>

```

mutationについて捕捉
> 実際に Vuex のストアの状態を変更できる唯一の方法は、ミューテーションをコミットすることです。Vuex のミューテーションはイベントにとても近い概念です: 各ミューテーションはタイプとハンドラを持ちます。ハンドラ関数は Vuex の状態（state）を第1引数として取得し、実際に状態の変更を行います:
[ミューテーション｜Vuex](https://vuex.vuejs.org/ja/guide/mutations.html)

ボタンの位置とか若干変えたので、ひとまずここまでのpages/todo.vueの内容全部

```vuejs:todo.vue
<template>
  <div>
    <Header />
    <v-container>
      <h2>TO DO List</h2>
    </v-container>
    <v-container>
      <v-form>
        <v-row align="center">
          <v-col class="d-flex" cols="12" sm="6">
            <v-text-field v-model="content" label="the thing to do" required>
            </v-text-field>
          </v-col>
        </v-row>
        <v-row align="center">
          <v-col class="d-flex" cols="12" sm="6">
            <v-select
              v-model="selectCat"
              :items="cats"
              label="Category"
              style="width: 250px"
              required
            >
            </v-select>
          </v-col>
          <v-col class="d-flex" cols="12" sm="6">
            <v-btn class="primary" @click="insert">ADD</v-btn>
          </v-col>
        </v-row>
      </v-form>
    </v-container>

    <v-container>
      <div class="filter">
        <v-row align="center" class="mb-6">
          <v-spacer></v-spacer>
          <v-col
            v-for="state in status"
            :key="state.name"
            class="pa-2"
            cols="auto"
          >
            <v-btn text elevation="0">{{ state }}</v-btn>
          </v-col>
          <v-spacer></v-spacer>
        </v-row>
      </div>
    </v-container>
    <v-container>
      <v-simple-table fixed-header height="400px">
        <template>
          <thead>
            <tr>
              <th class="text-left">TO DO</th>
              <th class="text-left">REGISTERED</th>
              <th class="text-left">CATEGORY</th>
              <th class="text-left">STATUS</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="todo in todos" :key="todo.name">
              <td>{{ todo.content }}</td>
              <td>{{ todo.created }}</td>
              <td>
                <v-btn outlined>{{ todo.cats }}</v-btn>
              </td>
              <td>
                <v-btn outlined>{{ todo.status }}</v-btn>
              </td>
              <td>
                <v-btn outlined color="error">DELETE</v-btn>
              </td>
            </tr>
          </tbody>
        </template>
      </v-simple-table>
    </v-container>
  </div>
</template>

<script>
// import { mapState } from 'vuex' 不要

export default {
  data() {
    return {
      content: '',
      selectCat: '',
      cats: ['TASK', 'BUG', 'ORDER', 'OTHER'],
      status: ['ALL', 'OPEN', 'IN PROGRESS', 'RESOLVED', 'CLOSED'],
    }
  },
  computed: {
    todos() {
      return this.$store.state.todo2.todos
    },
  },
  methods: {
    insert() {
      // 空のタスクが入力されないように
      if (this.content !== '') {
        this.$store.commit('todo2/insert', {
          content: this.content,
          cats: this.selectCat,
        })
        // 入力欄を空に
        this.content = ''
        this.selectCat = ''
      }
    },
  },
}

// console.log(JSON.stringify(this.$store.state.todo2.todos))
</script>

```

## 削除機能の追加
追加したタスクの削除機能を実装します。
まずはstore/todo.jsのmutationに以下を追加。

```vuejs:store/todo.js
  remove(state, obj) {
    for (let i = 0; i < state.todos.length; i++) {
      const ob = state.todos[i]
      if (ob.content === obj.content && ob.created === obj.created) {
        if (confirm('Are you sure to delete "' + ob.content + '" ?')) {
          state.todos.splice(i, 1)
          return
        }
      }
    }
  },

```

削除ボタンが押された際、todosに登録されている全てのtodoからcontentかつcreatedが一致する内容を探し、comfirmを出した上でtodosから削除します。

続きましてpages/todo.vueにてボタンにイベントを追加します。

```vuejs:pages/todo.vue
<v-btn outlined color="error" @click="remove(todo)"
　　>DELETE
</v-btn>

```

最後に、methodにremove　functionを実装

```vuejs:pages/todo.vue

<script>
  methods: {
    insert() {
      if (this.content !== '') {
        this.$store.commit('todo2/insert', {
          content: this.content,
          cats: this.selectCat,
        })
        this.content = ''
        this.selectCat = ''
      }
    },
    remove(todo) {
      this.$store.commit('todo2/remove', todo)
    },
  },
</script>

```
todoｓに登録されたタスクがdeleteで消せるようになりました！


## 「タスクの状態変更」機能の実装

①それぞれの状態を数字で定義
　→　store/todo.jsでstateのtodosの下にoptionを追加。

```vuejs:store/todo.js
<script>
  option: [
    { id: 0, label: 'OPEN' },
    { id: 1, label: 'IN PROGRESS' },
    { id: 2, label: 'RESOLVED' },
    { id: 3, label: 'COMPLETED' },
  ],
</script>
```

stateの状態を変えられるのはmutationなので、このstateを変えるfunctionをmutaionに追加します。


```vuejs:store/todo.js
<script>
  changeState(state, obj) {
    console.log('store changeState')
    // console.log(JSON.stringify(obj))
    // todosの中から該当のtodoを探して
    for (let i = 0; i < state.todos.length; i++) {
      const ob = state.todos[i]
      // console.log(JSON.stringify(ob))
      if (
        ob.content === obj.content &&
        ob.created === obj.created &&
        ob.status === obj.status
      ) {
        // console.log('object match!')
        // 現在のstatus idを割り振る
        let nowState
        for (let j = 0; j < state.option.length; j++) {
          if (state.option[j].label === ob.status) {
            nowState = state.option[j].id
          }
        }
        // 現在＋1のid statusにチェンジ
        nowState++
        // idの最後でidを振り出し（0）に
        if (nowState >= state.option.length) {
          nowState = 0
        }
        // idのラベルを表示
        obj.status = state.option[nowState].label
        return
      }
    }
  },
</script>
```
Mutationを追加したらお決まり、イベントをつける場所（今回はステータスボタン）に仕掛けを付与

```vuejs:pages/todo.vue
<script>
  <td>
    <v-btn outlined @click="changeState(todo)">
      {{ todo.status }}
    </v-btn>
  </td>
</script>
```
最後にmethodを追加
※removeの下

```vuejs:pages/todo.vue
<script>
  changeState(todo) {
    this.$store.commit('todo2/changeState', todo)
  },
</script>
```

せっかくなので、元記事にあるようにステータスに合わせてボタンのスタイルを変えてみようと思います。


```vuejs:pages/todo.vue
<script>

  <v-btn
    outlined
    :class="{
      openbtn: todo.status === 'OPEN',
      progressbtn: todo.status === 'IN PROGRESS',
      resolvebtn: todo.status === 'RESOLVED',
      closebtn: todo.status === 'CLOSED',
    }"
    @click="changeState(todo)"
  >
    {{ todo.status }}
  </v-btn>

</script>

```
末尾にはスタイルを追加

```vuejs:pages/todo.vue
<style scoped>
.openbtn {
  color: #e91e63;
}
.progressbtn {
  color: #2196f3;
}
.resolvebtn {
  color: #3f51b5;
}
.closebtn {
  color: #009688;
}
</style>


```
さらっと書いてみましたが、これ実はめちゃくちゃ時間かかった…
スタイルバインディングをして動的にクラスを制御しています。

ESLintのせいなのか（でもエラーが出たわけじゃないしな）、多分Nuxtの仕様なんでしょうけど、v-bind:classを省略した:class=以降のオブジェクトに入れたクラス名にはどうやら''(シングルクォート)付けちゃダメっぽいんですね。

ちゃんとドキュメント探せという話もありましょうが…堪忍してくだせえ


```vuejs:OK🙆‍♀️
:class="{
      openbtn: todo.status === 'OPEN',
}
```

```vuejs:NG🙅‍♀️
:class="{
      'openbtn': todo.status === 'OPEN',
}
```

あとクラス名は[減算演算子としての - とコンフリクトする](https://qiita.com/mascii/items/3202b9e18fd4a7366ac1#fn4)ため控えたほうがよろしいそう。


## 絞り込み機能の実装

まず、ステータスの内容を入れる箱（find_status）と、ステータスがAllの時の判定に使うfind_flｇをdataに定義します。

computedには、押されたステータスボタンに応じてtodosの表示を変えて描画するdisplay_todos()を書きます。
find_flgがtrueなら（ALLでなければ）配列arr[]を用意し、data変数にはtodosのデータを代入。
todos1つ1つについて、statusが同じものをarrに格納し、arrを返します。
find_flgがfalseなら（ALL。初期状態）なら、まんまstateに入っているtodosを全て返します。

```vuejs:pages/todo.vue
<script>

  data() {
    return {
      content: '',
      selectCat: '',
      cats: ['TASK', 'BUG', 'ORDER', 'OTHER'],
      status: ['ALL', 'OPEN', 'IN PROGRESS', 'RESOLVED', 'CLOSED'],
      find_status: '',
      find_flg: false,
    }
  },
  computed: {
    display_todos() {
      if (this.find_flg) {
        const arr = []
        const data = this.$store.state.todo.todos
        data.forEach((element) => {
          if (element.status === this.find_status) {
            arr.push(element)
          }
        })
        return arr
      } else {
        return this.$store.state.todo.todos
      }
    },
</script>

```
これにより、computedで表示の方法をtodosからdisplay_todosに変えたので、リストのv-forも変えます。


```vuejs:pages/todo.vue
<tr v-for="todo in display_todos" :key="todo.name">
```

そして絞り込みボタンには布石のイベントを設置！！

```vuejs:pages/todo.vue
  <v-col class="pa-2" cols="auto">
    <v-btn text elevation="0" @click="flag_reset">ALL</v-btn>
    <v-btn text elevation="0" @click="find('OPEN')">OPEN</v-btn>
    <v-btn text elevation="0" @click="find('IN PROGRESS')">
      IN PROGRESS
    </v-btn>
    <v-btn text elevation="0" @click="find('RESOLVED')">RESOLVED</v-btn>
    <v-btn text elevation="0" @click="find('CLOSED')">CLOSED</v-btn>
  </v-col>

```

ALLには、`find_flg: false` にするための `flag_reset` をクリックイベントに設定しています。

さて、準備は整いましたので、methodの設定に参りたいと思います。

```vuejs:pages/todo.vue
<script>
  find(findStatus) {
    this.find_status = findStatus
    this.find_flg = true
  },
  flag_reset() {
    this.find_flg = false
  },
</script>

```

ALL以外のボタンが押された際は、```find```メソッドが発動。押された文字列のデータが```find_status```に代入され、先ほどcomputedで設定した判定に使われます。

ALLボタンに設定した```flag_reset```では```find_flg```をfalseにし、```this.$store.state.todo.todos```の全てを出力するという仕組みです。

おしまい