1. Source Code
https://github.com/imxiaoer/WeChatMiniSelect
2. Preview

Note: The screen recording quality is poor, so you might see ghosting artiafcts.
3. Component Code
3.1 select.wxml
<view class="select-box">
<view class="select-current" catchtap="toggleDropdown">
<text class="current-label">{{current.name}}</text>
</view>
<view class="option-list" wx:if="{{isOpen}}" catchtap="onOptionTap">
<text class="option"
data-id="{{defaultOption.id}}"
data-name="{{defaultOption.name}}">
{{defaultOption.name}}
</text>
<text class="option"
wx:for="{{transformedOptions}}"
wx:key="{{item.id}}"
data-id="{{item.id}}"
data-name="{{item.name}}">
{{item.name}}
</text>
</view>
</view>
Explanation: Using catchtap instead of bindtap prevents event bubbling. To close the dropdown when tapping outside, the parent page (index.wxml) binds a bindtap="close" handler on its outermost view. If bubbling were not stopped, the parent's close method would execute incorrectly.
3.2 select.js
Component({
properties: {
options: {
type: Array,
value: []
},
defaultOption: {
type: Object,
value: {
id: '000',
name: 'All Cities'
}
},
keyField: {
type: String,
value: 'id'
},
textField: {
type: String,
value: 'name'
}
},
data: {
transformedOptions: [],
isOpen: false,
current: {}
},
methods: {
onOptionTap(e) {
const dataset = e.target.dataset;
this.setData({
current: dataset,
isOpen: false
});
// Emit event to parent component with selected data
this.triggerEvent("change", { ...dataset });
},
toggleDropdown() {
this.setData({
isOpen: !this.data.isOpen
});
},
// Method to be called from parent to close the dropdown
closeDropdown() {
this.setData({
isOpen: false
});
}
},
lifetimes: {
attached() {
// Normalize properties: convert custom keys to internal 'id' and 'name' keys
let transformed = [];
if (this.data.keyField !== 'id' || this.data.textField !== 'name') {
for (let item of this.data.options) {
let { [this.data.keyField]: id, [this.data.textField]: name } = item;
transformed.push({ id, name });
}
} else {
transformed = this.data.options;
}
this.setData({
current: Object.assign({}, this.data.defaultOption),
transformedOptions: transformed
});
}
}
});
Explanation: The keyField and textField properties allow attribute name conversion. For example, if the original data is:
[
{ city_id: '001', city_name: 'Beijing' },
{ city_id: '002', city_name: 'Shanghai' },
{ city_id: '003', city_name: 'Shenzhen' }
]
But the component expects:
[
{ id: '001', name: 'Beijing' },
{ id: '002', name: 'Shanghai' },
{ id: '003', name: 'Shenzhen' }
]
Thus, we map city_id to id and city_name to name using keyField and textField.
3.3 select.json
{
"component": true,
"usingComponents": {}
}
3.4 select.wxss
.select-box {
position: relative;
width: 100%;
font-size: 30rpx;
}
.select-current {
position: relative;
width: 100%;
padding: 0 10rpx;
line-height: 70rpx;
border: 1rpx solid #ddd;
border-radius: 6rpx;
box-sizing: border-box;
}
.select-current::after {
position: absolute;
display: block;
right: 16rpx;
top: 30rpx;
content: '';
width: 0;
height: 0;
border: 10rpx solid transparent;
border-top: 10rpx solid #999;
}
.current-label {
display: block;
width: 85%;
height: 100%;
word-wrap: normal;
overflow: hidden;
}
.option-list {
position: absolute;
left: 0;
top: 76rpx;
width: 100%;
padding: 12rpx 20rpx 10rpx 20rpx;
border-radius: 6rpx;
box-sizing: border-box;
z-index: 99;
box-shadow: 0rpx 0rpx 1rpx 1rpx rgba(0, 0, 0, 0.2) inset;
background-color: #fff;
}
.option {
display: block;
width: 100%;
line-height: 70rpx;
border-bottom: 1rpx solid #eee;
}
.option:last-child {
border-bottom: none;
padding-bottom: 0;
}
4. Usage
index.wxml
<view class="container" bindtap="closeDropdown">
<view class="select-wrapper">
<select
id="citySelect"
options="{{cityData}}"
keyField="city_id"
textField="city_name"
bind:change="onCityChange"
/>
</view>
</view>
Note: The parent page's closeDropdown method will close the select when tapping outside the dropodwn.