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`.
|
* `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.
|
||||||
|
|
|
@ -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>),
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
||||||
|
@ -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}>
|
||||||
|
@ -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}>
|
||||||
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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)}))}
|
||||||
|
|
|
@ -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)} />}
|
||||||
|
|
|
@ -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)}))}
|
||||||
|
|
|
@ -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={
|
||||||
|
|
|
@ -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]}
|
||||||
|
|
|
@ -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={
|
||||||
|
@ -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}
|
||||||
|
|
Loading…
Reference in New Issue