From a92ee4862f11b6f6a0d7cf55a5eee20978466ddc Mon Sep 17 00:00:00 2001 From: Dreamacro <305009791@qq.com> Date: Wed, 3 Jul 2019 13:49:41 +0800 Subject: [PATCH] Migration: Select to hooks component --- src/components/Select/index.tsx | 163 +++++++++++++------------------- 1 file changed, 66 insertions(+), 97 deletions(-) diff --git a/src/components/Select/index.tsx b/src/components/Select/index.tsx index aee1142..f670b4e 100644 --- a/src/components/Select/index.tsx +++ b/src/components/Select/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import React, { useRef, useLayoutEffect, useState, useMemo } from 'react' import classnames from 'classnames' import { Icon } from '@components' import { BaseComponentProps } from '@models' @@ -17,81 +17,59 @@ interface SelectProps extends BaseComponentProps { onSelect?: (value: OptionValue, e: React.MouseEvent) => void } -interface SelectState { - dropdownListStyles: React.CSSProperties - showDropDownList: boolean, - hasCreateDropList: boolean -} +export function Select (props: SelectProps) { + const { value, onSelect, children, className: cn, style } = props -export class Select extends React.Component { + const portalRef = useRef() + const attachmentRef = useRef() + const targetRef = useRef() - // portal container - $container: Element - // drop down list - $attachment = React.createRef() - - // target position element - $target = React.createRef() - - 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) + useLayoutEffect(() => { + document.addEventListener('click', handleGlobalClick, true) + return () => { + document.addEventListener('click', handleGlobalClick, true) + if (portalRef.current) { + document.body.removeChild(portalRef.current) + } } - document.removeEventListener('click', this.handleGlobalClick, true) - } + }, []) - shouldComponentUpdate (nextProps, nextState) { - if (nextProps.value === this.props.value && nextState.showDropDownList === this.state.showDropDownList) { - return false + const [showDropDownList, setShowDropDownList] = useState(false) + const [hasCreateDropList, setHasCreateDropList] = useState(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 = () => { - if (!this.state.hasCreateDropList) { - // 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 + function handleGlobalClick (e) { + const el = attachmentRef.current if (el && !el.contains(e.target)) { - this.setState({ showDropDownList: false }) + setShowDropDownList(false) } } - private calculateAttachmentPosition () { - const targetRectInfo = this.$target.current.getBoundingClientRect() - - return { - top: Math.floor(targetRectInfo.top) - 10, - left: Math.floor(targetRectInfo.left) - 10 + function handleShowDropList (e) { + if (!hasCreateDropList) { + if (!portalRef.current) { + // create container element + const container = document.createElement('div') + 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 = null React.Children.forEach(children, (child: React.ReactElement) => { @@ -101,13 +79,9 @@ export class Select extends React.Component { }) return matchChild - } + }, [value, children]) - private hookChildren = ( - children: React.ReactNode, - value: OptionValue, - onSelect: SelectProps['onSelect'] - ) => { + const hookedChildren = useMemo(() => { return React.Children.map(children, (child: React.ReactElement) => { if (!child.props || !child.type) { return child @@ -123,43 +97,40 @@ export class Select extends React.Component { return React.cloneElement(child, Object.assign({}, child.props, { onClick: (e: React.MouseEvent) => { onSelect(child.props.value, e) - this.setState({ showDropDownList: false }) + setShowDropDownList(false) rawOnClickEvent && rawOnClickEvent(e) }, className })) }) - } + }, [children, value, onSelect]) - render () { - const { value, onSelect, children, className: cn, style } = this.props - const { dropdownListStyles, showDropDownList, hasCreateDropList } = this.state - const matchChild = this.getSelectedOption(value, children) - const dropDownList = ( -
-
    - {this.hookChildren(children, value, onSelect)} -
-
- ) + const dropDownList = ( +
+
    + { hookedChildren } +
+
+ ) - return <> + return ( + <>
{matchChild && matchChild.props && matchChild.props.children}
- {hasCreateDropList && createPortal(dropDownList, this.$container)} + {hasCreateDropList && createPortal(dropDownList, portalRef.current)} - } + ) } interface OptionProps extends BaseComponentProps { @@ -169,13 +140,11 @@ interface OptionProps extends BaseComponentProps { onClick?: (e: React.MouseEvent) => void } -export class Option extends React.Component { - render () { - const { className: cn, style, key, disabled = false, children, onClick = () => {} } = this.props - const className = classnames('option', { disabled }, cn) +export const Option: React.SFC = props => { + const { className: cn, style, key, disabled = false, children, onClick = () => {} } = props + const className = classnames('option', { disabled }, cn) - return ( -
  • {children}
  • - ) - } + return ( +
  • {children}
  • + ) }