简介:在开发一个小程序时使用 taro + taro-ui 进行开发,开发过程中需要使用到 Dropdown 组件,但发现 taro-ui 中并没有已实现的具体组件可以使用,尝试自己来实现一个来使用,尽量能够覆盖到大多数场景。
Dropdown应用场景分析
首先,dropdown应用需要覆盖到单选、多选,因此对于该组件的封装要尽可能的简单,对具体下拉框内容不能做限制,关闭下拉框最好能够暴露出来,以便能够使用。
然后又参考了antd-mobile组件库的下拉组件,觉得大致是满足我的需求了。接下来就看看具体如何来实现这个Dropdown组件呢。
具体实现参考了antd-mobile的Dropdown组件
使用方式
具体使用方式也按照antd-mobile来实现
tsx
// 一个demo示例
<Dropdown ref={ref}>
<Dropdown.Item title='标题1' key={1}>
<View>111</View>
</Dropdown.Item>
<Dropdown.Item title='标题2' key={2}>
<>
<View>222</View>
<Button onClick={() => ref.current.close()}>close dropdown</Button>
</>
</Dropdown.Item>
</Dropdown>
设置ref用来获取组件的下拉框暴露出来收起方法,方便使用之后Dropdown.Item内部组件使用。 Dropdown.Item是每个下拉框的内容,title是下拉框的标题,key是之后组件内部实现用到的东西,用来确认渲染哪一个下拉框内容。
具体实现
第一步
获取Dropdown组件内全部的子组件(Dropdown.Item),然后拿到每个子组件的title,设置下拉框组件的头部。如下图区域:
然后给具体的每个navItem设置点击选中事件,用来判断具体展示哪一个下拉框的内容,点击时设置对应的Dropdown.Item组件的key属性值即可,当点击值不等于当前展开的key时,切换展示,否则清除选中的key值。
具体代码实现如下:
tsx
const handleClick = (itemKey) => {
setActiveKey(itemKey !== activeKey ? itemKey : '')
}
const navs = React.Children.map(props.children, (child) => {
return (
<View
className={classnames(styles.navItem)}
onClick={() => {
handleClick(child.key)
}}
>
{child.props.title}
<View className={classnames('at-icon at-icon-chevron-down', styles.icon)}></View>
</View>
)
})
选中的下拉框显示
上面设置头部时,记录了具体点击的哪一个下拉框的key,因此就可以通过key来判断具体显示哪一个下拉框的内容,从组件的props中children属性进行遍历判断,也正好通过是否有key值存在来设置该下拉框是否展示,具体实现逻辑如下:
tsx
<View
className={classnames(styles.popup, {
[styles.show]: !!activeKey,
[styles.hide]: !activeKey,
})}
>
{props.children.map((item) => item.key == activeKey && item.props.children)}
</View>
设置下拉框底部蒙层
当有选中值时,即activeKey为真时,显示具体蒙层。
tsx
{activeKey && (
<View
className={styles.mask}
onClick={() => {
ref.current.close()
}}
catchMove
></View>
)}
下拉框 Dropdown.Item
Item组件用来做写一些属性
tsx
/**
* @param key
* @param title 标题
* @param activeKey
*/
function Item(props: ItemProps) {
return props.children
}
Item挂到forwardRef Dropdown组件上
因为外部需要通过 ref 拿到 Dropdown 暴露出来的 close 方法,所以 Dropdown 组件需要使用 forwardRef。
tsx
// forwardRef 组件增添属性 来自 antd-mobile
export function attachPropertiesToComponent<C, P extends Record<string, any>>(
component: C,
properties: P,
): C & P {
const ret = component as any
for (const key in properties) {
if (properties.hasOwnProperty(key)) {
ret[key] = properties[key]
}
}
return ret
}
export default attachPropertiesToComponent(DropDown, {
Item,
})
完整代码如下
tsx
import { View, Text } from '@tarojs/components'
import Taro from '@tarojs/taro'
import classnames from 'classnames'
import React, { useImperativeHandle, useState } from 'react'
import "taro-ui/dist/style/components/icon.scss";
import styles from './index.module.scss'
// forwardRef 组件增添属性 来自 antd-mobile
export function attachPropertiesToComponent<C, P extends Record<string, any>>(
component: C,
properties: P,
): C & P {
const ret = component as any
for (const key in properties) {
if (properties.hasOwnProperty(key)) {
ret[key] = properties[key]
}
}
return ret
}
interface ItemProps {
key: string | number
title: string | React.ReactElement
activeKey?: string | number
children: React.ReactElement
}
/**
* @param key
* @param title 标题
* @param activeKey
*/
function Item(props: ItemProps) {
return props.children
}
interface DropDownProps {
children: React.ReactElement[]
}
/**
* @param close 关闭下拉框事件
*/
const DropDown = React.forwardRef((props: DropDownProps, ref: any) => {
const [activeKey, setActiveKey] = useState('')
useImperativeHandle(ref, () => ({
close: () => {
setActiveKey('')
},
}))
const handleClick = (itemKey) => {
setActiveKey(itemKey !== activeKey ? itemKey : '')
}
const navs = React.Children.map(props.children, (child) => {
return (
<View
className={classnames(styles.navItem, {
[styles.open]: child.key == activeKey,
[styles.selected]: child.props.activeKey,
})}
onClick={() => {
handleClick(child.key)
}}
>
{child.props.title}
<View className={classnames('at-icon at-icon-chevron-down', styles.icon)}></View>
</View>
)
})
return (
<View className={classnames(styles.wrapper)}>
<View className={styles.nav}>{navs}</View>
<View
className={classnames(styles.popup, {
[styles.show]: !!activeKey,
[styles.hide]: !activeKey,
})}
>
{props.children.map((item) => item.key == activeKey && item.props.children)}
</View>
{activeKey && (
<View
className={styles.mask}
onClick={() => {
ref.current.close()
}}
catchMove
></View>
)}
</View>
)
})
export default attachPropertiesToComponent(DropDown, {
Item,
})
css如下:
scss
.wrapper {
position: relative;
background-color: #FFFFFF;
.nav {
position: relative;
z-index: 12;
display: flex;
align-items: center;
height: 80px;
box-shadow: inset 0px -1px 0px 0px #e7e7e7;
background: #FFFFFF;
.navItem {
flex: 1;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
white-space: nowrap;
color: #4F5153;
font-size: 28px;
font-weight: 400;
-webkit-tap-highlight-color: transparent;
cursor: pointer;
.icon {
margin-left: 20px;
font-size: 36px;
transition: all 0.3s;
}
}
.open {
color: #1890ff;
.icon {
transform: rotate(180deg);
}
}
.selected {
color: #1890ff;
}
}
.popup {
box-sizing: border-box;
z-index: 11;
position: absolute;
top: 50%;
width: 100%;
background-color: #FFFFFF;
max-height: 600px;
overflow-y: auto;
transition: all 0.3s ease-in;
&.show {
top: 100%;
}
&.hide {
visibility: hidden;
}
}
.mask {
position: absolute;
top: 100%;
width: 100%;
height: 100vh;
background-color: rgba($color: #000000, $alpha: 0.4);
z-index: 10;
}
}