diff --git a/src/__tests__/FixedSizeGrid.js b/src/__tests__/FixedSizeGrid.js
index 3d7bf90e..fffdeaf5 100644
--- a/src/__tests__/FixedSizeGrid.js
+++ b/src/__tests__/FixedSizeGrid.js
@@ -4,6 +4,7 @@ import ReactTestRenderer from 'react-test-renderer';
import ReactTestUtils from 'react-dom/test-utils';
import { FixedSizeGrid } from '..';
import * as domHelpers from '../domHelpers';
+import { defaultCellRangeRenderer } from '../createGridComponent';
const findScrollContainer = rendered => rendered.root.children[0].children[0];
@@ -1085,4 +1086,52 @@ describe('FixedSizeGrid', () => {
);
});
});
+
+ describe('cellRangeRenderer', () => {
+ it('should use a custom cellRangeRenderer if specified', () => {
+ const width = 90;
+ const height = 70;
+ const columnWidth = 20;
+ const rowHeight = 40;
+ const overscanRowsCount = 1;
+ const overscanColumnsCount = 1;
+
+ const expectedColumnStopIndex = Math.floor(
+ width / columnWidth + overscanColumnsCount
+ );
+
+ const expectedRowStopIndex = Math.floor(
+ height / rowHeight + overscanRowsCount
+ );
+
+ const CustomRow = props =>
;
+ const cellRangeRenderer = jest
+ .fn()
+ .mockImplementation(defaultCellRangeRenderer);
+
+ const rendered = ReactTestRenderer.create(
+
+ );
+
+ expect(cellRangeRenderer).toHaveBeenCalledTimes(1);
+ expect(cellRangeRenderer).toHaveBeenCalledWith({
+ columnStartIndex: 0,
+ rowStartIndex: 0,
+ columnStopIndex: expectedColumnStopIndex,
+ rowStopIndex: expectedRowStopIndex,
+ childFactory: expect.any(Function),
+ });
+ });
+ });
});
diff --git a/src/__tests__/FixedSizeList.js b/src/__tests__/FixedSizeList.js
index 371b8cdb..74503d37 100644
--- a/src/__tests__/FixedSizeList.js
+++ b/src/__tests__/FixedSizeList.js
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
import ReactTestRenderer from 'react-test-renderer';
import ReactTestUtils from 'react-dom/test-utils';
import { FixedSizeList } from '..';
+import { defaultRowRangeRenderer } from '../createListComponent';
const simulateScroll = (instance, scrollOffset, direction = 'vertical') => {
if (direction === 'horizontal') {
@@ -870,4 +871,38 @@ describe('FixedSizeList', () => {
);
});
});
+
+ describe('rowRangeRenderer', () => {
+ it('should use a custom rowRangeRenderer if specified', () => {
+ const height = 70;
+ const itemSize = 40;
+ const overscanCount = 1;
+
+ const expectedStopIndex = Math.floor(height / itemSize + overscanCount);
+
+ const CustomRow = props => ;
+ const rowRangeRenderer = jest
+ .fn()
+ .mockImplementation(defaultRowRangeRenderer);
+
+ const rendered = ReactTestRenderer.create(
+
+ );
+
+ expect(rowRangeRenderer).toHaveBeenCalledTimes(1);
+ expect(rowRangeRenderer).toHaveBeenCalledWith({
+ startIndex: 0,
+ stopIndex: expectedStopIndex,
+ childFactory: expect.any(Function),
+ });
+ });
+ });
});
diff --git a/src/__tests__/VariableSizeGrid.js b/src/__tests__/VariableSizeGrid.js
index 71065399..3a71b456 100644
--- a/src/__tests__/VariableSizeGrid.js
+++ b/src/__tests__/VariableSizeGrid.js
@@ -4,6 +4,7 @@ import { Simulate } from 'react-dom/test-utils';
import ReactTestRenderer from 'react-test-renderer';
import { VariableSizeGrid } from '..';
import * as domHelpers from '../domHelpers';
+import { defaultCellRangeRenderer } from '../createGridComponent';
const simulateScroll = (instance, { scrollLeft, scrollTop }) => {
instance._outerRef.scrollLeft = scrollLeft;
@@ -584,4 +585,52 @@ describe('VariableSizeGrid', () => {
expect(innerRef.current.style.height).toEqual('106px');
expect(innerRef.current.style.width).toEqual('101px');
});
+
+ describe('cellRangeRenderer', () => {
+ it('should use a custom cellRangeRenderer if specified', () => {
+ const width = 90;
+ const height = 70;
+ const columnWidth = _ => 20;
+ const rowHeight = _ => 40;
+ const overscanRowsCount = 1;
+ const overscanColumnsCount = 1;
+
+ const expectedColumnStopIndex = Math.floor(
+ width / columnWidth() + overscanColumnsCount
+ );
+
+ const expectedRowStopIndex = Math.floor(
+ height / rowHeight() + overscanRowsCount
+ );
+
+ const CustomRow = props => ;
+ const cellRangeRenderer = jest
+ .fn()
+ .mockImplementation(defaultCellRangeRenderer);
+
+ const rendered = ReactTestRenderer.create(
+
+ );
+
+ expect(cellRangeRenderer).toHaveBeenCalledTimes(1);
+ expect(cellRangeRenderer).toHaveBeenCalledWith({
+ columnStartIndex: 0,
+ rowStartIndex: 0,
+ columnStopIndex: expectedColumnStopIndex,
+ rowStopIndex: expectedRowStopIndex,
+ childFactory: expect.any(Function),
+ });
+ });
+ });
});
diff --git a/src/__tests__/VariableSizeList.js b/src/__tests__/VariableSizeList.js
index aeffad52..279f1caf 100644
--- a/src/__tests__/VariableSizeList.js
+++ b/src/__tests__/VariableSizeList.js
@@ -3,6 +3,7 @@ import { render } from 'react-dom';
import { Simulate } from 'react-dom/test-utils';
import ReactTestRenderer from 'react-test-renderer';
import { VariableSizeList } from '..';
+import { defaultRowRangeRenderer } from '../createListComponent';
const simulateScroll = (instance, scrollOffset, direction = 'vertical') => {
if (direction === 'horizontal') {
@@ -349,4 +350,38 @@ describe('VariableSizeList', () => {
instance.setState({ itemCount: 3 });
expect(innerRef.current.style.height).toEqual('78px');
});
+
+ describe('rowRangeRenderer', () => {
+ it('should use a custom rowRangeRenderer if specified', () => {
+ const height = 70;
+ const itemSize = _ => 40;
+ const overscanCount = 1;
+
+ const expectedStopIndex = Math.floor(height / itemSize() + overscanCount);
+
+ const CustomRow = props => ;
+ const rowRangeRenderer = jest
+ .fn()
+ .mockImplementation(defaultRowRangeRenderer);
+
+ const rendered = ReactTestRenderer.create(
+
+ );
+
+ expect(rowRangeRenderer).toHaveBeenCalledTimes(1);
+ expect(rowRangeRenderer).toHaveBeenCalledWith({
+ startIndex: 0,
+ stopIndex: expectedStopIndex,
+ childFactory: expect.any(Function),
+ });
+ });
+ });
});
diff --git a/src/createGridComponent.js b/src/createGridComponent.js
index 9d725821..b6c599ae 100644
--- a/src/createGridComponent.js
+++ b/src/createGridComponent.js
@@ -46,7 +46,23 @@ type OnScrollCallback = ({
type ScrollEvent = SyntheticEvent;
type ItemStyleCache = { [key: string]: Object };
+type CellRangeRendererParams = {|
+ columnStartIndex: number,
+ columnStopIndex: number,
+ rowStartIndex: number,
+ rowStopIndex: number,
+ childFactory: (params: {|
+ rowIndex: number,
+ columnIndex: number,
+ |}) => React$Element>,
+|};
+
+type CellRangeRenderer = (
+ params: CellRangeRendererParams
+) => React$Element>[];
+
export type Props = {|
+ cellRangeRenderer?: CellRangeRenderer,
children: RenderComponent,
className?: string,
columnCount: number,
@@ -127,6 +143,26 @@ const IS_SCROLLING_DEBOUNCE_INTERVAL = 150;
const defaultItemKey = ({ columnIndex, data, rowIndex }) =>
`${rowIndex}:${columnIndex}`;
+export function defaultCellRangeRenderer({
+ rowStartIndex,
+ rowStopIndex,
+ columnStartIndex,
+ columnStopIndex,
+ childFactory,
+}: CellRangeRendererParams): React$Element>[] {
+ const items = [];
+ for (let rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) {
+ for (
+ let columnIndex = columnStartIndex;
+ columnIndex <= columnStopIndex;
+ columnIndex++
+ ) {
+ items.push(childFactory({ rowIndex, columnIndex }));
+ }
+ }
+ return items;
+}
+
// In DEV mode, this Set helps us only log a warning once per component instance.
// This avoids spamming the console every time a render happens.
let devWarningsOverscanCount = null;
@@ -348,6 +384,7 @@ export default function createGridComponent({
render() {
const {
+ cellRangeRenderer = (defaultCellRangeRenderer: CellRangeRenderer),
children,
className,
columnCount,
@@ -373,30 +410,29 @@ export default function createGridComponent({
] = this._getHorizontalRangeToRender();
const [rowStartIndex, rowStopIndex] = this._getVerticalRangeToRender();
- const items = [];
+ const resolvedIsScrolling = useIsScrolling ? isScrolling : undefined;
+
+ const childFactory = ({ rowIndex, columnIndex }) => {
+ return createElement(children, {
+ columnIndex,
+ data: itemData,
+ isScrolling: resolvedIsScrolling,
+ key: itemKey({ columnIndex, data: itemData, rowIndex }),
+ rowIndex,
+ style: this._getItemStyle(rowIndex, columnIndex),
+ });
+ };
+
+ let items = [];
+
if (columnCount > 0 && rowCount) {
- for (
- let rowIndex = rowStartIndex;
- rowIndex <= rowStopIndex;
- rowIndex++
- ) {
- for (
- let columnIndex = columnStartIndex;
- columnIndex <= columnStopIndex;
- columnIndex++
- ) {
- items.push(
- createElement(children, {
- columnIndex,
- data: itemData,
- isScrolling: useIsScrolling ? isScrolling : undefined,
- key: itemKey({ columnIndex, data: itemData, rowIndex }),
- rowIndex,
- style: this._getItemStyle(rowIndex, columnIndex),
- })
- );
- }
- }
+ items = cellRangeRenderer({
+ rowStartIndex,
+ rowStopIndex,
+ columnStartIndex,
+ columnStopIndex,
+ childFactory,
+ });
}
// Read this value AFTER items have been created,
diff --git a/src/createListComponent.js b/src/createListComponent.js
index a8c9b004..b6439533 100644
--- a/src/createListComponent.js
+++ b/src/createListComponent.js
@@ -38,6 +38,18 @@ type onScrollCallback = ({
type ScrollEvent = SyntheticEvent;
type ItemStyleCache = { [index: number]: Object };
+type RowRangeRendererParams = {|
+ startIndex: number,
+ stopIndex: number,
+ childFactory: (params: {| index: number |}) => React$Element<
+ RenderComponent
+ >,
+|};
+
+type RowRangeRenderer = (
+ params: RowRangeRendererParams
+) => React$Element>[];
+
export type Props = {|
children: RenderComponent,
className?: string,
@@ -58,6 +70,7 @@ export type Props = {|
outerElementType?: React$ElementType,
outerTagName?: string, // deprecated
overscanCount: number,
+ rowRangeRenderer?: RowRangeRenderer,
style?: Object,
useIsScrolling: boolean,
width: number | string,
@@ -107,6 +120,23 @@ const IS_SCROLLING_DEBOUNCE_INTERVAL = 150;
const defaultItemKey = (index: number, data: any) => index;
+export function defaultRowRangeRenderer({
+ startIndex,
+ stopIndex,
+ childFactory,
+}: RowRangeRendererParams): React$Element>[] {
+ const items = [];
+ for (let index = startIndex; index <= stopIndex; index++) {
+ items.push(
+ childFactory({
+ index,
+ })
+ );
+ }
+
+ return items;
+}
+
// In DEV mode, this Set helps us only log a warning once per component instance.
// This avoids spamming the console every time a render happens.
let devWarningsDirection = null;
@@ -266,6 +296,7 @@ export default function createListComponent({
layout,
outerElementType,
outerTagName,
+ rowRangeRenderer = (defaultRowRangeRenderer: RowRangeRenderer),
style,
useIsScrolling,
width,
@@ -282,19 +313,26 @@ export default function createListComponent({
const [startIndex, stopIndex] = this._getRangeToRender();
- const items = [];
+ const resolvedIsScrolling = useIsScrolling ? isScrolling : undefined;
+
+ const childFactory = ({ index }) => {
+ return createElement(children, {
+ data: itemData,
+ key: itemKey(index, itemData),
+ index,
+ isScrolling: resolvedIsScrolling,
+ style: this._getItemStyle(index),
+ });
+ };
+
+ let items = [];
+
if (itemCount > 0) {
- for (let index = startIndex; index <= stopIndex; index++) {
- items.push(
- createElement(children, {
- data: itemData,
- key: itemKey(index, itemData),
- index,
- isScrolling: useIsScrolling ? isScrolling : undefined,
- style: this._getItemStyle(index),
- })
- );
- }
+ items = rowRangeRenderer({
+ startIndex,
+ stopIndex,
+ childFactory,
+ });
}
// Read this value AFTER items have been created,
diff --git a/website/src/code/FixedSizeGridCellRangeRenderer.js b/website/src/code/FixedSizeGridCellRangeRenderer.js
new file mode 100644
index 00000000..b6ff8b34
--- /dev/null
+++ b/website/src/code/FixedSizeGridCellRangeRenderer.js
@@ -0,0 +1,19 @@
+export function defaultCellRangeRenderer({
+ rowStartIndex,
+ rowStopIndex,
+ columnStartIndex,
+ columnStopIndex,
+ childFactory
+}: CellRangeRendererParams): React$Element>[] {
+ const items = [];
+ // rowStartIndex, rowStopIndex, columnStartIndex, and columnStopIndex are all integers
+ for (let rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) {
+ for (let columnIndex = columnStartIndex; columnIndex <= columnStopIndex; columnIndex++) {
+ // childFactory is used to create the next item in the grid
+ const child = childFactory({ rowIndex, columnIndex });
+ items.push(child);
+ }
+ }
+ // Return the items to be rendered to the DOM.
+ return items;
+}
diff --git a/website/src/code/FixedSizeListRowRangeRenderer.js b/website/src/code/FixedSizeListRowRangeRenderer.js
new file mode 100644
index 00000000..467c79a3
--- /dev/null
+++ b/website/src/code/FixedSizeListRowRangeRenderer.js
@@ -0,0 +1,13 @@
+function rowRangeRenderer({ startIndex, stopIndex, childFactory }) {
+ const items = [];
+
+ // startIndex and stopIndex are integers.
+ for (let index = startIndex; index <= stopIndex; index++) {
+ // childFactory is used to retrieve the child that is to be rendered
+ const child = childFactory({ index });
+ items.push(child);
+ }
+
+ // Return the items to be rendered to the DOM.
+ return items;
+}
diff --git a/website/src/routes/api/FixedSizeGrid.js b/website/src/routes/api/FixedSizeGrid.js
index 0ba0261a..1aa0da1a 100644
--- a/website/src/routes/api/FixedSizeGrid.js
+++ b/website/src/routes/api/FixedSizeGrid.js
@@ -5,6 +5,7 @@ import ComponentApi from '../../components/ComponentApi';
import styles from './shared.module.css';
+import CODE_CELL_RANGE_RENDERER from '../../code/FixedSizeGridCellRangeRenderer.js';
import CODE_CHILDREN_CLASS from '../../code/FixedSizeGridChildrenClass.js';
import CODE_CHILDREN_FUNCTION from '../../code/FixedSizeGridChildrenFunction.js';
import CODE_ITEM_DATA from '../../code/FixedSizeGridItemData.js';
@@ -17,6 +18,27 @@ export default () => (
);
const PROPS = [
+ {
+ description: (
+
+
+ By default, lists render cells consecutively without decoration.
+ Occasionally, you may need to render dynamic content that spans
+ multiple cells. In general, this can be handled with the{' '}
+ children prop or the innerElementType prop.
+ However, if you truly need a fine level of control over the rendering
+ mechanism, rendering, you can do so here. This is an advanced
+ property.
+
+
+
+
+
+ ),
+ isRequired: false,
+ name: 'cellRangeRenderer',
+ type: 'function',
+ },
{
description: (
diff --git a/website/src/routes/api/FixedSizeList.js b/website/src/routes/api/FixedSizeList.js
index 51887087..33256bbb 100644
--- a/website/src/routes/api/FixedSizeList.js
+++ b/website/src/routes/api/FixedSizeList.js
@@ -10,6 +10,7 @@ import CODE_CHILDREN_FUNCTION from '../../code/FixedSizeListChildrenFunction.js'
import CODE_ITEM_DATA from '../../code/FixedSizeListItemData.js';
import CODE_ITEM_KEY from '../../code/FixedSizeListItemKey.js';
import CODE_ON_ITEMS_RENDERED from '../../code/FixedSizeListOnItemsRendered.js';
+import CODE_ROW_RANGE_RENDERER from '../../code/FixedSizeListRowRangeRenderer.js';
import CODE_ON_SCROLL from '../../code/FixedSizeListOnScroll.js';
export default () => (
@@ -325,6 +326,27 @@ const PROPS = [
name: 'overscanCount',
type: 'number',
},
+ {
+ description: (
+
+
+ By default, lists render rows consecutively without decoration.
+ Occasionally, you may need to render dynamic content that spans
+ multiple items. In general, this can be handled with the{' '}
+ children prop or the innerElementType prop.
+ However, if you truly need a fine level of control over the rendering
+ mechanism, rendering, you can do so here. This is an advanced
+ property.
+
+
+
+
+
+ ),
+ isRequired: false,
+ name: 'rowRangeRenderer',
+ type: 'function',
+ },
{
defaultValue: null,
description: (