diff --git a/source/src/components/DataSource/types.ts b/source/src/components/DataSource/types.ts index 1cf81992..02731f9c 100644 --- a/source/src/components/DataSource/types.ts +++ b/source/src/components/DataSource/types.ts @@ -536,7 +536,7 @@ export type DataSourceProps = { groupRowsState?: GroupRowsState | DataSourcePropGroupRowsStateObject; defaultGroupRowsState?: | GroupRowsState - | DataSourcePropGroupRowsStateObject; + | DataSourcePropGroupRowsStateObject; onGroupRowsStateChange?: (groupRowsState: GroupRowsState) => void; collapseGroupRowsOnDataFunctionChange?: boolean; diff --git a/www/content/docs/learn/working-with-data/grouped-lazy-load-example.page.tsx b/www/content/docs/learn/working-with-data/grouped-lazy-load-example.page.tsx new file mode 100644 index 00000000..5f250f2a --- /dev/null +++ b/www/content/docs/learn/working-with-data/grouped-lazy-load-example.page.tsx @@ -0,0 +1,152 @@ +import { + InfiniteTable, + DataSource, + DataSourceData, + InfiniteTablePropColumns, + DataSourceProps, +} from '@infinite-table/infinite-react'; +import * as React from 'react'; +import { useMemo } from 'react'; + +type Developer = { + id: number; + firstName: string; + lastName: string; + country: string; + city: string; + currency: string; + preferredLanguage: string; + stack: string; + canDesign: 'yes' | 'no'; + hobby: string; + salary: number; + age: number; +}; + +const columns: InfiniteTablePropColumns = { + country: { field: 'country', header: 'Country' }, + id: { field: 'id', header: 'ID', defaultWidth: 100 }, + salary: { + field: 'salary', + header: 'Salary', + renderValue: ({ value, rowInfo }) => { + if (rowInfo.isGroupRow) { + return ( + <> + Avg: {value} + + ); + } + return value; + }, + }, + age: { field: 'age', header: 'Age' }, + firstName: { field: 'firstName', header: 'First Name' }, + preferredLanguage: { + field: 'preferredLanguage', + header: 'Preferred Language', + }, + lastName: { field: 'lastName', header: 'Last Name' }, + + city: { field: 'city', header: 'City' }, + currency: { field: 'currency', header: 'Currency' }, + stack: { field: 'stack', header: 'Stack' }, + canDesign: { field: 'canDesign', header: 'Can Design' }, + hobby: { field: 'hobby', header: 'Hobby' }, +}; + +const groupBy: DataSourceProps['groupBy'] = [ + { + field: 'country', + }, + { + field: 'stack', + }, +]; + +const aggregationReducers: DataSourceProps['aggregationReducers'] = { + salary: { + field: 'salary', + reducer: 'avg', + }, +}; + +export default function App() { + const lazyLoad = useMemo(() => ({ batchSize: 40 }), []); + return ( + + data={dataSource} + primaryKey="id" + groupBy={groupBy} + lazyLoad={lazyLoad} + aggregationReducers={aggregationReducers} + > + + columns={columns} + columnDefaultWidth={130} + groupColumn={{ + id: 'group-col', + defaultSortable: false, + }} + groupRenderStrategy="single-column" + /> + + ); +} + +const dataSource: DataSourceData = ({ + pivotBy, + aggregationReducers, + groupBy, + + lazyLoadStartIndex, + lazyLoadBatchSize, + groupRowsState, + groupKeys = [], + sortInfo, +}) => { + if (sortInfo && !Array.isArray(sortInfo)) { + sortInfo = [sortInfo]; + } + const startLimit: string[] = []; + if (lazyLoadBatchSize && lazyLoadBatchSize > 0) { + const start = lazyLoadStartIndex || 0; + startLimit.push(`start=${start}`); + startLimit.push(`limit=${lazyLoadBatchSize}`); + } + const args = [ + ...startLimit, + pivotBy + ? 'pivotBy=' + JSON.stringify(pivotBy.map((p) => ({ field: p.field }))) + : null, + `groupKeys=${JSON.stringify(groupKeys)}`, + `prefetchGroupKeys=${JSON.stringify(groupRowsState?.expandedRows || [])}`, + groupBy + ? 'groupBy=' + JSON.stringify(groupBy.map((p) => ({ field: p.field }))) + : null, + sortInfo + ? 'sortInfo=' + + JSON.stringify( + sortInfo.map((s) => ({ + field: s.field, + dir: s.dir, + })), + ) + : null, + aggregationReducers + ? 'reducers=' + + JSON.stringify( + Object.keys(aggregationReducers).map((key) => ({ + field: aggregationReducers[key].field, + id: key, + name: aggregationReducers[key].reducer, + })), + ) + : null, + ] + .filter(Boolean) + .join('&'); + return fetch( + process.env.NEXT_PUBLIC_BASE_URL + `/developers10k-sql?` + args, + ).then((r) => r.json()); +}; diff --git a/www/content/docs/learn/working-with-data/lazy-loading.page.md b/www/content/docs/learn/working-with-data/lazy-loading.page.md index 25e4c465..6fdde024 100644 --- a/www/content/docs/learn/working-with-data/lazy-loading.page.md +++ b/www/content/docs/learn/working-with-data/lazy-loading.page.md @@ -16,7 +16,7 @@ We call this `"lazy loading"`, and it needs to be enabled by specifying the -The DataSource.lazyLoad prop can be either a boolean or an object with a `batchSize: number` property. If `batchSize` is not specified, it will load all records from the current row group (makes sense for grouped and/or pivoted data). For ungrouped and unpivoted data, make sure you `batchSize` to a conveninent number. +The DataSource.lazyLoad prop can be either a boolean or an object with a `batchSize: number` property. If `batchSize` is not specified, it will load all records from the current row group (makes sense for grouped and/or pivoted data). For ungrouped and unpivoted data, make sure you set `batchSize` to a conveninent number. Simply specifying `lazyLoad=true` makes more sense for grouped (or/and pivoted) data, where you want to load all records from the current level at once. If you want configure it this way, new data will only be requested when a group row is expanded. @@ -50,3 +50,30 @@ Find out about server-side grouping Find out about server-side pivoting + +## How lazy loading fetches data + +When lazy loading is enabled, and the changes (eg: user clicks on a column header), the DataGrid will discard current data and call the function prop again, to fetch the new data. The same happens when the or changes. This is done automatically by the component, and you don't need to do anything. + + + + + +This demo lazily loads grouped data as the user scrolls down. Expand some groups to see the lazy loading in action. + +When the user stops scrolling, after milliseconds, the DataGrid will fetch the next batch of data from the server. + + + +```ts file="grouped-lazy-load-example.page.tsx" + +``` + + + + + + +Batching also happens for groups - when a group is expanded, the DataGrid will fetch the first batch of data in the expanded group and then fetch additional batches as the user scrolls down. When scrolling goes beyound the group, the DataGrid is smart enough to request a batch of data from sibling groups. + + \ No newline at end of file diff --git a/www/content/docs/learn/working-with-data/simple-lazy-load-example.page.tsx b/www/content/docs/learn/working-with-data/simple-lazy-load-example.page.tsx index 44110d39..0a9baa6e 100644 --- a/www/content/docs/learn/working-with-data/simple-lazy-load-example.page.tsx +++ b/www/content/docs/learn/working-with-data/simple-lazy-load-example.page.tsx @@ -23,17 +23,21 @@ type Developer = { }; const columns: InfiniteTablePropColumns = { - id: { field: 'id' }, - salary: { field: 'salary' }, - age: { field: 'age' }, - firstName: { field: 'firstName' }, - preferredLanguage: { field: 'preferredLanguage' }, - lastName: { field: 'lastName' }, - country: { field: 'country' }, - city: { field: 'city' }, - currency: { field: 'currency' }, - stack: { field: 'stack' }, - canDesign: { field: 'canDesign' }, + id: { field: 'id', header: 'ID', defaultWidth: 100 }, + salary: { field: 'salary', header: 'Salary' }, + age: { field: 'age', header: 'Age' }, + firstName: { field: 'firstName', header: 'First Name' }, + preferredLanguage: { + field: 'preferredLanguage', + header: 'Preferred Language', + }, + lastName: { field: 'lastName', header: 'Last Name' }, + country: { field: 'country', header: 'Country' }, + city: { field: 'city', header: 'City' }, + currency: { field: 'currency', header: 'Currency' }, + stack: { field: 'stack', header: 'Stack' }, + canDesign: { field: 'canDesign', header: 'Can Design' }, + hobby: { field: 'hobby', header: 'Hobby' }, }; export default function App() { @@ -44,7 +48,7 @@ export default function App() { primaryKey="id" lazyLoad={lazyLoad} > - columns={columns} /> + columns={columns} columnDefaultWidth={130} /> ); } diff --git a/www/content/docs/reference/infinite-table-props.page.md b/www/content/docs/reference/infinite-table-props.page.md index 84c1913b..316ae8ef 100644 --- a/www/content/docs/reference/infinite-table-props.page.md +++ b/www/content/docs/reference/infinite-table-props.page.md @@ -33,6 +33,24 @@ The details for each city shows a DataGrid with developers in that city. + + +> The delay in milliseconds that the DataGrid waits until it considers scrolling to be stopped. Also used when lazy loading is to fetch the next batch of data. + +This also determines when the callback prop is called. + + + +```ts file="scrollStopDelay-lazy-load-example.page.tsx" + +``` + + + + + + + > Various header configurations for the DataGrid. @@ -2885,6 +2903,14 @@ It will never be called again after the component is ready. + + +> Triggered when the user has stopped scrolling (after milliseconds). + +This is called when the user stops scrolling for a period of time - as configured by (milliseconds). + + + > Triggered when the user has scrolled to the bottom of the component diff --git a/www/content/docs/reference/scrollStopDelay-lazy-load-example.page.tsx b/www/content/docs/reference/scrollStopDelay-lazy-load-example.page.tsx new file mode 100644 index 00000000..4485f286 --- /dev/null +++ b/www/content/docs/reference/scrollStopDelay-lazy-load-example.page.tsx @@ -0,0 +1,113 @@ +import { + InfiniteTable, + DataSource, + DataSourceData, + InfiniteTablePropColumns, +} from '@infinite-table/infinite-react'; +import * as React from 'react'; +import { useMemo } from 'react'; + +type Developer = { + id: number; + firstName: string; + lastName: string; + country: string; + city: string; + currency: string; + preferredLanguage: string; + stack: string; + canDesign: 'yes' | 'no'; + hobby: string; + salary: number; + age: number; +}; + +const columns: InfiniteTablePropColumns = { + id: { field: 'id', header: 'ID', defaultWidth: 100 }, + salary: { field: 'salary', header: 'Salary' }, + age: { field: 'age', header: 'Age' }, + firstName: { field: 'firstName', header: 'First Name' }, + preferredLanguage: { + field: 'preferredLanguage', + header: 'Preferred Language', + }, + lastName: { field: 'lastName', header: 'Last Name' }, + country: { field: 'country', header: 'Country' }, + city: { field: 'city', header: 'City' }, + currency: { field: 'currency', header: 'Currency' }, + stack: { field: 'stack', header: 'Stack' }, + canDesign: { field: 'canDesign', header: 'Can Design' }, + hobby: { field: 'hobby', header: 'Hobby' }, +}; + +export default function App() { + const lazyLoad = useMemo(() => ({ batchSize: 40 }), []); + return ( + + data={dataSource} + primaryKey="id" + lazyLoad={lazyLoad} + > + + columns={columns} + columnDefaultWidth={130} + scrollStopDelay={50} + /> + + ); +} + +const dataSource: DataSourceData = ({ + pivotBy, + aggregationReducers, + groupBy, + + lazyLoadStartIndex, + lazyLoadBatchSize, + groupKeys = [], + sortInfo, +}) => { + if (sortInfo && !Array.isArray(sortInfo)) { + sortInfo = [sortInfo]; + } + const startLimit: string[] = []; + if (lazyLoadBatchSize && lazyLoadBatchSize > 0) { + const start = lazyLoadStartIndex || 0; + startLimit.push(`start=${start}`); + startLimit.push(`limit=${lazyLoadBatchSize}`); + } + const args = [ + ...startLimit, + pivotBy + ? 'pivotBy=' + JSON.stringify(pivotBy.map((p) => ({ field: p.field }))) + : null, + `groupKeys=${JSON.stringify(groupKeys)}`, + groupBy + ? 'groupBy=' + JSON.stringify(groupBy.map((p) => ({ field: p.field }))) + : null, + sortInfo + ? 'sortInfo=' + + JSON.stringify( + sortInfo.map((s) => ({ + field: s.field, + dir: s.dir, + })), + ) + : null, + aggregationReducers + ? 'reducers=' + + JSON.stringify( + Object.keys(aggregationReducers).map((key) => ({ + field: aggregationReducers[key].field, + id: key, + name: aggregationReducers[key].reducer, + })), + ) + : null, + ] + .filter(Boolean) + .join('&'); + return fetch( + process.env.NEXT_PUBLIC_BASE_URL + `/developers10k-sql?` + args, + ).then((r) => r.json()); +};