32

I have scenario as below:

enter image description here

I want to achieve is:

  1. When user click on All then all options shall be selected and when user click All again then all options shall be deselcted.
  2. If All option is checked and user click any other checkbox than All then All and clicked checkbox shall be deselected.
  3. When user selects 4 options one by one then All shall be selected.

HTML file

<mat-select placeholder="User Type" formControlName="UserType" multiple>
    <mat-option *ngFor="let filters of userTypeFilters" [value]="filters.key">
          {{filters.value}}
    </mat-option>
        <mat-option #allSelected (click)="toggleAllSelection()" [value]="0">All</mat-option>
</mat-select>

TS file

this.searchUserForm = this.fb.group({
  userType: new FormControl('')
});

userTypeFilters = [
  {
    key: 1, value: 'Value 1',
  },
  {
    key: 2, value: 'Value 2',
  },
  {
    key: 3, value: 'Value 3',
  },
  {
    key: 4, value: 'Value 4',
  }
]

toggleAllSelection() {
  if (this.allSelected.selected) {
    this.searchUserForm.controls.userType
    .patchValue([...this.userTypeFilters.map(item => item.key), 0]);
  } else {
    this.searchUserForm.controls.userType.patchValue([]);
  }
}

Now, how to achieve 2nd and 3rd point

Stackblitz is: https://stackblitz.com/edit/angular-material-with-angular-v5-znfehg?file=app/app.component.html

Gags
  • 3,759
  • 8
  • 49
  • 96

6 Answers6

50

Use code as below create function on click each mat-option and select()/deselect() all option:

See stackblitz:https://stackblitz.com/edit/angular-material-with-angular-v5-jsgvx6?file=app/app.component.html

TS:

togglePerOne(all){ 
   if (this.allSelected.selected) {  
    this.allSelected.deselect();
    return false;
}
  if(this.searchUserForm.controls.userType.value.length==this.userTypeFilters.length)
    this.allSelected.select();

}
  toggleAllSelection() {
    if (this.allSelected.selected) {
      this.searchUserForm.controls.userType
        .patchValue([...this.userTypeFilters.map(item => item.key), 0]);
    } else {
      this.searchUserForm.controls.userType.patchValue([]);
    }
  }

HTML:

<form [formGroup]="searchUserForm" fxFlex fxLayout="column" autocomplete="off" style="margin: 30px">
    <mat-select placeholder="User Type" formControlName="userType" multiple>
        <mat-option *ngFor="let filters of userTypeFilters" [value]="filters.key" (click)="togglePerOne(allSelected.viewValue)">
            {{filters.value}}
        </mat-option>
        <mat-option #allSelected (click)="toggleAllSelection()" [value]="0">All</mat-option>
    </mat-select>
</form>
Arkadi
  • 1,153
  • 1
  • 14
  • 35
לבני מלכה
  • 15,925
  • 2
  • 23
  • 47
  • 1
    And if I want to avoid seeing "all" in the list of selected items, what do I do? – Raul Jun 29 '21 at 10:36
  • 1
    sorry, maybe I have not explained myself well, I would like to avoid it being in the list of selected items, but I would not want to remove the respective checkbox – Raul Jun 29 '21 at 10:51
  • @Raul Use mat-select-trigger to change the title of selected items. – AmirHossein Rezaei Feb 25 '23 at 10:27
37

Simply you can do it without adding a new option to your data source by adding a checkbox.

See the: Demo

import { Component, VERSION, ViewChild } from '@angular/core';
import { FormControl } from '@angular/forms';
import { MatSelect } from '@angular/material/select';
import { MatOption } from '@angular/material/core';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  @ViewChild('select') select: MatSelect;

  allSelected=false;
   foods: any[] = [
    {value: 'steak-0', viewValue: 'Steak'},
    {value: 'pizza-1', viewValue: 'Pizza'},
    {value: 'tacos-2', viewValue: 'Tacos'}
  ];
  toggleAllSelection() {
    if (this.allSelected) {
      this.select.options.forEach((item: MatOption) => item.select());
    } else {
      this.select.options.forEach((item: MatOption) => item.deselect());
    }
  }
   optionClick() {
    let newStatus = true;
    this.select.options.forEach((item: MatOption) => {
      if (!item.selected) {
        newStatus = false;
      }
    });
    this.allSelected = newStatus;
  }
}
.select-all{
  margin: 5px 17px;
} 
<mat-form-field>
  <mat-label>Favorite food</mat-label>
  <mat-select  #select multiple>
    <div class="select-all">
        <mat-checkbox [(ngModel)]="allSelected"
                        [ngModelOptions]="{standalone: true}"
                        (change)="toggleAllSelection()">Select All</mat-checkbox>
    </div>
    <mat-option (click)="optionClick()" *ngFor="let food of foods" [value]="food.value">
      {{food.viewValue}}
    </mat-option>
  </mat-select>
</mat-form-field>
Yaseen
  • 598
  • 6
  • 7
  • 2
    This should be the accepted answer, as it doesn't add an unnecessary extra item to the values. – Norbert Huurnink Aug 05 '20 at 13:31
  • 3
    I forked you demo. Instead a checkbox I did it using a button. Encapsulating it in a custom component, check it out: https://stackblitz.com/edit/angular-material-select-all-button – marcosalpereira May 11 '21 at 12:38
20

Another way to do this is with the @ViewChild selector to get the mat-select component and troggle the mat-options items selected or unselected. We need also a variable to save the selected actual status to select or unselect all the elements on every click. Hope will help.

  import {MatOption, MatSelect} from "@angular/material";
  
  export class ExampleAllSelector {
  
    myFormControl = new FormControl();
    elements: any[] = [];

    allSelected = false;

    @ViewChild('mySel') skillSel: MatSelect;

    constructor() {}

    toggleAllSelection() {
      this.allSelected = !this.allSelected;  // to control select-unselect
      
      if (this.allSelected) {
        this.skillSel.options.forEach( (item : MatOption) => item.select());
      } else {
        this.skillSel.options.forEach( (item : MatOption) => {item.deselect()});
      }
      this.skillSel.close();
    }
  }
      <mat-select #mySel placeholder="Example" [formControl]="myFormControl" multiple>
        <mat-option [value]="0" (click)="toggleAllSelection()">All items</mat-option>
        <mat-option *ngFor="let element of elements" [value]="element">{{skill.name}}</mat-option>
      </mat-select>
Marc
  • 289
  • 2
  • 5
  • skillAllSelected what should be? – Takatalvi Mar 27 '20 at 22:25
  • 2
    @Takatalvi It should be "allSelected" – Antonio Stipić Mar 30 '20 at 13:39
  • Thanks, this worked for me in a case with a loop of selects and instead of using viewChild, i used @ViewChildren('mySel') skillSel: QueryList; and to get access to the options should be something like const select = this.skillSel.toArray()[index]; – Takatalvi Mar 30 '20 at 20:54
  • @Marc Hi..how can we use the same logic if the select box is dynamic, like on click button multiple select box will be added in DOM, need to do that same functionality in all textbox! – mevr May 10 '22 at 08:35
  • this solution emits valueChanges after each item.select() call, which may cause issues in some scenarios – Juraj Suchár May 16 '23 at 13:46
5

Here is an example of how to extend a material option component.

See stackblitz Demo

Component:

import { ChangeDetectorRef, Component, ElementRef, HostListener, HostBinding, Inject, Input, OnDestroy, OnInit, Optional } from '@angular/core';
import { MAT_OPTION_PARENT_COMPONENT, MatOptgroup, MatOption, MatOptionParentComponent } from '@angular/material/core';
import { AbstractControl } from '@angular/forms';
import { MatPseudoCheckboxState } from '@angular/material/core/selection/pseudo-checkbox/pseudo-checkbox';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Component({
  selector: 'app-select-all-option',
  templateUrl: './select-all-option.component.html',
  styleUrls: ['./select-all-option.component.css']
})
export class SelectAllOptionComponent extends MatOption implements OnInit, OnDestroy {
  protected unsubscribe: Subject<any>;

  @Input() control: AbstractControl;
  @Input() title: string;
  @Input() values: any[] = [];

  @HostBinding('class') cssClass = 'mat-option';

  @HostListener('click') toggleSelection(): void {
    this. _selectViaInteraction();

    this.control.setValue(this.selected ? this.values : []);
  }

  constructor(elementRef: ElementRef<HTMLElement>,
              changeDetectorRef: ChangeDetectorRef,
              @Optional() @Inject(MAT_OPTION_PARENT_COMPONENT) parent: MatOptionParentComponent,
              @Optional() group: MatOptgroup) {
    super(elementRef, changeDetectorRef, parent, group);

    this.title = 'Select All';
  }

  ngOnInit(): void {
    this.unsubscribe = new Subject<any>();

    this.refresh();

    this.control.valueChanges
      .pipe(takeUntil(this.unsubscribe))
      .subscribe(() => {
        this.refresh();
      });
  }

  ngOnDestroy(): void {
    super.ngOnDestroy();

    this.unsubscribe.next();
    this.unsubscribe.complete();
  }

  get selectedItemsCount(): number {
    return this.control && Array.isArray(this.control.value) ? this.control.value.filter(el => el !== null).length : 0;
  }

  get selectedAll(): boolean {
    return this.selectedItemsCount === this.values.length;
  }

  get selectedPartially(): boolean {
    const selectedItemsCount = this.selectedItemsCount;

    return selectedItemsCount > 0 && selectedItemsCount < this.values.length;
  }

  get checkboxState(): MatPseudoCheckboxState {
    let state: MatPseudoCheckboxState = 'unchecked';

    if (this.selectedAll) {
      state = 'checked';
    } else if (this.selectedPartially) {
      state = 'indeterminate';
    }

    return state;
  }

  refresh(): void {
    if (this.selectedItemsCount > 0) {
      this.select();
    } else {
      this.deselect();
    }
  }
}

HTML:

<mat-pseudo-checkbox class="mat-option-pseudo-checkbox"
                     [state]="checkboxState"
                     [disabled]="disabled"
                     [ngClass]="selected ? 'bg-accent': ''">
</mat-pseudo-checkbox>

<span class="mat-option-text">
  {{title}}
</span>

<div class="mat-option-ripple" mat-ripple
     [matRippleTrigger]="_getHostElement()"
     [matRippleDisabled]="disabled || disableRipple">
</div>

CSS:

.bg-accent {
  background-color: #2196f3 !important;
}
Dinesh
  • 1,711
  • 2
  • 20
  • 41
Artem Horovyi
  • 51
  • 1
  • 4
  • I love this solution: is there a way to make this keyboard friendly? looks like the custom option is completely ignored by the MatSelect component.. – fusio Jul 01 '20 at 09:21
  • 2
    On Angular 10 I had to remove the call _selectViaInteraction because the Select All option was not changing the state. – Ariel Moraes Oct 26 '20 at 23:45
  • Excellent answer. But I was unable to uncheck select all after clicking select all. I had to remove this. _selectViaInteraction(); in click hostlistener because it is getting called from somewhere (It is toggling this.selected). So calling the method again toggles back the selected property . I am using angular 14. – A Yashwanth Nov 04 '22 at 19:34
0

Another possible solution:

using <mat-select [(value)]="selectedValues" in the template and set the selectedValues via toggle function in the component.

Working Stackblitz Demo.

Component

export class AppComponent {

  selectedValues: any;
  allSelected = false;

   public displayDashboardValues = [
    {key:'0', valuePositionType: 'undefined', viewValue:'Select all'},
    {key:'1', valuePositionType: 'profit-loss-area', viewValue:'result'},
    {key:'2', valuePositionType: 'cash-area', viewValue:'cash'},
    {key:'3', valuePositionType: 'balance-area', viewValue:'balance'},
    {key:'4', valuePositionType: 'staff-area' ,viewValue:'staff'},
    {key:'5', valuePositionType: 'divisions-area', viewValue:'divisions'},
    {key:'6', valuePositionType: 'commisions-area', viewValue:'commisions'},    
  ];

  toggleAllSelection() {
      this.allSelected = !this.allSelected;
      this.selectedValues = this.allSelected ? this.displayDashboardValues : [];
    }
}

Template

        <mat-select  [(value)]="selectedValues" (selectionChange)="selectionChange($event)" formControlName="dashboardValue" multiple>
          <mat-option [value]="displayDashboardValues[0]" (click)="toggleAllSelection()">{{ displayDashboardValues[0].viewValue }}</mat-option>
          <mat-divider></mat-divider>
          <div *ngFor="let dashboardPosition of displayDashboardValues">
            <mat-option class="dashboard-select-option" *ngIf="dashboardPosition.key>0" [value]="dashboardPosition">
              {{ dashboardPosition.viewValue }}
            </mat-option>
          </div>
        </mat-select>
zerocewl
  • 11,401
  • 6
  • 27
  • 53
0

There are some problems with other answers. The most important one is that they're listening to the click event which is not complete (user can select an option via space key on the keyboard).

I've created a component that solves all the problems:

@Component({
  selector: 'app-multi-select',
  templateUrl: './multi-select.component.html',
  styleUrls: ['./multi-select.component.scss'],
})
export class MultiSelectComponent<V> implements OnInit {
  readonly _ALL_SELECTED = '__ALL_SELECTED__' as const;
  @Input() options: ReadonlyArray<{ value: V; name: string }> = [];
  @Input('selectControl') _selectControl!: FormControl | AbstractControl | null | undefined;
  get selectControl(): FormControl {
    return this._selectControl as FormControl;
  }
  @Input() label: string = '';
  @Input() hasSelectAllOption = false;
  selectedValues: (V | '__ALL_SELECTED__')[] = [];

  constructor() {}

  ngOnInit(): void {}

  onSelectAllOptions({ isUserInput, source: { selected } }: MatOptionSelectionChange) {
    if (!isUserInput) return;
    this.setValues(selected ? this.options.map(o => o.value) : []);
  }

  private setValues(values: (V | '__ALL_SELECTED__')[]) {
    const hasAllOptions = ArrayUtils.arraysAreSame(
      values,
      this.options.map(o => o.value),
    );
    if (!values.includes(this._ALL_SELECTED)) {
      if (hasAllOptions) {
        values = [...values, this._ALL_SELECTED];
      }
    } else if (!hasAllOptions) {
      values = values.filter(o => o !== this._ALL_SELECTED);
    }

    setTimeout(() => {
      this.selectedValues = values;
    });
    this.selectControl.setValue(values.filter(o => (o as any) !== this._ALL_SELECTED));
  }

  onSelectOtherOptions({ isUserInput, source: { selected, value } }: MatOptionSelectionChange) {
    if (!isUserInput) return;
    this.setValues(
      selected ? [...this.selectedValues, value] : this.selectedValues.filter(o => o !== value),
    );
  }
}


<mat-form-field>
  <mat-label>Choose some options</mat-label>
  <mat-select multiple [value]="selectedValues">
    <mat-option
      *ngFor="let d of options"
      [value]="d.value"
      (onSelectionChange)="onSelectOtherOptions($event)"
    >
      {{ d.name }}
    </mat-option>
    <mat-option
      *ngIf="hasSelectAllOption"
      [value]="_ALL_SELECTED"
      (onSelectionChange)="onSelectAllOptions($event)"
    >
      Select all
    </mat-option>
  </mat-select>
</mat-form-field>

Vahid
  • 6,639
  • 5
  • 37
  • 61