1
votes

I have tried all day to get mat-selection-list to retain the selected objects when refreshing data ie. Pagination but without success.

If I select object A ( {name, id} ) in page 1, and switch to page 2 and back to page 1 I would expect object A to still be marked as checked but this doesn't happen and I'm losing my mind.

I have tried several things:

  1. 2 way binding with [(ngModel)]="selectedOptions" [compareWith]="compareFunction". But as soon as I switch to page 2, due to the 2 way binding, mat-selection-list automatically clears selectedOptions because it can't find the objects in the list as I have switched to page 2.

Stackblitz demo 1

  1. Use reactive forms with [formControl]="itemsControl" (selectionChange)="onSelectionChanged($event)" and form.setValue``. This way the mat-selection-listdoesn' clear it but when returning to page 1 objects are no rechecked automatically. If I select one thenonSelectionChange` reports the current one as the only object selected.

Stackblitz demo 2

I have ran out of ideas, googled for hours, I'm starting to think it's a bug?

Any help is greatly appreciated.

3
I'm seeing very similar behavior when inserting an item to an existing list. Even when the selected item state is set via [selected]="item.selected" it gets unchecked even if {{ item.selected }} is clearly still true. - Simon_Weaver

3 Answers

0
votes

You should update the selectedOptions variable only with selections changes.

onSelectionChanged(event) event type is MatSelectionListChange and from docs we can see. you can access to option selected or unselected. In this way you could know when an option is selected and which one was.

So I introduce an performance trick. selectedOptions is an Object where key is the typesOfShoe id and value is the typesOfShoes

This will help us to add and delete items more faster than add-delete an array.

onSelectionChanged(event) {
    const isSelected = event.option.selected;
    const value = event.option.value;

    if (isSelected) {
      this.selectedOptions[value.id] = value;
    } else {
      delete this.selectedOptions[value.id];
    }

    this.form.setValue({
      selected: Object.values(this.selectedOptions)
    });
  }

Working example: https://stackblitz.com/edit/angular-3fffzf-faqvxp?file=app%2Flist-selection-example.ts

0
votes

HTML FILE

<mat-selection-list (selectionChange)="changeSelection($event)">
  <mat-list-option [selected]="shoe.isSelected" *ngFor="let shoe of typesOfShoes"  [value]="shoe">
    {{shoe.name}}
  </mat-list-option>
</mat-selection-list>

<button (click)="switchPage(1)">Page 1</button>
<button (click)="switchPage(2)">Page 2</button>

TS FILE

selectedItems: any[] = [];
  typesOfShoes: { name: string, id: number, isSelected: boolean }[] = [
    {name: 'Boots', id: 1, isSelected: false},
    {name: 'Clogs', id: 2, isSelected: false},
    {name: 'Loafers', id: 3, isSelected: false},
    {name: 'Moccasins', id: 4, isSelected: false},
    {name: 'Sneakers', id: 5, isSelected: false}
  ];

  switchPage(page: number): void {
    if (page === 1) {
      this.typesOfShoes = [
        {name: 'Boots', id: 1, isSelected: false},
        {name: 'Clogs', id: 2, isSelected: false},
        {name: 'Loafers', id: 3, isSelected: false},
        {name: 'Moccasins', id: 4, isSelected: false},
        {name: 'Sneakers', id: 5, isSelected: false}
      ];
    } else {
      this.typesOfShoes = [
        {name: 'Flyers', id: 6, isSelected: false},
        {name: 'Dr.Martens', id: 7, isSelected: false},
      ];
    }
    this.selectedItems.forEach(j => {
      this.typesOfShoes.forEach(i => {
     
        if (j.id === i.id) {
          i.isSelected = true;
        }
      });
    });
  }

  changeSelection($event: MatSelectionListChange): void {
    if ($event.options[0].selected) {
      this.selectedItems.push($event.options[0].value);
    } else {
      this.selectedItems.filter(i => i.id !== $event.options[0].value.id);
    }
  }

Please don't use NgModel nor Form here because state is mutable. I have added an isSelected to a Model this is most stable way and is most preferred, Good old classic and doesn't break.

This is closest to your solution with addition of arrays and manipulating them

0
votes

I would create a property that stores the selection and on new update, make a new selection

Below is my code using reactive approach to enable dynamic number of items rather than hard coding the items in the selection

export class ListSelectionExample {
  selectedOptions = [{ name: "Boots", id: 1 }];
  selectedOptionsTemp = this.selectedOptions;
  compareFunction = (o1: any, o2: any) => o1.id === o2.id;

  typesOfShoes: { name: string; id: number }[] = [
    { name: "Boots", id: 1 },
    { name: "Clogs", id: 2 },
    { name: "Loafers", id: 3 },
    { name: "Moccasins", id: 4 },
    { name: "Sneakers", id: 5 },
    { name: "Flyers", id: 6 },
    { name: "Dr.Martens", id: 7 }
  ];

  typesOfShoes$ = of(this.typesOfShoes);

  selectedPageSubject$ = new BehaviorSubject(1);
  selectedPage$ = this.selectedPageSubject$.asObservable();

  typesOfShoesFiltered$ = combineLatest([
    this.typesOfShoes$,
    this.selectedPage$
  ]).pipe(
    map(([shoes, selected]) => shoes.slice((selected - 1) * 5, selected * 5))
  );

  form: FormGroup = this.formBuilder.group({
    selected: [this.selectedOptions]
  });
  get selected(): FormControl {
    return this.form.get("selected") as FormControl;
  }
  @ViewChild("selectionList") selectionList: MatSelectionList;

  constructor(private formBuilder: FormBuilder) {}

  switchPage = (page: number) => this.selectedPageSubject$.next(page);

  onSelectionChanged(event) {
    const selected = [
      ...new Set(
        [...this.selectedOptionsTemp, ...this.selectedOptions].map(item =>
          JSON.stringify(item)
        )
      )
    ].map(item => JSON.parse(item));
    this.selectedOptionsTemp = selected;
    this.selected.setValue(selected);
  }
}
<form [formGroup]="form">
    <mat-selection-list #selectionList [compareWith]="compareFunction"  [(ngModel)]="selectedOptions"
  [ngModelOptions]="{standalone: true}"
        (selectionChange)="onSelectionChanged($event)">
        <mat-list-option *ngFor="let item of typesOfShoesFiltered$ | async" [value]="item">
            {{item.name}}
        </mat-list-option>
    </mat-selection-list>

    <button (click)="switchPage(1)">Page 1</button>
    <button (click)="switchPage(2)">Page 2</button>

    <pre>
  form.value: {{form.value | json}}
  <!-- Copyright 2018 Google Inc. All Rights Reserved.
    Use of this source code is governed by an MIT-style license that
    can be found in the LICENSE file at http://angular.io/license -->
</pre>

See Link to stackblitz demo