Permalink
Browse files

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 #13718

Differential Revision: D4993711

Pulled By: sahrens

fbshipit-source-id: 055b40f709067071e40308bdf5a37cedaa223dc5
  • Loading branch information...
Minishlink authored and facebook-github-bot committed May 4, 2017
1 parent 37f3ce1 commit 264d67c424841660874bcf11ad686aea57fe7a44
@@ -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}

4 comments on commit 264d67c

@gregblass

This comment has been minimized.

Show comment
Hide comment
@gregblass

gregblass May 23, 2017

It doesn't look like its possible to have the empty component render text in the middle of the screen. It looks like the list's height is being dynamically adjusted, so using flexbox within it doesn't achieve the desired effect.

gregblass replied May 23, 2017

It doesn't look like its possible to have the empty component render text in the middle of the screen. It looks like the list's height is being dynamically adjusted, so using flexbox within it doesn't achieve the desired effect.

@gregblass

This comment has been minimized.

Show comment
Hide comment
@gregblass

gregblass May 23, 2017

Might not be working on a section list either? Does the underlying component need to be updated too?

gregblass replied May 23, 2017

Might not be working on a section list either? Does the underlying component need to be updated too?

@vonovak

This comment has been minimized.

Show comment
Hide comment
@vonovak

vonovak Nov 23, 2017

Contributor

@Minishlink I'm having the same issue as @gregblass, does that use case work for you?

Contributor

vonovak replied Nov 23, 2017

@Minishlink I'm having the same issue as @gregblass, does that use case work for you?

@Minishlink

This comment has been minimized.

Show comment
Hide comment
@Minishlink

Minishlink Nov 23, 2017

Contributor

@vonovak @gregblass I didn't test and thought about this use case indeed, so it might not work! I'll try to take a look by next week, please remind me

Contributor

Minishlink replied Nov 23, 2017

@vonovak @gregblass I didn't test and thought about this use case indeed, so it might not work! I'll try to take a look by next week, please remind me

Please sign in to comment.