Vue.jsでポートフォリオサイトを作ってみる

# 概要

Vue.jsとgitさんともう少し仲良くなりたかったので、[ポートフォリオサイト](https://a-pompom.github.io/portfolio_Vue/#/)を作りました。

それなりに頑張って作ったので仲良し度が3ぐらい上がったはずです。

![portfolio-overview.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307733/2fc058a5-1e2d-af2b-bdf7-50675bdc9190.gif)

以下では、作成に際して頑張った機能なんかについて備忘録として書いていきます。
間違い等ございましたら優しく教えて頂けるとうれしいです（╹◡╹）

ソースコードは[GitHub](https://github.com/a-pompom/portfolio_Vue)に置いてあります。

# レスポンシブ(仮)

今回はレスポンシブ対応(仮)として、縦長・横長のディスプレイで表示を切り替えるよう設定してみました。

解像度単位での対応はサーバーサイド言語と組み合わせるときに頑張ります。頑張りたい。

![responsive.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307733/bce1f899-4674-ec30-b0eb-5773821f05b3.gif)

## 縦長と横長で切り替える

切り替えるためのコードは以下のようになります。


```scss:プロフィール画面抜粋

&__content {
    margin-top: 20px;
			
    @include for-landscape() {
        @include flex-center;
    }
    // 中略...
}

```

```scss:つくったもの画面抜粋
// 縦長ディスプレイでは一つずつ、横長ディスプレイでは横並びで表示
&-list {

    @include for-landscape() {
        @include flex-table;
    }

    &__item {
	@include for-landscape() {
	    width: 40%;
	    max-height: 350px;
        }
        @include for-portrait() {
            width: 70%;
            max-height: 650px;
        }
        //中略...
    }

}
```

「include」はSassで外部に定義したミックスインを利用するために用います。
今回利用したミックスインは大きく以下の2つに分かれます。

* スタイルの再利用
 
→ フレックスボックスは様々な場所で再利用されるので、ミックスインとして定義しています。

```scss:フレックスボックス
//flex-box
@mixin flex-center {
    display: flex;
    justify-content: center;
}
```

* コンテンツブロック

→縦長・横長の切り替えは、メディアクエリを内包したコンテンツブロックの形式でのミックスインで定義されます。

メディアクエリの詳細については[MDNさん](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries)に詳しく書かれています。

```sass:コンテンツブロック
// デバイスが縦長か横長かでレイアウトを作り分ける用
@mixin for-portrait {
    @media screen and (orientation: portrait) { @content; }
}
@mixin for-landscape {
    @media screen and (orientation: landscape) { @content; }
}
```

ポイントとなるのは「portrait, landscape」の記述と「@content」です。
これらにより、「for-portrait」ミックスイン用のコンテンツブロックでは縦長画面用のスタイル、そして、「for-landscape」ミックスイン用のコンテンツブロックでは、横長画面用のスタイルを定義できるようになります。

Sassさんの力を借りればこうした複雑なスタイルも一箇所で共通化しておくことで、実装では画面のスタイルに注力することができるようになります。

といってもまだ全然使いこなせていないので、もう少し勉強したいです。

[ミックスイン 参考](https://sass-lang.com/documentation/at-rules/mixin)

# オーバーレイテキスト

続いては、オーバーレイテキストです。
名前だけ聞くと何だか難しそうに見えますが、実体はマウスを近づけると出てくるあいつです。

<img width="640" alt="overlay.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307733/51b0cbfe-d573-366e-7374-3f45f818d0c9.png">

今回は、機能自体の実装に加えて、別のアプリでも再利用できるようコンポーネントとしての切り出しも行ったので、それぞれについて見ていきます。

## マウスを近づけたら文字を出したい

よく見かける機能ではあるのですが、実際に作ってみるまではJavaScriptとかでごりごり書いて複雑な制御とか必要なんだろうなーと、思っていました。

実際に作ってみると思いのほかシンプルにまとまったので、作り方についてざっくりと見てみたいと思います。

### オーバーレイ

まずはオーバーレイの概念についてです。意味を辞書で調べてみると、
> to cover something with a layer of something

(あるものをレイヤー的な何かで覆う)

という感じのことが書かれています。

「レイヤー」は、お絵描きを嗜まれている方にはお馴染みかと思われますが、イメージを大雑把に図で描くと以下のようになります。

![overlay-image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307733/1b72d57b-29af-ebe9-6fdb-87182eea4b08.png)

以下では、こうしたオーバーレイをコードで実現するための方法について見ていきます。

まずは、オーバーレイを実現するためのスタイルです。

```sass:オーバーレイ
&-mask {
    position: absolute;
    top: 0;
    left: 0;
    z-index: 99;

    width: 100%;
    height: 100%;
    background: rgba(0,0,0,.6);

    // フォーカスで表示されるテキスト
    &--text {
        padding-top: 25%;
        width: 100%;
        height: 100%;
        color: #fff;
        margin: 0;

    }
}
```

オーバーレイと覆われる要素は親子関係ではなく「sibling」関係にあるので、「position: absolute」と「top: 0, left: 0」でオーバーレイ要素を定義することで、「sibling」要素と同じ位置に配置できるようになります。

そして、「z-index」は「position」プロパティと併せて用いられ、オーバーレイの「覆い被さる」機能を実現するために利用されます。

今回はCSSのあれこれについて書く気力がなかったので、詳細は[この辺り](https://stackoverflow.com/questions/14391208/how-does-z-index-really-work)をば。

---

上記スタイルにより、オーバーレイ要素を実現するためのHTML要素のイメージは以下のようになります。

```html:オーバーレイ要素
<div class="parent">
    <div class="overlay">
        <p>overlay text</p>
    </div>
    <div class="content">
        <p>something...</p>
    </div>
</div>
```

これにオーバーレイテキスト要素にVueでマウスが近づいたか否かで表示を管理させれば実現することはできます。

しかし、オーバーレイでテキストを表示させる機能は他でも使いたいので、コンポーネントとして切り出すことを目指してみます。

## オーバーレイテキストコンポーネント

まずはコードから全体像を見ていきます。

```vue:OverlayText.vue(抜粋)
<div
    v-on:mouseover="visible = true"
    v-on:mouseleave="visible = false" 
    class="item">
    
    <!-- オーバーレイが覆いかぶさる要素 -->
    <slot></slot>

    <!-- オーバーレイ要素 フォーカス時に表示し、フォーカスが外れると非表示となる -->
    <transition name="overlay">
        <div
           v-show="visible"
           class="item-mask">
           <h2 class="item-mask--text">
               {{ textContent }}
           </h2>
        </div>
    </transition>
         
</div>
```

「transition」という見慣れないタグがありますが、こちらは後で詳しく見ていきます。
ここで注目して頂きたいのは、「visible」という要素です。

これはオーバーレイテキストのコンポーネントに属するもので、「true」、「false」いずれかの値をとります。
マウスを近づけると「true」に、マウスを離すと「false」へと切り替わります。

これを「v-show」ディレクティブと組み合わせることで、マウスが近づいたか否かで表示・非表示を切り替えることができるようになります。

---

続いて、オーバーレイテキストが覆い被さる要素を表す「slot」タグについて見ていきます。

スロットと言うと「7」が三つ揃うと楽しくなるアイツが思い浮かびますが、Vueでのスロットは、意味的には「コインの投入口のような狭い穴」のニュアンスとして用いられていると思われます。

使い方としては、オーバーレイテキストコンポーネントが実際に使われている箇所を見ると、イメージがしやすくなるかと思います。

```vue:オーバーレイテキスト
<!-- 概要(キャプチャ,  アプリ名) -->
<overlay-text
    v-bind:textContent="'Read More...'"
    >
    <img v-bind:src="workSummary.appImageSource" class="item-image">
    <h3>{{ workSummary.appName }}</h3>
</overlay-text>
```

コンポーネントによるカスタムタグの「overlay-text」の中にHTML要素が定義されていることが分かります。
スロットを利用すると、コンポーネント内の「slot」タグが上記要素によって置換されます。

これにより、スロットは、「要素を埋め込む為の穴」という言葉通りの意味を表していることが何となくしっくり来るかと思います。

ゲーマーの方であれば「武器スロット」なんかをイメージすると覚えやすいかもしれません。

---

このようにオーバーレイテキスト要素をコンポーネントとして切り出すことにより、以下の形で再利用できるようになりました。

* OverlayTextコンポーネントをimportし、カスタムタグとしてtemplate上に配置
* 引数として表示させたいテキストを設定
* オーバーレイで覆われる要素はスロットで置換されるので、OverlayTextコンポーネントのカスタムタグ内に配置

探せば先人が既に更にイカしたコードでコンポーネントとして配布されているかと思いますが、このように自分で一から書いてみると色々と発見があって楽しいので、普段何気なく使っている機能も実際に作ってみるとよいかもしれません。

---

## トランジション

と、一旦話が完結した形となってしまいましたが、まだ一つ残っていましたね。

どうせオーバーレイで表示させるなら今風にイカした感じにしたいので、今回はVueのトランジションを利用してみました。

### トランジションとは

まずは例のごとく辞書的な意味から見てみることにします。

> the process or a period of changing from one state or condition to another.
 
(ある状態から別の状態への変化を表すプロセス、あるいは期間)

ものすごくざっくり言うとぱらぱら漫画の「ぱらぱら」を表すものが近いかと思われます。

今回の例で言うと、オーバーレイテキストが表示されてから消えるまでの描画状態を制御するための設定として、「transition」が利用されます。

こう書くと難しそうに見えますが、Vueのトランジションさんは、本来CSSで複雑な管理が必要なアニメーション表示を**クラスの付け替え**という形で、楽に実現できるようにしてくれるとても便利なものです。

アニメーションの中でどのようにクラスの付け替えが行われるか、ざっくりイメージで表すと以下の図となります。

![transition.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307733/ed58fd26-7e59-d45d-52b6-5c1fce3e4e47.png)

それぞれの状態に応じてクラスを自動で付け替えてくれるので、スタイルをシンプルに保つことができます。
Vueでは更に描画状態に応じてフックをJavaScriptのコードで取得することもできますが、その辺りの話はまた別の機会に...。

---

トランジションのイメージはざっくりと掴めたかと思いますので、実際のコードを見てみましょう。

```vue:トランジション
<transition name="overlay">
    <div>アニメーションさせる要素</div>
</transition>

//中略...

<style lang="scss" scoped>
// アニメーション
.overlay-enter, .overlay-leave-to {
    opacity: 0;
}
.overlay-enter-active, .overlay-leave-active {
    transition: opacity 1.1s;
    cursor: pointer;
}
</style>
```

このように、コードもすっきりした形で記述することができます。
また、「transition」タグの「name」属性に設定した値によって、クラス名のプレフィックスを「v」から任意のものに切り替えられるようになります。

[Vue トランジション 参考](https://vuejs.org/v2/guide/transitions.html)
[CSS トランジション 参考](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions)

---

# モーダルダイアログ

最後にモーダルダイアログ機能について見ていきます。

![modal.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307733/0945b537-85fa-5570-61c4-a6f62f4644c1.gif)

モーダルの意味については、[こちら](https://wa3.i-3-i.info/word11432.html)がすごく分かりやすかったです。

さて、モーダルダイアログ自体は、これまでに紹介してきた機能を応用すれば作れてしまいます。
しかし、オーバーレイテキスト等とは若干勝手が異なる部分もあるので、詰まった部分を中心にざっくりと書いていきます。

---

## モーダルコンポーネント

モーダルダイアログは他のアプリでもガンガン使っていきたいので、コンポーネントとして切り出すことを考えてみます。
まずはコードを載せますので、コメント等から雰囲気を掴んで頂ければと思います。

```vue:Modal.vue

<div v-show="visible">
    <!-- オーバーレイ要素 要素クリックでモーダルを閉じる -->
    <div
        v-on:click.stop="close"
        class="modal__overlay">
    </div>

    <!-- モーダル要素本体 表示されて初めて幅・高さが決定されるので
        位置を指定する場合は、nextTickを利用した方がよい
        - content: モーダル本体のコンポーネント
        - params: 本体コンポーネントへ渡すprops
        - visible: modal要素の可視
        - closeイベント: 閉じる処理は親要素へ伝播させる  -->
    <div
        v-bind:is="content"
        v-bind="params"
        v-on:close="close"

        class="modal__content">
    </div>

</div>
    
<script>
export default {
    /**
     * - params: モーダル要素のコンポーネントへ渡す引数
     * - visible: モーダル要素の可視
     * - content: モーダルコンポーネント本体 is属性で動的に描画させる
     */
    props: { 
        params: Object,
        visible: Boolean,
        content: Object
        },

    methods: {
        // 親コンポーネントへモーダルを閉じるイベントを発火
        close() {
            this.$emit('close');
        }
    }

}
</script>

```

モーダルコンポーネントは機能を大まかに挙げると以下のようになります。

* 利用する側のコンポーネントからは引数として以下を渡す

    * params...モーダルダイアログで利用するパラメータ
    * visible...モーダルダイアログが表示されるフラグ
    * content...モーダルダイアログ要素を格納したコンポーネント

* モーダルダイアログとして表示する要素はコンポーネントとしてまとめられる
* モーダルダイアログから「閉じる」イベントが発火された場合、親要素へ同様のイベントを発火させることで、閉じる操作を親側で制御可能とする

この中で特に躓いたのは、モーダルダイアログ要素をコンポーネントで渡して動的に描画する処理です。
検索しても中々出てこなかったのですが、[公式さん](https://vuejs.org/v2/guide/components.html#Dynamic-Components)にありました。もっと早く知りたかった。

これらを踏まえて実際に利用した例が以下のようになります。

```vue:モーダル要素
<modal-component
    v-bind:params="getModalParams(summaryIndex, workSummary.id)"
    v-bind:visible="currentModalIndex === workSummary.id"
    v-bind:content="worksDetailContent"

    v-on:close="closeModal"
    >
</modal-component>
```

---

## モーダルダイアログを中央に配置する

モーダルダイアログは「visible」プロパティが「true」になると表示されます。
よって、「visible」プロパティをVueインスタンスの「watch」プロパティで監視し、「true」になったときに中央になるよう位置を指定してあげればOK。

と、思っていましたが中々上手くいきませんでした。

「visible」プロパティが変化することにで、DOM操作によってモーダルダイアログ要素のスタイルの「display」プロパティが書き換えられるので、
書き換えが終わるまでは要素の**高さ・幅は0**となります。

よって、正しく位置を導出するためには、DOM操作後にモーダルダイアログが表示されるまで待機する必要があります。
これを実現するため、Vueには「nextTick」というAPIが用意されています。

このAPIのコールバック関数にモーダルダイアログの位置を指定する処理を渡すことによって、きちんとダイアログが中央に表示されるようになります。

これをコードで表すと、以下のようになります。

```javascript:位置を指定する処理
/**
* 描画後に得られるモーダル要素の高さ・幅から
* モーダルを中心に配置するための位置スタイルを設定
*/
setWorksDetailPosition() {	
    // モーダル要素は非表示の段階では幅・高さを持たないので、nextTickで描画後に当該処理を発火させる
    this.$nextTick(() => {
        //画面
        let windowWidth = window.innerWidth;
        let windowHeight = window.innerHeight;

        //詳細要素
        let worksDetailDOM = document.getElementsByClassName("works-detail")[this.index];
        let worksDetailHeight = worksDetailDOM.offsetHeight;
        let worksDetailWidth = worksDetailDOM.offsetWidth;

        // 詳細要素の絶対位置を画面・詳細要素の幅・高さをもとに算出し、更新
        worksDetailDOM.style.top = (windowHeight - worksDetailHeight) / 2 + "px";
        worksDetailDOM.style.left = (windowWidth - worksDetailWidth) / 2 + "px";
    });
				
}
```

---

これでめでたくモーダルダイアログも再利用可能なコンポーネントとして使うことができるようになりました。やったぜ。

[nextTick 参考](https://vuejs.org/v2/api/#Vue-nextTick)

# まとめ

今回のポートフォリオサイトを作ってみて、イマイチ苦手意識のあったトランジションとほんの少しだけ仲良くなれたり、今後もWebアプリを作る際に使いそうな機能をコンポーネントとして切り出せたりと、色々な発見があって楽しかったです。

本当はGitのブランチなんかにも慣れる為にも色々試行錯誤したりもしていましたが、その辺についてはもう少し慣れてきてからまとめたいですね。

ポートフォリオの形で公開できそうな作ったものを並べてみると、まだまだ少ないなー...と思うので、もっと強そうなオーラを身に纏えるよう頑張っていきたいです。（╹◡╹）