Vue.js自定義下拉選單,如何實現在下拉選單區域外點選即可關閉下拉選單的功能

zhangchao19890805發表於2017-05-30

在開發過程中,為了效果好看,往往需要自己開發一個下拉選單,而不是使用 HTML 自身的 select 下拉選單。然而當編寫自定義下拉選單的時候,就會碰到一個問題:如果使用者在下拉選單的範圍外進行滑鼠點選的操作,如何關閉已經開啟的下拉選單?

解決思路如下:在 DOM 的根節點上新增一個 click 事件,同時下拉選單內阻止事件的預設行為和冒泡。當響應這個點選事件的時候,說明是在下拉選單範圍外的點選(因為下拉選單內阻止了事件的冒泡),就可以關閉已經開啟的下拉選單。

如果是純 JS 程式碼,有人可能會使用 document.onclick 來新增根節點事件。不過,我現在使用 Vue.js,會選擇使用 Vue.js 的方式處理這個問題。

Vue.js 使用元件化的方式組織程式碼,會有一個根元件,可以在這個根元件上加上 @click 事件,來響應區域外的點選事件。在一個完整的應用中,可能有多種場景需要這種區域外點選關閉的功能。除了最普通的表單裡的下拉選單外,還可能是網站右上角的訊息提示框,或者選單。比較合適的做法是把點選事件的具體處理邏輯放到各個元件中去。

那麼如何讓各個子元件響應根元件上的點選事件呢?可以使用Vuex來做到這一點。在這裡 Vuex 起到了元件之間互相傳遞資訊的作用。

讀者可以在這個網址下載我編寫的 Demo 專案:http://download.csdn.net/detail/zhangchao19890805/9855750
推薦讀者使用 yarn install 安裝所需的依賴。

下面說一下關鍵程式碼:

程式入口 main.js:

import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router'
import routes from './router'
import VueSuperagent from 'vue-superagent'
import Vuex from 'vuex'
import 'babel-polyfill';
import store from './vuex/store';

Vue.use(VueRouter);
Vue.use(VueSuperagent);
Vue.use(Vuex);

const router = new VueRouter({
  mode: 'history',
  routes
})

new Vue({
  el: '#app',
  router,
  store,
  render: h => h(App)
})

根節點 App.vue,新增了點選事件。

<template>
  <div @click="clickRoot">
    <router-view></router-view>
  </div>
</template>

<script>
    export default {
        methods:{
            clickRoot(event){
                this.$store.dispatch("clickRootNumAction", 1);
            }
        }
    }
</script>

Vuex 檔案結構

vuex
 │
 └─modules
      ├─clickRoot
      │    ├─actions.js
      │    ├─getters.js
      │    ├─index.js
      │    └─mutations.js
      │
      └─store.js

actions.js

export default {
    // action 允許非同步載入,實際專案中
    // 這裡可以發起個請求,再返回。

    clickRootNumAction(context, value) {
        context.commit('clickRootNum', value);
    }
}

getters.js

export default {
    getClickRootNum(state) {
        return state.clickRootNum;
    }
}

index.js

import actions from './actions'
import getters from './getters'
import mutations from './mutations'

const state = {
    clickRootNum: 0
}

export default {
    state,
    actions,
    getters,
    mutations
}

mutations.js

export default {
    clickRootNum(state, value) {
        let sum = state.clickRootNum + value
        state.clickRootNum = sum;
    }
}

store.js

import Vue from 'vue';
import Vuex from 'vuex';
import clickRoot from './modules/clickRoot'
Vue.use(Vuex);

const debug = process.env.NODE_ENV !== 'production';

export default new Vuex.Store({
    modules: {
        clickRoot
    },
    strict: debug
})

頁面程式碼 test.vue

<template>
    <div >
        <p>測試</p>
        <table>
            <tbody>
                <tr>
                    <td style="vertical-align: top;">
                        <div class="dropDownList">
                            <button class="controll" @click.prevent.stop="listShow()" 
                                    @keydown.prevent.40="arrowDown1" @keydown.prevent.38="arrowUp1">
                                {{selectItem}}
                                <span  :class="['triangle',showList==false?'triangleShow':'triangleHidden']"></span>
                            </button>
                            <ul class="showList" v-if="showList" @click.prevent.stop>
                                <input v-model="filterText" class="search"/>
                                <li v-for="item in newObj" class="optionArea" @click="selectOption(item)">  {{item.type}} </li>
                            </ul>
                        </div>
                    </td>
                    <td style="vertical-align: top;">
                        <div class="dropDownList">
                            <button class="controll" @click.prevent.stop="listShow2()" 
                                    @keydown.prevent.40="arrowDown2" @keydown.prevent.38="arrowUp2">
                                {{selectItem2}}
                                <span  :class="['triangle',showList2==false?'triangleShow':'triangleHidden']"></span>
                            </button>
                            <ul class="showList" v-if="showList2" @click.prevent.stop>
                                <input v-model="filterText2" class="search"/>
                                <li v-for="item in newObj2" class="optionArea" @click="selectOption2(item)">  {{item.type}} </li>
                            </ul>
                        </div>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>

</template>
<script>
    export default {
        data(){
            return {
                showList:false,
                obj:[
                    {type:"男裝"},
                    {type:"女裝"},
                    {type:"童裝"},
                    {type:"老年裝"},
                ],
                filterText:"",
                selectItem:"請選擇",

                showList2:false,
                obj2:[
                    {type:"賓士"},
                    {type:"桑塔納"},
                    {type:"大眾"},
                    {type:"比亞迪"},
                ],
                filterText2:"",
                selectItem2:"請選擇"
            };

        },
        methods:{
            listShow(){
                this.showList=!this.showList;
                if (this.showList2) {
                    this.showList2 = false;
                }
            },
            selectOption(item){
                this.selectItem=item.type;
                this.showList=false;
            },
            // 第一個下拉選單 按鍵:向下的箭頭
            arrowDown1(e){
                if (!this.showList) {
                    this.showList = true;
                }
                if (this.showList2) {
                    this.showList2 = false;
                }
            },
            // 第一個下拉選單 按鍵:向上的箭頭
            arrowUp1(e){
                if (this.showList) {
                    this.showList = false;
                }
                if (this.showList2) {
                    this.showList2 = false;
                }
            },
            listShow2(){
                this.showList2=!this.showList2;
                if (this.showList) {
                    this.showList = false;
                }
            },
            selectOption2(item){
                this.selectItem2=item.type;
                this.showList2=false;
            },
            // 第二個下拉選單 按鍵:向下的箭頭
            arrowDown2(e){
                if (!this.showList2) {
                    this.showList2 = true;
                }
                if (this.showList) {
                    this.showList = false;
                }
            },
            // 第一個下拉選單 按鍵:向上的箭頭
            arrowUp2(e){
                if (this.showList2) {
                    this.showList2 = false;
                }
                if (this.showList) {
                    this.showList = false;
                }
            }
        },
        computed:{
            newObj:function(){
                let self = this;
                return self.obj.filter(function (item) {
                    return item.type.toLowerCase().indexOf(self.filterText.toLowerCase()) !== -1;
                })
            },
            newObj2:function(){
                let self = this;
                return self.obj2.filter(function (item) {
                    return item.type.toLowerCase().indexOf(self.filterText2.toLowerCase()) !== -1;
                })
            }
        },
        watch:{
            '$store.getters.getClickRootNum': function () {
                if (this.showList){
                    this.showList = false;
                }
                if (this.showList2) {
                    this.showList2 = false;
                }
            }
        }
    };
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
    .dropDownList{
        margin-left:50px;
        width: 150px;
        .controll{
            position: relative;
            width: 150px;
            border: 1px solid #E3E9EF;
            cursor: pointer;
            .triangle{
                display: inline-block;
                position: absolute;
                top: 7px;
                right: 10px;
                cursor: pointer;
            }
            .triangleHidden{
                border-left: 5px solid transparent;
                border-right: 5px solid transparent;
                border-bottom: 8px solid #676F7F;
            }
            .triangleShow{
                border-left: 5px solid transparent;
                border-right: 5px solid transparent;
                border-top: 8px solid #676F7F;
            }
        }
        .showList{
            margin: 0;
            padding: 0;
            border: 1px solid #E3E9EF;
            // padding-top: 5px;
            padding-bottom: 5px;
            margin-top: 2px;
            width: 145px;
            .search{
                width: 141px;
                border: 1px solid #E3E9EF;
            }
            .optionArea{
                list-style: none;
                cursor: pointer;
                font-size: 14px;
                margin-left: 5px;
                &:hover{
                    background-color: #B2CFEB;
                    color: #fff;
                }
            }
        }
    }
</style>

相關文章