mat autocomplete not filtering properly in angular

Viewed 1937

I'm customizing angular material select/autocomplete to allow nested dropdowns.

Here, I was trying to search both parent or child values, but it doesn't filtered out.

Also, it should display like Alaska (+2 others).

STACKBLITZ

<mat-form-field appearance="fill">
  <mat-label>Toppings</mat-label>
    <!-- <input type="text" matInput placeholder="States Group" formControlName="stateGroup" required [matAutocomplete]="autoGroup">
     <mat-autocomplete #autoGroup="matAutocomplete"> -->
           <input type="text" matInput placeholder="Select Users" aria-label="Select Users" matInput [matAutocomplete]="auto" [formControl]="states">
<mat-autocomplete #auto="matAutocomplete">

  <!-- <mat-select [formControl]="states" multiple> -->
    <mat-select-trigger>
      {{states.value ? states.value[0] : ''}}
      <span *ngIf="states.value?.length > 1" class="example-additional-selection">
        (+{{states.value.length - 1}} {{states.value?.length === 2 ? 'other' : 'others'}})
      </span>
    </mat-select-trigger>

  <mat-optgroup *ngFor="let group of stateList">
          <div>
          <mat-checkbox [checked]="group.selected" (change)="toggleParent($event, group)"
            (click)="$event.stopPropagation()">
                    {{group.letter}}
                </mat-checkbox>
          <button mat-button (click)="expandDocumentTypes(group)">
            <mat-icon>keyboard_arrow_down</mat-icon>
          </button>
          </div>  
          <mat-option *ngFor="let name of group.names" [value]="name"
             [ngClass]="isExpandCategory[group.letter] ? 'list-show' : 'list-hide'">
             <mat-checkbox [checked]="group.checked" (change)="toggleSelection($event, name, group)" 
            (click)="$event.stopPropagation()">
                    {{name.type}}
                </mat-checkbox>
          </mat-option>
  </mat-optgroup>

  <!-- </mat-select> -->
</mat-autocomplete>
     <!-- </mat-autocomplete> -->
</mat-form-field>


export class SelectCustomTriggerExample {
  constructor(private _formBuilder: FormBuilder){}

  // stateForm: FormGroup = this._formBuilder.group({
  //   stateGroup: '',
  // });
  // toppings = new FormControl();
  isExpandCategory: boolean[] = [];
  toppingList: string[] = ['Extra cheese', 'Mushroom', 'Onion', 'Pepperoni', 'Sausage', 'Tomato'];
  stateRecord:any = [];
  states = new FormControl();

  expandDocumentTypes(group: any) {
    console.log("expanding dropdown", group);
    this.isExpandCategory[group.letter] = !this.isExpandCategory[group.letter];
    // expand only selected parent dropdown category with that childs
  }

toggleSelection(event:any, name: any, group: any) {
  debugger;
  console.log("toggleSelection", name, event.checked, group);
  if(event.checked) {
    console.log("stastateRecordtelist", this.stateRecord);
    this.stateRecord.push(name.type);
    this.states.setValue(this.stateRecord);
    console.log("toggleselection ", this.states.value);
  }
  else {
      this.stateRecord = this.stateRecord.filter((x:any) => x!== name.type);
      console.log("else toggleselection", name, group, this.states.value);
      this.states.setValue(this.states.value.filter((x:any) => x!== name.type));
      console.log("after filter ", this.states.value);
      //this.states.setValue([]);
  }
}

 toggleParent(event: any, group: any) {
    debugger;
    group.checked = event.checked;
    console.log("event", event.checked, "group", group, "states value", this.states.value);
    let states = this.states.value;
    states = states ? states : [];
    if(event.checked) {
      states.push(...group.names.filter((x: any) => !states.includes(x.type)).map((x: any) => x.type))
    } else {
      console.log("else", states);
      group.names.forEach((x: any) => {
          if(states.indexOf(x.type) > -1) {
            states.splice(states.indexOf(x.type), 1)
          }
        });
    }
    this.states.setValue(states);
    console.log("statesvalue", this.states.value);
      if(!event.checked) {
          this.states.setValue(this.states.value.filter((x:any) => !x.includes(group.names)))
        //this.states.setValue([]);
    }
    console.log("final statesvalue", this.states.value);
    this.stateRecord = this.states.value;
  }

  stateList = [
   {
      "letter":"A",
      "checked":false,
      "names":[
         {
            "id":1,
            "type":"Alabama"
         },
         {
            "id":2,
            "type":"Alaska"
         },
         {
            "id":3,
            "type":"Arizona"
         },
         {
            "id":4,
            "type":"Arkansas"
         }
      ]
   },
   {
      "letter":"C",
      "checked":false,
      "names":[
         {
            "id":8,
            "type":"California"
         },
         {
            "id":9,
            "type":"Colorado"
         },
         {
            "id":10,
            "type":"Connecticut"
         }
      ]
   },
   {
      "letter":"D",
      "checked":false,
      "names":[
         {
            "id":18,
            "type":"Delaware"
         },
         {
            "id":19,
            "type":"Denwer"
         }
      ]
   }
];
}

References:

https://stackblitz.com/edit/angular-evacck-qubgyy

https://stackblitz.com/angular/eboprqqnooy

Can someone help on this?

1 Answers

This task is quite complex and requires more clarifications and maybe can be solved by some dedicated library like https://www.npmjs.com/package/mat-select-autocomplete

But I like that you're moving forward with this custom control step by step and want to have something better than you already implemented.

Let's try to solve two your issues which you pointed out in this question:

It should filter both parent or child values

Angular material allows us to define custom filter. You can use similar logic in your example. Just create an Observable which is listener to FormControl changes:

this.stateGroupOptions$ = this.states.valueChanges.pipe(
      startWith(EMPTY_STRING),
      ...
      map(value => this._filterGroup(value))
    );
...

private _filterGroup(value: any) {
  if (!this.searching || !value.trim()) {
    return this.stateList;
  }

  value = value.trim().toLowerCase();

  return this.stateList
    .map(group => ({
      ...group,
      names: group.names.filter(item => item.type.toLowerCase().includes(value))
    }))
    .filter(
      group => group.letter.toLowerCase().includes(value) || group.names.length > 0
    );
}

and then use it in your options list

<mat-optgroup *ngFor="let group of stateGroupOptions$ | async">

It should display like Alaska (+2 others).

Again, there is dedicated functionality for this case - displayWith input property

@Input() displayWith: ((value: any) => string) | null Function that maps an option's control value to its display value in the trigger.

html

<mat-autocomplete ... [displayWith]="displayWith"

ts

displayWith(names: StateName[]) {
  if (!names || !names.length) {
    return "";
  }
  return (
    names[0].type +
    (names.length > 1
      ? ` (+${names.length - 1} ${names.length === 2 ? "other" : "others"})`
      : "")
  );
}

Stackblitz Example

The main trick in this custom implementation is to keep model consistent across different types of data: string(when user starts typying), Array(when you click on checkbox and update control manually) and Object(when you select option from dropdown).

NOTE: you can still optimize it and there are maybe many issue you haven't discovered yet.

Latest version

Related