来试试实现一个自己的下拉组件吧!

简介:在开发一个小程序时使用 taro + taro-ui 进行开发,开发过程中需要使用到 Dropdown 组件,但发现 taro-ui 中并没有已实现的具体组件可以使用,尝试自己来实现一个来使用,尽量能够覆盖到大多数场景。

首先,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>
)}

Item组件用来做写一些属性

tsx 复制代码
/**
 * @param key
 * @param title 标题
 * @param activeKey
 */
function Item(props: ItemProps) {
  return props.children
}

因为外部需要通过 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;
  }
}
相关推荐
GISer_Jing17 分钟前
Vue前端进阶面试题目(二)
前端·vue.js·面试
乐闻x35 分钟前
Pinia 实战教程:构建高效的 Vue 3 状态管理系统
前端·javascript·vue.js
weixin_431449681 小时前
web组态软件
前端·物联网·低代码·编辑器·组态
橘子味小白菜1 小时前
el-table的树形结构后端返回的id没有唯一键怎么办
前端·vue.js
前端Hardy1 小时前
HTML&CSS:比赛记分卡
前端·javascript·css·3d·html
疯狂的沙粒2 小时前
Vue项目开发 element-UI 前端实现 1到10排列选择的按钮
前端·vue.js·ui
刺客-Andy2 小时前
React第六节 组件属性prop的propTypes类型使用介绍
前端·javascript·react.js·typescript
Mr.Liu62 小时前
小程序24-滚动效果:scroll-view组件详解
前端·微信小程序·小程序
三金121383 小时前
局部使用Vue
前端·javascript·vue.js
LinXunFeng3 小时前
Flutter - 子部件任意位置观察滚动数据
前端·flutter·开源