vue.jsを使ってシングルアプリケーションを構築するとモーダルウィンドウを使いたいという場面に遭遇します。cssのフレームワークを使うことで簡単に実装することができますが、簡単なモーダルウィンドウの作り方を理解して知識を深めておきましょう。本文書では、モーダルウィンドウの作り方からvue.jsのコンポーネントでの実装方法まで入門者でもわかるように説明を行っていきます。

この文書を読み終えると簡単なモーダルウィンドウの作り方、vue.jsのイベント、コンポーネントの作り方、コンポーネント間のデータの受け渡し($this.emit)とslotを理解することができます。

モーダルウィンドウとは

ある要素をクリックすると画面中央にウィンドウが表示され、表示されたウィンドウ以外の背景を薄暗く表示させることでユーザに表示させたいウィンドウの内容を際立たせるための技術です。Webの世界ではさまざまな場所で利用されているので、モーダルウィンドウという言葉を知らない人でもこの技術に触れたことのない人はほとんどいないでしょう。

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

モーダルウィンドウの作成方法はネットに溢れていますが、CSSの設定の箇所で挫折してしまう人もいます。自作での作成を諦めてしまわないようにできるだけシンプルなモーダルウィンドウを作成していきます。CSSによる細かな設定はモーダルウィンドウが作成できてから行ってください。本文書では意図的に触れていません。

クリックボタンを作る

モーダルウィンドウをボタンを押したら開くという動きにしたいのでボタンを追加します。

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

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

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

</body>
クリックボタンを表示
クリックボタンを表示

画面に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つに分類することができ、一つ一つはシンプルなものです。


#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の値を設定します。

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

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

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

z-index=1ではclickボタンよりもoverlayのほうが上の要素になり、Clickボタンは透明度により見ることはできますが、クリックすることはできません。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にします。


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

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

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

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

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

$emitメソッドとイベント

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

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


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

次に親側のコンポーネントには、from-childを受け取るイベントを設定します。from-childイベントを受け取ったら、closeModalメソッドを実行します。


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

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

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><slot></slot></p>
      <button v-on:click="clickEvent">close</button>
    </div>
</div>

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


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

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を使う上で基本となるものなので、しっかりと理解してください。

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


<!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">
          <p><slot></slot></p>
          <button v-on:click="clickEvent">close</button>
        </div>
    </div>
    `,
  methods :{
    clickEvent: function(){
      this.$emit('from-child')
     }
  }
})

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