Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

expose currentDataSource of Table, update when dataSource/filters/sorters are changed by application #24022

Open
1 task done
urrri opened this issue May 10, 2020 · 34 comments
Labels

Comments

@urrri
Copy link

urrri commented May 10, 2020

  • I have searched the issues of this repository and believe that this is not a duplicate.

What problem does this feature solve?

It allows to perform operations on filtered&sorted data, e.g. export data to file, calculate number of items in table, etc.
The problem is that currentDataSource that I receive via onChange is not updated when dataSource/filters/sorters are changed by application.

What does the proposed API look like?

Event prop onCurrentDataSourceChanged, which should be fired after each calculation of currentDataSource, on any related change and initial.
If it is possible, please add it to both 3.* and 4.* versions of Ant Design.

@urrri
Copy link
Author

urrri commented Jul 4, 2020

Is "inactive" because nobody interested in it or nobody read it at all?
The issue that @afc163 added, covers the idea very partially.
Main idea not to show total, but use items to do something with them (e.g. Export to CSV/PDF).
If not to expose it, I need to filter/sort myself outside the table, but in this case I duplicate a lot of code/logic.
Also implementation in ver.4 is very simple. Just add event exactly after calculating internal dataSource. Really 5 minutes (not including tests)

@sherleyshen
Copy link

the event named "change" can get { currentDataSource }.

@urrri
Copy link
Author

urrri commented Sep 14, 2020

onChange fires only when USER changed something, but when app sets/updates data to table this event is NOT fired. To have currentDS always I proposed this addition. Especially, because it is really simple to implement in v4+: one line of code, just after creating DS need to add effect, that fires event with the data.

@joehiggs
Copy link

joehiggs commented Oct 14, 2020

I'd like to second this. Without support of ref on the table either, it seems there is no clean way to track the current values displayed.

For example, if a user were to select rows, then apply a programmatic filter, there seems to be no good way to know which of the checked boxes are not filtered out.

@diw1
Copy link
Contributor

diw1 commented Aug 17, 2021

Any update for this feature request?

@github-actions github-actions bot removed the Inactive label Aug 17, 2021
@kaotypr
Copy link

kaotypr commented Aug 24, 2021

Any update for this feature request?

try destructuring the dataSource data. it works in my case
<Table columns={columns} dataSource={[...dataSource]} rowKey="id" />

@PureStream
Copy link

PureStream commented Nov 11, 2021

Any update for this feature request?

try destructuring the dataSource data. it works in my case <Table columns={columns} dataSource={[...dataSource]} rowKey="id" />

How does this allow you to keep up with new dataSource after applying a programmatic filter?

@nickkrein
Copy link

Would also love this feature. Currently having to use a janky workaround with currentPageData from summary.

@SwissTazMan
Copy link

Having the same problem. There should be an easy possibility to access the filtered data.

@jeffsupancic
Copy link

I'd like to second this. Without support of ref on the table either, it seems there is no clean way to track the current values displayed.

For example, if a user were to select rows, then apply a programmatic filter, there seems to be no good way to know which of the checked boxes are not filtered out.

+1

@ozgurkisa
Copy link

ozgurkisa commented Jan 17, 2022

+1 We are waiting for this development to export, also... This development will allow column filters to be sorted based on current data. It works for many other things.

Edit: This development should also cover default filters. Access with ref may be the most accurate way.

@acailly
Copy link

acailly commented Feb 24, 2022

I almost missed the @nickkrein comment, using the summary prop gives us the filtered and sorted row array

summary={(currentData) => {
  onSortOrFilter(currentData)
  return null // or whatever you want
}}

Thank you @nickkrein 👍

(also related to #19415 and #10462)

@jeffsupancic
Copy link

I almost missed the @nickkrein comment, using the summary prop gives us the filtered and sorted row array

summary={(currentData) => {
  onSortOrFilter(currentData)
  return null // or whatever you want
}}

Thank you @nickkrein 👍

(also related to #19415 and #10462)

This only works for the current page.

@acailly
Copy link

acailly commented Feb 24, 2022

Oh, that's right. I don't have pagination in my use case but thanks you for pointing that 🙏

@hamudeshahin
Copy link

What about if i want to fetch data from api if user choose an item from filter list ?? i have a problem with that and i think the problem is currentDataSource: [] .. do we have any solution for this problem ?

@xucongli1989
Copy link

Having the same problem.

@Vincz
Copy link

Vincz commented May 10, 2022

I kind of have the same problem too. Why not expose a getData method on the table ref or simply a prop to pass a state updater? We need a way to know what is exactly displayed to the user after all the filter / sorting stuff.

@vpotluri1217
Copy link

vpotluri1217 commented May 26, 2022

Hi why is it taking this long to fix, this should be very simple .. apparently I need on change to be triggered when filters and sorting is done programmatically or when table data is changed..

@Jef-I
Copy link

Jef-I commented Jun 17, 2022

I think I have the same problem (or at least a similar problem with the same solution):

I have a table with Data on which I apply front-end filters (no pagination). The data repeatedly gets updated when extra calls are being done to the backend, but this does not trigger the onChange. Because of this, the counters for my filtered data and total data don't match, unless I manually apply these filters outside of the table. It would be a lot easier if there was a certain fix for this, now I'm just working in circles to update the table/filters when data is updated.

@zxlvera
Copy link

zxlvera commented Aug 4, 2022

What I did was extract the currentDataSource to first define pagination and exportData with useState. When dataSource is first loaded, setExportData(dataSource). When user sort/changes page, we setExportData(extra.currentDataSource)

  const [exportData, setExportData] = useState<IDataSourceList[]>([]);
  const [pagination, setPagination] = useState<TablePaginationConfig>({
    current: 1,
    pageSize: 10
  });

  const handleChange: TableProps<IDataSourceType>['onChange'] = (pagination, _filters, _sorter, extra) => {
    setPagination(pagination);
    setExportData(extra.currentDataSource);
  };

  const getCurrentData = () => {
    console.log(exportData);
  };

  const handleDataSource = useCallback(() => {
    setExportData(dataSource);
  }, [dataSource]);

  const handleSearch = useCallback(() => {
    fetchData().catch((e) => {
      addError(e);
    });
  }, [addError]);

  useEffect(() => {
    if (!loading) {
      handleDataSource();
    }
  }, [handleDataSource, loading]);

  return (
            <Table
              loading={loading}
              dataSource={dataSource}
              columns={columns}
              rowKey={`id`}
              bordered
              pagination={pagination}
              onChange={handleChange}
            />
  );
};

@ozgurkisa
Copy link

ozgurkisa commented Aug 9, 2022

What I did was extract the currentDataSource to first define pagination and exportData with useState. When dataSource is first loaded, setExportData(dataSource). When user sort/changes page, we setExportData(extra.currentDataSource)

  const [exportData, setExportData] = useState<IDataSourceList[]>([]);
  const [pagination, setPagination] = useState<TablePaginationConfig>({
    current: 1,
    pageSize: 10
  });

  const handleChange: TableProps<IDataSourceType>['onChange'] = (pagination, _filters, _sorter, extra) => {
    setPagination(pagination);
    setExportData(extra.currentDataSource);
  };

  const getCurrentData = () => {
    console.log(exportData);
  };

  const handleDataSource = useCallback(() => {
    setExportData(dataSource);
  }, [dataSource]);

  const handleSearch = useCallback(() => {
    fetchData().catch((e) => {
      addError(e);
    });
  }, [addError]);

  useEffect(() => {
    if (!loading) {
      handleDataSource();
    }
  }, [handleDataSource, loading]);

  return (
            <Table
              loading={loading}
              dataSource={dataSource}
              columns={columns}
              rowKey={`id`}
              bordered
              pagination={pagination}
              onChange={handleChange}
            />
  );
};

@zxlvera This doesn't solve the problem. Because onChange event with default filter is not triggered. (Example: I cannot export a default filtered data to an excel file.)

@dperetti
Copy link

Come on!

@Shlok-Zanwar
Copy link

Any updates on this one?
This is a much-needed feature when we consider exporting the table data.

@afc163

@Turkitutu
Copy link

Any updates?

@Shlok-Zanwar
Copy link

In this file https://github.com/ant-design/ant-design/blob/master/components/table/Table.tsx

Just adding

    const triggerOnChange = (
      info: Partial<ChangeEventInfo<RecordType>>,
      action: TableAction,
      reset: boolean = false,
    ) => {
.............................


  **React.useEffect(function () {
    triggerOnChange({}, 'dataSource', false);
  }, [rawData]);**

would solve the issue.

I am new to open-source contributions, it would be great if someone could help me add this feature or add it by themselves.

@aminsaedi
Copy link

I'm using footer prop as a temporary solution.

<Table {...props} footer={(currentDataSource) => { someFns(); return <></>   }} />

The issue with this trick is that it only shows the current page data

@lovelymod
Copy link

lovelymod commented Nov 28, 2022

What I did was extract the currentDataSource to first define pagination and exportData with useState. When dataSource is first loaded, setExportData(dataSource). When user sort/changes page, we setExportData(extra.currentDataSource)

  const [exportData, setExportData] = useState<IDataSourceList[]>([]);
  const [pagination, setPagination] = useState<TablePaginationConfig>({
    current: 1,
    pageSize: 10
  });

  const handleChange: TableProps<IDataSourceType>['onChange'] = (pagination, _filters, _sorter, extra) => {
    setPagination(pagination);
    setExportData(extra.currentDataSource);
  };

  const getCurrentData = () => {
    console.log(exportData);
  };

  const handleDataSource = useCallback(() => {
    setExportData(dataSource);
  }, [dataSource]);

  const handleSearch = useCallback(() => {
    fetchData().catch((e) => {
      addError(e);
    });
  }, [addError]);

  useEffect(() => {
    if (!loading) {
      handleDataSource();
    }
  }, [handleDataSource, loading]);

  return (
            <Table
              loading={loading}
              dataSource={dataSource}
              columns={columns}
              rowKey={`id`}
              bordered
              pagination={pagination}
              onChange={handleChange}
            />
  );
};

this worked for me ! thx

@CodeFly16
Copy link

So, has any problem to prevent solving this issue?

@ahmadatiya-citylitics
Copy link

ahmadatiya-citylitics commented Jan 18, 2023

It's very frustrating that this issue still hasn't been resolved years later. As a result, exporting table data is hacky, at best, and impossible, at worse.

@EnanoFurtivo
Copy link

+1

2 similar comments
@hakanai03
Copy link

+1

@Anurag-Raut
Copy link

+1

@kriscarle
Copy link

kriscarle commented Oct 4, 2023

We found a solution for this. It is very much a hack, but appears to work. filterDropdown has callback functions so you can write your own popup UI for filters. If we add a hidden filter to a column and save the callbacks after the Table component is loaded, we can call them to trigger a call to onChange.

You need a column where you don't need a filter interface. For example, we have an "Action" column with buttons in it, so we put the hidden filter there.

There is a full working example here https://codesandbox.io/s/customized-filter-panel-antd-5-9-4-forked-6tfqtw?file=/demo.tsx:5542-5597

import React, { useEffect, useRef, useState } from 'react'
import {Table} from 'antd'

const TableWithRestoredState = () => {
 // used to track if the Table has loaded and trigger a re-render
 const [loaded, setLoaded] = useState(false)

// used to store a ref to the callback from antd
  const triggerDataRef = React.useRef<() => void | void>(null)

// count the renders so we only do this once
  const renderTimesRef = React.useRef<number>(0)

  useEffect(() => {
    if (renderTimesRef.current === 1 && triggerDataRef.current) {
      triggerDataRef.current()
    }
    renderTimesRef.current += 1
  }, [loaded])

  const columns  = [
    {
      title: "My Column",
      filterDropdown: ({ setSelectedKeys, confirm }) => {
        if (!triggerDataRef.current) {
          triggerDataRef.current = () => {
            setSelectedKeys(["fake key"])
            confirm()
          };
          setLoaded(true)
        }
        return <div></div>
      },
      onFilter: (value, record) => {
        return true; // needed so no rows are actually filtered by our fake filter
      },
      filterIcon: () => {
        return <div></div>
      }
    }
   ]

// filtered data will store a local copy of the sorted or filtered data from the antd Table
 const [filteredData, setFilteredData] = useState([])

const handleChange = (pagination, filters, sorter, extra) => {
    if (extra.action === "filter" || extra.action === "sort") {
      setFilteredData(extra.currentDataSource)
    }
  }

 return <Table columns={columns} dataSource={data} onChange={handleChange} />
} 

@BertrandBordage
Copy link

None of the above workarounds were working 100% for me (extra column shown + change not triggered on load when I have GET parameters filtering the table or change not triggered when clearing a filter).

Here is a fully working approach:

  • capture all onFilter calls, remember which ones returned true/false
  • modify the supplied onChange to detect individual filter changes
  • trigger a callback with the filtered records as argument on component mount + each filter change/clear

With TypeScript, this makes it a drop-in replacement for Table.
It tried to make it as fast as possible, but there is obviously extra CPU cycles spent on it. No noticeable slowdown on 10 000 rows.

How to use it:

<TableWithFilteredRecordsCallback
  dataSource={your_data_as_usual}
  rowKey={}  // rowKey is mandatory for this to work.
  columns={}  // your columns must provide key or dataIndex.
  onDataFiltered={(currentRecords) => {
    // Do what you need in here!
  }}
  // other usual Table props.
/>

And now the modified Table itself (sorry, a bit long, could not find how to make it collapsible):

// TableWithFilteredRecordsCallback.tsx

import { Table, TablePaginationConfig, TableProps } from "antd";
import { ColumnType } from "antd/es/table/interface";
import React, { useCallback, useEffect, useMemo, useRef } from "react";
import type { FilterValue, SorterResult, TableCurrentDataSource } from "antd/lib/table/interface";

function getColumnKey<RecordType extends object>(
  column: ColumnType<RecordType>,
  columnIndex: number
) {
  let columnKey = column.key ?? ("dataIndex" in column ? column.dataIndex : undefined);
  if (typeof columnKey === "string") {
    return columnKey;
  }
  if (columnKey) {
    throw Error(`number or string[] dataIndex is not supported.`);
  }
  throw Error(
    `Column with index ${columnIndex} did not have \`key\` defined, required by \`TableWithFilteredRecordsCallback\`.`
  );
}

export default function TableWithFilteredRecordsCallback<RecordType extends object>({
  dataSource,
  columns,
  rowKey,
  onChange,
  onDataFiltered,
  ...otherProps
}: TableProps<RecordType> & {
  rowKey: TableProps<RecordType>["rowKey"]; // Makes rowKey mandatory.
  onDataFiltered: (currentRecords: RecordType[]) => void;
  filterTriggerDebounceMs?: number;
}) {
  const filteredCellsRef = useRef<{
    [columnKey: string]: { [recordKey: React.Key]: boolean };
  }>({});
  const filterValuesRef = useRef<{ [columnKey: string]: FilterValue | null }>({});

  const getRecordKey = useCallback(
    (record: RecordType) => {
      // Due to bad typing detection.
      if (rowKey === undefined) {
        return undefined;
      }
      if (typeof rowKey === "string") {
        // @ts-ignore
        return record[rowKey];
      }
      return rowKey(record);
    },
    [rowKey]
  );

  const triggerFilterCallback = useCallback(() => {
    const columnsFilteredCells = Object.values(filteredCellsRef.current);
    onDataFiltered(
      (dataSource ?? []).filter((row) => {
        const recordKey = getRecordKey(row);
        return columnsFilteredCells.every((records) => records[recordKey] ?? true);
      })
    );
  }, [onDataFiltered, dataSource, getRecordKey]);

  useEffect(() => {
    triggerFilterCallback();
  }, [triggerFilterCallback]);

  const interceptedColumns = useMemo(() => {
    if (columns === undefined) {
      return undefined;
    }
    return columns.map((column, index) => {
      const columnKey = getColumnKey(column, index);
      const originalOnFilter = column.onFilter;
      if (originalOnFilter === undefined) {
        return column;
      }
      if (!(columnKey in filteredCellsRef.current)) {
        filteredCellsRef.current[columnKey] = {};
      }
      const columnFilteredCells = filteredCellsRef.current[columnKey];
      return {
        ...column,
        onFilter: (value: string | number | boolean, record: RecordType) => {
          const isFiltered = originalOnFilter(value, record);
          columnFilteredCells[getRecordKey(record)] = isFiltered;
          return isFiltered;
        },
      };
    });
  }, [columns, getRecordKey]);

  const modifiedOnChange = useCallback(
    (
      pagination: TablePaginationConfig,
      filters: Record<string, FilterValue | null>,
      sorter: SorterResult<RecordType> | SorterResult<RecordType>[],
      extra: TableCurrentDataSource<RecordType>
    ) => {
      if (onChange !== undefined) {
        onChange(pagination, filters, sorter, extra);
      }
      let filtersHaveChanged: boolean = false;
      for (const [columnKey, filterValue] of Object.entries(filters)) {
        if (filterValuesRef.current[columnKey] === filterValue) {
          continue;
        }
        filterValuesRef.current[columnKey] = filterValue;
        filtersHaveChanged = true;
        if (filterValue === null) {
          const columnFilteredCells = filteredCellsRef.current[columnKey];
          // This filter is cleared. Since clearing a filter (or unchecking all selected items)
          // does not call onFilter, this is our only way to detect that we have to clear this filter.
          // We therefore simulate a call to onFilter with no value selected in that filter.
          for (const recordKey of Object.keys(columnFilteredCells)) {
            columnFilteredCells[recordKey] = true;
          }
        }
      }
      if (filtersHaveChanged) {
        triggerFilterCallback();
      }
    },
    [triggerFilterCallback, onChange]
  );

  return (
    <Table<RecordType>
      dataSource={dataSource}
      columns={interceptedColumns}
      onChange={modifiedOnChange}
      {...otherProps}
    />
  );
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests