見出し画像

とにかくシンプルにVue3のComposition APIでTodoアプリを作る

 Vue3が正式リリースされたとの噂なので、賑わってるComposition APIを使って、何はともあれTodoアプリを作成してみます。Composition APIの書き方は……まあ公式のドキュメント見るのが一番なんじゃないかな……まだ日本語訳されてないけど……。

 この記事で作るTodoアプリの特徴は以下の点です。

・CSSは必要最低限しか書かない(Composition APIを試したいだけなので)
・No-Class CSSフレームワークは使う(特にCSSクラスとか付けなくてもいい感じに装飾してくれるため)
・TypeScriptも使わない(めちゃくちゃ使いたいけど)
・CodePenで作成する(とにかく手軽にやりたいので)

 先に完成品を貼っておきます。

 では順番にやっていきます。

まず見た目を作る

 まずはVue.jsを使わず、ひとまずHTMLだけを書いて、見た目部分を完成させます。

 以下のCSS、JavaScriptライブラリをCDNで取り込んでください。

CSS
https://fonts.xz.style/serve/inter.css
https://cdn.jsdelivr.net/npm/@exampledev/new.css@1.1.2/new.min.css
https://newcss.net/theme/terminal.css

JavaScript
https://unpkg.com/vue@next

 そして、HTMLに以下のコードを書いてください。

<div id="todo-container">
 <form>
   <input type="text" />
   <button>追加</button>
 </form>
 <ol>
  <li>
    <input type="checkbox" />
    <span>Todo1</span>
    <button>削除</button>
  </li>
  <li>
    <input type="checkbox" checked />
    <span class="todo__content--completed">Todo2</span>
    <button>削除</button>
  </li>
  <li>
    <input type="checkbox" />
    <span>Todo3</span>
    <button>削除</button>
  </li>
 </ol>
</div>

 最後に、以下のCSSを書いてください。この記事で書くCSSはこのひとつだけです。

.todo__content--completed {
  text-decoration: line-through;
}

 ここまで行ったら、以下のような画面が表示されたと思います。

画像1

 なぜターミナル風画面なのか疑問に思った方もいらっしゃるかもしれませんが、単に私の趣味なので気にしないでください。

Composition API入れ込む

 見た目は完成したので、Composition APIを用いて動かしていきます。

 まず、神聖なるマウントの儀式を行いましょう。Vue3からはマウントの方法が2までと少し変わりました。JavaScriptに以下の一文を書いてください。

Vue.createApp({}).mount("#todo-container");

 次に、todoの一覧を描画するコードを書いていきましょう。ここからがComposition APIなんです。JavaScriptを以下のように書き換えます。

Vue.createApp({
 setup() {
   const todoList = Vue.ref([
     {content: "Todo1", isCompleted: false},
     {content: "Todo2", isCompleted: true},
     {content: "Todo3", isCompleted: false},
   ])
   
   const deleteTodo = (index) => {
     todoList.value.splice(index, 1);
   }
   return { todoList, deleteTodo }
 }
}).mount("#todo-container");

 Vue2.xまでの書き方とは全然違いますね。setup()とかいうのがいますが、Composition APIではVue2.xまでのdataやmethodsに分けて書く代わりに、ここにいっぺんに色々書くようになりました。またVue.refについては、私が説明するより公式サイト見たほうが良い気がするので、公式でrefを説明しているところのリンクを貼っておきます。

HTMLは以下のように書いてください。ここの書き方はVue2.xまでと変わっていません。

<div id="todo-container">
 <form>
   <input type="text" />
   <button>追加</button>
 </form>
 <ol>
   <li v-for="(todo, index) in todoList" :key="index">
     <input type="checkbox" v-model="todo.isCompleted" />
     <span :class="{'todo__content--completed': todo.isCompleted}">
       {{todo.content}}
     </span>
     <button @click="deleteTodo(index)">削除</button>
   </li>
 </ol>
</div>

 ここまでで、todoの一覧を完了したり削除したりができるようになったと思います。

 あとはtodoを追加する機能を作成できれば完成です。JavaScriptを以下のように書き換えます。

Vue.createApp({
 setup() {
   const newTodo = Vue.ref("") //←追加
   const todoList = Vue.ref([
     {content: "Todo1", isCompleted: false},
     {content: "Todo2", isCompleted: true},
     {content: "Todo3", isCompleted: false},
   ])
   
   //↓追加
   const addTodo = () => {
     if (!newTodo.value) return;
     todoList.value.push({
       content: newTodo.value,
       isCompleted: false
     });
     newTodo.value = "";
   }
   
   const deleteTodo = (index) => {
     todoList.value.splice(index, 1);
   }
   return { todoList, deleteTodo, newTodo, addTodo } //←追加
 }
}).mount("#todo-container");

 HTMLを以下のように書き換えます。

<div id="todo-container">
 <form @submit.prevent="addTodo"> ←追加
   <input type="text" v-model="newTodo" /> ←追加
   <button>追加</button>
 </form>
 <ol>
   <li v-for="(todo, index) in todoList" :key="index">
     <input type="checkbox" v-model="todo.isCompleted" />
     <span :class="{'todo__content--completed': todo.isCompleted}">
       {{todo.content}}
     </span>
     <button @click="deleteTodo(index)">削除</button>
   </li>
 </ol>
</div>

 Composition APIを用いたTodoアプリの完成です。お疲れ様でした。

せっかくなのでコンポーネント分割する

 せっかくなので分割します。まずはtodo一覧部分から切り離していきます。

JavaScript

const TodoListComponent = {
 props: {
   todoList: Array
 },
 setup(_, context) {
   const deleteTodo = (index) => context.emit("deleteTodo", index);
   return {deleteTodo}
 },
 template: document.getElementById("todo-list-component")
}

Vue.createApp({
 components: {
   'todo-list-component': TodoListComponent
 },
 setup() {
   const newTodo = Vue.ref("")
   const todoList = Vue.ref([
     {content: "Todo1", isCompleted: false},
     {content: "Todo2", isCompleted: true},
     {content: "Todo3", isCompleted: false},
   ])
   
   const addTodo = () => {
     if (!newTodo.value) return;
     todoList.value.push({
       content: newTodo.value,
       isCompleted: false
     });
     newTodo.value = "";
   }
   
   const deleteTodo = (index) => {
     todoList.value.splice(index, 1);
   }
   return { todoList, deleteTodo, newTodo, addTodo }
 }
}).mount("#todo-container");
HTML

<div id="todo-container">
 <form @submit.prevent="addTodo">
   <input type="text" v-model="newTodo" />
   <button>追加</button>
 </form>
 <todo-list-component :todo-list="todoList" @delete-todo="deleteTodo"></todo-list-component>
</div>

<template id="todo-list-component">
 <ol>
   <li v-for="(todo, index) in todoList" :key="index">
     <input type="checkbox" v-model="todo.isCompleted" />
     <span :class="{'todo__content--completed': todo.isCompleted}">
       {{todo.content}}
     </span>
     <button @click="deleteTodo(index)">削除</button>
   </li>
 </ol>
</template>

 Vue2.xから変わった点といえば、emitの方法でしょうか。2.xまでは「this.$emit」を用いていたと思いますが、Composition APIではsetup関数の第二引数を用いて「context.emit」と書きます。

 最後に、Todo追加欄を分割します。

JavaScript

const TodoListComponent = {
 props: {
   todoList: Array
 },
 setup(_, context) {
   const deleteTodo = (index) => context.emit("deleteTodo", index);
   return {deleteTodo}
 },
 template: document.getElementById("todo-list-component")
}

const AddTodoComponent = {
 setup(_, context) {
   const newTodo = Vue.ref("");
   const addTodo = () => {
     if (!newTodo.value) return;
     context.emit("addTodo", newTodo.value);
     newTodo.value = "";
   }
   return {newTodo, addTodo}
 },
 template: document.getElementById("add-todo-component")
}

Vue.createApp({
 components: {
   "todo-list-component": TodoListComponent,
   "add-todo-component": AddTodoComponent,
 },
 setup() {
   const todoList = Vue.ref([
     {content: "Todo1", isCompleted: false},
     {content: "Todo2", isCompleted: true},
     {content: "Todo3", isCompleted: false},
   ])
   
   const addTodo = (newTodo) => {
     todoList.value.push({
       content: newTodo,
       isCompleted: false
     });
   }
   
   const deleteTodo = (index) => {
     todoList.value.splice(index, 1);
   }
   return { todoList, deleteTodo, addTodo }
 }
}).mount("#todo-container");
HTML

<div id="todo-container">
 <add-todo-component @add-todo="addTodo"></add-todo-component>
 <todo-list-component :todo-list="todoList" @delete-todo="deleteTodo"></todo-list-component>
</div>

<template id="todo-list-component">
 <ol>
   <li v-for="(todo, index) in todoList" :key="index">
     <input type="checkbox" v-model="todo.isCompleted" />
     <span :class="{'todo__content--completed': todo.isCompleted}">
       {{todo.content}}
     </span>
     <button @click="deleteTodo(index)">削除</button>
   </li>
 </ol>
</template>

<template id="add-todo-component">
 <form @submit.prevent="addTodo">
   <input type="text" v-model="newTodo" />
   <button>追加</button>
 </form>
</template>

 分割も完了しました。お疲れ様でした。

おまけ:使ってみての感想

 書き味がReactに近くなった気がしました。今までのVue.jsって初心者に優しいって印象が強かったんですが、Composition APIはもうちょっとJavaScript書ける人向けという印象です。

 フロントエンド弱いところはVue2.xまでの書き方から始めるのが良いのかなと思いました。まあまだ公式ドキュメント日本語化されてないし、弱いところがわざわざComposition API導入するかって感じですけど。

この記事が気に入ったらサポートをしてみませんか?