Add ListEmptyComponent prop
Summary: Hey there :) Please let me know if the name `ListEmptyComponent` should be changed. I also thought about `ListNoItemsComponent`. Or maybe `ListPlaceholderComponent`? - [x] Explain the **motivation** for making this change. - [x] Provide a **test plan** demonstrating that the code is solid. - [x] Match the **code formatting** of the rest of the codebase. - [x] Target the `master` branch, NOT a "stable" branch. In a FlatList, I wanted to show some placeholder when my data is empty (while keeping eventual Header/Footer/RefreshControl). A way around this issue would be to do something like adding a `ListHeaderComponent` that checks if the list is empty, like so: ```js ListHeaderComponent={() => (!data.length ? <Text style={styles.noDataText}>No data found</Text> : null)} ``` But I felt it was not easily readable as soon as you have an actual header. This PR adds a `ListEmptyComponent` that is rendered when the list is empty. I added tests for VirtualizedList, FlatList and SectionList and ran `yarn test -- -u`. I then checked that the snapshots changed like I wanted. I also tested this against one of my project, though I had to manually add my changes because the project is on RN 0.43. Here are the docs screenshots: - [VirtualizedList](https://cloud.githubusercontent.com/assets/82368/25566000/0ebf2b82-2dd2-11e7-8b80-d8c505f1f2d6.png) - [FlatList](https://cloud.githubusercontent.com/assets/82368/25566005/2842ab42-2dd2-11e7-81b4-32c74c2b4fc3.png) - [SectionList](https://cloud.githubusercontent.com/assets/82368/25566010/368aec1e-2dd2-11e7-9425-3bb5e5803513.png) Thanks for your work! Closes https://github.com/facebook/react-native/pull/13718 Differential Revision: D4993711 Pulled By: sahrens fbshipit-source-id: 055b40f709067071e40308bdf5a37cedaa223dc5
This commit is contained in:
parent
37f3ce1f2c
commit
264d67c424
|
@ -72,6 +72,11 @@ type OptionalProps<ItemT> = {
|
|||
* `separators.updateProps`.
|
||||
*/
|
||||
ItemSeparatorComponent?: ?ReactClass<any>,
|
||||
/**
|
||||
* Rendered when the list is empty. Can be a React Component Class, a render function, or
|
||||
* a rendered element.
|
||||
*/
|
||||
ListEmptyComponent?: ?(ReactClass<any> | React.Element<any>),
|
||||
/**
|
||||
* Rendered at the bottom of all the items. Can be a React Component Class, a render function, or
|
||||
* a rendered element.
|
||||
|
|
|
@ -87,11 +87,18 @@ type OptionalProps<SectionT: SectionBase<any>> = {
|
|||
*/
|
||||
ItemSeparatorComponent?: ?ReactClass<any>,
|
||||
/**
|
||||
* Rendered at the very beginning of the list.
|
||||
* Rendered at the very beginning of the list. Can be a React Component Class, a render function, or
|
||||
* a rendered element.
|
||||
*/
|
||||
ListHeaderComponent?: ?(ReactClass<any> | React.Element<any>),
|
||||
/**
|
||||
* Rendered at the very end of the list.
|
||||
* Rendered when the list is empty. Can be a React Component Class, a render function, or
|
||||
* a rendered element.
|
||||
*/
|
||||
ListEmptyComponent?: ?(ReactClass<any> | React.Element<any>),
|
||||
/**
|
||||
* Rendered at the very end of the list. Can be a React Component Class, a render function, or
|
||||
* a rendered element.
|
||||
*/
|
||||
ListFooterComponent?: ?(ReactClass<any> | React.Element<any>),
|
||||
/**
|
||||
|
|
|
@ -82,6 +82,21 @@ type OptionalProps = {
|
|||
*/
|
||||
initialScrollIndex?: ?number,
|
||||
keyExtractor: (item: Item, index: number) => string,
|
||||
/**
|
||||
* Rendered when the list is empty. Can be a React Component Class, a render function, or
|
||||
* a rendered element.
|
||||
*/
|
||||
ListEmptyComponent?: ?(ReactClass<any> | React.Element<any>),
|
||||
/**
|
||||
* Rendered at the bottom of all the items. Can be a React Component Class, a render function, or
|
||||
* a rendered element.
|
||||
*/
|
||||
ListFooterComponent?: ?(ReactClass<any> | React.Element<any>),
|
||||
/**
|
||||
* Rendered at the top of all the items. Can be a React Component Class, a render function, or
|
||||
* a rendered element.
|
||||
*/
|
||||
ListHeaderComponent?: ?(ReactClass<any> | React.Element<any>),
|
||||
/**
|
||||
* The maximum number of items to render in each incremental render batch. The more rendered at
|
||||
* once, the better the fill rate, but responsiveness my suffer because rendering content may
|
||||
|
@ -394,14 +409,14 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const {ListFooterComponent, ListHeaderComponent} = this.props;
|
||||
const {ListEmptyComponent, ListFooterComponent, ListHeaderComponent} = this.props;
|
||||
const {data, disableVirtualization, horizontal} = this.props;
|
||||
const cells = [];
|
||||
const stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices);
|
||||
const stickyHeaderIndices = [];
|
||||
if (ListHeaderComponent) {
|
||||
const element = React.isValidElement(ListHeaderComponent)
|
||||
? ListHeaderComponent
|
||||
? ListHeaderComponent // $FlowFixMe
|
||||
: <ListHeaderComponent />;
|
||||
cells.push(
|
||||
<View key="$header" onLayout={this._onLayoutHeader}>
|
||||
|
@ -476,10 +491,19 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||
<View key="$tail_spacer" style={{[spacerKey]: tailSpacerLength}} />
|
||||
);
|
||||
}
|
||||
} else if (ListEmptyComponent) {
|
||||
const element = React.isValidElement(ListEmptyComponent)
|
||||
? ListEmptyComponent // $FlowFixMe
|
||||
: <ListEmptyComponent />;
|
||||
cells.push(
|
||||
<View key="$empty" onLayout={this._onLayoutEmpty}>
|
||||
{element}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
if (ListFooterComponent) {
|
||||
const element = React.isValidElement(ListFooterComponent)
|
||||
? ListFooterComponent
|
||||
? ListFooterComponent // $FlowFixMe
|
||||
: <ListFooterComponent />;
|
||||
cells.push(
|
||||
<View key="$footer" onLayout={this._onLayoutFooter}>
|
||||
|
@ -585,6 +609,10 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||
this._maybeCallOnEndReached();
|
||||
};
|
||||
|
||||
_onLayoutEmpty = (e) => {
|
||||
this.props.onLayout && this.props.onLayout(e);
|
||||
};
|
||||
|
||||
_onLayoutFooter = (e) => {
|
||||
this._footerLength = this._selectLength(e.nativeEvent.layout);
|
||||
};
|
||||
|
|
|
@ -48,6 +48,7 @@ describe('FlatList', () => {
|
|||
const component = ReactTestRenderer.create(
|
||||
<FlatList
|
||||
ItemSeparatorComponent={() => <separator />}
|
||||
ListEmptyComponent={() => <empty />}
|
||||
ListFooterComponent={() => <footer />}
|
||||
ListHeaderComponent={() => <header />}
|
||||
data={new Array(5).fill().map((_, ii) => ({id: String(ii)}))}
|
||||
|
|
|
@ -40,6 +40,7 @@ describe('SectionList', () => {
|
|||
const component = ReactTestRenderer.create(
|
||||
<SectionList
|
||||
ItemSeparatorComponent={(props) => <defaultItemSeparator v={propStr(props)} />}
|
||||
ListEmptyComponent={(props) => <empty v={propStr(props)} />}
|
||||
ListFooterComponent={(props) => <footer v={propStr(props)} />}
|
||||
ListHeaderComponent={(props) => <header v={propStr(props)} />}
|
||||
SectionSeparatorComponent={(props) => <sectionSeparator v={propStr(props)} />}
|
||||
|
|
|
@ -54,10 +54,39 @@ describe('VirtualizedList', () => {
|
|||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders empty list with empty component', () => {
|
||||
const component = ReactTestRenderer.create(
|
||||
<VirtualizedList
|
||||
data={[]}
|
||||
ListEmptyComponent={() => <empty />}
|
||||
ListFooterComponent={() => <footer />}
|
||||
ListHeaderComponent={() => <header />}
|
||||
getItem={(data, index) => data[index]}
|
||||
getItemCount={(data) => data.length}
|
||||
renderItem={({item}) => <item value={item.key} />}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders list with empty component', () => {
|
||||
const component = ReactTestRenderer.create(
|
||||
<VirtualizedList
|
||||
data={[{key: 'hello'}]}
|
||||
ListEmptyComponent={() => <empty />}
|
||||
getItem={(data, index) => data[index]}
|
||||
getItemCount={(data) => data.length}
|
||||
renderItem={({item}) => <item value={item.key} />}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders all the bells and whistles', () => {
|
||||
const component = ReactTestRenderer.create(
|
||||
<VirtualizedList
|
||||
ItemSeparatorComponent={() => <separator />}
|
||||
ListEmptyComponent={() => <empty />}
|
||||
ListFooterComponent={() => <footer />}
|
||||
ListHeaderComponent={() => <header />}
|
||||
data={new Array(5).fill().map((_, ii) => ({id: String(ii)}))}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
exports[`FlatList renders all the bells and whistles 1`] = `
|
||||
<RCTScrollView
|
||||
ItemSeparatorComponent={[Function]}
|
||||
ListEmptyComponent={[Function]}
|
||||
ListFooterComponent={[Function]}
|
||||
ListHeaderComponent={[Function]}
|
||||
data={
|
||||
|
|
|
@ -86,6 +86,7 @@ exports[`SectionList rendering empty section headers is fine 1`] = `
|
|||
exports[`SectionList renders all the bells and whistles 1`] = `
|
||||
<RCTScrollView
|
||||
ItemSeparatorComponent={undefined}
|
||||
ListEmptyComponent={[Function]}
|
||||
ListFooterComponent={[Function]}
|
||||
ListHeaderComponent={[Function]}
|
||||
SectionSeparatorComponent={[Function]}
|
||||
|
|
|
@ -241,6 +241,7 @@ exports[`VirtualizedList handles separators correctly 3`] = `
|
|||
exports[`VirtualizedList renders all the bells and whistles 1`] = `
|
||||
<RCTScrollView
|
||||
ItemSeparatorComponent={[Function]}
|
||||
ListEmptyComponent={[Function]}
|
||||
ListFooterComponent={[Function]}
|
||||
ListHeaderComponent={[Function]}
|
||||
data={
|
||||
|
@ -375,6 +376,96 @@ exports[`VirtualizedList renders empty list 1`] = `
|
|||
</RCTScrollView>
|
||||
`;
|
||||
|
||||
exports[`VirtualizedList renders empty list with empty component 1`] = `
|
||||
<RCTScrollView
|
||||
ListEmptyComponent={[Function]}
|
||||
ListFooterComponent={[Function]}
|
||||
ListHeaderComponent={[Function]}
|
||||
data={Array []}
|
||||
disableVirtualization={false}
|
||||
getItem={[Function]}
|
||||
getItemCount={[Function]}
|
||||
horizontal={false}
|
||||
initialNumToRender={10}
|
||||
keyExtractor={[Function]}
|
||||
maxToRenderPerBatch={10}
|
||||
onContentSizeChange={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
onLayout={[Function]}
|
||||
onMomentumScrollEnd={[Function]}
|
||||
onScroll={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={21}
|
||||
>
|
||||
<View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
>
|
||||
<header />
|
||||
</View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
>
|
||||
<empty />
|
||||
</View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
>
|
||||
<footer />
|
||||
</View>
|
||||
</View>
|
||||
</RCTScrollView>
|
||||
`;
|
||||
|
||||
exports[`VirtualizedList renders list with empty component 1`] = `
|
||||
<RCTScrollView
|
||||
ListEmptyComponent={[Function]}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"key": "hello",
|
||||
},
|
||||
]
|
||||
}
|
||||
disableVirtualization={false}
|
||||
getItem={[Function]}
|
||||
getItemCount={[Function]}
|
||||
horizontal={false}
|
||||
initialNumToRender={10}
|
||||
keyExtractor={[Function]}
|
||||
maxToRenderPerBatch={10}
|
||||
onContentSizeChange={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
onLayout={[Function]}
|
||||
onMomentumScrollEnd={[Function]}
|
||||
onScroll={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={21}
|
||||
>
|
||||
<View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
>
|
||||
<item
|
||||
value="hello"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</RCTScrollView>
|
||||
`;
|
||||
|
||||
exports[`VirtualizedList renders null list 1`] = `
|
||||
<RCTScrollView
|
||||
data={undefined}
|
||||
|
|
Loading…
Reference in New Issue