Vue.jsでpopup componentを作ってみる


この記事はVue.js Advent Calendar 22日目の記事です。

前回の記事「browserify + debowerify + vueifyを使ったvue.js component開発」の続きです。
今回は少し実用的なwidgetのようなものとしてpopupのcomponentを作る方法を紹介します。
(※Vue.jsのバージョンはv0.11.4です)

とりあえず、どんなものかはデモを見てもらうとわかり易いです。ボタンをクリックしてみてください。

Demo

このポップアップの部品は以下の2つのコンポーネントから構成されています。

vue-popup: 閉じるボタンが付いた外側の枠となるコンポーネント
vue-sample: popupの枠の中に入るコンテンツとなるコンポーネント

htmlとしては以下のようにvue-popupという外側のコンポーネントが、内容となるvue-sampleコンポーネントを入れ子で抱える形になります。

コンポーネントの使用例

    <vue-popup title="タイトル">
        <vue-sample v-with="item"></vue-sample>
    </vue-popup>

今回紹介するコンポーネントの構成では、vue-popupは閉じるボタンを持つ外側の部分のみを提供する汎用的なコンポーネントとなります。中身のコンポーネントはvue-sampleに限らず好きなコンポーネントに切り替えて使用することができます。

popupコンポーネント(vue-popup)の実装

コンポーネントの実装はvueifyを使った.vueファイル形式です。vueifyについては以前の記事をご参照ください。
実装はコンポーネントのコードを見ていただいた方が早いと思います。

<style lang="stylus">
.popup
    position relative
    width 280px
    padding 20px
    border 1px solid #eee
    background-color #fff

    .title
        font-weight bold
        font-size 22px

    .closeWrap
        position absolute
        top 20px
        right 20px

    .close
        display block
        width 32px
        height 32px
        background-repeat no-repeat
        background-image url(unquote('data:image/png;base64,<クローズボタンimageのデータURI>'))
</style>

<template>
    <div class="popup">
        <div class="title">{{title}}</div>
        <content></content>
        <div class="closeWrap">
            <a href="javascript:;" v-on="click: close()" class="close" title="Close"></a>
        </div>
    </div>
</template>

<script>
module.exports = {
    data: function(){
        return {
            title: ""
        }
    },

    paramAttributes: ["title"],

    methods: {
        close: function(){
            this.$dispatch("popupClose");
        }
    }
};
</script>

このpopupのコンポーネントのコードにはいくつかポイントがあります。

CSS

  • stylusでスタイルを記述し、クローズボタンの画像をdataURIで指定します。

HTML

  • Insertion pointを指定するcontentタグにより、この外側のDOMでこのコンポーネントの下にセットされたノードをこのコンポーネントの特定の位置にセットします。(PolymerなどのWeb ComponentsにあるLight DOMとして渡されたものをShadowでセットする時に使うcontentタグと同じ挙動のようです。)

JS

  • paramAttributesを使用して、titleを外側から受け取り表示します
  • close時にはコンポーネントの外側でイベントをハンドリングできるように$dispatchを使って親コンポーネントに対して通知します
    (今回は$dispatchと$onを使用したコンポーネント間通信の方法を紹介しますが、実装の方法としては外側からオブジェクトを渡してデータバインディングの性質を利用して開閉をコントロールする方法もあります。)

コンテンツとなるコンポーネント(vue-sample)の実装

今度はpopupされる中身となるコンポーネントの実装です。前回の記事で紹介した部品を再利用しています。このコンポーネントはただ受け取ったデータをバインディングにより表示するだけなので、特に説明は必要ないと思います。

<style lang="stylus">
.sample
    overflow auto
    max-height 360px

    .body
        padding 16px 0
        text-align center
</style>

<template>
<div class="sample">
    <div class="body">
        <img v-attr="src: logo">
        <div>{{description}}</div>
        <a v-attr="href: url" target="_blank">{{url}}</a>
    </div>
</div>
</template>

<script>
module.exports = {
    data: function () {
        return {
            logo: "",
            url: "",
            description: ""
        };
    }
};
</script>

mainのJSの実装

まずはvue.js本体と自作コンポーネント2つをrequireし、それぞれvue-popupタグとvue-sampleタグとしてコンポーネントをセットします。

$dataには表示用のデータであるitemとpopupの開閉をコントロールするpopupOpenedというフラグを用意します。
初期化時にpopupコンポーネントのcloseボタンがclickされた時に$dispatchされるpopupCloseを$onでlistenして、開閉状態を変更します。

    var Vue = require("vue");
    var vuePopup = require("../../components/popup.vue");
    var vueSample = require("../../components/sample.vue");

    var app = module.exports = new Vue({

        el: '#app',

        components: {
            "vue-popup": vuePopup,
            "vue-sample": vueSample
        },

        data: {
            item: null,
            popupOpened: false
        },

        created: function() {
            this.item = {
                "title": "Dev Morning",
                "logo": "https://connpass-tokyo.s3.amazonaws.com/thumbs/de/dc/dedc44c50713733d06b9121186469c18.png",
                "url": "http://devmorning.connpass.com/",
                "description": "エンジニアからデザイナ、学生から大人まで、週末の朝に趣味で集まってアプリやサービスをつくります。"
            };
            this.$on("popupClose", function(){
                this.popupOpened = false;
            }.bind(this));
        }
    });

HTMLの定義

mainのHTMLではpopupの開閉をトグルするボタンと、コンポーネントの配置を行って、v-withでvue-sampleタグにデータであるitemを渡します。
読み込むJSはbrowserify + debowerify + vueifyによって出力されたファイルです。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Popup Components Sample</title>
</head>
<body id="app">
    <button v-on="click: popupOpened = !popupOpened">Popup Toggle</button>
    <!-- components -->
    <vue-popup v-show="popupOpened" title="{{item.title}}">
        <vue-sample v-with="item"></vue-sample>
    </vue-popup>
    <!--/ components -->
    <!-- script -->
    <script src="../../dist/popup/main.js"></script>
    <!--/ script -->
</body>
</html>

buttonはpopupOpenedの状態をトグルして、popupのコンポーネントのv-showにpopupOpenedを指定しています。

このhtmlを開いてボタンを押すと、この記事の冒頭で紹介したdemoのコンポーネントのようにクローズボタン付きのpopupがコンテンツと一緒に表示されました(アニメーションはしないはず)。
以上、今回の記事ではvue.jsの$dispatchでコンポーネント間通信をして、外側と内側のコンポーネントの実装方法を紹介しました。前回の記事と比べると、少し実用的なコンポーネントになってきたかと思います。

今回はデモでは付けているアニメーションの部分については言及していませんでしたので、明日の記事でv-transitionについて紹介します。