Skip to content
sumnail

Vue3でタスク管理アプリケーション開発

created at : 2024/05/15

Vue3
Sample

Sample ToDo

  • ToDoアプリを作成

記事概要

シンプルなタスク管理アプリケーションを題材に、Vue3の基本概念を解説する記事です。
開発完成イメージは、上記になります。

開発概要

タスク管理に必要な機能を考えます。
シンプルな機能であれば、次の5つでしょうか?

  • タスクを新規登録する
  • タスクを一覧表示する
  • タスクを削除する
  • 完了タスクにチェックする
  • タスクをリセットする

開発内容

ref( )

refはリアクティブな状態を宣言するために使用します。
Composition APIでリアクティブな状態を宣言する方法として、ref()が推奨されています。

ts
const tasks = ref<{name: string, completed: boolean}[]>([
  { name: 'ToDoアプリを作成', completed: false },
]);
const newTask = ref(''); 

refで宣言した変数を扱う際、templateタグ内の扱いとscriptタグ内の扱いで注意が必要です。 templateタグ内では、変数名でアクセスできます。
scriptタグ内では、変数名.valueで値にアクセスできます。

script setup

<script setup>を宣言することでscriptタグで宣言したインポート、変数、関数は、 同じコンポーネントのtemplateタグ内で変数や関数を使用できます。

ts

<script setup lang="ts">
import { ref } from 'vue';

データバインディング

idやclassといったhtml属性に、動的に文字列を割り当てることが出来ます。

例えば、タスクを完了しチェックした時に打ち消し線で消すスタイルを当てる時にも使用できます。task.completed(boolean)によってcompletedを動的に割り当てています。

ts
        >
          <span :class="{ task, completed: task.completed }">{{ task.name }}</span>
        </input>

イベントリスナー

@(v-onの省略記法)を使ってイベント発火時の処理を記述できます。
例えば、タスク入力後のエンター押下後にタスクを追加する処理が発火します。
@keyup.enterでエンターキーを押下後に、addTaskメソッドを呼び出します。

ts
    <input
      class="input-field"
      v-model="newTask"
      @keyup.enter="addTask"
      placeholder="タスクを追加" 
    />

双方向バインディング

v-modelを使うと、値のバインディングとイベントリスナーを兼用できます。
チェックボックスのチェックイベントでtask.comleted(boolean)の値が反転します。

ts
        <input
          class="checkbox"
          v-model="task.completed"
          type="checkbox"
        >
          <span :class="{ task, completed: task.completed }">{{ task.name }}</span>
        </input>

リスト

v-forを使うと、一覧表示することが可能です。
同じアイテムの表示など反復処理を行うときに使用します。
:keyには、一意となる値を割り当てます。
インデックス値以外にも、タイトルなど割り当てることは可能です。

ts
      <li
        v-for="(task, index) in tasks"
        :key="index"
        class="item"
      >

条件分岐

v-ifを使うと、条件に応じて表示を制御することが可能です。
v-showと似ていますが、v-ifは表示される条件を満たすまでは描写されません。
一方、v-showは裏側(描写してるがnoneで非表示に見える)では表示しているため、初期レンダリングコストは高いです。 今回の例で言うと、v-showが適切かもしれません。

ts
    <div class="all-remove">
      <button
        v-if="tasks.length"
        class="btn all-remove-btn"
        @click="removeAllTask"
      >全て削除</button>
    </div>

ソースコード

今回開発した全体の内容です。

vue
<template>
  <div class="todo-app">
    <div>
      <h1 class="title">{{ title }}</h1>
    </div>
    <div class="create-area">
      <input
        class="input-field"
        v-model="newTask"
        @keyup.enter="addTask"
        placeholder="タスクを追加" 
      />
      <button
        class="btn"
        @click="addTask"
      >追加</button>
    </div>
    <ul class="list-area">
      <li
        v-for="(task, index) in tasks"
        :key="index"
      >
        <div class="task-area">
          <div class="task">
            <input
              class="checkbox"
              v-model="task.completed"
              type="checkbox"
            >
              <span
                class="task-title"
                :class="{ completed: task.completed }"
              >{{ task.name }}</span>
            </input>
          </div>
          <button
            class="btn"
            @click="removeTask(index)"
          >削除</button>
        </div>
      </li>
    </ul>
    <button
      v-if="tasks.length"
      class="btn all-delete"
      @click="removeAllTask"
    >全て削除</button>
  </div>
</template>
vue
<script setup lang="ts">
import { ref } from 'vue';

const title = 'Sample ToDo'
const tasks = ref<{name: string, completed: boolean}[]>([
  { name: 'ToDoアプリを作成', completed: false },
]);
const newTask = ref('');

const addTask = () => {
  if (newTask.value.trim()) {
    tasks.value.push({ name: newTask.value, completed: false });
    newTask.value = '';
  }
};

const removeTask = (index: number) => {
  tasks.value.splice(index, 1);
};

const removeAllTask = () => {
  tasks.value.splice(0);
}
</script>
vue
<style scoped>
/* common */
.btn {
  padding: 5px 10px;
  margin: auto;
  border: 1px solid var(--vp-c-brand);
  border-radius: 5px;
}

/* section */
.todo-app {
  margin: 10px auto;
}

.create-area {
  margin-top: 10px;
  gap: 10px;
  display: grid;
  /* grid 縦フレームの割合 */
  grid-template-columns: 3fr 1fr;
}

.task-area {
  margin: auto;
  gap: 10px;
  display: grid;
  /* grid 縦フレームの割合 */
  grid-template-columns: 3fr 1fr;
}

.list-area {
  margin: 0;
  padding: 0;
  list-style-type: none;
}

/* components */
.input-field {
  margin: 0 0 0 15px;
  border: 1px solid #000;
  border-radius: 5px;
}

.task {
  margin: 10px 25px;
}

.task-title {
  margin: 0 15px;
}

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

.all-delete {
  display: grid;
  /* grid 縦フレームの割合 */
  grid-template-columns: 1fr;
}
</style>