LoginSignup
2
1

More than 1 year has passed since last update.

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

Posted at

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

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

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

さて以下備忘録。

まずは情報設計

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

職場ではbacklogを使っているので、それに習ってカテゴリを追加したり、ステータスの文言を変えてみることにしました。多分このくらいのカスタマイズが私の限界と思われ…
ia_rough.png

雛形作成

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

スクリーンショット 2021-06-17 9.00.47.png

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

そんなわけで
Nuxt公式見つつVuex公式見つつ、エイヤっと見よう見真似で書いてみる。

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

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

クラシックモードでは

index.js
const store = new Vuex.Store({
  // ステートを定義
  state: {
    unco: 1
  }
})

と書かなければいけなかったところは、モジュールモードでは

index.js
export const state = () => ({
  unco: 1,
})

でOKとなった模様。
はてmutationとはなんぞや?と公式を読んでいたのですが、storeの状態を変更する関数?みたいなもので、イベントに近いものらしい。ん?ってことは今は配列を格納したいだけだから、mutationはいらないのか?

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

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

store/todo.js

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に、仮で入力したタスクのリスト部分を以下のように変更。

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のデータを取って来ます。

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

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

登録された日時を取得して整形した上で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には「選択したセレクトボックスの値を格納する変数」を指定しています。
データの初期値も追加。

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であればこの名前空間の指定は不要。

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

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

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に以下を追加。

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にてボタンにイベントを追加します。

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

最後に、methodにremove functionを実装

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>

todosに登録されたタスクがdeleteで消せるようになりました!

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

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

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に追加します。

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を追加したらお決まり、イベントをつける場所(今回はステータスボタン)に仕掛けを付与

pages/todo.vue
<script>
  <td>
    <v-btn outlined @click="changeState(todo)">
      {{ todo.status }}
    </v-btn>
  </td>
</script>

最後にmethodを追加
※removeの下

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

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

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>

末尾にはスタイルを追加

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


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

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

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

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

あとクラス名は減算演算子としての - とコンフリクトするため控えたほうがよろしいそう。

絞り込み機能の実装

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

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

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も変えます。

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

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

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の設定に参りたいと思います。

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の全てを出力するという仕組みです。

おしまい

2
1
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
2
1