From 9ff7f31dfefe9c12294009765f87ee1229f2bda0 Mon Sep 17 00:00:00 2001 From: Tom Xu Date: Sun, 26 Apr 2020 21:23:25 +0800 Subject: [PATCH] refactor(list): rewrite with hook (#23542) * refactor(list): rewrite with hook * fix lint * fix lint * fix lint * fix Empty style dep Co-authored-by: afc163 --- .depslintrc.js | 4 + components/list/Item.tsx | 208 ++++++++++++------------ components/list/index.tsx | 323 ++++++++++++++++++-------------------- 3 files changed, 254 insertions(+), 281 deletions(-) diff --git a/.depslintrc.js b/.depslintrc.js index 7daa46f78b..e763e9d5ed 100644 --- a/.depslintrc.js +++ b/.depslintrc.js @@ -10,6 +10,10 @@ module.exports = { '**/*.json', ], modulePattern: [ + { + pattern: /ConfigContext.*renderEmpty/ms, + module: '../empty', + }, { pattern: /ConfigConsumer.*renderEmpty/ms, module: '../empty', diff --git a/components/list/Item.tsx b/components/list/Item.tsx index 0edce23ce8..006466150e 100644 --- a/components/list/Item.tsx +++ b/components/list/Item.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; import classNames from 'classnames'; -import { ListGridType, ColumnType } from './index'; +import { ListGridType, ColumnType, ListContext } from './index'; import { Col } from '../grid'; -import { ConfigConsumer, ConfigConsumerProps } from '../config-provider'; +import { ConfigContext } from '../config-provider'; import { cloneElement } from '../_util/reactNode'; export interface ListItemProps extends React.HTMLAttributes { @@ -26,54 +26,48 @@ export interface ListItemMetaProps { title?: React.ReactNode; } -export const Meta = (props: ListItemMetaProps) => ( - - {({ getPrefixCls }: ConfigConsumerProps) => { - const { - prefixCls: customizePrefixCls, - className, - avatar, - title, - description, - ...others - } = props; +export const Meta: React.FC = ({ + prefixCls: customizePrefixCls, + className, + avatar, + title, + description, + ...others +}) => { + const { getPrefixCls } = React.useContext(ConfigContext); - const prefixCls = getPrefixCls('list', customizePrefixCls); - const classString = classNames(`${prefixCls}-item-meta`, className); + const prefixCls = getPrefixCls('list', customizePrefixCls); + const classString = classNames(`${prefixCls}-item-meta`, className); - const content = ( -
- {title &&

{title}

} - {description &&
{description}
} -
- ); + const content = ( +
+ {title &&

{title}

} + {description &&
{description}
} +
+ ); - return ( -
- {avatar &&
{avatar}
} - {(title || description) && content} -
- ); - }} -
-); + return ( +
+ {avatar &&
{avatar}
} + {(title || description) && content} +
+ ); +}; function getGrid(grid: ListGridType, t: ColumnType) { return grid[t] && Math.floor(24 / grid[t]!); } -export default class Item extends React.Component { - static Meta: typeof Meta = Meta; +export interface ListItemTypeProps extends React.FC { + Meta: typeof Meta; +} - static contextTypes = { - grid: PropTypes.any, - itemLayout: PropTypes.string, - }; +const Item: ListItemTypeProps = props => { + const { grid, itemLayout } = React.useContext(ListContext); + const { getPrefixCls } = React.useContext(ConfigContext); - context: any; - - isItemContainsTextNodeAndNotSingular() { - const { children } = this.props; + const isItemContainsTextNodeAndNotSingular = () => { + const { children } = props; let result; React.Children.forEach(children, (element: React.ReactElement) => { if (typeof element === 'string') { @@ -81,79 +75,73 @@ export default class Item extends React.Component { } }); return result && React.Children.count(children) > 1; - } + }; - isFlexMode() { - const { extra } = this.props; - const { itemLayout } = this.context; + const isFlexMode = () => { + const { extra } = props; if (itemLayout === 'vertical') { return !!extra; } - return !this.isItemContainsTextNodeAndNotSingular(); - } - - renderItem = ({ getPrefixCls }: ConfigConsumerProps) => { - const { grid, itemLayout } = this.context; - const { - prefixCls: customizePrefixCls, - children, - actions, - extra, - className, - ...others - } = this.props; - const prefixCls = getPrefixCls('list', customizePrefixCls); - const actionsContent = actions && actions.length > 0 && ( -
    - {actions.map((action: React.ReactNode, i: number) => ( - // eslint-disable-next-line react/no-array-index-key -
  • - {action} - {i !== actions.length - 1 && } -
  • - ))} -
- ); - const Tag = grid ? 'div' : 'li'; - const itemChildren = ( - - {itemLayout === 'vertical' && extra - ? [ -
- {children} - {actionsContent} -
, -
- {extra} -
, - ] - : [children, actionsContent, cloneElement(extra, { key: 'extra' })]} -
- ); - - return grid ? ( - - {itemChildren} - - ) : ( - itemChildren - ); + return !isItemContainsTextNodeAndNotSingular(); }; - render() { - return {this.renderItem}; - } -} + const { prefixCls: customizePrefixCls, children, actions, extra, className, ...others } = props; + const prefixCls = getPrefixCls('list', customizePrefixCls); + const actionsContent = actions && actions.length > 0 && ( +
    + {actions.map((action: React.ReactNode, i: number) => ( + // eslint-disable-next-line react/no-array-index-key +
  • + {action} + {i !== actions.length - 1 && } +
  • + ))} +
+ ); + const Tag = grid ? 'div' : 'li'; + const itemChildren = ( + + {itemLayout === 'vertical' && extra + ? [ +
+ {children} + {actionsContent} +
, +
+ {extra} +
, + ] + : [children, actionsContent, cloneElement(extra, { key: 'extra' })]} +
+ ); + + return grid ? ( + + {itemChildren} + + ) : ( + itemChildren + ); +}; + +Item.Meta = Meta; + +Item.contextTypes = { + grid: PropTypes.any, + itemLayout: PropTypes.string, +}; + +export default Item; diff --git a/components/list/index.tsx b/components/list/index.tsx index 60e09e48fc..879c368294 100644 --- a/components/list/index.tsx +++ b/components/list/index.tsx @@ -1,9 +1,8 @@ import * as React from 'react'; -import * as PropTypes from 'prop-types'; import classNames from 'classnames'; import omit from 'omit.js'; import Spin, { SpinProps } from '../spin'; -import { ConfigConsumer, ConfigConsumerProps, RenderEmptyHandler } from '../config-provider'; +import { RenderEmptyHandler, ConfigContext } from '../config-provider'; import Pagination, { PaginationConfig } from '../pagination'; import { Row } from '../grid'; @@ -58,73 +57,49 @@ export interface ListLocale { emptyText: React.ReactNode | (() => React.ReactNode); } -interface ListState { - paginationCurrent: number; - paginationSize: number; +export interface ListConsumerProps { + grid?: any; + itemLayout?: string; } -export default class List extends React.Component, ListState> { - static Item: typeof Item = Item; +export const ListContext = React.createContext({}); - static childContextTypes = { - grid: PropTypes.any, - itemLayout: PropTypes.string, - }; +export const ListConsumer = ListContext.Consumer; - static defaultProps = { - dataSource: [], - bordered: false, - split: true, - loading: false, - pagination: false as ListProps['pagination'], - }; +function List({ pagination, ...props }: ListProps) { + const paginationObj = pagination && typeof pagination === 'object' ? pagination : {}; - defaultPaginationProps = { + const [paginationCurrent, setPaginationCurrent] = React.useState( + paginationObj.defaultCurrent || 1, + ); + const [paginationSize, setPaginationSize] = React.useState(paginationObj.defaultPageSize || 10); + + const { getPrefixCls, renderEmpty, direction } = React.useContext(ConfigContext); + + const defaultPaginationProps = { current: 1, total: 0, }; - private keys: { [key: string]: string } = {}; + const keys: { [key: string]: string } = {}; - private onPaginationChange = this.triggerPaginationEvent('onChange'); - - private onPaginationShowSizeChange = this.triggerPaginationEvent('onShowSizeChange'); - - constructor(props: ListProps) { - super(props); - - const { pagination } = props; - const paginationObj = pagination && typeof pagination === 'object' ? pagination : {}; - - this.state = { - paginationCurrent: paginationObj.defaultCurrent || 1, - paginationSize: paginationObj.defaultPageSize || 10, - }; - } - - getChildContext() { - return { - grid: this.props.grid, - itemLayout: this.props.itemLayout, - }; - } - - triggerPaginationEvent(eventName: string) { + const triggerPaginationEvent = (eventName: string) => { return (page: number, pageSize: number) => { - const { pagination } = this.props; - this.setState({ - paginationCurrent: page, - paginationSize: pageSize, - }); + setPaginationCurrent(page); + setPaginationSize(pageSize); if (pagination && (pagination as any)[eventName]) { (pagination as any)[eventName](page, pageSize); } }; - } + }; - renderItem = (item: any, index: number) => { - const { renderItem, rowKey } = this.props; - if (!renderItem) return null; + const onPaginationChange = triggerPaginationEvent('onChange'); + + const onPaginationShowSizeChange = triggerPaginationEvent('onShowSizeChange'); + + const renderItem = (item: any, index: number) => { + const { rowKey } = props; + if (!props.renderItem) return null; let key; @@ -140,138 +115,136 @@ export default class List extends React.Component, ListState> { key = `list-item-${index}`; } - this.keys[index] = key; + keys[index] = key; - return renderItem(item, index); + return props.renderItem(item, index); }; - isSomethingAfterLastItem() { - const { loadMore, pagination, footer } = this.props; + const isSomethingAfterLastItem = () => { + const { loadMore, footer } = props; return !!(loadMore || pagination || footer); - } + }; - renderEmpty = (prefixCls: string, renderEmpty: RenderEmptyHandler) => { - const { locale } = this.props; + const renderEmptyFunc = (prefixCls: string, renderEmptyHandler: RenderEmptyHandler) => { + const { locale } = props; return (
- {(locale && locale.emptyText) || renderEmpty('List')} + {(locale && locale.emptyText) || renderEmptyHandler('List')}
); }; - renderList = ({ getPrefixCls, renderEmpty, direction }: ConfigConsumerProps) => { - const { paginationCurrent, paginationSize } = this.state; - const { - prefixCls: customizePrefixCls, - bordered, - split, - className, - children, - itemLayout, - loadMore, - pagination, - grid, - dataSource = [], - size, - header, - footer, - loading, - ...rest - } = this.props; + const { + prefixCls: customizePrefixCls, + bordered, + split, + className, + children, + itemLayout, + loadMore, + grid, + dataSource = [], + size, + header, + footer, + loading, + ...rest + } = props; - const prefixCls = getPrefixCls('list', customizePrefixCls); - let loadingProp = loading; - if (typeof loadingProp === 'boolean') { - loadingProp = { - spinning: loadingProp, - }; + const prefixCls = getPrefixCls('list', customizePrefixCls); + let loadingProp = loading; + if (typeof loadingProp === 'boolean') { + loadingProp = { + spinning: loadingProp, + }; + } + const isLoading = loadingProp && loadingProp.spinning; + + // large => lg + // small => sm + let sizeCls = ''; + switch (size) { + case 'large': + sizeCls = 'lg'; + break; + case 'small': + sizeCls = 'sm'; + break; + default: + break; + } + + const classString = classNames(prefixCls, className, { + [`${prefixCls}-vertical`]: itemLayout === 'vertical', + [`${prefixCls}-${sizeCls}`]: sizeCls, + [`${prefixCls}-split`]: split, + [`${prefixCls}-bordered`]: bordered, + [`${prefixCls}-loading`]: isLoading, + [`${prefixCls}-grid`]: grid, + [`${prefixCls}-something-after-last-item`]: isSomethingAfterLastItem(), + [`${prefixCls}-rtl`]: direction === 'rtl', + }); + + const paginationProps = { + ...defaultPaginationProps, + total: dataSource.length, + current: paginationCurrent, + pageSize: paginationSize, + ...(pagination || {}), + }; + + const largestPage = Math.ceil(paginationProps.total / paginationProps.pageSize); + if (paginationProps.current > largestPage) { + paginationProps.current = largestPage; + } + const paginationContent = pagination ? ( +
+ +
+ ) : null; + + let splitDataSource = [...dataSource]; + if (pagination) { + if (dataSource.length > (paginationProps.current - 1) * paginationProps.pageSize) { + splitDataSource = [...dataSource].splice( + (paginationProps.current - 1) * paginationProps.pageSize, + paginationProps.pageSize, + ); } - const isLoading = loadingProp && loadingProp.spinning; + } - // large => lg - // small => sm - let sizeCls = ''; - switch (size) { - case 'large': - sizeCls = 'lg'; - break; - case 'small': - sizeCls = 'sm'; - break; - default: - break; - } + let childrenContent; + childrenContent = isLoading &&
; + if (splitDataSource.length > 0) { + const items = splitDataSource.map((item: any, index: number) => renderItem(item, index)); - const classString = classNames(prefixCls, className, { - [`${prefixCls}-vertical`]: itemLayout === 'vertical', - [`${prefixCls}-${sizeCls}`]: sizeCls, - [`${prefixCls}-split`]: split, - [`${prefixCls}-bordered`]: bordered, - [`${prefixCls}-loading`]: isLoading, - [`${prefixCls}-grid`]: grid, - [`${prefixCls}-something-after-last-item`]: this.isSomethingAfterLastItem(), - [`${prefixCls}-rtl`]: direction === 'rtl', + const childrenList: Array = []; + React.Children.forEach(items, (child: any, index) => { + childrenList.push( + React.cloneElement(child, { + key: keys[index], + }), + ); }); - const paginationProps = { - ...this.defaultPaginationProps, - total: dataSource.length, - current: paginationCurrent, - pageSize: paginationSize, - ...(pagination || {}), - }; + childrenContent = grid ? ( + {childrenList} + ) : ( +
    {childrenList}
+ ); + } else if (!children && !isLoading) { + childrenContent = renderEmptyFunc(prefixCls, renderEmpty); + } - const largestPage = Math.ceil(paginationProps.total / paginationProps.pageSize); - if (paginationProps.current > largestPage) { - paginationProps.current = largestPage; - } - const paginationContent = pagination ? ( -
- -
- ) : null; + const paginationPosition = paginationProps.position || 'bottom'; - let splitDataSource = [...dataSource]; - if (pagination) { - if (dataSource.length > (paginationProps.current - 1) * paginationProps.pageSize) { - splitDataSource = [...dataSource].splice( - (paginationProps.current - 1) * paginationProps.pageSize, - paginationProps.pageSize, - ); - } - } - - let childrenContent; - childrenContent = isLoading &&
; - if (splitDataSource.length > 0) { - const items = splitDataSource.map((item: any, index: number) => this.renderItem(item, index)); - - const childrenList: Array = []; - React.Children.forEach(items, (child: any, index) => { - childrenList.push( - React.cloneElement(child, { - key: this.keys[index], - }), - ); - }); - - childrenContent = grid ? ( - {childrenList} - ) : ( -
    {childrenList}
- ); - } else if (!children && !isLoading) { - childrenContent = this.renderEmpty(prefixCls, renderEmpty); - } - - const paginationPosition = paginationProps.position || 'bottom'; - - return ( + return ( +
{(paginationPosition === 'top' || paginationPosition === 'both') && paginationContent} {header &&
{header}
} @@ -283,10 +256,18 @@ export default class List extends React.Component, ListState> { {loadMore || ((paginationPosition === 'bottom' || paginationPosition === 'both') && paginationContent)}
- ); - }; - - render() { - return {this.renderList}; - } +
+ ); } + +List.defaultProps = { + dataSource: [], + bordered: false, + split: true, + loading: false, + pagination: false as ListProps['pagination'], +}; + +List.Item = Item; + +export default List;