在开发过程中,为了效果好看,往往需要自己开发一个下拉列表,而不是使用 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>
Logo

前往低代码交流专区

更多推荐