はじめに
どうも、
フロントエンジニアとして働き始めて、気づけば4ヶ月目に入った者です。
ここ最近は、たくさんチケットを振っていただけるのですが思うようにこなせず、日々強い危機感を抱きながら業務に取り組んでいます。
4ヶ月目に入った今でも、本当に簡単なタスクをようやくこなせる程度で、調査にはまだ膨大な時間がかかってしまいます。
どこで何が起きているのかを読み解くだけでも、相当な時間を取られてしまうのが現状で、前職のWeb制作で培ったスキルを活かす場面はほとんどなく、十分な成果を出せない日々が続いています。
ただ、多くのエンジニアの記事やYouTubeを見ていると、「最初の1年は本当にきつかった」と口を揃えて語っていました。
それを知ってからは、「エンジニアとはそういうものだ」と少しずつ受け止められるようになってきました。
今はとにかく、目の前のタスクを一日でも早く終わらせる。その気持ちだけで毎日に向き合っています。正直、それ以外のことを考える余裕はありません。
また、実務で分からないことを丸投げでAIに頼って実装すると、思わぬバグにつながってしまうこともしばしば…
だからこそ、AIはあくまで補助で時間がかかっても自分でコードを丁寧に読み、手元でログなど出しながら検証して理解を深めることを意識して働いています。
さて、前置きが長くなりましたが、そんなエンジニアライフを送る中で、最近はAIに頼らず自分の手でアプリを作ることを意識して学習する時間が増えてきています。
もちろんAIが悪いということではなく、私もハンズオン形式などで学んだ後はAIにお題を出してもらって理解を深めたり、実務でも毎回助けてもらっています。
それでも今は、自分の手だけでコードを書き上げる時間 が大事だと感じていて、今回は Vue.js で AIに頼らずシンプルなTodoアプリ を作ってみました。
ここからは、そのアプリについて紹介していきたいと思います。
自分で作ったアプリ だからこそ、どんなふうに動くのかを説明できるはず!
※本記事で紹介するコードは Vue 2 Options API を使用しています。
Vue.js Todo App
URL (※PC推奨 ) :
まず、下記のような構造で作成しました。
本当は App.vue だけでも完結できるのですが、今回は値の受け渡しに Props や $emit を実際に使って理解したかったため、このようにコンポーネントを分割しています。
src/
├── App.vue
├── main.js
├── components/
│ ├── InputForm.vue # 入力フォーム
│ └── TodoList.vue # Todoリストの表示や削除など
InputForm.vue
<template>
<div class="child">
<p>Input Form</p>
<div>
<input type="text" v-model="todoText">
<button @click="addTodoText(todoText)">add</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
todoText: '',
}
},
methods: {
addTodoText(text) {
if (text === '') {
alert('Please enter text :)')
return;
}
this.$emit("add-todo", text);
this.todoText = ''; /
}
}
}
</script>
まずは InputForm.vue で、ユーザーが入力した値を v-model で双方向バインディングし、
「add」ボタンをクリックすると、その値を引数にして methods 内の addTodoText を実行する流れです。
addTodoText メソッドでは、引数に text を受け取り、this.$emit("add-todo", text) を使って親コンポーネントにイベントを渡しています。
また、if (text === '') で入力が空だった場合は return して処理を中止するようにしました。
さらに、親コンポーネントに値を渡したあとには this.todoText を初期化して入力欄を空に戻すようにしています。
これらの改善は、作っては壊してを何度も繰り返す中で追加されていったもの です。
App.vue
<!-- 入力 送信するコンポーネント -->
<!-- 描画するコンポーネント -->
<!-- 削除機能 -->
<!-- 完了したタスクはチェックボックのtrue判定で線を引く -->
<template>
<div id="app" class="parent">
<h2>Parent</h2>
<input-form @add-todo="addTodo"/>
<todo-list :propsTodosList="todoLists" @delete-id="deleteId"/>
</div>
</template>
<script>
import TodoList from './components/TodoList.vue';
import InputForm from './components/InputForm.vue';
export default {
name: 'App',
components: {
TodoList,
InputForm
},
data() {
return {
todoLists: []
}
},
methods: {
addTodo(list) {
this.todoLists.push({
text: list,
id: this.todoLists.length + 1,
complete: false
})
},
deleteId(id) {
this.todoLists = this.todoLists.filter(array => array.id !== id)
}
}
}
</script>
子コンポーネントで $emit("add-todo") したイベントは、親の App.vue 側で addTodo メソッドとして受け取ります。
methods: {
addTodo(list) {
this.todoLists.push({
text: list,
id: this.todoLists.length + 1,
complete: false
})
},
ここで引数 list に子コンポーネントから渡されたテキストが入り、それを this.todoLists.push({...}) で空の配列 todoLists: [] に追加しています。
タスクはオブジェクト形式で格納しており、入力内容を text として保存するほか、
id は this.todoLists.length + 1 とすることで、配列の要素数に1を足した数をIDとして使い、タスクごとに「1、2、3…」と順番に番号を割り振っています。これは削除機能を実装する際に必要になるものです。
また、チェックボックスで完了状態を切り替えるためのフラグとして complete を定義し、初期値を false として配列に追加しています。
push されたオブジェクトは、親コンポーネントの data() で定義した todoLists: [] に格納されます。
そしてテンプレート部分では、
<todo-list :propsTodosList="todoLists" @delete-id="deleteId"/>
として、todoLists を子コンポーネント TodoList.vue に props として渡しています。
TodoList.vue
<template>
<div class="child">
<p>Todo List</p>
<div>
<ul>
<li v-for="list in propsTodosList" :key="list.id">
<input type="checkbox" v-model="list.complete">
<span :class="{complete: list.complete}">{{ list.text }}</span>
<button @click="deleteTodo(list.id)">delete</button>
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
props: {
propsTodosList: Array
},
methods: {
deleteTodo(id) {
this.$emit("delete-id", id)
}
}
}
</script>
App.vue から渡ってきた props は、propsTodosList という名前で受け取り、型を Array と指定しています。
props: {
propsTodosList: Array
},
また、propsTodosList として受け取った配列を v-for によって一つずつ取り出し list として liタグで描画しています。
この仕組みによって、InputForm.vue で addボタン をクリックすると、新しいタスクが todoLists に追加され、そのまま TodoList.vue に渡されてリスト表示されるようになります。
削除機能
削除機能についても実装しました。
App.vueで this.todoLists.push({ ... })した際に、各タスクに id を定義していました。これを v-for の key に指定。
TodoList.vue 側では削除ボタンに @click="deleteTodo(list.id)" を付けて、削除したいタスクの id を引数として渡します。そして deleteTodo(id) メソッドの中で
this.$emit("delete-id", id)
と書き、削除したい id を親コンポーネントに通知しています。
親の App.vue では、この delete-id を受け取って deleteId(id) メソッドを実行し、
deleteId(id) {
this.todoLists = this.todoLists.filter(array => array.id !== id)
}
とすることで、該当する id のタスクを配列から除外し、表示からも削除できるようにしました。
ここでは filter関数 を使い、配列の中(this.todoLists)の id と子コンポーネントから渡ってきた id を比較し、一致した場合は false にすることでタスクを削除する仕組みにしています。
完了したタスクに取り消し線をつける
チェックボックスに
<input type="checkbox" v-model="list.complete">
を指定することで、list.complete の値と双方向バインディングされます。
これにより、チェックを入れると list.complete が true になり、外すと false になります。
次に、<span> タグに:class="{ complete: list.complete }"と書くことで、list.complete が trueのときだけ completeクラスが適用されます。
<span :class="{complete: list.complete}">{{ list.text }}</span>
CSS では .complete { text-decoration: line-through; } と定義してあるため、チェックを入れたタスクには自動的に取り消し線が表示される仕組みです。
まとめ
今回のTodoアプリでは、子コンポーネントで入力した値を親に渡し、配列に追加して描画するという基本的な流れを一通り実装しました。
- InputForm.vue でユーザーの入力を v-model で管理し、ボタンをクリックすると $emit("add-todo") で親に値を渡す
- App.vueでその値を addTodo() メソッドとして受け取り todoLists[] 配列にオブジェクト形式で格納
- TodoList.vue に props で渡し、v-for を使って描画。削除は $emit("delete-id") で親に通知し、filter で配列から除外
- チェックボックスと :class="{ complete: list.complete }" を使うことで、完了したタスクに取り消し線を表示
とてもシンプルなアプリですが、Vueの基礎を一通り実際に触れながら理解することができました。
また、AI時代だからこそ、自分で考え、作っては壊してを繰り返す学習スタイルが大切だと実感しています。
それでは最後まで読んでいただき、ありがとうございました!!