描画関数で画像ポップアップのコンポーネント

ブログ記事中に使う画像ポップアップ用のコンポーネントを作ります。

最近 Vue って打ちすぎて view が書けなくなってきました。前回親子関係のないコンポーネントのやり取りの勉強をしたのでそれを応用します。

最初は jQuery の ColorBox から移行させるためにいろんなコンテンツに対応させようとしてクソキモなコードになってたので後日作り直しました。前フリなしに Vuex とか使ってるので勉強する記事の順番がズレています。先に状態管理の記事を読んでもらった方が良いかもです。

2つの非親子コンポーネントを作る

ブログ記事の中で使う <popup-img> の記述をリンク化するコンポーネントと、ポップアップモーダル用の2つのコンポーネントを作りました。この2つはネストされない他人なコンポーネントなのでデータのやり取りには Vuex を使用します。

  • <popup-img>リンクテキストor画像</popup-img> スロットをリンク化するコンポーネント
  • <popup-modal-window> popup-img のプロパティをポップアップ表示するコンポーネント

ルートインスタンスとストア

Vue.component('popup-img', PopupImg)
Vue.component('popup-modal-window', PopupModalWindow)
// 親子関係ではないのでStoreを使います
// ポップアップ用のストアモジュール
const popup = {
  namespaced: true,
  state: {
    render: null
  },
  mutations: {
    create: function(state, payload) {
      state.render = payload
    },
    reset: function(state) {
      state.render = null
    }
  }
}
// ストアに登録
const store = new Vuex.Store({
  modules: {
    popup
  }
})
// ルートインスタンス
new Vue({
  store,
  el: '#app',
  template: '<App/>',
  components: { App }
})

PopupImg.vue

<template>
  <span @click="open" @keyup.enter="open" tabindex="0">
    <slot/>
  </span>
</template>
<script>
export default {
  props: ['image', 'title'],
  methods: {
    open() {
      const slot = this.$slots.default[0]
      const image = this.image ? this.image : slot.tag === 'img' ? slot.data.attrs.src : null
      const render = this.$createElement('img', {
        attrs: {
          'src': image
        }
      })
      this.$store.commit('popup/create', render)
    }
  }
}
</script>
<style scoped>
span {
  cursor: pointer;
  text-decoration: underline;
}
</style>

 props には src を設定出来ます。もしスロットが画像なら画像の src をポップアップ内容に使いスロットがテキストなら props の設定を使います。

PopupModalWindow.vue

<template>
  <div class="window" v-if="body" @click.self="close">
    <div class="body">
      <div class="content">
        <popup-body></popup-body>
      </div>
    </div>
  </div>
</template>
<script>
import store from '@/vuex/index.js'
export default {
  computed: {
    body() {
      return this.$store.state.popup.render
    }
  },
  methods: {
    close() {
      this.$store.commit('popup/reset')
    }
  },
  components: {
    'popup-body': {
      functional: true,
      render() {
        return store.state.popup.render
      }
    }
  }
}
</script>
<style scoped>
.window {
  z-index: 110;
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
}
.body {
  position: relative;
  display: inline-block;
  padding: 10px;
}
.body img {
  max-width: 100%;
}
</style>

popup-img をクリックするとスロットやプロパティを使ってレンダー関数用のエレメントを生成してストアに送信します。ストアのエレメントデータが更新されると popup-modal-window でそれを使ってモーダルウィンドウを開きます。背景のオーバーレイだけクリックして閉じたかったので v-on:click.self モディファイアはすごく便利!

動作はこんな感じです。トランジションとか付けるとちょっといい感じです。

<popup-img><img src="dotmosic_01.png" width="100" height="100"></popup-img>
<popup-img src="dotmosic_01.png">リンクテキスト</popup-img>
<popup-modal-window></popup-modal-window>

リンクテキスト

他の要素に対応する

モーダルウィンドウではストアのエレメントをそのまま表示させるので色んなコンテンツに対応できるようにしました。Iframe 用のコンポーネントを追加します。 <popup-iframe> を作ります。

export default {
  props: ['src', 'width', 'height'],
  methods: {
    open() {
      const render = this.$createElement('iframe', {
        attrs: {
          src: this.src,
          width: this.width,
          height: this.height,
          frameborder: '0'
        }
      })
      this.$store.commit('popup/create', render)
    }
  }
}

まとめ

なるべくストア使わずに完結させたいけどこのやり方だと使わないでやり取りするのは難しいなのかなぁ…。基本的には公式サイトのモーダルウィンドウの方法がいいと思うんですけど、<popup-img> をブログの記事本文に書くので記述を出来るだけ楽にしたくてこんな感じで実装してみました(•ө•)ノ まだもっと改善出来そうな感じはあるので引き続き模索してみます(っ'ω'c)