美文网首页
自定义protal组件,使用dom-align来定位下拉框

自定义protal组件,使用dom-align来定位下拉框

作者: 云鹤道人张业斌 | 来源:发表于2021-07-29 14:54 被阅读0次

    业务场景:如下图,要求input能输入,能远程搜索,出现下拉框,下拉框中的内容高度自定义:既可以选中到input,又能点击具体结果跳转页面。虽然ant的select能做的东西很多,但是还是达不到我想要的效果,那就自行动手

    场景一下图描述
    企业微信截图_16276264882801.png
    场景二下图描述
    企业微信截图_1627626415326.png

    解决思路:

    (1) 仔细观察ant的select组件并模仿:选中input利用createPortal 在body下面生成dom,也就是一个下拉组件
    (2)利用dom-align定位到input下面:ant Select基于rc-select,rc-select的下拉框基于rc-trigger,rc-trigger的下拉框定位基于第三方库dom-align。
    (3)点击其他区域要隐藏下拉框,点击下拉区域和input不隐藏。使用起来要求跟使用Modal一样方便

    思路已定,我们来写代码

    1. protals组件,实现在body下面挂载组件,组件销毁也会从body移除。这是基础部分,可以在此基础上再创造各种在body下的组件
    import { useLayoutEffect, useRef } from 'react'
    import { createPortal } from 'react-dom'
    
    type ProtalsProps = {
     children: any
    }
    
    const Protals = ({ children }: ProtalsProps) => {
     const containerRef = useRef<HTMLDivElement | null>(null)
    
     if (!containerRef.current) {
       containerRef.current = document.createElement('div')
       document.body.appendChild(containerRef.current)
     }
    
     useLayoutEffect(() => {
       return () => {
         const node = containerRef.current
         if (node) {
           document.body.removeChild(node)
         }
       }
     }, [])
     return createPortal(children, containerRef.current)
    }
    
    export default Protals
    
    
    2. 创造定位组件alignRender,利用dom-align用来定位input和下拉框。该组件拥有显示,隐藏的方法,利用visible来实现首次挂载,基础样式也直接拿select的样式,嘿嘿
    // alignRender.tsx
    import { FC, useEffect, useImperativeHandle, useRef } from 'react'
    import Protals from '../protals'
    import domAlign from 'dom-align'
    
    type AlignRenderProps = {
      children: any
      targetNode: any
      childRef?: any
      points?: any[]
      offset?: any[]
      // 显示挂载,首次设置为true即可
      visible: boolean
    }
    
    const AlignRender: FC<AlignRenderProps> = ({
      children,
      visible,
      targetNode,
      childRef,
      points = ['tl', 'bl'],
      offset = ['0', '0'],
    }) => {
      const sourceNode: any = useRef<HTMLDivElement | null>(null)
      const open = () => {
        domAlign(sourceNode.current, targetNode.current, {
          points,
          offset,
        })
      }
      const close = () => {
        domAlign(sourceNode.current, targetNode.current, {
          points,
          offset: ['-1000%', '0'],
        })
      }
      // 暴露隐藏和显示的方法
      useImperativeHandle(childRef, () => ({
        open,
        close,
      }))
      function clickCallback(e: Event) {
        if (
          sourceNode.current.contains(e.target) ||
          targetNode.current.contains(e.target)
        ) {
          return
        }
    
        close()
      }
    监听document,只有点击下拉和input才不关闭
      useEffect(() => {
        if (visible) {
          open()
          document.addEventListener('click', clickCallback, false)
          return () => {
            document.removeEventListener('click', clickCallback, false)
          }
        }
      }, [visible])
      if (!visible) return null
    
      return (
        <Protals>
          <div
            ref={sourceNode}
            style={{ padding: '0' }}
            className={'ant-select-dropdown'}
          >
            {children}
          </div>
        </Protals>
      )
    }
    
    export default AlignRender
    
    

    ok。到这里我们就实现了些业务的基石。可以实现多中定位场景。

    3. 使用组件AlignRender ,下面的业务需要各种参数的传递,我这里使用useContext实现,所以下面使用了useMemo,具体可以看我的这篇文章优化使用useContext + useReducer 做数据管理
    const SearchSelect: FC = () => {
     const [visible, setVisible] = useState<boolean>(false)
     const { type, keyWords } = useBarState()
     const dispatch = useDispatchBarState()
     const [value, setValue] = useState<string>('')
     const targetNode: any = useRef<HTMLDivElement | null>(null)
     const childRef: any = useRef()
     // 切换国内外
     useEffect(() => {
       if (keyWords && visible) {
         childRef.current.open()
       }
     }, [type])
    
     return useMemo(() => {
       const changeInput = ({ target }: any) => {
         childRef.current.open()
         setValue(target.value)
         // 如果更改词汇,则去掉城市id
         dispatch &&
           dispatch({
             keyWords: target.value,
             cityId: '',
             stateId: '',
             countryId: '',
           })
       }
       const AlignProp = {
         visible,
         targetNode,
         childRef,
       }
       return (
         <div className={'relative inline-block'} ref={targetNode}>
           <Input
             className={styles.input}
             bordered={false}
             value={value}
             placeholder="城市/酒店名称"
             onChange={changeInput}
             onFocus={() => {
               if (visible) {
                 childRef.current.open() // 再次点击
               } else {
                 setVisible(true) //visible置为true,首次挂载到body
               }
             }}
           />
           <AlignRender {...AlignProp}>
             <div className={classnames(value ? 'hidden' : 'block')}>
               <DistrictTab {...districtProps} />
             </div>
             <div className={classnames(value ? 'block' : 'hidden')}>
               <Hotels {...districtProps} />
             </div>
           </AlignRender>
         </div>
       )
     }, [keyWords, visible, value])
    }
    

    不仅仅是这个下拉组件,另有一个业务场景我也用到了该组件:点击按钮,按钮下方出现日历。
    有兴趣可以看我这边日历遇到的问题ant使用calender自定义头部,区分onPanelChange onSelect 事件

    以上就是思考解决问题的全部,文章很短,实现的过程确实一步步摸索得来,先写成一坨,再逐步拆分优化。这个AlignRender 的封装实现在后面能做很多事情,比如自定义modal,tooptip等等,希望对你有帮助

    相关文章

      网友评论

          本文标题:自定义protal组件,使用dom-align来定位下拉框

          本文链接:https://www.haomeiwen.com/subject/wftcvltx.html