本文書ではVue.jsのPortalVueプラグインを利用したモーダルウィンドウの作成方法を紹介します。コピー&ペースするだけでモーダルウィンドウを作成することができますが、自分でも作成できるように理解しながら読み進めてください。

モーダルウィンドウの作成を通して以下の機能についての理解を深めることができます。

  • PortalVueの設定方法
  • なぜPortalVueを使うのか
  • vue.jsのイベント
  • vue.jsのtransitionの設定
  • CSSのstacking contextについて

PortalVueとは

PortalVueはvue.jsに記述したHTMLの要素を記述した場所に表示させるのではなく全く別の場所に表示させることができるプラグインです。別の場所に要素を表示させるといえばCSSのpositionプロパティを想像するかもしれません。postitionプロパティも記述した場所とは異なる場所で要素を表示させることができますが、要素毎にCSSで細かく表示させたい位置を指定する必要があります。PortalVueでは移動先の場所を予め作成しておき、移動元の要素はその場所を指定するだけでいいため要素毎に細かな設定を行う必要がない上、複数の要素から同じ場所を移動先として指定することも可能です。

Vue3ではTeleportという同様の機能を利用することが可能です。Teleportはある場所から別の場所に瞬間に移動する意味を持つ単語でその意味通りの動作をします。Portalではイメージが湧きにくいのでTeleportという言葉を念頭に動作確認を行えばPortalVueの理解も容易だと思います。

PortalVueはシンプルなプラグインで本ブログでも基本機能については公開済みでPortalVueがわからない人は下記の文書を確認の上読み進めてください。

PortalVueで解決する問題

モーダルウィンドウの作成を例にPortalVueでどのような問題が解決できるかを説明していきます。

コンポーネントの組み合わせにより複雑に要素が重なり合った状態ではz-indexを設定したにも関わらず期待通りに表示されない、また複数のコンポーネントで別々のモーダルウィンドウを持っている場合にコンポーネント毎にモーダルウィンドウの設定が必要なので手間がかかるといった経験はないですか?

この問題を解決するため、コンポーネント毎で個別に要素の重なりを調整していくのではなく”ここにモーダルウィンドウの要素をおけば要素の重なりを意識することなくモーダルウィンドウを表示できますよ”という共有の場所をPortalVueの機能を利用して設定します。その共有の場所が設定できたら、各コンポーネントの中でその場所に移動したい要素(モーダルウィンドウの要素)に対して設定した共有場所を指定します。その結果、各コンポーネントのモーダルウィンドは要素の重なりの調整から解放され、作成した場所(コンポーネント内)ではなく指定した場所でモーダルウィンドウの要素を表示させることができるようになります。

PHPの人気フレームワークであるLaravel8でもモーダル(確認用とダイアログ用)の機能の実装でPortalVueプラグインを利用しています。PortalVueでモーダルを表示させるための共通の場所を設定し、モーダルを使う場合はかならずその場所を指定しています。

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

手元のPCでも簡単にPorvalVueのモーダルウィンドウの動作確認を行えるようにcdnを利用して行います。


<script src="http://unpkg.com/vue/dist/vue.js"></script>
<script src="http://unpkg.com/portal-vue"></script>

vue.jsの初期設定

モーダルウィンドウを作成するためにvueインスタンスの初期設定を行います。データプロパティshowを設定し、showの値によってモーダルウィンドウの開閉を行います。動作確認をするために{{ show }}を設置し、ボタンを押すとshowの値がfalseからtrueになることを確認します。


<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>PortalVueでModal</title>
</head>
<body>
	<div id="app">
    <button @click="show = true">Click</button>
    <p<{{ show }}</p>
	</div>
	<script src="http://unpkg.com/vue/dist/vue.js"></script>
	<script src="http://unpkg.com/portal-vue"></script>
	<script>
		new Vue({
			el: '#app',
			data:{
				show:false,
			}
		})
	</script>
</body>
</html>

ボタンを押してshowの値がfalseからtrueになればvue.jsの初期設定は完了です。

ボタンをクリックでshowの値がtrueに
ボタンをクリックでshowの値がtrueに

動作確認ができたら{{ show }}は削除しておいてください。

PortalVueの設定

オーバーレイの設定

オーバーレイはモーダルウィンドウの背景に設定されブラウザの画面全体に覆われる幕です。警告や一時的なメッセージであればオーバーレイは半透明でその部分をクリックするとモーダルウィンドウが閉じる場合が大半です。

下記の画像のグレーの半透明の部分がオーバーレイです。

コンテンツ要素の装飾
コンテンツ要素の装飾

PortalVueを利用してオーバーレイの設定を行いますが、PortalVueでは2つのタグが必要となります。

  • portal-targetタグ・・・移動先を指定する際に利用するタグ
  • portalタグ・・・移動したい場所を指定し、タグの内側に移動させたい要素を記述するタグ

タグの他に移動する場所が移動先を識別するためのnameも設定する必要があります。

通常はportal-tagetタグはbodyタグの閉じタグの直前に設定を行います。bodyタグの閉じタグつまり一番最後に追加するのには意味があり、HTMLでは後に記述した要素のほうが上に表示されるためです。

portal-targetタグの場所よりも前の要素でportal-targetよりも上に表示される設定を行っている場合はモーダルウィンドウがその下に隠れてしまう場合があります。モーダルウィドウが表示されない場合はportal-targetタグ以外の要素との重なりを確認する必要があります。

<div id="app">
	<button @click="show = true">Click</button>
	<portal to="modal">
		<div style="position:fixed;top:0;right:0;bottom:0;left:0;background-color: gray; opacity:0.5"></div>
	</portal>
	<p>上記のClickボタンを押すとモーダルウィンドウが表示されます。</p>
	<portal-target name="modal"> </portal-target>
</div>

オーバーレイはportalタグの内部に追加しており下記の設定でブラウザ全体にグレーの半透明の幕を表示することができます。



<div style="position:fixed;top:0;right:0;bottom:0;left:0;background-color: gray; opacity:0.5"></div>

ブラウザで確認するとグレーの幕が表示されボタンやpタグのパラグラフが幕の下に表示されます。この状態ではボタンは幕の下にあるのでクリックすることはできません。

オーバーレイが表示される
オーバーレイが表示される

この画面だけを見ただけではportalタグの内部に記述した内容がportal-targetに移動したのかわかりません。デベロッパーツールを確認してみましょう。

pタグの下にportalタグの内部で設定したオーバーレイの設定があることが確認できます。

PortalVueで移動していることを確認
PortalVueで移動していることを確認

v-showイベントの設定

オーバーレイを表示するのはClickボタンを押したタイミングなのでvue.jsのv-showディレクティブを利用して設定を行います。

divタグにv-showディレクティブを追加し、showを設定します。


<div style="position:fixed;top:0;right:0;bottom:0;left:0;background-color: gray; opacity:0.5" v-show="show"></div>

ボタンを押す前はshowがfalseになっているためオーバーレイは表示されません。

オーバーレイは表示されません。
オーバーレイは表示されません。

Clickボタンを押してください。オーバーレイが表示されます。

クリックでオーバーレイが表示
クリックでオーバーレイが表示

オーバーレイは表示されましたが、オーバーレイを閉じる方法がありません。divタグさらにclickイベントを追加します。

閉じるためのclickイベント設定

divタグにオーバーレイを閉じるためのclickイベントを追加します。オーバーレイをクリックすることでshowがfalseになるように設定を行います。


<div style="position:fixed;top:0;right:0;bottom:0;left:0;background-color: gray; opacity:0.5" v-show="show" @click="show=false"></div>

設定が完了するとclickボタンで開いたオーバーレイは自身をクリックすると閉じられ、開閉を繰り返すことができるようになります。

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

オーバーレイの開閉をVuePortalとvue.jsの機能のv-showディレクティブを利用して実装することができました。オーバーレイの要素の中にコンテンツ(メッセージやフォームや確認ボタンを設置)を追加してモーダルウィンドウを作成することができますが、本文書で以下のように作成を行っていきます。下記の説明も最初は”??”かもしれませんが、設定を行う中で理解することができます。

  • オーバーレイの要素とコンテンツの要素に親子関係をもたせない
  • オーバーレイとコンテンツの要素の下にもう一つ要素を追加する(ベースの要素)
  • モーダルは3つの要素を利用して実装する(ベース、オーバーレイ、コンテンツ)
  • スタッキングコンテキスト(Stacking Context)を利用してオーバーレイの要素の上にコンテンツの要素をのせる
  • 3つの要素をportalタグで囲み、portal-targetタグに移動させる

ベース要素の作成

最初にベースの要素を作成します。ベース要素の中で後ほど追加するコンテンツを中央に表示できるようにflexboxを設定しています。どのような設定を行っているかわかるように背景と文字列の”ベース”を入れています。これは必要ないので後ほど削除します。


<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>PortalVueでModal</title>
</head>
<style>
	.base{
		position: fixed;
		top:0;
		left:0;
		right:0;
		display: flex;
		justify-content: center;
		background-color: rgba(0,0,0,0.1);
	}
</style>
<body>
	<div id="app">
		<button @click="show = true">Click</button>
		<portal to="modal">
			<div class="base" v-show="show">
				ベース
			</div>
		</portal>
		<p>上記のClickボタンを押すとモーダルウィンドウが表示されます。</p>
		<portal-target name="modal"> </portal-target>
	</div>
	<script src="http://unpkg.com/vue/dist/vue.js"></script>
	<script src="http://unpkg.com/portal-vue"></script>
	<script>
		new Vue({
			el: '#app',
			data:{
				show:false,
			}
		})
	</script>
</body>
</html>

ブラウザで確認するとボタンをクリックすると上部にブラウザの横幅に広がった要素が表示されます。flexboxにより文字も中央に表示されます。

ベース要素を追加する
ベース要素を追加する

オーバーレイの要素の作成

オーバーレイの要素はベースの子要素として追加するのでベースの要素の中に追加します。先ほど動作確認のために追加したベース要素の文字と背景は削除します。


<style>
	.base{
		position: fixed;
		top:0;
		left:0;
		right:0;
		display: flex;
		justify-content: center;
	}
	.overlay{
		position:fixed;
		top:0;
		left:0;
		right:0;
		bottom:0;
		background-color: gray;
		opacity:0.5;
	}
</style>
<body>
	<div id="app">
		<button @click="show = true">Click</button>
		<portal to="modal">
			<div class="base" v-show="show">
				<div class="overlay" v-show="show">
				</div>
			</div>
		</portal>
		<p>上記のClickボタンを押すとモーダルウィンドウが表示されます。</p>
		<portal-target name="modal"> </portal-target>
	</div>

Clickボタンを押すとオーバーレイが画面いっぱいに広がります。

オーバーレイが画面に表示
オーバーレイが画面に表示

オーバーレイをクリックするとオーバーレイが非表示になるようにオーバーレイのdiv要素にclickイベントを設定します。


<div class="overlay" v-show="show" @click="show=false">

Clickボタンを押すとオーバーレイが表示され、オーバーレイをクリックするとオーバーレイが非表示になるか確認を行ってください。クリックによりオーバーレイの開閉ができたらここまでの設定は問題ありません。

コンテンツ要素の作成

ベース要素の中にコンテンツ要素を追加します。


<style>
	.base{
		position: fixed;
		top:0;
		left:0;
		right:0;
		display: flex;
		justify-content: center;
	}
	.overlay{
		position:fixed;
		top:0;
		left:0;
		right:0;
		bottom:0;
		background-color: gray;
		opacity:0.5;
	}
	.content{
		background-color: white;
	}
</style>
<body>
	<div id="app">
		<button @click="show = true">Click</button>
		<portal to="modal">
			<div class="base" v-show="show">
				<div class="overlay" v-show="show" @click="show=false">
				</div>
				<div class="content" v-show="show">
					<p>コンテンツの内容はここです。</p>
				</div>
			</div>

背景色が白の”コンテンツの内容はここです”と文字列がオーバーレイの上に表示できればモーダルウィンドウの完成ですが、コンテンツはオーバーレイの上ではなく下に表示されます。”コンテンツの内容はここです”の文字列にアクセスしようとするとオーバーレイが文字列の上にある状態であるためクリックイベントによりオーバーレイが非表示になり文字列にアクセスすることはできません。

オーバーレイの下にコンテンツ
オーバーレイの下にコンテンツ

これはスタッキングコンテキスト(重ね合わせコンテキスト)により、fixedが設定されたオーバーレイのほうがコンテンツの要素より上の層として判断されるためです。

コンテンツの要素をオーバーレイの上の層として表示させるためには、positionの設定、opacityを1以下、transformなどのCSS設定を行う必要があります。スタッキングコンテキスト(重ね合わせコンテキスト)のルールを理解する必要がありますが試しにpositionをrelative、またはopacityを0.9に設定して本当に反映されるのか確認してみましょう。


.content{
    background-color: white;
    position: relative;
}

relativeを設定するとコンテンツの要素がオーバーレイの要素の上に表示されました。

コンテンツ要素にpostion:relative設定
コンテンツ要素にpostion:relative設定

またposition:relativeを外して、opacityを0.5にしてみましょう。


.content{
    background-color: white;
    opacity:0.5
}

opacity:0.5にすると一見オーバーレイの下にあるように思うかもしれませんがコンテンツに文字列にアクセスすることが可能で、オーバーレイの上の層として機能しています。

opacityを設定
opacityを設定

オーバーレイの要素とコンテンツの要素を入れ替えたらどうなるのかも気になるところなので実行してみましょう。contentクラスではpositionをrelativeに設定します。


<div class="content" v-show="show">
	<p>コンテンツの内容はここです。</p>
</div>
<div class="overlay" v-show="show" @click="show=false">
</div>

コンテンツ要素のpositionをrelativeに設定してもオーバーレイの方が後ろに記述されているのでオーバーレイの要素が上の層として判断されます。

オーバーレイが上の層として認識
オーバーレイが上の層として認識

このようにpositionや他のCSSのプロパティによって要素の重ね順が変わってしまうのでスタッキングコンテキスト(重ね合わせコンテキスト)がどのようなものか理解しておく必要があります。

ここまでのコードでモーダルウィンドウは完成し、オーバーレイをクリックするとオーバーレイは非表示になります。コンテンツの領域をクリックしてもオーバーレイの上の層にありオーバーレイとは親子関係もないためクリックイベントは発生しません。

もしオーバーレイの要素の中にコンテンツ要素があるような親子関係がある場合はオーバーレイにクリックイベントを設定すると子にもクリックイベントが設定されます。子要素でクリックイベントを発生させないため別の処理が必要になります。またオーバーレイとコンテンツに親子関係がある場合にオーバーレイにopacityを設定した場合は子要素にもそのopacityが継承されます。opacityは子要素で変更することができないためopacityではなくbackgroundを設定する場合はrgbaで透明度を設定する必要があります。

コンテンツの位置を調整

例えばコンテンツの位置やpadding, margin, border-radiusを設定することで表示されるコンテンツの表示を変更することができます。


.base{
	position: fixed;
	top:0;
	left:0;
	right:0;
	display: flex;
	justify-content: center;
	margin-top:1em;	
}
.overlay{
	position:fixed;
	top:0;
	left:0;
	right:0;
	bottom:0;
	background-color: gray;
	opacity:0.5;
}
.content{
	background-color: white;
	position: relative;
	padding:1em;
	border-radius: 10px;
}
コンテンツ要素の装飾
コンテンツ要素の装飾

transitionの設定

モーダルウィンドウには、vue.jsのtransitionを利用してアニメーションを設定することもできます。


.v-enter-active, .v-leave-active {
    transition: opacity .5s;
}
.v-enter, .v-leave-to {
    opacity: 0;
}	

ベース要素の外側にtransitionタグを設定します。


<portal to="modal">
	<transition>
	<div class="base" v-show="show">
		<div class="overlay" v-show="show" @click="show=false">
		</div>
		<div class="content" v-show="show">
			<p>コンテンツの内容はここです。</p>
		</div>
	</div>
	</transition>
</portal>

アニメーションでふわっとモーダルウィンドウが表示されるようになったかと思います。

オーバーレイとコンテンツで別のtransitionを付与したい場合は各要素でv-showが設定されているので各要素にtransitionタグをつけ、transitionタグにname属性とCSSを設定することでオーバーレイとコンテンツで動きの異なるアニメーションを設定することも可能です。

ここまでの説明で、PortalVueを使ったモーダルウィンドウの作成方法の基礎は理解できました。ぜひPortalVueを活用してオリジナルのモーダルウィンドウを作成してみてください。

今回はemitイベントを利用していませんでしたが親子関係を持つコンポーネントを利用した場合は、データプロパティのshowの値の受け渡しが必要となるのでvue.jsのemitイベントの理解が必要となります。

emitイベントについては下記の記事が参考になります。