【Vue.js】タスク詳細ページをモーダルで表示する

はじめに

タスクの詳細ページをモーダルで表示する。

前提

前提として下記のtaskテーブルがあります。

class Task < ApplicationRecord
  validates :title, presence: true
  validates :description, length: { maximum: 1000 }
end
  create_table "tasks", force: :cascade do |t|
    t.string "title", null: false
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.text "description"
  end

詳細ページのコンポーネントを作成

まずは詳細ページのコンポーネントを作成します。

$ mkdir app/javascript/pages/task

$ mkdir app/javascript/pages/task/components

$ touch app/javascript/pages/task/components/TaskDetailModal.vue

モーダルはbootstrapのモーダルをベースにします。

<template>
  <div :id="'task-detail-modal-' + task.id">
    <div class="modal" @click.self="handleCloseModal">//  :1
      <div class="modal-dialog">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title">{{ task.title }}</h5>
            <button type="button" class="close" @click="handleCloseModal">// :2
              <span>&times;</span>
            </button>
          </div>
          <div class="modal-body" v-if="task.description">
            <p>{{ task.description }}</p>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary" @click="handleCloseModal">閉じる</button>//  :2
          </div>
        </div>
      </div>
    </div>
    <div class="modal-backdrop show"></div>
  </div>
</template>

<script>
export default {
  name: "TaskDetailModal",
  props: {     // :3
    task: {
      title: {
        type: String,   
        required: true   
      },
      description: {
        type: String,
        required: true
      }
    }
  },
  methods: {
    handleCloseModal() {
      this.$emit('close-modal')    // :4
    }
  }
}
</script>

<style scoped>
 .modal {
  display: block;    // モーダルの表示・非表示をvueでコントールするので最初から表示状態にする
}
</style>

:1

<div class="modal" @click.self="handleCloseModal"> 

モーダル以外の部分をクリックしたときにもモーダルを非表示にします。

:2

<button type="button" class="close" @click="handleCloseModal"> 

<button type="button" class="btn btn-secondary" @click="handleCloseModal">閉じる</button> 

モーダルのxボタンと閉じるボタンをクリックした時にモーダルを非表示にします。

:3

  props: {  
    task: {
      title: {
        type: String,   //  データのタイプを指定
        required: true    // 必須かどうか
      },
      description: {
        type: String,
        required: true
      }
    }

propsを使って親コンポーネントからデータを受け取ります。

:4

  methods: {
    handleCloseModal() {
      this.$emit('close-modal')  
    }
  }

$emitメソッドを使って親コンポーネントのイベントを発火します。

詳細ページのモーダルを呼び出す

<template>
  <div>
    <div class="d-flex">
      <div class="col-4 bg-light rounded shadow m-3 p-3">
        <div class="h4">TODO</div>
        <div v-for="task in tasks" 
            :key="task.id" 
            :id="'task-' + task.id"
            class="bg-white border shadow-sm rounded my-2 p-4"
            @click="handleShowTaskDetailModal(task)">// :1
          <span>{{ task.title }}</span>
        </div>
      </div>
    </div>
    <div class="text-center">
      <router-link :to="{ name: 'TopIndex' }" class="btn btn-dark mt-5">戻る</router-link>
    </div>
    <transition name="fade">    // モーダルの表示非表示にtrannsitionをつける。
      <TaskDetailModal v-if="isVisibleTaskDetailModal" @close-modal="handleCloseTaskDetailModal" :task="taskDetail" /> 
         //  :2
    </transition>
  </div>
</template>

<script>
import TaskDetailModal from './components/TaskDetailModal' 
//  詳細ページのコンポーネントをインポート

export default {
  components: {
    TaskDetailModal
  },
  name: "TaskIndex",
  data() {
    return {
      tasks: [],
      taskDetail: {}, 
      isVisibleTaskDetailModal: false  //  モーダルをデフォルトで非表示に設定
    }
  },

  created() {
    this.fetchTasks();
  },

  methods: {
    fetchTasks() {
      this.$axios.get("tasks")
        .then(res => this.tasks = res.data)
        .catch(err => console.log(err.status));
    },
    handleCloseTaskDetailModal() {    //  子コンポーネントから呼び出しているメソッド
      this.isVisibleTaskDetailModal = false;   //  モーダルを非表示にする
      this.taskDetail = {};  //  引数のタスクを空にする
    },
    handleShowTaskDetailModal(task) {
      this.isVisibleTaskDetailModal = true;   // モーダルを表示
      this.taskDetail = task;   //  引数のタスクを代入
    }
  }
}
</script>

//  :4
<style scoped>
.fade-enter-active, .fade-leave-active {
  transition: opacity .5s;
}

.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>

:1

@click="handleShowTaskDetailModal(task)">  

クリックされたtaskのデータを引数で渡します。

:2

<TaskDetailModal v-if="isVisibleTaskDetailModal" @close-modal="handleCloseTaskDetailModal" :task="taskDetail" /> 

v-ifを使ってisVisibleTaskDetailModalがtrueのときは表示、falseのときは非表示にします。

先程の子コンポーネントから呼んでるイベントが@close-modal

(v-bind):task="taskDetail"でtaskのデータを子コンポーネントに渡しています。

そして子コンポーネントpropsで受け取っています。

:3

下記4つのクラスはenter/leaveトランジションを適用するための特殊なクラスです。

fadeの部分はname属性です。

name属性(fadeなど) + 特殊クラス(-enter-activeなど)
<style scoped>
.fade-enter-active, .fade-leave-active {
  transition: opacity .5s;
}

.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>

今回だとモーダルが表示されるトランジションの開始から終了まで。

今回だとモーダルが非表示されるトランジションの開始から終了まで。

完成画面

f:id:study-output:20210622060148p:plain

f:id:study-output:20210622060202p:plain