Skip to content

Commit

Permalink
Add ListEmptyComponent prop
Browse files Browse the repository at this point in the history
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 264d67c
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 5 deletions.
5 changes: 5 additions & 0 deletions Libraries/Lists/FlatList.js
Expand Up @@ -72,6 +72,11 @@ type OptionalProps<ItemT> = {
* `separators.updateProps`. * `separators.updateProps`.
*/ */
ItemSeparatorComponent?: ?ReactClass<any>, 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 * Rendered at the bottom of all the items. Can be a React Component Class, a render function, or
* a rendered element. * a rendered element.
Expand Down
11 changes: 9 additions & 2 deletions Libraries/Lists/SectionList.js
Expand Up @@ -87,11 +87,18 @@ type OptionalProps<SectionT: SectionBase<any>> = {
*/ */
ItemSeparatorComponent?: ?ReactClass<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>), 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>), ListFooterComponent?: ?(ReactClass<any> | React.Element<any>),
/** /**
Expand Down
34 changes: 31 additions & 3 deletions Libraries/Lists/VirtualizedList.js
Expand Up @@ -82,6 +82,21 @@ type OptionalProps = {
*/ */
initialScrollIndex?: ?number, initialScrollIndex?: ?number,
keyExtractor: (item: Item, index: number) => string, 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 * 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 * once, the better the fill rate, but responsiveness my suffer because rendering content may
Expand Down Expand Up @@ -394,14 +409,14 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
}; };


render() { render() {
const {ListFooterComponent, ListHeaderComponent} = this.props; const {ListEmptyComponent, ListFooterComponent, ListHeaderComponent} = this.props;
const {data, disableVirtualization, horizontal} = this.props; const {data, disableVirtualization, horizontal} = this.props;
const cells = []; const cells = [];
const stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices); const stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices);
const stickyHeaderIndices = []; const stickyHeaderIndices = [];
if (ListHeaderComponent) { if (ListHeaderComponent) {
const element = React.isValidElement(ListHeaderComponent) const element = React.isValidElement(ListHeaderComponent)
? ListHeaderComponent ? ListHeaderComponent // $FlowFixMe
: <ListHeaderComponent />; : <ListHeaderComponent />;
cells.push( cells.push(
<View key="$header" onLayout={this._onLayoutHeader}> <View key="$header" onLayout={this._onLayoutHeader}>
Expand Down Expand Up @@ -476,10 +491,19 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
<View key="$tail_spacer" style={{[spacerKey]: tailSpacerLength}} /> <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) { if (ListFooterComponent) {
const element = React.isValidElement(ListFooterComponent) const element = React.isValidElement(ListFooterComponent)
? ListFooterComponent ? ListFooterComponent // $FlowFixMe
: <ListFooterComponent />; : <ListFooterComponent />;
cells.push( cells.push(
<View key="$footer" onLayout={this._onLayoutFooter}> <View key="$footer" onLayout={this._onLayoutFooter}>
Expand Down Expand Up @@ -585,6 +609,10 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
this._maybeCallOnEndReached(); this._maybeCallOnEndReached();
}; };


_onLayoutEmpty = (e) => {
this.props.onLayout && this.props.onLayout(e);
};

_onLayoutFooter = (e) => { _onLayoutFooter = (e) => {
this._footerLength = this._selectLength(e.nativeEvent.layout); this._footerLength = this._selectLength(e.nativeEvent.layout);
}; };
Expand Down
1 change: 1 addition & 0 deletions Libraries/Lists/__tests__/FlatList-test.js
Expand Up @@ -48,6 +48,7 @@ describe('FlatList', () => {
const component = ReactTestRenderer.create( const component = ReactTestRenderer.create(
<FlatList <FlatList
ItemSeparatorComponent={() => <separator />} ItemSeparatorComponent={() => <separator />}
ListEmptyComponent={() => <empty />}
ListFooterComponent={() => <footer />} ListFooterComponent={() => <footer />}
ListHeaderComponent={() => <header />} ListHeaderComponent={() => <header />}
data={new Array(5).fill().map((_, ii) => ({id: String(ii)}))} data={new Array(5).fill().map((_, ii) => ({id: String(ii)}))}
Expand Down
1 change: 1 addition & 0 deletions Libraries/Lists/__tests__/SectionList-test.js
Expand Up @@ -40,6 +40,7 @@ describe('SectionList', () => {
const component = ReactTestRenderer.create( const component = ReactTestRenderer.create(
<SectionList <SectionList
ItemSeparatorComponent={(props) => <defaultItemSeparator v={propStr(props)} />} ItemSeparatorComponent={(props) => <defaultItemSeparator v={propStr(props)} />}
ListEmptyComponent={(props) => <empty v={propStr(props)} />}
ListFooterComponent={(props) => <footer v={propStr(props)} />} ListFooterComponent={(props) => <footer v={propStr(props)} />}
ListHeaderComponent={(props) => <header v={propStr(props)} />} ListHeaderComponent={(props) => <header v={propStr(props)} />}
SectionSeparatorComponent={(props) => <sectionSeparator v={propStr(props)} />} SectionSeparatorComponent={(props) => <sectionSeparator v={propStr(props)} />}
Expand Down
29 changes: 29 additions & 0 deletions Libraries/Lists/__tests__/VirtualizedList-test.js
Expand Up @@ -54,10 +54,39 @@ describe('VirtualizedList', () => {
expect(component).toMatchSnapshot(); 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', () => { it('renders all the bells and whistles', () => {
const component = ReactTestRenderer.create( const component = ReactTestRenderer.create(
<VirtualizedList <VirtualizedList
ItemSeparatorComponent={() => <separator />} ItemSeparatorComponent={() => <separator />}
ListEmptyComponent={() => <empty />}
ListFooterComponent={() => <footer />} ListFooterComponent={() => <footer />}
ListHeaderComponent={() => <header />} ListHeaderComponent={() => <header />}
data={new Array(5).fill().map((_, ii) => ({id: String(ii)}))} data={new Array(5).fill().map((_, ii) => ({id: String(ii)}))}
Expand Down
Expand Up @@ -3,6 +3,7 @@
exports[`FlatList renders all the bells and whistles 1`] = ` exports[`FlatList renders all the bells and whistles 1`] = `
<RCTScrollView <RCTScrollView
ItemSeparatorComponent={[Function]} ItemSeparatorComponent={[Function]}
ListEmptyComponent={[Function]}
ListFooterComponent={[Function]} ListFooterComponent={[Function]}
ListHeaderComponent={[Function]} ListHeaderComponent={[Function]}
data={ data={
Expand Down
Expand Up @@ -86,6 +86,7 @@ exports[`SectionList rendering empty section headers is fine 1`] = `
exports[`SectionList renders all the bells and whistles 1`] = ` exports[`SectionList renders all the bells and whistles 1`] = `
<RCTScrollView <RCTScrollView
ItemSeparatorComponent={undefined} ItemSeparatorComponent={undefined}
ListEmptyComponent={[Function]}
ListFooterComponent={[Function]} ListFooterComponent={[Function]}
ListHeaderComponent={[Function]} ListHeaderComponent={[Function]}
SectionSeparatorComponent={[Function]} SectionSeparatorComponent={[Function]}
Expand Down
Expand Up @@ -241,6 +241,7 @@ exports[`VirtualizedList handles separators correctly 3`] = `
exports[`VirtualizedList renders all the bells and whistles 1`] = ` exports[`VirtualizedList renders all the bells and whistles 1`] = `
<RCTScrollView <RCTScrollView
ItemSeparatorComponent={[Function]} ItemSeparatorComponent={[Function]}
ListEmptyComponent={[Function]}
ListFooterComponent={[Function]} ListFooterComponent={[Function]}
ListHeaderComponent={[Function]} ListHeaderComponent={[Function]}
data={ data={
Expand Down Expand Up @@ -375,6 +376,96 @@ exports[`VirtualizedList renders empty list 1`] = `
</RCTScrollView> </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`] = ` exports[`VirtualizedList renders null list 1`] = `
<RCTScrollView <RCTScrollView
data={undefined} data={undefined}
Expand Down

4 comments on commit 264d67c

@gregblass
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@vonovak
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@Minishlink
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.