Fluentui: DetailsList - Selection does not stick to item when filtering

Created on 7 Jun 2018  路  8Comments  路  Source: microsoft/fluentui

Bug Report

  • __Package version(s)__: ^7.3.0

Priorities and help requested (not applicable if asking question):

Are you willing to submit a PR to fix? No

Requested priority: Normal

Describe the issue:

When using a DetailsList with filtering as per the Office Fabric React components page examples, the selection of an item does not "stick" when the list is filtered. Ideally I would like my users to be able to filter the list one way, select item(s), apply another filter, select item(s), etc. and have all of the items selected after each filter returned.

Would it be possible to get example code demonstrating this?

Actual behavior:

Every filter action clears selected items

DetailsList Backlog review Won't Fix Feature

Most helpful comment

I recently stumbled upon on a similar feature request and came up with the following solution which _allows to preserve the selection_ in DetailsList while the data is getting filtered.

First a separate component is introduced which implements the logic to preserve the selection...

...

Here is a demo

Cross posted as answer on StackOverflow

This is really a good answer, @vgrem. I used that for a while, but I noticed that the problem is elsewhere. The problem doesn't appears if the key of the viewItems is named exactly 'key'.
I use the version "6.202.0" of Fabric.

In my implementation, the constructor contain this:

...
this._allItems = this.props.listItems.map((item, index) => ({ key: index, ...item }));
...

In the render() now I don't use ViewSelection nor MarqueeSelection:

<DetailsList
    componentRef={this.detailsList}
    items={this.state.viewItems}
    compact={true}
    columns={columns}
    selectionMode={this.props.multiselect ? SelectionMode.multiple : SelectionMode.single}
    setKey="set"
    getKey={this.getKey}
    layoutMode={DetailsListLayoutMode.justified}
    isHeaderVisible={true}
    selection={this._selection}
    selectionPreservedOnEmptyClick={true}
/>

The getKey:

    private getKey = (item: any, index?: number) => {
        return item.key;
    }

And finally my handleChange (this mantain the items already selected when I filter and starts only after the third char insered, or on a reset of the filter):

private handleChange = (text: string): void => {
    if (text.length > 2 || text.length == 0) {
        let selection = this._selection.getSelection();
        let filtered_items = this._allItems.filter(i => i.title.toLowerCase().indexOf(text.toLocaleLowerCase()) > -1);
        let newItems = text ? [...selection, ...filtered_items].filter(distinct) : this._allItems;
        this.setState({
            viewItems: newItems
        });
    }
}

This works great.
But if I change the name of 'key' with 'uniqueKey' or other... it doesn't.

Saddly, also your implementation doesn't works well with keys named differently to 'key' (or so it seemed to me to notice). I think is a bug of the DetailsList.

All 8 comments

+1! I've had to code this and it wasn't easy. It's working reasonably well except for issues with the search textbox losing focus when I restore a previous selection after the filter changes to adjust for the list item's new index. So if the user wants to keep backspacing their search filter they have to stop after each key press and put the cursor back in the textbox. Setting focus on the element isn't working and this only seems to happen after I call focusIndex() to scroll a restored selection into view if it's far down the list. Perhaps there's an event I can capture after this to restore the cursor, but I don't think the TextField.setSelection* methods are relevant and there's nothing else AFAIK that can set the cursor position at a certain index (the end).

Regardless, I'm also now afraid my selection state management code will break completely if this feature is ever completed, but I suppose I can control the deployed Fabric version or redo my code to accommodate.

I recently stumbled upon on a similar feature request and came up with the following solution which allows to preserve the selection in DetailsList while the data is getting filtered.

First a separate component is introduced which implements the logic to preserve the selection:

export interface IViewSelection {}

export interface IViewSelectionProps
  extends React.HTMLAttributes<HTMLDivElement> {
  componentRef?: IRefObject<IViewSelection>;

  /**
   * The selection object to interact with when updating selection changes.
   */
  selection: ISelection;

  items: any[];
}

export interface IViewSelectionState {}

export class ViewSelection extends BaseComponent<
  IViewSelectionProps,
  IViewSelectionState
> {
  private items: any[];
  private selectedIndices: any[];
  constructor(props: IViewSelectionProps) {
    super(props);
    this.state = {};
    this.items = this.props.items;
    this.selectedIndices = [];
  }

  public render() {
    const { children } = this.props;
    return <div>{children}</div>;
  }

  public componentWillUpdate(
    nextProps: IViewSelectionProps,
    nextState: IViewSelectionState
  ) {
    this.saveSelection();
  }

  public componentDidUpdate(
    prevProps: IViewSelectionProps,
    prevState: IViewSelectionState
  ) {
    this.restoreSelection();
  }

  private toListIndex(index: number) {
    const viewItems = this.props.selection.getItems();
    const viewItem = viewItems[index];
    return this.items.findIndex(listItem => listItem === viewItem);
  }

  private toViewIndex(index: number) {
    const listItem = this.items[index];
    const viewIndex = this.props.selection
      .getItems()
      .findIndex(viewItem => viewItem === listItem);
    return viewIndex;
  }

  private saveSelection(): void {
    const newIndices = this.props.selection
      .getSelectedIndices()
      .map(index => this.toListIndex(index))
      .filter(index => this.selectedIndices.indexOf(index) === -1);

    const unselectedIndices = this.props.selection
      .getItems()
      .map((item, index) => index)
      .filter(index => this.props.selection.isIndexSelected(index) === false)
      .map(index => this.toListIndex(index));

    this.selectedIndices = this.selectedIndices.filter(
      index => unselectedIndices.indexOf(index) === -1
    );
    this.selectedIndices = [...this.selectedIndices, ...newIndices];
  }

  private restoreSelection(): void {
    const indices = this.selectedIndices
      .map(index => this.toViewIndex(index))
      .filter(index => index !== -1);
    for (const index of indices) {
      this.props.selection.setIndexSelected(index, true, false);
    }
  }
}

Now DetailsList component needs to be wrapped with ViewSelection component to save and restore the selection while filtering is applied:

const items = generateItems(20);

export default class DetailsListBasicExample extends React.Component<
  {},
  {
    viewItems: any[];
  }
> {
  private selection: Selection;
  private detailsList = React.createRef<IDetailsList>();

  constructor(props: {}) {
    super(props);

    this.selection = new Selection({
    });
    this.state = {
      viewItems: items
    };
    this.handleChange = this.handleChange.bind(this);
  }

  public render(): JSX.Element {
    return (
      <div>
        <TextField label="Filter by name:" onChange={this.handleChange} />
        <ViewSelection selection={this.selection} items={this.state.viewItems} >
          <DetailsList
            componentRef={this.detailsList}
            items={this.state.viewItems}
            columns={columns}
            setKey="set"
            layoutMode={DetailsListLayoutMode.fixedColumns}
            selection={this.selection}
            selectionMode={SelectionMode.multiple}
            selectionPreservedOnEmptyClick={true}
          />
        </ViewSelection>
      </div>
    );
  }

  private handleChange = (
    ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
    text: string
  ): void => {
    const viewItems = text
      ? items.filter(item => item.name.toLowerCase().indexOf(text.toLocaleLowerCase()) > -1)
      : items;
    this.setState({ viewItems });
  };
}

Here is a demo

Cross posted as answer on StackOverflow

Running into the same issue. Could we add an option to Selection to retain memory of previously seen, selected items, and then choose when we want to clear that memory?

I recently stumbled upon on a similar feature request and came up with the following solution which _allows to preserve the selection_ in DetailsList while the data is getting filtered.

First a separate component is introduced which implements the logic to preserve the selection...

...

Here is a demo

Cross posted as answer on StackOverflow

This is really a good answer, @vgrem. I used that for a while, but I noticed that the problem is elsewhere. The problem doesn't appears if the key of the viewItems is named exactly 'key'.
I use the version "6.202.0" of Fabric.

In my implementation, the constructor contain this:

...
this._allItems = this.props.listItems.map((item, index) => ({ key: index, ...item }));
...

In the render() now I don't use ViewSelection nor MarqueeSelection:

<DetailsList
    componentRef={this.detailsList}
    items={this.state.viewItems}
    compact={true}
    columns={columns}
    selectionMode={this.props.multiselect ? SelectionMode.multiple : SelectionMode.single}
    setKey="set"
    getKey={this.getKey}
    layoutMode={DetailsListLayoutMode.justified}
    isHeaderVisible={true}
    selection={this._selection}
    selectionPreservedOnEmptyClick={true}
/>

The getKey:

    private getKey = (item: any, index?: number) => {
        return item.key;
    }

And finally my handleChange (this mantain the items already selected when I filter and starts only after the third char insered, or on a reset of the filter):

private handleChange = (text: string): void => {
    if (text.length > 2 || text.length == 0) {
        let selection = this._selection.getSelection();
        let filtered_items = this._allItems.filter(i => i.title.toLowerCase().indexOf(text.toLocaleLowerCase()) > -1);
        let newItems = text ? [...selection, ...filtered_items].filter(distinct) : this._allItems;
        this.setState({
            viewItems: newItems
        });
    }
}

This works great.
But if I change the name of 'key' with 'uniqueKey' or other... it doesn't.

Saddly, also your implementation doesn't works well with keys named differently to 'key' (or so it seemed to me to notice). I think is a bug of the DetailsList.

Having the same problem. Any updates on this ?

I recently stumbled upon on a similar feature request and came up with the following solution which _allows to preserve the selection_ in DetailsList while the data is getting filtered.
First a separate component is introduced which implements the logic to preserve the selection...
...
Here is a demo
Cross posted as answer on StackOverflow

This is really a good answer, @vgrem. I used that for a while, but I noticed that the problem is elsewhere. The problem doesn't appears if the key of the viewItems is named exactly 'key'.
I use the version "6.202.0" of Fabric.

In my implementation, the constructor contain this:

...
this._allItems = this.props.listItems.map((item, index) => ({ key: index, ...item }));
...

In the render() now I don't use ViewSelection nor MarqueeSelection:

<DetailsList
  componentRef={this.detailsList}
  items={this.state.viewItems}
  compact={true}
  columns={columns}
  selectionMode={this.props.multiselect ? SelectionMode.multiple : SelectionMode.single}
  setKey="set"
  getKey={this.getKey}
  layoutMode={DetailsListLayoutMode.justified}
  isHeaderVisible={true}
  selection={this._selection}
  selectionPreservedOnEmptyClick={true}
/>

The getKey:

    private getKey = (item: any, index?: number) => {
        return item.key;
    }

And finally my handleChange (this mantain the items already selected when I filter and starts only after the third char insered, or on a reset of the filter):

private handleChange = (text: string): void => {
  if (text.length > 2 || text.length == 0) {
      let selection = this._selection.getSelection();
      let filtered_items = this._allItems.filter(i => i.title.toLowerCase().indexOf(text.toLocaleLowerCase()) > -1);
      let newItems = text ? [...selection, ...filtered_items].filter(distinct) : this._allItems;
      this.setState({
          viewItems: newItems
      });
  }
}

This works great.
But if I change the name of 'key' with 'uniqueKey' or other... it doesn't.

Saddly, also your implementation doesn't works well with keys named differently to 'key' (or so it seemed to me to notice). I think is a bug of the DetailsList.

hi im just having trouble replicating this, it persist when the selected items are in filter items but when the filter doesnt return any item selecteds when i go back the selection is lost, do you use any selection methos as setKeySelected and when you used it , thanks!

Having the same problem, im doing setKeySelected inside useEffect in any changes of filterItems, i get the current selectedITems with selection.getSelection() but no changes happens in list.

Due to the complexity and dependencies of our List components, we are not able to take new feature requests at this time.

Was this page helpful?
0 / 5 - 0 ratings