タスク詳細モーダルの実装

前回:APIでデータを取得してVueで呼び出す - Rails技術ブログ

①モーダルのコンポーネントを作成。
$ touch app/javascript/pages/task/components/TaskDetailModal.vue


bootstrapを活用してデザインを記載。

<template>
  <transition name="fade">
      <div class="modal">
        <div class="modal-dialog">
          <div class="modal-content">
            <div class="modal-header">
              <h5 class="modal-title">タイトル</h5>
              <button type="button" class="close">
                <span>&times;</span>
              </button>
            </div>
            <div class="modal-body">
              <p>ダイアログの中身</p>
            </div>
            <div class="modal-footer">
              <button type="button" class="btn btn-secondary">
                閉じる
              </button>
            </div>
          </div>
        </div>
      </div>
      <div class="modal-backdrop show"></div>
  </transition>
</template>

<script>
export default {
  name: 'TaskDetailModal'
}
</script>

<style scoped>
/* 表示/非表示はvueで制御するので最初から表示状態にする */
 .modal {
  display: block;
}

/* vueのtransitionを使わないなら不要 */
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.5s;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>


②モーダルを表示したいページで呼び出す。

app/javascript/pages/task/index.vue

<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"
             class="bg-white border shadow-sm rounded my-2 p-4">
          <span>{{ task.title }}</span>
        </div>
      </div>
    </div>
    <div class="text-center">
      <router-link to="/" class="btn btn-dark mt-5">戻る</router-link>
    </div>
    <TaskDetailModal />  // ←←←
  </div>
</template>

<script>
import TaskDetailModal from './components/TaskDetailModal'  // ←←←

export default {
  components: {  // ←←←
    TaskDetailModal
  },
  name: 'TaskIndex',
  data() {
    return {
      tasks: []
    }
  },
  created() {
    this.fetchTasks();
  },
  methods: {
    fetchTasks() {
      this.$axios.get("tasks")
        .then(res => this.tasks = res.data)
        .catch(err => console.log(err.status));
    },
  }
}
</script>

<style scoped>
</style>

このままだとモーダルが表示されっぱなしなので表示非表示を切り替えられるようにしていく。

③モーダルの表示非表示を切り替えるためのBooleanデータを設定。
<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"
             class="bg-white border shadow-sm rounded my-2 p-4">
          <span>{{ task.title }}</span>
        </div>
      </div>
    </div>
    <div class="text-center">
      <router-link to="/" class="btn btn-dark mt-5">戻る</router-link>
    </div>
    // "isVisibleTaskDetailModal"がtrueなら表示され、falseなら表示されない。
    <TaskDetailModal v-if="isVisibleTaskDetailModal" />  // ←←←
  </div>
</template>

<script>
import TaskDetailModal from './components/TaskDetailModal'

export default {
  components: {
    TaskDetailModal
  },
  name: 'TaskIndex',
  data() {
    return {
      tasks: [],
      isVisibleTaskDetailModal: false  // ←←←
    }
  },
  created() {
    this.fetchTasks();
  },
  methods: {
    fetchTasks() {
      this.$axios.get("tasks")
        .then(res => this.tasks = res.data)
        .catch(err => console.log(err.status));
    },
  }
}
</script>

<style scoped>
</style>

これでisVisibleTaskDetailModalをtrueにするかfalseにするかで表示非表示を切り替えらるようになった。

④clickイベントでモーダルを表示させるメソッドを作成。
<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"
             class="bg-white border shadow-sm rounded my-2 p-4"
             @click="handleShowTaskDetailModal>   // ←←←
          <span>{{ task.title }}</span>
        </div>
      </div>
    </div>
    <div class="text-center">
      <router-link to="/" class="btn btn-dark mt-5">戻る</router-link>
    </div>
    <TaskDetailModal v-if="isVisibleTaskDetailModal" />
  </div>
</template>

<script>
import TaskDetailModal from './components/TaskDetailModal'

export default {
  components: {
    TaskDetailModal
  },
  name: 'TaskIndex',
  data() {
    return {
      tasks: [],
      isVisibleTaskDetailModal: false
    }
  },
  created() {
    this.fetchTasks();
  },
  methods: {
    fetchTasks() {
      this.$axios.get("tasks")
        .then(res => this.tasks = res.data)
        .catch(err => console.log(err.status));
    },
    handleShowTaskDetailModal {  // ←←←
      this.isVisibleTaskDetailModal = true;
    },
  }
}
</script>

<style scoped>
</style>

これでタスクをクリックすることでモーダルを表示できるようになった。

⑤emitを使ってモーダルコンポーネントで発生したイベントを親コンポーネントに補足させてモーダルを閉じる。

モーダルコンポーネント

<template>
  <transition name="fade">
      <div class="modal" @click.self="handleCloseModal">  // ←←←
        <div class="modal-dialog">
          <div class="modal-content">
            <div class="modal-header">
              <h5 class="modal-title">タイトル</h5>
              <button type="button" class="close" @click="handleCloseModal">//←←
                <span>&times;</span>
              </button>
            </div>
            <div class="modal-body">
              <p>ダイアログの中身</p>
            </div>
            <div class="modal-footer">
              <button type="button"  //←←←
                      class="btn btn-secondary" @click="handleCloseModal">
                閉じる
              </button>
            </div>
          </div>
        </div>
      </div>
      <div class="modal-backdrop show"></div>
  </transition>
</template>

<script>
export default {
  name: 'TaskDetailModal',
  methods :{
    // clickイベントの発生を@close-modalで親コンポーネントに知らせることができる。
    handleCloseModal() {  // ←←←
      this.$emit('close-modal')
    }
  }
}
</script>
.
.
.

コンポーネント

<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"
             class="bg-white border shadow-sm rounded my-2 p-4"
             @click="handleShowTaskDetailModal>
          <span>{{ task.title }}</span>
        </div>
      </div>
    </div>
    <div class="text-center">
      <router-link to="/" class="btn btn-dark mt-5">戻る</router-link>
    </div>
    <TaskDetailModal v-if="isVisibleTaskDetailModal" 
                     @close-modal="handleCloseTaskDetailModal" />  // ←←←
  </div>
</template>

<script>
import TaskDetailModal from './components/TaskDetailModal'

export default {
  components: {
    TaskDetailModal
  },
  name: 'TaskIndex',
  data() {
    return {
      tasks: [],
      isVisibleTaskDetailModal: false
    }
  },
  created() {
    this.fetchTasks();
  },
  methods: {
    fetchTasks() {
      this.$axios.get("tasks")
        .then(res => this.tasks = res.data)
        .catch(err => console.log(err.status));
    },
    handleShowTaskDetailModal {
      this.isVisibleTaskDetailModal = true;
    },
    handleCloseTaskDetailModal() {  // ←←←
      this.isVisibleTaskDetailModal = false; 
    },
  }
}
</script>

<style scoped>
</style>

これでモーダルの表示非表示をクリックでできるようになった。

⑥親コンポーネントからモーダルコンポーネントにデータを渡す。
<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"
             class="bg-white border shadow-sm rounded my-2 p-4"
             @click="handleShowTaskDetailModal(task)>  // ←←←
          <span>{{ task.title }}</span>
        </div>
      </div>
    </div>
    <div class="text-center">
      <router-link to="/" class="btn btn-dark mt-5">戻る</router-link>
    </div>
    // :〇〇に変数を代入しておくことで子コンポーネントでpropsを使ってデータを読み込める。
    <TaskDetailModal v-if="isVisibleTaskDetailModal"
                     @close-modal="handleCloseTaskDetailModal"
                     :task="taskDetail" />  // ←←←
  </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));
    },
    handleShowTaskDetailModal {
      this.isVisibleTaskDetailModal = true;
      this.taskDetail = task;  // ←←←
    },
    handleCloseTaskDetailModal() {
      this.isVisibleTaskDetailModal = false;
      this.taskDetail = {};  // ←←←
    }
  }
}
</script>

<style scoped>
</style>


⑦モーダルコンポーネントでpropsを使ってデータを受け取り、モーダルに動的データを表示させる。
<template>
  <transition name="fade">
      <div class="modal" @click.self="handleCloseModal">
        <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">
                <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>
            </div>
          </div>
        </div>
      </div>
      <div class="modal-backdrop show"></div>
  </transition>
</template>

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



参考記事:
vue.js で bootstrapのmodalを表示する(簡易版) at softelメモ
モーダルウィンドウ | 基礎から学ぶ Vue.js
propsと$emitでデータを引き渡す - Qiita
Vueのpropsの使い方 - Qiita
【Vue.js】動的なモーダルウインドウの作り方を解説