Vue.jsでドロップダウンを作り込む

f:id:yusuke_kuwa:20171208145625p:plainこちらはVue.js #4 Advent Calendar 2017 - Qiita 8日目の記事です。

株式会社SCOUTERの鍬(id:yusuke_kuwa) です。

今回は、Vue.jsらしい美しいコードだなと思ったドロップダウンのコンポーネント作成に焦点を当てて、作っていく工程を振り返ってみました。

検証実施環境

  • Vue.js ^2.5.0

  • Element ^2.0.0

  • lodash-es ^4.17.4

  • yarn ^1.0.0

コンポーネントの要件

今回は、以下の要件で使用されるコンポーネントとして作成しています。

  • テーブルや検索結果の件数指定・並び替えなどに使用できる

  • 今なにを選択しているのか分かる

  • 選択した直後にリクエストが送信される

完成イメージはgithubのDropDownです。

f:id:yusuke_kuwa:20171208102852j:plain
github.com-PullRequests

変数とpropsの設計

まずは、コンポーネントとして受け取る値を決めていきます。 上の要件を満たすように実装していきます。

props: {
        label: {
            type: String,
            required: true,
        },
        listItems: {
            type: Object,
            required: true,
        },
        activeItemKey: {
            type: [String, Number],
            required: false,
        },
        action: {
            type: Function,
            required: true,
        },
    },

data() {
        return {
            isActive: false,
        };
    },
  • labelgithubの図でいう "sort"の部分

  • listItemsは選択肢をkey:valueで受け取るObject

  • activeItemKeyは選択中のlistItemのkeyの値

  • actionはドロップダウンの中身のlistItemを選択した時の実行関数

となっています。

今回作成するドロップダウンはあくまでもパーツなので、実行関数は親コンポーネントに持たせます。

コンポーネント側のactionの役割は、

  • リクエストを送る

  • activeItemKeyの値を更新する

などを想定しています。

実装

コンポーネントから渡ってくる値が決まったので、ガシガシと作っていきます。

templateタグ周り

<template>
    <div>
        <div class="all-wrapper">
            <div class="dropdown-wrapper" @click="isActive = !isActive">
                <div class="dropdown-text">
                    {{label}}
                </div>
                <i class="el-icon-caret-bottom"></i>
            </div>
            <transition>
                <div class="list-items" v-if="isActive">
                    <template v-if="existsListItems">
                        <template v-for="(value, key) in listItems">
                            <div class="list-item"
                                 :class="[key == activeItemKey ? 'active' : '' ]"
                                 @click="handleClickItem(key)"
                            >
                                {{value.name}}
                            </div>
                        </template>
                    </template>
                    <template v-else>
                        <div class="list-item">
                            選択肢がありません
                        </div>
                    </template>
                </div>
            </transition>
        </div>
        <div class="dropdown-bg" @click="isActive = false" v-if="isActive"></div>
    </div>
</template>

propsから受け取る値の表示・制御はtemplateタグの中で済んでしまいます。

選択中の要素かどうかのactive判定も、propsで受け取ったactiveItemKeyを使ってテンプレート内で処理してしまいます。

最終的にscriptタグに残る記述は、

computed: {
    existsListItems() {
        return !isEmpty(this.listItems);
    },
},

methods: {
    handleClickItem(key) {
        if (key == this.activeItemKey) {
            return;
        }

        this.isActive = false;
        this.action(key);
    },
},

たったこれだけになりました。良いですね!

疑似背景

また、背景押下時にドロップダウンを閉じる挙動を

<div class="dropdown-bg" @click="isActive = false" v-if="isActive"></div>

が扱っており、CSSで疑似背景を全画面に展開しています。

.dropdown-bg {
    width: 100vw;
    height: 100vh;
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 2;
}

スタイリング

ではいよいよ、list-itemにスタイルを当てていきます。

.all-wrapper {
    position: relative;

    .dropdown-wrapper {
        color: #666666;

        display: flex;
        align-items: center;

        &:hover {
            cursor: pointer;
        }

        .dropdown-text {
            font-size: 14px;
        }

        i {
            font-size: 10px;
            margin-left: 6px;
        }
    }

    .list-items {
        width: 260px;
        max-height: 300px;
        background-color: #fff;
        border-radius: 2px;
        border: 1px solid #B9BFC9;
        box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12), 0 0 6px 0 rgba(0, 0, 0, 0.04);
        position: absolute;
        right: 0;
        overflow-y: scroll;
        z-index: 3;
        padding: 0.5rem 0;

        .list-item {
            color: #333;
            font-size: 14px;
            line-height: 16px;
            padding: 0.75rem 1rem;

            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;

            position: relative;

            &:not(.active):hover {
                background-color: #F3F4F6;
                cursor: pointer;
            }

            &.active {
                color: #fff;
                background-color: #182A4B;
            }
        }
    }
}

背景の上にlits-itemが乗るので、こちらはz-index:3となります。

仕上げのtransition

最後にtransition に、ヌルっと表れるcssを当てて仕上がりです。

Vue.jsのtransactionコンポーネントを使用しているので、クラスの制御はVue.jsに任せて、CSSを書くだけで実装できます。

参考: Enter/Leave とトランジション一覧 — Vue.js

.v-enter-active, .v-leave-active {
    transition: all 0.3s
}

.v-enter {
    transform: translateY(-10px);
}

.v-enter, .v-leave-to {
    opacity: 0
}

完成品

f:id:yusuke_kuwa:20171208114531g:plain

まとめ

今回はVue.jsの習熟度的な観点で難しいことは特にしていませんが、props,template,cssの役割をうまく分散させる設計が肝でしたね。

jQuery等はもちろんですが、Reactでもここまで綺麗に書けないのではないかなと思います。笑

よきVue.jsライフを!