Reputation: 1195
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
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
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