先上效果圖:
組件特點:
- 模擬下拉框
- 可輸入文字搜索選項,keyup或input事件觸發搜索(並優化了原生keyup和input事件的問題)
- 數據源異步加載
- 滾動加載選項數據
- 同一頁面可重復使用該組件
技術工具說明:
- 基礎框架 vue.js
- jquery.js輔助
- 樣式 element-ui.js
- 注意:非單頁面,非前后端分離開發
進入正文:
創建項目
1. 新建項目文件夾 cw-input-select
打開,以下內容都是在此文件夾內操作,不贅述
2. 新建demo.html
- 引用jquery,vue,element-ui(cdn)
- 創建一個id="app"的根元素
- 在body元素尾部添加<script></script>標簽,創建vue實例(這部分代碼也可以單獨寫一個js文件)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script> <script src="https://cdn.bootcss.com/element-ui/2.10.1/index.js"></script> </head> <body> <div id="app"> </div> <script> new Vue({ el: '#app' }) </script> </body> </html>
3. 新建文件夾component,並在里面新建cw-input-select.js和cw-input-select.css文件
結構如下:
| cw-input-select
| demo.html
| component
| cw-input-select.js
| cw-input-select.css
4. 在demo.html header標簽里引入cw-input-select.js和cw-input-select.css
<!-- 引入cw-input-select組件 --> <link rel="stylesheet" href="component/cw-input-select.css"> <script src="component/cw-input-select.js"></script>
編寫組件
1.編寫一個vue全局組件 cw-input-select
component/cw-input-select.js
Vue.component('cw-input-select', { template: '<div></div>', data: function () { return {} }, created () {}, methods: {}, })
2.在demo.html 添加組件標簽
demo.html
<div id="app"> <cw-input-select></cw-input-select> </div>
3. 實現靜態(無動態數據無交互)的下拉框布局和樣式,並用靜態數組模擬選項(后面替換動態數據源)
component/cw-input-select.js
Vue.component('cw-input-select', {
template: `<div class="cw-input-select_wrap">
<div class="cw-input-select">
<div class="cw-input-select_box">
<span>請選擇</span>
</div> <div class="cw-input-select_pop"> <input type="text" class="cw-input-select_ipt" placeholder="搜索" /> <ul class="cw-input-select_options"> <li> <span>不限</span> </li> <li v-for="option in optionsList"> {{option}} </li> </ul> <span class="cw-input-select_arrow"></span> </div> </div> </div>`, data: function () { return { optionsList: ['選項1', '選項2', '選項3'] } }, created () {}, methods: {}, })
component/cw-input-select.css

/* 基礎樣式reset */ input { box-sizing: border-box; outline: 0; } ul { margin: 0; padding: 0; } ul, li { list-style: none; } /* 組件整體容器 */ .cw-input-select_wrap { position: relative; width: 198px; height: 28px; font-size: 14px; } /* 組件內容 */ .cw-input-select { width: 198px; position: absolute; } /* 基本下拉框 */ .cw-input-select_box { height: 28px; border: 1px solid #b7b7b7; border-radius: 4px; background-color: white; position: relative; cursor: pointer; } /* 基本下拉框里面右邊的線體上下箭頭(可旋轉) */ .cw-arrow { content: ''; display: block; position: absolute; right: 10px; top: 8px; border-top: 1px solid #C0C4CC; border-right: 1px solid #C0C4CC; border-radius: 1px; width: 8px; height: 8px; background: transparent; transition: transform .3s, -webkit-transform .3s; transform: rotate(135deg); z-index: 10; } /* 基本下拉框 文字 */ .cw-input-select_box > span { display: inline-block; line-height: 28px; padding: 0 30px 0 15px; font-size: 12px; color: #606266; /* 文字超出用省略號 */ white-space: nowrap; text-overflow: ellipsis; width: 100%; overflow: hidden; } /* 選項列表盒子 */ .cw-input-select_pop { position: relative; background-color: white; border: 1px solid #E4E7ED; border-radius: 4px; max-height: 274px; box-shadow: 0 2px 12px 0 rgba(0,0,0,.1); margin-top: 12px; padding: 5px; box-sizing: border-box; z-index: 9; } /* 選項列表盒子上方的三角形箭頭 */ .cw-input-select_arrow { position: absolute; display: block; width: 0; height: 0; border-color: transparent; border-style: solid; border-width: 6px; filter: drop-shadow(0 2px 12px rgba(0, 0, 0, .03)); top: -6px; left: 35px; margin-right: 3px; border-top-width: 0; border-bottom-color: #fff; z-index: 99; } /* 選項列表盒子里面的輸入框 */ .cw-input-select .cw-input-select_pop .cw-input-select_ipt { position: absolute; top: 5px; z-index: 99; height: 24px; line-height: 20px; width: 94%; border: 1px solid #DCDFE6; padding: 1px 5px; font-size: 12px; } /* 選項列表內容 */ .cw-input-select_options { display: block; margin-top: 26px; max-height: 234px; } /* 選項單元 */ .cw-input-select_options li { padding: 8px 15px; background-color: white; cursor: pointer; } /* 選項單元hover */ .cw-input-select_options li:hover { background-color: #F5F7FA; }
css注釋我盡量寫詳細,因為很難把css分開講解。
至此,效果如下:
-
4. 實現點擊基本框顯示或隱藏選項列表盒子,線體箭頭可上下旋轉
(1)在基本框class=cw-input-select_box的div上添加click事件調用selectHandle方法
<div class ="cw-input-select_box" v-on:click="selectHandle">
(2)在組件實例data選項里添加一個變量isShowPop,methods選項里添加selectHandle方法
data: function () { return { optionsList: ['選項1', '選項2', '選項3'], isShowPop: false } }, methods: { // 點擊基本框顯示或隱藏選項列表盒子 selectHandle: function () { this.isShowPop = !this.isShowPop; }, },
(3)在class=cw-input-select_pop的div上添加v-if="isShowPop"
<div class="cw-input-select_pop" v-if="isShowPop">
(4)為了測試多個組件是否互相干擾,可在demo.html添加多個<cw-input-select></cw-input-select>看看效果。不會干擾。
(5)此時有個小問題
每次點擊組件,很容易把文字選中,我們使用下拉框並不需要文字選中效果。
解決辦法:
在組件最外層div加上 onselectstart="return false"
<div class="cw-input-select_wrap" onselectstart="return false">
就好了。現在無論點擊多少次文字都不會被選中。
(6)基本框右側箭頭的旋轉
這個只需要css就可以搞定
在cw-input-select.css文件的.cw-arrow下面添加.cw-arrow.up
/* 箭頭向上 */ .cw-arrow.up { transform: rotate(-45deg); top: 12px; }
回到組件js文件
<i class="cw-arror"></i> 添加 v-bind:class="{'up': isShowPop}"
<i class="cw-arrow" v-bind:class="{'up': isShowPop}"></i>
至此完成了點擊基本框顯示和隱藏選項列表的功能。
5. 點擊本組件以外的范圍,隱藏選項列表盒子(如果它正顯示着)
怎么實現?在body上添加一個點擊事件?組件內部是無法操縱調用它的父級頁面的,也不是無法操縱,記得vue文檔上說過,最好不要這么干。 但現在沒有別的辦法,且先如此嘗試一下。
(1)先在組件methods里定義一個專門用來隱藏的方法hidePop
hidePop: function () { this.isShowPop = false; }
(2)再在組件created鈎子函數里面給body綁定click事件,調用hidePop方法,並排除組件自身的范圍
created: function () { // 點全局范圍收起下拉框 var that = this; $('body').click(function (e) { console.log(e); if (e.target.className=='cw-input-select_wrap' || $(e.target).parents('.cw-input-select_wrap').length>0) { return; } that.hidePop(); }); },
這里注意,因為頁面上內容少,body的高度也很小,所以點擊頁面空白處是不會觸發body上的click事件的。此時,需要給body設置高度。至此demo.html完整代碼如下:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script> <script src="https://cdn.bootcss.com/element-ui/2.10.1/index.js"></script> <!-- 引入cw-input-select組件 --> <link rel="stylesheet" href="component/cw-input-select.css"> <script src="component/cw-input-select.js"></script> <style> body { height: 100vh; } </style> </head> <body> <div id="app"> <div style="display: inline-block;"> <cw-input-select></cw-input-select> </div> <div style="display: inline-block;"> <cw-input-select></cw-input-select> </div> </div> <script> new Vue({ el: '#app' }) </script> </body> </html>
這一需求基本完成,但是又有個小問題,點擊另一個組件的時候,其他組件的選項列表盒子是不會隱藏的。因為所有的組件都滿足 e.target.className=='cw-input-select_wrap' ,如何區別讓當前點擊的這個組件?
思來想去,我也沒有更好的辦法,最簡單直接的辦法就是在父頁面調用組件的時候傳入一個唯一識別——componentId,當然我試過在組件內部用13位時間戳生成唯一componentId,結果就是多個組件可能生成重復的componentId,這是為什么?說明vue組件生成的時間比時間戳還快。
(3)組件props選項,添加componentId接口
props: ['componentId'],
(4)在組件最外層容器上添加 v-bind:id="componentId"
<div v-bind:id="componentId" class="cw-input-select_wrap" v-bind:class="{'open': isShowPop}" onselectstart="return false">
(5)在demo.html 組件標簽上添加 component-id屬性傳入不一樣的值
<div id="app"> <div style="display: inline-block;"> <cw-input-select component-id="cw-input-select-1"></cw-input-select> </div> <div style="display: inline-block;"> <cw-input-select component-id="cw-input-select-2"></cw-input-select> </div> <div style="display: inline-block;"> <cw-input-select component-id="cw-input-select-3"></cw-input-select> </div> </div>
注意:相信你們很多人都知道,就是props里面寫的駝峰名稱,在標簽里面要使用-拼接式,比如componentId,對應component-id. 我當初就掉進過這個坑。
為了測試效果,我們放3個組件
至此,我們可以區分組件,並且在組件內部可以拿到component-id進行操作。
(6)回到組件$('body').click()事件里面,修改如下:
created: function () { // 點全局范圍收起下拉框 var that = this; $('body').click(function (e) { if (e.target.id==that.componentId || $(e.target).parents('#' + that.componentId).length>0) { return; } that.hidePop(); }); },
注意:click function里面一定要用that代替this,否則,呵呵
完美實現了點擊組件以外范圍隱藏選項列表盒子
外部的事情解決了,現在完善組件內部的功能。
6.模擬下拉框選中效果
(1)在組件data選項里添加一個變量selectedValue來保存選中值
component/cw-input-select.js data 選項:
data: function () { return { optionsList: ['選項1', '選項2', '選項3'], isShowPop: false, selectedValue: '', // 選中值 } },
(2)在基本框的span里渲染selectedValue,有值則顯示值,無值則顯示“請選擇”
component/cw-input-select.js template選項:
<div class="cw-input-select_box" v-on:click="selectHandle"> <span>{{selectedValue || '請選擇'}}</span> <i class="cw-arrow" v-bind:class="{'up': isShowPop}"></i> </div>
(3)現在,在選項li上添加點擊事件,來修改選中值
component/cw-input-select.js template選項:
<ul class="cw-input-select_options"> <li v-on:click="selected('不限')"> <span>不限</span> </li> <li v-for="option in optionsList" v-on:click="selected(option)"> {{option}} </li> </ul>
(4)在組件methods選項 添加selected方法
// 點擊選項 selected: function (val) { this.selectedValue = val; this.isShowPop = false; }
選中值功能完成。
到此為止,這個模擬的下拉框只適用於簡單的選項,但我們工作中經常遇到對象數組的選項。
7. 兼容由對象組成的數組選項列表
(1)在組件data選項里的添加一個由對象組成的數組optionsList2(optionsList和optionsList2都是臨時的,后面會講動態獲取選項列表)
data: function () { return { optionsList: ['選項1', '選項2', '選項3'], optionsList2: [{ id: 1, name: '選項A', },{ id: 2, name: '選項B', },{ id: 3, name: '選項C', }], isShowPop: false, selectedValue: '', // 選中值 } },
(2)在選項li里面添加v-if判斷
<li v-for="option in optionsList2" v-on:click="selected(option)"> <span v-if="typeof option == 'object'">{{option.name}}</span> <span v-else>{{option}}</span> </li>
現在你手動修改v-for里面的數據源,無論是optionsList還是optionsList2都能正確地渲染選項名稱。
但是點擊選中值好像有點奇怪,基本框里直接顯示了一個對象,這是正常的,接下來我們需要修改selected方法,讓它兼容兩種數據
(3)修改selected方法
// 點擊選項 selected: function (val) { var value; if (typeof val == 'object') { // 對象數據 value = val.name; } else { // 簡單數據 value = val; } this.selectedValue = value; this.isShowPop = false; }
這段代碼可以用三元表達式寫得更簡潔
// 點擊選項 selected: function (val) { this.selectedValue = typeof val == 'object' ? val.name : val; this.isShowPop = false; }
數據本身並不屬於組件,所以需要把數據分離出來。
8. 動態傳入選項列表
(1)在根目錄下新建一個data.js ,創建幾個有代表性的數組
data.js代碼如下:

const MUSICALS = ['鋼琴', '吉他', '小提琴', '架子鼓'] const BOOKS = [ { bookId: '1222ssw', name: '海邊的卡夫卡' }, { bookId: '998435j', name: '樹上的男爵' }, { bookId: 'iwihsn222', name: '挪威的森林' }, { bookId: '2231ff', name: '1Q84' } ] const PLANTS = [ { id: 001, ename: 'Sinocrassula' }, { id: 002, ename: 'Orostachys' }, { id: 003, ename: 'Hylotelephium' }, { id: 004, ename: 'Phedimus' }, { id: 005, ename: 'Sempervivum' }, { id: 006, ename: 'Monanthes' }, { id: 007, ename: 'Aeonium' }, { id: 008, ename: 'Dudleya' } ]
(2)在demo.html 引入data.js
<script src="data.js"></script>
(3)在demo.html <script> Vue實例data選項里添加3個變量,引用data.js里面的數據
<script> new Vue({ el: '#app', data () { return { musicals: MUSICALS, // 樂器 books: BOOKS, // 書 plants: PLANTS, // 植物 } }, }) </script>
(4)現在需要改寫cw-input-select組件,讓它能夠接收外來的數據作為選項
首先在組件props選項里面添加 'options'
component/cw-input-select.js
props: ['componentId', 'options'],
在組件created鈎子函數里給optionsList賦值
created: function () { this.optionsList = JSON.parse(JSON.stringify(this.options)); // 點全局范圍收起下拉框 var that = this; $('body').click(function (e) { if (e.target.id==that.componentId || $(e.target).parents('#' + that.componentId).length>0) { return; } that.hidePop(); }); },
組件data選項修改如下:
data: function () { return { optionsList: [], isShowPop: false, selectedValue: '', // 選中值 searchTxt: '', // 搜索詞 } },
刪掉臨時數據源optionsList2
(5)在demo.html 組件標簽上通過options傳入數據
<div id="app"> <div style="display: inline-block;"> <cw-input-select component-id="cw-input-select-1" v-bind:options="musicals"></cw-input-select> </div> <div style="display: inline-block;"> <cw-input-select component-id="cw-input-select-2" v-bind:options="books"></cw-input-select> </div> <div style="display: inline-block;"> <cw-input-select component-id="cw-input-select-3" v-bind:options="plants"></cw-input-select> </div> </div>
現在瀏覽一下效果,應該是實現了。但是第三個下拉框選項列表沒有名稱,是空白的。這是因為我們把對象選項渲染的名稱寫死了,選項名稱必須是name,否則就不顯示。這可不行。我們需要適應不同的名稱。
9. 適應不同的選項名稱
(1)在組件props添加'labelName'
props: ['componentId', 'options', 'labelName'],
(2)在demo.html 需要的組件標簽上添加屬性 label-name
像第一個組件上傳入的是簡單的數據,就不需要label-name
<div id="app"> <div style="display: inline-block;"> <cw-input-select component-id="cw-input-select-1" v-bind:options="musicals"></cw-input-select> </div> <div style="display: inline-block;"> <cw-input-select component-id="cw-input-select-2" v-bind:options="books" label-name="name"></cw-input-select> </div> <div style="display: inline-block;"> <cw-input-select component-id="cw-input-select-3" v-bind:options="plants" label-name="ename"></cw-input-select> </div> </div>
(3)修改組件寫死的name,有2處,
一個是選項li里面,
<li v-for="option in optionsList" v-on:click="selected(option)"> <span v-if="typeof option == 'object'">{{option[labelName]}}</span> <span v-else>{{option}}</span> </li>
一個是selected方法里面
// 點擊選項 selected: function (val) { this.selectedValue = typeof val == 'object' ? val[this.labelName] : val; this.isShowPop = false; },
現在第三個下拉框的選項也顯示了,不過內容超出了...
10. 選項列表滾動條
(1)給cw-input-select_options添加overflow-y: scroll (或auto,就是不能hidden)
component/cw-input-select.css .cw-input-select_options
/* 選項列表內容 */ .cw-input-select_options { display: block; margin-top: 26px; max-height: 234px; overflow-y: scroll; /* 超出滾動 */ }
滾動是有了,不過內容沒超出的也有一個難看的滾動條背景。接下來就寫美化滾動條。
(2)美化滾動條
component/cw-input-select.css 添加如下代碼:
/*自定義滾動條樣式*/ .cw-input-select_options::-webkit-scrollbar { /*滾動條整體樣式*/ width: 6px; /*高寬分別對應橫豎滾動條的尺寸*/ height: 0; } .cw-input-select_options::-webkit-scrollbar-thumb { /*滾動條里面小方塊*/ border-radius: 6px; background-color: rgba(144,147,153,0.3); transition: background-color 0.3s; } .cw-input-select_options::-webkit-scrollbar-track { /*滾動條里面軌道*/ border-radius: 6px; background: transparent; }
這是一段可通用的滾動條美化代碼
現在選項列表滾動條好看多了。但是我發現element-ui里面的下拉框組件只有鼠標移上去才顯示滾動條,如何做到這個效果?
把.cw-input-select_options::-webkit-scrollbar-thumb里面的背景顏色alpha值改成0,hover的時候改成0.3
.cw-input-select_options::-webkit-scrollbar-thumb { /*滾動條里面小方塊*/ border-radius: 6px; background-color: rgba(144,147,153,0); /* 重點在這里!! */ transition: background-color 0.3s; }
.cw-input-select_options:hover::-webkit-scrollbar-thumb { background-color: rgba(144,147,153,0.3); }
這就是為什么我用rgba作background-color的值,用其他的值實現不了這個效果的。
好了,今天就寫到這里吧。馬上要下班了。沒想到一個不算太難的組件教程我寫了一整天。並且還沒寫完。我都開始懷疑有沒有寫的必要,到底是寫給誰看呢?是不是該提高下寫教程的效率?
11. 選項搜索
不同的數據源,選項搜索方式也不同。主要有兩種,一種是傳入固定的數組作為選項數據源,一種是請求接口動態獲取數據。
我自己工作中用到的是后者,在這里為了方便演示,我用前者。
(1)給組件選項搜索框添加input事件
<input type="text" v-model="searchTxt" v-on:input="searchHandle(searchTxt)" class="cw-input-select_ipt" placeholder="搜索" />
(2)在組件methods選項里添加searchHandle方法
// 選項搜索 searchHandle (val) { // 深拷貝一份源數據 var originList = JSON.parse(JSON.stringify(this.options)); // filter過濾函數 this.optionsList = originList.filter((item, index) => { // 根據選項類型給名稱賦值 var content = typeof item == 'object' ? item[this.labelName] : item; return content.indexOf(val) > -1; }); }
這里要注意,如果不用箭頭函數,函數體內的this不代表外面的this.
此時,如果輸入空格,也一樣會執行搜索,解決辦法有2個,一是在input標簽v-model上加.trim修飾符
二是在searchHandle方法里面過濾空格。
12.搜索清除
(1)先寫布局和樣式
這里清除圖標將和輸入框作為一個整體,所以先在輸入框外面包裹一層div
<div class="cw-input-select_ipt_wrap"> <input type="text" v-model="searchTxt" v-on:input="searchHandle(searchTxt)" class="cw-input-select_ipt" placeholder="搜索" /> <span class="icon-clear"> <svg t="1575258400555" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2468" width="16" height="16"><path d="M509.866667 32C245.333333 32 32 247.466667 32 512s213.333333 480 477.866667 480S987.733333 776.533333 987.733333 512 774.4 32 509.866667 32z m0 896C281.6 928 96 742.4 96 512S281.6 96 509.866667 96 923.733333 281.6 923.733333 512s-185.6 416-413.866666 416z" fill="#8a8a8a" p-id="2469"></path><path d="M693.333333 330.666667c-12.8-12.8-32-12.8-44.8 0L512 467.2l-136.533333-136.533333c-12.8-12.8-32-12.8-44.8 0-12.8 12.8-12.8 32 0 44.8l136.533333 136.533333-136.533333 136.533333c-12.8 12.8-12.8 32 0 44.8 6.4 6.4 14.933333 8.533333 23.466666 8.533334s17.066667-2.133333 23.466667-8.533334l136.533333-136.533333 136.533334 136.533333c6.4 6.4 14.933333 8.533333 23.466666 8.533334s17.066667-2.133333 23.466667-8.533334c12.8-12.8 12.8-32 0-44.8L556.8 512l136.533333-136.533333c12.8-12.8 12.8-32 0-44.8z" fill="#8a8a8a" p-id="2470"></path></svg> </span> </div>
圖標使用iconfont svg,缺點是代碼一大堆,優點是用法簡單,復制粘貼即可使用,樣式可控。對於這種演示demo非常適合。項目中一半用font awesome,引入成套的樣式組件等等……
添加樣式
cw-input-select.css
.cw-input-select_ipt_wrap { position: relative; } .icon-clear { color: #aaa; position: absolute; right: 18px; top: 9px; z-index: 99; cursor: pointer; }
(2)清除功能
清除圖標默認隱藏,搜索框有內容時才顯示。
<span class="icon-clear" v-if="searchTxt">...</span>
給清除圖標添加點擊事件綁定清除方法
<span class="icon-clear" v-if="searchTxt" v-on:click="clearHandle">...</span>
清除方法
cw-input-select.js methods選項:
// 清除搜索 clearHandle () { this.searchTxt = ''; // 深拷貝一份源數據 this.optionsList = JSON.parse(JSON.stringify(this.options)); }
此時清除是實現了,不過效果有點奇怪,清除的同時選項列表盒子也瞬間隱藏了,應該是body的點擊事件沒有過濾掉清除圖標。解決辦法就是在clearHandle添加阻止冒泡方法。
clearHandle方法修改如下:
// 清除搜索 clearHandle (e) { e.stopPropagation(); this.searchTxt = ''; // 深拷貝一份源數據 this.optionsList = JSON.parse(JSON.stringify(this.options)); }
這樣就完美了。
完整代碼
組件github地址
https://github.com/cathy1024/cw-ui/tree/master/cw-ui/cw-input-select
(完)