自定義protal組件,使用dom-align來定位下拉框

業(yè)務(wù)場景:如下圖,要求input能輸入,能遠程搜索,出現(xiàn)下拉框,下拉框中的內(nèi)容高度自定義:既可以選中到input,又能點擊具體結(jié)果跳轉(zhuǎn)頁面。雖然ant的select能做的東西很多,但是還是達不到我想要的效果,那就自行動手

場景一下圖描述
企業(yè)微信截圖_16276264882801.png
場景二下圖描述
企業(yè)微信截圖_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)點擊其他區(qū)域要隱藏下拉框,點擊下拉區(qū)域和input不隱藏。使用起來要求跟使用Modal一樣方便

思路已定,我們來寫代碼

1. protals組件,實現(xiàn)在body下面掛載組件,組件銷毀也會從body移除。這是基礎(chǔ)部分,可以在此基礎(chǔ)上再創(chuàng)造各種在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. 創(chuàng)造定位組件alignRender,利用dom-align用來定位input和下拉框。該組件擁有顯示,隱藏的方法,利用visible來實現(xiàn)首次掛載,基礎(chǔ)樣式也直接拿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[]
  // 顯示掛載,首次設(shè)置為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()
  }
監(jiān)聽document,只有點擊下拉和input才不關(guān)閉
  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。到這里我們就實現(xiàn)了些業(yè)務(wù)的基石??梢詫崿F(xiàn)多中定位場景。

3. 使用組件AlignRender ,下面的業(yè)務(wù)需要各種參數(shù)的傳遞,我這里使用useContext實現(xiàn),所以下面使用了useMemo,具體可以看我的這篇文章優(yōu)化使用useContext + useReducer 做數(shù)據(jù)管理
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()
 // 切換國內(nèi)外
 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])
}

不僅僅是這個下拉組件,另有一個業(yè)務(wù)場景我也用到了該組件:點擊按鈕,按鈕下方出現(xiàn)日歷。
有興趣可以看我這邊日歷遇到的問題ant使用calender自定義頭部,區(qū)分onPanelChange onSelect 事件

以上就是思考解決問題的全部,文章很短,實現(xiàn)的過程確實一步步摸索得來,先寫成一坨,再逐步拆分優(yōu)化。這個AlignRender 的封裝實現(xiàn)在后面能做很多事情,比如自定義modal,tooptip等等,希望對你有幫助

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一、自定義原因 1.項目定制化需求,下拉框的樣式包括滾動條都需要同現(xiàn)有客戶端(QT qml開發(fā))版本保持一致;2....
    小魔女_b68a閱讀 363評論 0 1
  • 這次需求里面需要自定義下拉選擇框,所以自己用vue寫了個下拉選擇框。 開發(fā)過程中主要解決的是點擊其他空白地方,下拉...
    AAA前端閱讀 5,834評論 0 3
  • 最近接手了一個新項目,設(shè)計獅要求嚴格按照設(shè)計稿來,so,只能放棄UI框架,自己寫組件了,略苦逼。 平時用的最多的是...
    徐向博閱讀 2,506評論 0 0
  • 在處理表單的網(wǎng)頁里,原生的select標簽可以滿足功能,但是現(xiàn)在的產(chǎn)品一般對用戶體驗及設(shè)計要求比較高,比如下拉框后...
    菜菜___閱讀 3,449評論 0 7
  • 今天做h5遇到個問題,我發(fā)現(xiàn)h5原生的下拉框很不好用,而且更改樣式也不好=改。所以我索性自己想辦法自定義了一個下拉...
    為夢想而努力奮斗閱讀 10,638評論 0 1

友情鏈接更多精彩內(nèi)容