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