vue.jsを使ってシングルページアプリケーションを構築するとモーダルウィンドウを使いたいという場面に多々遭遇します。bootstrapのcssフレームワークやVuetifyを利用してマニュアル通りに従うことで簡単に実装することができます。しかしいざという時のためにモーダルウィンドウの作り方を理解して自作できるように知識を深めておきましょう。

本文書では、モーダルウィンドウの作り方からvue.jsのコンポーネントでの実装方法まで入門者でもわかるように説明を行っていきます。利用しているVue.jsのバージョンは2とバージョン3です。バージョン3ではOptinos APIとComposition APIを利用した方法を記述しています。

モーダルウィンドウを作成するためにはvue.jsのいくつかの機能を利用して実装するためモーダルウィンドウについてだけではなく一緒にvue.jsの理解も深まります。この文書を読み終えると簡単なモーダルウィンドウの作り方、vue.jsのイベント、コンポーネントの作り方、コンポーネント間のデータの受け渡し($this.emit)とslotを理解することができます。

モーダルウィンドウとは

ある要素をクリックすると画面中央にウィンドウが表示され、表示されたウィンドウ以外の背景を薄暗く表示させることでユーザに表示させたいウィンドウの内容を際立たせるための通知などに利用される技術です。通知以外にもシンプルページアプリケーションなどではモーダルウィンドウを入力フォームとしても利用します。Webの世界ではさまざまな場所で利用されているので、モーダルウィンドウという言葉を知らない人でもこの技術に触れたことのない人はいないでしょう。英語ではmodalという単語を使うので検索する場合はmodalと検索することで情報を見つけることができます。

自分でモーダルウィンドウを作ろう

モーダルウィンドウの作成方法はネット上に溢れていますが、CSSの設定の箇所がわかりにくいために挫折してしまう人もいるのではないでしょうか。自作での作成を諦めてしまわないようにできるだけシンプルなモーダルウィンドウを作成していきます。CSSによる細かなデザイン、装飾はモーダルウィンドウの仕組みが理解できてから行ってください。本文書では入門者がCSSとの兼ね合いで挫折しないように見栄えをよくするためのCSSの設定については意図的に触れていません。

クリックボタンを作る

モーダルウィンドウはボタンをクリックすると開くという動きにしたいのでボタンを追加します。vue.jsはcdnを利用するためパソコンさえあれば特別な環境は必要ありません。またコードを記述するindex.htmlファイルを任意の場所に作成してください。バージョン2と3で利用するcdnが異なるので利用するどちらかのバージョンのcdnを設定してください。Options APIとComposition APIは同じcdnを利用します。

モーダルウィンドウはボタン要素をクリックするたけではなくリンク要素をクリックした場合やページにアクセスしてから数秒後に自動で開くものさまざまなパターンがあります。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>modalをcomponentで作る</title>
  </head>
  <style>
    /* CSSを記述 */
  </style>

  <body>
    <div id="app">
      <button>Click</button>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    //バージョン2の場合のcdn
    <script src="https://unpkg.com/vue@3"></script>
    //バージョン3の場合のcdn
    <script>
      /* Vueのコードを記述 */
    </script>
  </body>
</html>
クリックボタンを表示
クリックボタンを表示

画面にClickボタンが表示されるだけでクリックしても何も起こりません。

<div id=”app”></div>はvue.jsのインスタンスがマウントする場所で、vue.jsを使用するためには必須の要素です。モーダルウィンドウの動作とvue.jsを連携する際に必要となります。

オーバーレイの要素を追加

オーバーレイはモーダルウィンドウが開いた時に画面全体を覆う薄暗い要素です。id=overlayを持つdiv要素を追加します。


  <div id="app">

    <button>Click</button>

    <div id="overlay">
    </div>
  </div>

overlayの要素を追加しましたが、これだけでは画面には何も変化はありません。

オーバーレイを作るためにはCSSの設定が必要となります。CSSに複数のプロパティが設定されているので複雑そうに見えますが、以下の3つに分類することができ、一つ一つはシンプルなものです。


<style>
#overlay{
  /* 要素を重ねた時の順番 */
  z-index:1;

  /* 画面全体を覆う設定 */
  position:fixed;
  top:0;
  left:0;
  width:100%;
  height:100%;
  background-color:rgba(0,0,0,0.5);

  /* 画面の中央に要素を表示させる設定 */
  display: flex;
  align-items: center;
  justify-content: center;

}
</style>

z-indexは要素を重ねた時の順番を表しています。小さな数字ほど重ね順が下になります。overlayは画面を薄暗くする要素のため1を設定しています。オーバーレーの上に表示させる要素には1よりも大きなz-indexの値を設定します。

背景色はbackground-color:rgba(0,0,0,0.5)を設定しています。透過にはopacityを利用することができますが、opacityを利用するとこの要素に含まれる子要素もopacityが継承されることになるのでrgba(0,0,0,0.5)を設定しています。

position:fixedからの設定が画面全体を覆うオーバーレイの主要部分を表しています。この部分を追加するだけで透明度0.5の黒い要素が画面全体を覆います。

display:flexはフレックスボックスの設定でここでの設定はオーバーレイの中の要素すべてが画面中央に表示されるように設定しています。この設定は後ほどのoverlayの中への子要素の追加ではっきり理解することができます。

この状態でブラウザで確認すると画面全体がグレーに変わります。透明度を0.5にしているのでoverlayの要素の下にあるclickボタンを見つけることができますが、クリックすることはできません。

overlayの要素はz-index=1を設定しているためにclickボタンよりもoverlayのほうが上の要素になります。clickボタンはoverlayの要素の透明度が0.5のためブラウザ上で確認することできますが、クリックすることはできません。overlayの要素のz-index=-1に設定するとclickボタンの要素のほうが上の要素となり、clickボタンを押すことができます。
オーバーレイを追加
オーバーレイを追加

コンテンツを追加

オーバーレイの中心にコンテンツを表示させる設定を行います。id=”content”のdiv要素を追加し、p要素とbutton要素を追加します。


<div id="overlay">
    <div id="content">
      <p>これがモーダルウィンドウです。</p>
      <p><button>close</button></p>
    </div>
</div>

CSSの設定も追加します。overlayの要素よりも上に表示させるためz-index=”2″を設定し、幅は画面の50%で背景は白に設定します。


#content{
  z-index:2;
  width:50%;
  padding: 1em;
  background:#fff;
}

モーダルウィンドウが開いている状態を作成することができました。

overlay要素の中にcontentを追加
overlay要素の中にcontentを追加

vue.jsの設定

ここからはvue.jsの知識が必要になってきます。下記の内容が難しくて読み進められない場合は下記の文書を読むことをおすすめします。

v-show, v-on:clickイベントの設定

ブラウザでページを開いた瞬間に下記の画面が表示されるのではなく、実現したいのはClickボタンを押した時にモーダルウィンドウが開き、モーダルウィンドウの中のcloseボタンを押した時にウィンドウが閉じる動作です。

overlay要素の中にcontentを追加
overlay要素の中にcontentを追加

showContentという名前のdataプロパティをVueインスタンスに追加しshowContentとv-showを組み合わせることでモーダルウィンドウの開閉を実装します。showContentがfalseの場合はモーダルウィンドウは表示されず、trueになるとモーダルウィンドウが表示されます。

2つのメソッドの追加を行い、openModalはshowContentの値をtrueにし、closeModalはshowContentの値をfalseにします。デフォルトはfalseに設定するのでモーダルウィンドウは表示されない状態となります。

Vue2の場合


<script>
new Vue({
  el: '#app',
  data: {
    showContent: false
  },
  methods:{
    openModal: function(){
      this.showContent = true
    },
    closeModal: function(){
      this.showContent = false
    }
  }
})
</script>

Vue3のOptions APIの場合

Vue3ではVueのインスタンスの作成にcreateAppを利用します。


<script>
  const { createApp } = Vue;

  createApp({
    data() {
      return {
        showContent: false,
      };
    },
    methods: {
      openModal() {
        this.showContent = true;
      },
      closeModal() {
        this.showContent = false;
      },
    },
  }).mount('#app');
</script>

Vue3のComposition APIの場合

Vue2とVue3のoptions APIでの記述方法はほとんど変わりませんが、Composition APIでは記述方法が全く異なります。setup関数の中でref関数を利用してリアクティブなデータを定義します。methodsの記述はなくそのまま関数を記述します。


<script>
  const { createApp, ref } = Vue;
  createApp({
    setup() {
      const showContent = ref(false);

      const openModal = () => {
        console.log('click');
        showContent.value = true;
      };

      const closeModal = () => {
        showContent.value = false;
      };

      return {
        showContent,
        openModal,
        closeModal,
      };
    },
  }).mount('#app');
</script>

v-showディレクティブは<div id=”overlay”>要素に設定し、ClickボタンをクリックするとopenModalメソッドが実行されます。closeModalメソッドはcloseボタンに設定します。


  <div id="app">

    <button v-on:click="openModal">Click</button>

    <div id="overlay" v-show="showContent">
        <div id="content">
          <p>これがモーダルウィンドウです。</p>
          <button v-on:click="closeModal">Close</button>
        </div>
    </div>
  </div>

Clickボタンを押したら、モーダルウィンドウが画面に表示され、Closeボタンを押したら、モーダルウィンドウが画面から消えます。

v-showがfalseの場合は、styleのdisplayプロパティの設定値がnoneに設定されるため画面に表示されませんが実際には要素は存在しています。

closeボタンだけではなくモーダルウィンドウの背景のどこをクリックしてもモーダルウィンドウを閉じるためには、<div id=”overlay”>にもcloseModalイベントを設定します。白のコンテンツをクリックしてもモーダルウィンドウが閉じてしまいますが後ほど閉じ内容に設定を行います。


<div id="overlay" v-show="showContent" v-on:click="closeModal">

ここまでの手順でvue.jsを利用したモーダルウィンドウを作成し、動作確認することができました。

コンポーネントでモーダルウィンドウ

次にモーダルウィンドウの部分をコンポーネント化します。

モーダルウィンドウをコンポーネント化した場合ただコンポーネントに分けるだけでは動作しません。Clickボタンの存在する親側とcloseボタンが存在するoverlayの子側で通知(データの受け渡し)が必要になります。通知には$emitメソッドとイベントの理解が必要になります。ここではモーダルウィンドウのコンポーネントとvue.jsの連携を通して、$emitメソッドとイベントを理解します。

コンポーネントの作成

モーダルウィンドウをコンポーネント化するためにopen-modalという名前でコンポーネントを作成します。

【Vue 2の場合】


Vue.component('open-modal',{
  template : `
    <div id="overlay">
        <div id="content">
          <p>これがモーダルウィンドウです。</p>
          <button v-on:click="closeModal">close</button>
        </div>
    </div>
    `
})

【Vue 3の場合】

Vue3の場合、options APIもcomposition APIもtemplate optionsの作成については同じです。


const { createApp } = Vue;

const app = createApp({
//略
});

app.component('open-modal',{
  template : `
    <div id="overlay">
        <div id="content">
          <p>これがモーダルウィンドウです。</p>
          <button v-on:click="closeModal">close</button>
        </div>
    </div>
    `
})

app.mount('#app');

コンポーネントを使ってコードを書き換えます。


<div id="app">
  <button v-on:click="openModal">Click</button>
  <open-modal v-show="showContent" />
</div>

コンポーネントを追加して動作確認するとClickボタンを押すとモーダルウィンドウは開きますが、closeボタンを押してもモーダルウィンドウは閉じません。

overlay要素の中にcontentを追加
closeボタンを押してもウィンドウは閉じない

コンポーネント化する前はVueインスタンスのshowContentのデータへのアクセスとcloseModalメソッドの実行を行うことができました。しかし、コンポーネントを分けるとデータshowContent、メソッドcloseModalに対してopen-modalコンポーネントから直接アクセスすることができなくなってしまいます。

$emitメソッドとイベント

Vueインスタンスがマウントしている#appのコンポーネントと追加したopen-modalコンポーネントとの間に親子関係ができてしまうため追加の設定を行わなければ通知(データの受け渡し)を行うことができません。子コンポーネントから親コンポーネントへの通知には$emitメソッドとイベントを使う必要があります。

open-modalコンポーネントのclickイベントのメソッドを変更(closeModalからclickEvent)し、新たにopen-modalコンポーネントにメソッドclickEventを追加します。追加したclickEventメソッドの中で$emitメソッドを実行させます。this.$emitの引数には、任意の名前のイベント名を入力することができます。ここでは子コンポーネントから通知を行うのでイベント名をfrom-childとしています。このfrom-childイベントが親への通知に使われます。


Vue.component('open-modal',{
  template : `
    <div id="overlay">
        <div id="content">
          <p>これがモーダルウィンドウです。</p>
          <button v-on:click="clickEvent">close</button>
        </div>
    </div>
    `,
  methods :{
    clickEvent: function(){
      this.$emit('from-child')
     }
  }
})

Vue3のComposition APIではsetup関数の引数で渡されるcontextオブジェクトのemitメソッドを利用してemitを実行します。


app.component('open-modal', {
  template: `
    <div id="overlay" v-on:click="clickEvent">
        <div id="content" v-on:click="stopEvent">
          <p><slot></slot></p>
          <button v-on:click="clickEvent">close</button>
        </div>
    </div>
    `,
  setup(props, context) {
    const clickEvent = () => {
      context.emit('from-child');
    };
    return {
      clickEvent,
    };
  },
});

親側のコンポーネントではfrom-childを受け取るイベントを設定します。イベントを受け取るのにはv-onディレクティブを利用することができます。from-childイベントを受け取ったら、closeModalメソッドを実行します。


<open-modal v-show="showContent" v-on:from-child="closeModal"></open-modal>

ボタンを押すとモーダルウィンドウを閉じることができるようになりました。先程と同じ動作にするためにidがoverlayを持つ要素にもclickイベントでclickEventを設定しておきます。


<div id="overlay" v-on:click="clickEvent">
  <div id="content">
    <p>これがモーダルウィンドウです。</p>
    <button v-on:click="clickEvent">close</button>
  </div>
</div>

これでコンポーネントを分割する前と同じ動作を行うようにすることができます。

stopPropagationの設定

現在の設定ではモーダルウィンドウの背景が白のコンテンツをクリックをしてもモーダルウィンドウが閉じてしまいます。id=”content”の要素をクリックしてもモーダルウィンドウが閉じないようにstopPropagationを設定します。新たにid=”content”のdiv要素にv-on:click=”stopEvent”を設定します。


<div id="overlay" v-on:click="clickEvent">
    <div id="content" v-on:click="stopEvent">
      <p>これがモーダルウィンドウです。</p>
      <button v-on:click="clickEvent">close</button>
    </div>
</div>

stopEventメソッドの中でstopPropagationを設定します。イベントの伝搬をストップできるため、モーダルウィンドウが閉じる処理を行うことができません。


  methods :{
    clickEvent: function(){
      this.$emit('from-child')
     },
    stopEvent: function(){
      event.stopPropagation()
    }
  }

Vue3のComposition APIでは下記のように記述します。


setup(props, context) {
  const clickEvent = () => {
    context.emit('from-child');
  };
  const stopEvent = (e) => {
    event.stopPropagation();
  };
  return {
    clickEvent,
    stopEvent,
  };
},

slotを使って文字列をコンポーネントに渡す

コンテンツの中身はハードコーディングしていましたが、slotを使用するとcontentの中身を自由に変更することができます。id=contentのpタグの中身に<slot></slot>を追加します。


<div id="content">
  <p><slot></slot></p>
  <button v-on:click="clickEvent">close</button>
</div>

open-modalタグの中に渡したい文字列を追加します。


<open-modal v-show="showContent" v-on:from-child="closeModal">slotからモーダルウィンドウへ</open-modal>
slotを使ってデータを渡す
slotを使ってデータを渡す

モーダルの作成からvue.jsのイベントの設定、コンポーネントの作成、さらにslotの使用方法の説明を行いました。どの機能もvue.jsを使う上で基本となるものなので、しっかりと理解してください。

完成したコード

今回作成したコード全体は下記の通りです。

Vue2の場合


<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>modalをcomponentで作る</title>
</head>

<style>
#content{
  z-index:10;
  width:50%;
  padding: 1em;
  background:#fff;
}

#overlay{
  /* 要素を重ねた時の順番 */

  z-index:1;

  /* 画面全体を覆う設定 */
  position:fixed;
  top:0;
  left:0;
  width:100%;
  height:100%;
  background-color:rgba(0,0,0,0.5);

  /* 画面の中央に要素を表示させる設定 */
  display: flex;
  align-items: center;
  justify-content: center;

}
</style>

<body>
  <div id="app">

    <button v-on:click="openModal">Click</button>

    <open-modal v-show="showContent" v-on:from-child="closeModal">slotからモーダルウィンドウへ</open-modal>

  </div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
Vue.component('open-modal',{
  template : `
    <div id="overlay" v-on:click="clickEvent">
        <div id="content"  v-on:click="stopEvent">
          <p><slot></slot></p>
          <button v-on:click="clickEvent">close</button>
        </div>
    </div>
    `,
  methods :{
    clickEvent: function(){
      this.$emit('from-child')
     },
    stopEvent: function(){
      event.stopPropagation()
    }    
  }
})

new Vue({
  el: '#app',
  data: {
    showContent: false
  },
  methods:{
    openModal: function(){
      this.showContent = true
    },    
    closeModal: function(){
      this.showContent = false
    },
  }
})
</script> 
</body>
</html>

Vue3の場合(options API)


<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>modalをcomponentで作る</title>
  </head>

  <style>
    #content {
      z-index: 10;
      width: 50%;
      padding: 1em;
      background: #fff;
    }

    #overlay {
      /* 要素を重ねた時の順番 */

      z-index: 1;

      /* 画面全体を覆う設定 */
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background-color: rgba(0, 0, 0, 0.5);

      /* 画面の中央に要素を表示させる設定 */
      display: flex;
      align-items: center;
      justify-content: center;
    }
  </style>

  <body>
    <div id="app">
      <button v-on:click="openModal">Click</button>

      <open-modal v-show="showContent" v-on:from-child="closeModal"
        >slotからモーダルウィンドウへ</open-modal
      >
    </div>
    <script src="https://unpkg.com/vue@3"></script>
    <script>
      const { createApp } = Vue;

      const app = createApp({
        data() {
          return {
            showContent: false,
          };
        },
        methods: {
          openModal() {
            this.showContent = true;
          },
          closeModal() {
            this.showContent = false;
          },
        },
      });

      app.component('open-modal', {
        template: `
          <div id="overlay" v-on:click="clickEvent">
              <div id="content" v-on:click="stopEvent">
                <p><slot></slot></p>
                <button v-on:click="clickEvent">close</button>
              </div>
          </div>
          `,
        methods: {
          clickEvent() {
            this.$emit('from-child');
          },
          stopEvent() {
            event.stopPropagation();
          },
        },
      });

      app.mount('#app');
    </script>
  </body>
</html>

Vue3の場合(composition API)の場合


<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>modalをcomponentで作る</title>
  </head>
  <style>
    #overlay {
      /* 要素を重ねた時の順番 */
      z-index: 1;

      /* 画面全体を覆う設定 */
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background-color: rgba(0, 0, 0, 0.5);

      /* 画面の中央に要素を表示させる設定 */
      display: flex;
      align-items: center;
      justify-content: center;
    }

    #content {
      z-index: 2;
      width: 50%;
      padding: 1em;
      background: #fff;
    }
  </style>

  <body>
    <div id="app">
      <button v-on:click="openModal">Click</button>

      <div id="overlay" v-show="showContent">
        <div id="content">
          <p>これがモーダルウィンドウです。</p>
          <button v-on:click="closeModal">Close</button>
        </div>
      </div>
    </div>

    <script src="https://unpkg.com/vue@3"></script>
    <script>
      const { createApp, ref } = Vue;
      const app = createApp({
        setup() {
          const showContent = ref(false);

          const openModal = () => {
            console.log('click');
            showContent.value = true;
          };

          const closeModal = () => {
            showContent.value = false;
          };

          return {
            showContent,
            openModal,
            closeModal,
          };
        },
      });

      app.component('open-modal', {
        template: `
          <div id="overlay" v-on:click="clickEvent">
              <div id="content" v-on:click="stopEvent">
                <p><slot></slot></p>
                <button v-on:click="clickEvent">close</button>
              </div>
          </div>
          `,
        setup(props, context) {
          const clickEvent = () => {
            context.emit('from-child');
          };
          const stopEvent = (e) => {
            event.stopPropagation();
          };
          return {
            clickEvent,
            stopEvent,
          };
        },
      });

      app.mount('#app');
    </script>
  </body>
</html>