NuxtどころかVueもJsも、プログラミングも超絶初心者な私が頑張ってNuxt.jsを使ってtodoアプリを模写?してみました。
参考にしたのはこちらのサイト。ほんまにありがとう…感謝しかありません。インターネット万歳。
https://www.boel.co.jp/tips/vol107/
初心者ゆえ、言い回しや誤ったやり方などあると思いますが、お気づきの際は教えていただけますと幸いです!
さて以下備忘録。
まずは情報設計
iPadを使ってラフに描いてみました。
使用したのはConceptというアプリ。ベクターデータなのでズームしても線がボケないのが気に入っています。
職場ではbacklogを使っているので、それに習ってカテゴリを追加したり、ステータスの文言を変えてみることにしました。多分このくらいのカスタマイズが私の限界と思われ…
雛形作成
pagesディレクトリにtodo.vueを作成して、雛形を作る。
Vuetifyを使っているのでこのようにしました。
headerやfooterはコンポーネント化して別のファイル(Header.vue/Footer.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公式見つつ、エイヤっと見よう見真似で書いてみる。
- Store/index.jsにstateを関数で、mutationをオブジェクトとしてexport
はじめに、ステートを関数で、ミューテーションとアクションをオブジェクトでエクスポートしましょう。
by 公式
クラシックモードでは
const store = new Vuex.Store({
// ステートを定義
state: {
unco: 1
}
})
と書かなければいけなかったところは、モジュールモードでは
export const state = () => ({
unco: 1,
})
でOKとなった模様。
はてmutationとはなんぞや?と公式を読んでいたのですが、storeの状態を変更する関数?みたいなもので、イベントに近いものらしい。ん?ってことは今は配列を格納したいだけだから、mutationはいらないのか?
というわけで、
store/index.js
export const state = () => ({
todos: [],
})
store/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に、仮で入力したタスクのリスト部分を以下のように変更。
<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のデータを取って来ます。
<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>
「タスクの追加」機能の実装
登録された日時を取得して整形した上でcreatedに代入し、入力されたタスクをcontentに入れる処理を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を追加。
<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には「選択したセレクトボックスの値を格納する変数」を指定しています。
データの初期値も追加。
<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であればこの名前空間の指定は不要。
<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の内容全部
<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に以下を追加。
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にてボタンにイベントを追加します。
<v-btn outlined color="error" @click="remove(todo)"
>DELETE
</v-btn>
最後に、methodにremove functionを実装
<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を追加。
<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に追加します。
<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を追加したらお決まり、イベントをつける場所(今回はステータスボタン)に仕掛けを付与
<script>
<td>
<v-btn outlined @click="changeState(todo)">
{{ todo.status }}
</v-btn>
</td>
</script>
最後にmethodを追加
※removeの下
<script>
changeState(todo) {
this.$store.commit('todo2/changeState', todo)
},
</script>
せっかくなので、元記事にあるようにステータスに合わせてボタンのスタイルを変えてみようと思います。
<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>
末尾にはスタイルを追加
<style scoped>
.openbtn {
color: #e91e63;
}
.progressbtn {
color: #2196f3;
}
.resolvebtn {
color: #3f51b5;
}
.closebtn {
color: #009688;
}
</style>
さらっと書いてみましたが、これ実はめちゃくちゃ時間かかった…
スタイルバインディングをして動的にクラスを制御しています。
ESLintのせいなのか(でもエラーが出たわけじゃないしな)、多分Nuxtの仕様なんでしょうけど、v-bind:classを省略した:class=以降のオブジェクトに入れたクラス名にはどうやら''(シングルクォート)付けちゃダメっぽいんですね。
ちゃんとドキュメント探せという話もありましょうが…堪忍してくだせえ
:class="{
openbtn: todo.status === 'OPEN',
}
: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を全て返します。
<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も変えます。
<tr v-for="todo in display_todos" :key="todo.name">
そして絞り込みボタンには布石のイベントを設置!!
<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の設定に参りたいと思います。
<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
の全てを出力するという仕組みです。
おしまい