Migration: Select to hooks component

This commit is contained in:
Dreamacro 2019-07-03 13:49:41 +08:00
parent 724cd44d69
commit a92ee4862f

View File

@ -1,4 +1,4 @@
import * as React from 'react' import React, { useRef, useLayoutEffect, useState, useMemo } from 'react'
import classnames from 'classnames' import classnames from 'classnames'
import { Icon } from '@components' import { Icon } from '@components'
import { BaseComponentProps } from '@models' import { BaseComponentProps } from '@models'
@ -17,81 +17,59 @@ interface SelectProps extends BaseComponentProps {
onSelect?: (value: OptionValue, e: React.MouseEvent<HTMLLIElement>) => void onSelect?: (value: OptionValue, e: React.MouseEvent<HTMLLIElement>) => void
} }
interface SelectState { export function Select (props: SelectProps) {
dropdownListStyles: React.CSSProperties const { value, onSelect, children, className: cn, style } = props
showDropDownList: boolean,
hasCreateDropList: boolean
}
export class Select extends React.Component<SelectProps, SelectState> { const portalRef = useRef<HTMLDivElement>()
const attachmentRef = useRef<HTMLDivElement>()
const targetRef = useRef<HTMLDivElement>()
// portal container
$container: Element
// drop down list useLayoutEffect(() => {
$attachment = React.createRef<HTMLDivElement>() document.addEventListener('click', handleGlobalClick, true)
return () => {
// target position element document.addEventListener('click', handleGlobalClick, true)
$target = React.createRef<HTMLDivElement>() if (portalRef.current) {
document.body.removeChild(portalRef.current)
state = { }
dropdownListStyles: {},
showDropDownList: false,
hasCreateDropList: false
}
componentDidMount () {
document.addEventListener('click', this.handleGlobalClick, true)
this.setState({ dropdownListStyles: this.calculateAttachmentPosition() })
}
componentWillUnmount () {
if (this.state.hasCreateDropList) {
document.body.removeChild(this.$container)
} }
document.removeEventListener('click', this.handleGlobalClick, true) }, [])
}
shouldComponentUpdate (nextProps, nextState) { const [showDropDownList, setShowDropDownList] = useState(false)
if (nextProps.value === this.props.value && nextState.showDropDownList === this.state.showDropDownList) { const [hasCreateDropList, setHasCreateDropList] = useState(false)
return false const dropdownListStyles = useMemo(() => {
if (targetRef.current) {
const targetRectInfo = targetRef.current.getBoundingClientRect()
return {
top: Math.floor(targetRectInfo.top) - 10,
left: Math.floor(targetRectInfo.left) - 10
}
} }
return true return {}
} }, [])
handleShowDropList = () => { function handleGlobalClick (e) {
if (!this.state.hasCreateDropList) { const el = attachmentRef.current
// create container element
const container = document.createElement('div')
document.body.appendChild(container)
this.$container = container
this.setState({
hasCreateDropList: true
})
}
this.setState({
showDropDownList: true
})
}
private handleGlobalClick = (e) => {
const el = this.$attachment.current
if (el && !el.contains(e.target)) { if (el && !el.contains(e.target)) {
this.setState({ showDropDownList: false }) setShowDropDownList(false)
} }
} }
private calculateAttachmentPosition () { function handleShowDropList (e) {
const targetRectInfo = this.$target.current.getBoundingClientRect() if (!hasCreateDropList) {
if (!portalRef.current) {
return { // create container element
top: Math.floor(targetRectInfo.top) - 10, const container = document.createElement('div')
left: Math.floor(targetRectInfo.left) - 10 document.body.appendChild(container)
portalRef.current = container
}
setHasCreateDropList(true)
} }
setShowDropDownList(true)
} }
private getSelectedOption = (value: OptionValue, children: React.ReactNode) => { const matchChild = useMemo(() => {
let matchChild: React.ReactElement<any> = null let matchChild: React.ReactElement<any> = null
React.Children.forEach(children, (child: React.ReactElement<any>) => { React.Children.forEach(children, (child: React.ReactElement<any>) => {
@ -101,13 +79,9 @@ export class Select extends React.Component<SelectProps, SelectState> {
}) })
return matchChild return matchChild
} }, [value, children])
private hookChildren = ( const hookedChildren = useMemo(() => {
children: React.ReactNode,
value: OptionValue,
onSelect: SelectProps['onSelect']
) => {
return React.Children.map(children, (child: React.ReactElement<any>) => { return React.Children.map(children, (child: React.ReactElement<any>) => {
if (!child.props || !child.type) { if (!child.props || !child.type) {
return child return child
@ -123,43 +97,40 @@ export class Select extends React.Component<SelectProps, SelectState> {
return React.cloneElement(child, Object.assign({}, child.props, { return React.cloneElement(child, Object.assign({}, child.props, {
onClick: (e: React.MouseEvent<HTMLLIElement>) => { onClick: (e: React.MouseEvent<HTMLLIElement>) => {
onSelect(child.props.value, e) onSelect(child.props.value, e)
this.setState({ showDropDownList: false }) setShowDropDownList(false)
rawOnClickEvent && rawOnClickEvent(e) rawOnClickEvent && rawOnClickEvent(e)
}, },
className className
})) }))
}) })
} }, [children, value, onSelect])
render () { const dropDownList = (
const { value, onSelect, children, className: cn, style } = this.props <div
const { dropdownListStyles, showDropDownList, hasCreateDropList } = this.state className={classnames('select-list', { 'select-list-show': showDropDownList })}
const matchChild = this.getSelectedOption(value, children) ref={attachmentRef}
const dropDownList = ( style={dropdownListStyles}
<div >
className={classnames('select-list', { 'select-list-show': showDropDownList })} <ul className="list">
ref={this.$attachment} { hookedChildren }
style={dropdownListStyles} </ul>
> </div>
<ul className="list"> )
{this.hookChildren(children, value, onSelect)}
</ul>
</div>
)
return <> return (
<>
<div <div
className={classnames('select', cn)} className={classnames('select', cn)}
style={style} style={style}
ref={this.$target} ref={targetRef}
onClick={this.handleShowDropList} onClick={handleShowDropList}
> >
{matchChild && matchChild.props && matchChild.props.children} {matchChild && matchChild.props && matchChild.props.children}
<Icon type="triangle-down" /> <Icon type="triangle-down" />
</div> </div>
{hasCreateDropList && createPortal(dropDownList, this.$container)} {hasCreateDropList && createPortal(dropDownList, portalRef.current)}
</> </>
} )
} }
interface OptionProps extends BaseComponentProps { interface OptionProps extends BaseComponentProps {
@ -169,13 +140,11 @@ interface OptionProps extends BaseComponentProps {
onClick?: (e: React.MouseEvent<HTMLLIElement>) => void onClick?: (e: React.MouseEvent<HTMLLIElement>) => void
} }
export class Option extends React.Component<OptionProps, {}> { export const Option: React.SFC<OptionProps> = props => {
render () { const { className: cn, style, key, disabled = false, children, onClick = () => {} } = props
const { className: cn, style, key, disabled = false, children, onClick = () => {} } = this.props const className = classnames('option', { disabled }, cn)
const className = classnames('option', { disabled }, cn)
return ( return (
<li className={className} style={style} key={key} onClick={onClick}>{children}</li> <li className={className} style={style} key={key} onClick={onClick}>{children}</li>
) )
}
} }