これまでに公開した2回の記事でガントチャートを作成することができました。最終回の本記事では、ガントチャートのタスクを登録/更新/削除を行うことができるフォームの作成を行います。

シングルページアプリケーションを実現するためにフォームにはモーダルウィンドウを利用します。モーダルウィンドウを作成する際にはVue3の新機能であるteleportを活用します。ガントチャート のフォームを追加するだけではなくtelesportの実践的な利用方法も理解することができます。

タスク追加ボタンの作成

ヘッダー領域にあるガントチャートの文字列の右側にタスクボタンの追加を行います。


<div id="gantt-header" class="h-12 p-2 flex items-center">
  <h1 class="text-sm font-bold">ガントチャート</h1>
  <button class="bg-indigo-700 hover:bg-indigo-900 text-white py-2 px-4 rounded-lg text-xs ml-4">
    タスクを追加する
  </button>
</div>

ブラウザで確認すると”タスクを追加する”ボタンが表示されることが確認できます。

タスクを追加するボタンを追加
タスクを追加するボタンを追加

必須ではありませんが”タスクを追加する”ボタンにSVGアイコンの追加を行います。SVGはhttps://heroicons.com/のplusを利用します。SVGの情報をコピー&ペーストするだけでSVGのアイコンを利用することができます。


<div id="gantt-header" class="h-12 p-2 flex items-center">
  <h1 class="text-sm font-bold">ガントチャート</h1>
    <button
      class="bg-indigo-700 hover:bg-indigo-900 text-white py-2 px-4 rounded-lg flex items-center">
      <svg class="w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
      </svg>
      <span class="font-bold text-xs">
        タスクを追加する
      </span>
    </button>
</div>

SVGを追加後、再度ブラウザで確認するとSVGアイコンの+が表示されます。

SVGアイコンを追加
SVGアイコンを追加

”タスクを追加する”ボタンを追加後、ボタンを押すとモーダルウィンドウを表示させる必要があるため、Clickイベントを追加し動作確認を行います。


<button
  @click="addTask" //追加
  class="bg-indigo-700 hover:bg-indigo-900 text-white py-2 px-4 rounded-lg flex items-center">
  <svg class="w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
  </svg>
  <span class="font-bold text-xs">
    タスクを追加する
  </span>
</button>

Vue.jsのmethodsにClickイベントで指定したaddTaskメソッドを追加します。


addTask(){
  console.log('click')
}

”タスクを追加する”ボタンをクリックしてデベロッパーツールのコンソールに”click”の文字列が表示されることを確認します。表示されればclickイベントの設定は正常に行われています。

モーダルウィンドウの作成

モーダルウィンドウはVue 3で追加されたTeleportという機能を利用します。

”タスクを追加する”ボタンをクリックするとモーダルウィンドウが表示されるようにteleportの設定を行います。

Vue 2では同様の機能にPortal Vueがあります。

teleportの設定

teleport機能ではHTMLに記述したある要素を記述した場所とは異なる場所に表示させることができる機能です。記述した場所とは異なる場所に表示させることができるため、bodyタグの直後に記述した要素をbodyタグの閉じタグの前に表示させるといったことが可能です。

bodyの閉じタグの前ということはdivのid=”app”のVue.jsの適用の範囲外になります。teleport機能はappの外側でも設定することが可能です。今回はVue3なのでteleport機能を持っていますが、Vueのバージョン2の場合はportal-vueライブラリを利用することで同様の実装を行うことができます。

モーダルウィンドウを表示させたい場所のbodyタグの閉じタグの前にid=”form”をつけたdiv要素を追加します。idにはformという名前を付けています。識別できるように任意の名前をつけてください。


  <div id="form">
  </div>
</body>

設定したdivタグ(id=”form”)の中に表示させたい内容をteleportタグの中に記述します。teleportタグの属性にはtoで表示させたい場所であるdiv要素(idに設定したform)を設定します。

teleportタグは”タスクを追加する”ボタンの閉じタグの下に追加します。動作確認のためteleportタグの中には文字列テストを挿入しています。


  <span class="font-bold text-xs">
    タスクを追加する
  </span>
</button>
<teleport to="#form">
  テスト
</teleport>

以上でteleportの設定は完了です。通常であればテストの文字列は”タスクを追加する”ボタンの右横に表示されるはずです。しかしteleportの設定により表示先はbodyタグの閉じタグの前になっているのでガントチャートの全体の表示領域の下に表示されます。

teleportに挿入した中身の表示場所
teleportに挿入した中身の表示場所

teleport機能により意図通りに動作することが確認できました。

オーバーレイの設定

モーダルウィンドウを利用する場合に背景全体にオーバーレイという幕の設定を行います。

“タスクを追加する”ボタンをクリックするとオーバーレイが表示されるようにVue.jsのデータプロパティにオーバーレイの表示・非表示を制御するデータプロパティshowを追加します。デフォルトでは非表示にするためfalseに設定します。


data() {
  return {
    //略
    show:false,

teleportタグの中にオーバーレイの要素を追加しますが、背景全体をグレーにするためclassのoverlayを設定しています。またv-showを利用してデータプロパティshowがtrueの場合のみ表示する設定にしています。オーバーレイ要素をクリックするとshowをfalseにするためClickイベントを設定し、オーバーレイを非表示にする設定を行っています。


<teleport to="#form">
  <div class="overlay" v-show="show" @click="show=false">
  </div>
</teleport>

overlayクラスはstyleタグを追加してstyleタグの中に記述します。


<style>
  .overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: gray;
    opacity: 0.5;
  }
</style>

addTaskメソッド更新します。


addTask() {
  this.show = true;
},

設定が完了したら、”タスクを追加する”ボタンをクリックします。画面全体にグレーの幕が表示されます。

ボタンをクリックするとオーバーレイが表示
ボタンをクリックするとオーバーレイが表示

表示されているグレーのオーバーレイをクリックすると画面全体に広がるグレーの幕は非表示となります。オーバーレイが一番手間に表示されているためタスク領域やカレンダー領域にはオーバーレイが表示されている時はアクセスすることはできません。

入力フォームの領域を表示

オーバーレイの上に入力フォームの領域を表示させるために設定を行います。新たにオーバーレイの外側にdiv要素を追加し、オーバーレイ要素の下に入力フォームを設定する要素を追加します。


<teleport to="#form">
  <div class="base" v-show="show">
    <div class="overlay" v-show="show" @click="show=false">
    </div>
    <div class="content" v-show="show">
      フォーム
    </div>
  </div>
</teleport>

追加した各divにはそれぞれclassが設定されているのでstyleタグの中にCSSを記述します。


<style>
    .base {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      display: flex;
      justify-content: center;
      margin-top: 50px;
    }

    .overlay {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background-color: gray;
      opacity: 0.5;
    }

    .content {
      background-color: white;
      position: relative;
      border-radius: 10px;
      padding: 40px;
    }
</style>

設定内容を確認するために”タスクを追加するボタン”をクリックしてください。

入力フォームの領域が表示
入力フォームの領域が表示

オーバーレイの中に背景色が白のフォームの領域が表示されます。この領域であればクリックしてもオーバーレイは閉じることはありません。外側のグレーの部分をクリックするとオーバーレイは非表示になります。

今回は作成した領域の中にフォームを記述していきますが、一時的なメッセージやアラートを表示させたい時にもこのモーダルウィンドウを利用することができます。

入力フォームの作成

入力フォームについては最低限のCSSのみ適用してバリデーションなどの設定は全く行いません。

入力フォームの追加を行う前にVue.jsのデータプロパティformを追加します。formオブジェクトにはタスクに必要となるid、nameなどのプロパティを設定します。


data() {
  return {
    //略
    form: {
      category_id: '',
      id: '',
      name: '',
      start_date: '',
      end_date: '',
      incharge_user: '',
      percentage: 0
    },

項目毎にlabelとinput要素を設定しているシンプルな入力フォームです。カテゴリーIDのみデータプロパティcategoriesから選択するselect要素を利用しています。


<teleport to="#form">
  <div class="base" v-show="show">
    <div class="overlay" v-show="show" @click="show=false">
    </div>
    <div class="content" v-show="show">
      <h2 class="font-bold">タスクの追加</h2>
      <div class="my-4">
        <label class="text-xs">カテゴリーID:</label>
        <select v-model="form.category_id" class="text-xs border px-4 py-2 rounded-lg">
          <option v-for="category in categories" :key="category.id" :value="category.id">{{ category.name }}
          </option>
        </select>
      </div>
      <div class="my-4">
        <label class="text-xs">ID:</label>
        <input class="text-xs border rounded-lg px-4 py-2" v-model.number="form.id">
      </div>
      <div class="my-4">
        <label class="text-xs">タスク名:</label>
        <input class="text-xs border rounded-lg px-4 py-2" v-model="form.name">
      </div>
      <div class="my-4">
        <label class="text-xs">担当者:</label>
        <input class="text-xs border rounded-lg px-4 py-2" v-model="form.incharge_user">
      </div>
      <div class="my-4">
        <label class="text-xs">開始日:</label>
        <input class="text-xs border rounded-lg px-4 py-2" v-model="form.start_date" type="date">
      </div>
      <div class="my-4">
        <label class="text-xs">完了期限日:</label>
        <input class="text-xs border rounded-lg px-4 py-2" v-model="form.end_date" type="date">
      </div>
      <div class="my-4">
        <label class="text-xs">進捗度:</label>
        <input class="text-xs border rounded-lg px-4 py-2" v-model="form.percentage" type="number">
      </div>
      <div>
        <button class="bg-indigo-500 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-lg flex items-center">
          <svg class="w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
          </svg>
          <span class="font-bold text-xs">
            タスクを追加する
          </span>
        </button>
      </div>
    </div>
  </div>
</teleport>

”タスクを追加する”ボタンをクリックすると設定した入力フォームが表示されます。

タスク入力フォーム表示
タスク入力フォーム表示

入力フォームができたので”タスクを追加する”ボタンにClickイベントを追加して入力したタスクの情報が保存されるように設定を行います。


<button
  @click="saveTask" //追加
  class="bg-indigo-500 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-lg flex items-center">
  <svg class="w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
  </svg>
  <span class="font-bold text-xs">
    タスクを追加する
  </span>
</button>

clickイベントにsaveTaskメソッドを設定したので、saveTaskメソッドをVue.jsのmethodsに追加し、tasks配列へのデータの追加を行います。


saveTask() {
  this.tasks.push(
    this.form
  )
  this.form = {}
  this.show = false
},

設定が完了したら、入力フォームに入力を行います。IDは通常は自動採番を行いますが、ここでは手動でIDを入れます。ここまでに登録されているIDとは異なるIDを入れる必要があるため7としています。残りの項目も入力を行ってください。入力完了後に”タスクを追加する”ボタンを押すとタスク一覧へのタスクの登録が行われます。

入力フォームへの入力
入力フォームへの入力

新たにタスクAのカテゴリーの一番下に入力したデータが登録されていることが確認できます。

タスクデータの追加
タスクデータの追加

追加したデータのタスクバーを移動したり、タスクバーの拡大・縮小も他のタスクと同様に行うことができます。タスクを追加する機能の実装ができました。

更新フォームの作成

作成済のタスクを更新する方法が実装されていないのでタスクの情報を更新することができません。タスクの更新が行えるように入力フォームを再利用して更新フォームを作成します。

タスク一覧にあるタスク名をクリックすると更新したいタスク情報を取得できるようにClickイベントを設定します。


<template v-else>
  <div
    @click="editTask(task)" 
    class="border-r flex items-center font-bold w-48 text-sm pl-4">
    {{task.name}}
  </div>

Clickイベントに指定したeditTaskをVue.jsのmethodsに追加します。


editTask(task){
  console.log(task)
}

左側のタスク領域に表示されているタスク名をクリックするとデベロッパーツールのコンソールにクリックしたタスクの情報が表示されます。

左側のタスク名をクリック
左側のタスク名をクリック

Clickイベントでタスク情報が取得できることを確認したら、タスク名をクリックするとモーダルウィンドウが表示されるようにデータプロパティのshowをtrueにし、データプロパティformにクリックしたタスク情報を設定します。


editTask(task){
  this.show = true;
  Object.assign(this.form, task);
}

Object.assignの使い方は以下の文書を参考にしてください。

設定後テスト3をクリックすると入力フォームにはタスク情報が入力されて表示されます。

入力フォームにタスク情報が埋められて表示
入力フォームにタスク情報が埋められて表示

今回はタスクの追加ではなくタスクの更新ですが、”タスクを追加する”ボタンを押した時と同様に入力フォームのタイトルは”タスクの追加”になり、ボタンには”タスクを追加する”と表示されています。

同じ入力フォームを再利用を行い、一部の要素を追加と更新で切り替えるため新たにデータプロパティのupdate_modeを追加します。この値により追加と更新を行っているかを識別します。


data() {
  return {
    //略
    update_mode:false,

editTaskを実行する際に追加したupdate_modeの値をfalseからtrueに設定します。


editTask(task){
  this.update_mode=true;
  this.show = true;
  Object.assign(this.form, task);
}

update_modeの値がtrueの場合は”タスクを更新する”ボタンを表示し、falseの場合は”タスクを追加する”ボタンを表示するようにv-ifディレクティブを利用して条件分岐を行います。タイトルについても同様にv-ifディレクティブを利用して条件分岐によって表示するテキストを変更します。

タイトルをupdate_modeの値によって分岐


<h2 class="font-bold" v-if="update_mode">タスクの更新</h2>
<h2 class="font-bold" v-else>タスクの追加</h2>

ボタンをupdate_modeの値によって分岐


<div v-if="update_mode" class="flex items-center justify-between">
  <button
    class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-lg text-xs flex items-center">
    <svg class="w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
        d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
    </svg>
    <span class="text-xs font-bold text-white">タスクを更新</span>
  </button>
</div>
<div v-else>
  <button
    @click="saveTask"
    class="bg-indigo-500 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-lg flex items-center">
    <svg class="w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
    </svg>
    <span class="font-bold text-xs">
      タスクを追加する
    </span>
  </button>
</div>

タスク名をクリックして下記のようにタイトルが”タスクを更新”になり、ボタンが”タスクを更新”になっていることが確認できます。

タスクを更新するに変更
タスクを更新するに変更

設定後、”タスクを追加する”ボタンをクリックするとタスクの更新が表示されるので、addTaskの実行時にupdate_mode=falseを設定します。またタスクを新規で追加するため、formの値を空にしておきます。


addTask() {
  this.update_mode = false;
  this.form = {}
  this.show = true;
},

設定後、”タスクを追加する”ボタンを押すと”タスクを追加”の入力フォームが表示されます。

タスクを追加するボタンをクリック
タスクを追加するボタンをクリック

”タスクを追加する”ボタンを押すとタスクを追加するモーダルウィンドウが表示され、タスク名をクリックするとタスクを更新するモーダルウィンドウが表示されます。

”タスクを更新”ボタンを更新処理が行われるようにclickイベントを追加します。clickイベントにはupdateTaskを指定し、引数にはform.idの設定します。form.idを利用してupdateTask内で更新したいタスクを取り出します。


<button
    @click="updateTask(form.id)"

Vue.jsのmethodsにupdateTaskを追加します。task.idを利用してtaskを取り出し、Objectd.assignを利用して更新を行っています。更新後はformを空にしてshowをfalseにしてモーダルウィンドウを非表示にしています。


updateTask(id) {
  let task = this.tasks.find(task => task.id === id);
  Object.assign(task, this.form);
  this.form = {}
  this.show = false;
},

タスク名のテスト3をクリックして、更新フォームのnameでテスト3の名前を更新テスト3に変更します。”タスクを更新”ボタンをクリックすると更新内容が反映されます。

更新した内容の反映
更新した内容の反映

入力フォームを利用して既存のタスク情報を更新できることが確認できました。

タスクを削除する

タスクの追加、更新の機能を実装した最後にタスクの削除機能を実装します。

タスクの削除ボタンを更新フォームに追加します。


<div v-if="update_mode" class="flex items-center justify-between">
  <button
    @click="updateTask(form.id)"
    class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-lg text-xs flex items-center">
    <svg class="w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
        d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
    </svg>
    <span class="text-xs font-bold text-white">タスクを更新</span>
  </button>
  <button @click="deleteTask(form.id)"
    class="bg-red-500 hover:bg-red-700 text-white py-2 px-4 rounded-lg flex items-center ml-2">
    <svg class="w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
        d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
    </svg>
    <span class="text-xs font-bold text-white">タスクを削除</span>
  </button>
</div>

追加を行うとタスクを更新ボタン横に削除ボタンが表示されます。

タスクを削除するボタン表示
タスクを削除するボタン表示

タスクを削除ボタンをクリックするとタスクが削除できるようにdeleteTaskメソッドをmethodsに追加します。


deleteTask(id) {
  let delete_index;
  this.tasks.map((task, index) => {
    if (task.id === id) delete_index = index;
  })
  this.tasks.splice(delete_index, 1)
  this.form = {}
  this.show = false;
},

タスクを削除ボタンを押すとタスク一覧から削除したタスクが消えることを確認します。

削除ボタンを押したタスク3が一覧から消える
削除ボタンを押したタスク3が一覧から消える

削除機能も実装することができました。

まとめ

3回の記事に分けてきた”スクラッチからVue.jsで作る自作ガントチャート ”も今回で完了です。バックエンドのデータベースへのタスクデータ保存等実際に利用する場合はやらなければならないことがたくさんありません。本特集に人気があるようならばバックエンドとの通信やさらなる機能拡張についても追加していこうと思います。