Mad Eddie
Mad Eddie

Reputation: 1195

Angular Material Select list with filter

I am trying to create a simple select list using Angular Material List Module

The trouble is - I am trying to combine this with a filter. I have achieved this functionality however I am unable to filter and un-filter the list without it losing track of which items within the list as selected. This is because during the filter, items which were previously selected are removed when they are "filtered out".

A second version was a simple @for loop of mat-checkbox's however this requires a function to determine if the item is selected or not which is VERY expensive. Something like:

HTML

<mat-form-field class="w-full">
   <mat-label>Filter</mat-label>
   <input matInput formControlName="itemFilter" (ngModelChange)="filterItem($event)" />
</mat-form-field>

<div class="w-full flex-column my-list d-flex align-items-center justify-content-start">
@for(s of filteredOptions; track s){
    <mat-checkbox class="w-full" [checked]="isSelected(s)" (click)="toggleSelected(s)">{{s.name}} ({{s.code}})</mat-checkbox>
}
</div>

Component

filterItem($event: any): void {
    if (!$event) {
      this.filteredOptions = this.origList;
    }
    if (typeof $event === 'string') {
      this.filteredOptions = this.origList.filter((a) =>
        a.name.toLowerCase().startsWith($event.toLowerCase())
      );
    }
  }


isSelected(item: MyItem): boolean {
    return (this.searchForm.get('selectedItems')?.value as MyItem[]).indexOf(item) > -1;
  }

toggleSelectedShed(item: MyItem) {
    const current = this.searchForm.get('selectedItems')
      ?.value as MyItem[];
    if (current.indexOf(item) > -1) {
      // Remove it
      current.splice(current.indexOf(item), 1);
    } else {
      // Add it
      current.push(item);
    }

    this.searchForm.get('selectedItems')?.patchValue(current);
  }

This function is basically run for every item during any change.

Is there another option which allows the filtering but doesn't have these drawbacks?

I have dropped the filtering requirement to pursue the simple checkbox array. This too seems problematic Stackblitz to my work

Upvotes: 1

Views: 921

Answers (2)

Geo242
Geo242

Reputation: 597

Here is a way to solve this with using a FormArray and a pipe to do the filtering. The selected state is stored in each of the form array items, so you never need to check it's selected state on each item.

Component

const items: Item[] = [
  { id: 1, name: 'Item 1', selected: false },
  { id: 2, name: 'Item 2', selected: false },
  { id: 3, name: 'Item 3', selected: false },
  { id: 4, name: 'Item 4', selected: false },
  { id: 5, name: 'Item 5', selected: false },
];

@Component({
  selector: 'app-select-list',
  standalone: true,
  imports: [FormsModule, ReactiveFormsModule, FilterItemsPipe],
  template: `
    <input type="text" [(ngModel)]="filterText">
    <form [formGroup]="form">
      <ul formArrayName="items">
        @for (item of itemsFormArray.controls | filterItems : filterText; track $index) {
          <li [formGroup]="item">
            <label>
              <input type="checkbox" formControlName="selected">
              {{ item.value.name }}
            </label>
          </li>
        }
      </ul>
    </form>
  `,
})
export class SelectListComponent {
  filterText = '';
  itemsFormArray = new FormArray(items.map((item) => new FormGroup({
    selected: new FormControl(item.selected),
    id: new FormControl(item.id),
    name: new FormControl(item.name),
  })));
  form = new FormGroup({
    items: this.itemsFormArray,
  });
}

Pipe

@Pipe({
  name: 'filterItems',
  standalone: true,
})
export class FilterItemsPipe implements PipeTransform {

  transform(items: FormGroup[], filterText: string | undefined | null): FormGroup[] {
    if (!filterText) {
      return items;
    }
    return items.filter((item) => (item.value.name ?? '').toLowerCase().includes(filterText.toLowerCase()));
  }

}

Upvotes: 0

K2D2
K2D2

Reputation: 31

I solved this by creating two lists then modifying the display:

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';

@Component({
  selector: 'app-select-list-example',
  templateUrl: './select-list-example.component.html',
  styleUrls: ['./select-list-example.component.css']
})
export class SelectListExampleComponent implements OnInit {
  items = [{ name: 'Item 1', code: '001' }, { name: 'Item 2', code: '002' }]; // Example items array
  filteredItems = [...this.items]; // Initialize filteredItems with the full items array
  selectForm: FormGroup;

  constructor() {
    // Initialize the form with an empty array for selected items
    this.selectForm = new FormGroup({
      selectedItems: new FormControl([]),
    });
  }

  ngOnInit() {
    this.applyFilter(''); // Apply no filter initially
  }

  applyFilter(filterValue: string) {
    // Filter the items based on the filterValue
    // For simplicity, this example filters by name
    this.filteredItems = this.items.filter(item =>
      item.name.toLowerCase().includes(filterValue.toLowerCase())
    );
  }

  toggleSelected(item: any) {
    const currentSelectedItems = this.selectForm.get('selectedItems')?.value;
    const index = currentSelectedItems.findIndex(selectedItem => selectedItem.code === item.code);
    if (index > -1) {
      // Item is already selected, remove it
      currentSelectedItems.splice(index, 1);
    } else {
      // Item is not selected, add it
      currentSelectedItems.push(item);
    }
    this.selectForm.get('selectedItems')?.setValue([...currentSelectedItems]);
  }

  isSelected(item: any): boolean {
    const currentSelectedItems = this.selectForm.get('selectedItems')?.value;
    return currentSelectedItems.some(selectedItem => selectedItem.code === item.code);
  }
}

and then the html:

<div class="w-full flex-column my-list d-flex align-items-center justify-content-start">
      <form [formGroup]="selectForm">
        <div *ngFor="let item of filteredItems">
          <mat-checkbox class="w-full" [checked]="isSelected(item)" (click)="toggleSelected(item)">
            {{item.name}} ({{item.code}})
          </mat-checkbox>
        </div>
      </form>
    </div>

Upvotes: 0

Related Questions