How to implement toggle all option in Angular Material Select

shhdharmen

Dharmen Shah

Posted on January 27, 2024

How to implement toggle all option in Angular Material Select

In this tutorial, we will learn how to implement toggle all functionality in mat-select using a directive.

mat-select

<mat-select> is a form control for selecting a value from a set of options, similar to the native <select> element. It is designed to work inside of a <mat-form-field> element.

To add options to the select, add <mat-option> elements to the <mat-select>. Each <mat-option> has a value property that can be used to set the value that will be selected if the user chooses this option. The content of the <mat-option> is what will be shown to the user.

Below is the very basic example of mat-select:

Now, generally in the applications, we sometimes need to provide an option so that users can simply select or deselect all the options. mat-select does not have that feature provided by default, but we can easily achieve it using a directive.

Simple MatSelect

Let's start by creating a simple mat-select. which will allow users to select toppings:

import { Component } from '@angular/core';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    MatFormFieldModule,
    MatSelectModule,
    FormsModule,
    ReactiveFormsModule,
    MatInputModule,
  ],
  templateUrl: './app.component.html',
})
export class AppComponent {
  toppingList: string[] = [
    'Extra cheese',
    'Mushroom',
    'Onion',
    'Pepperoni',
    'Sausage',
    'Tomato',
  ];
  toppings = new FormControl<string[] | undefined>([]);
}

Enter fullscreen mode Exit fullscreen mode
<mat-form-field>
  <mat-label>Toppings</mat-label>
  <mat-select [formControl]="toppings" multiple>
    <mat-select-trigger>
      {{toppings.value?.[0] || ''}}
      @if ((toppings.value?.length || 0) > 1) {
      <span class="additional-selection">
        (+{{ (toppings.value?.length || 0) - 1 }}
        {{ toppings.value?.length === 2 ? "other" : "others" }})
      </span>
      }
    </mat-select-trigger>
    @for (topping of toppingList; track topping) {
    <mat-option [value]="topping">{{ topping }}</mat-option>
    }
  </mat-select>
</mat-form-field>
Enter fullscreen mode Exit fullscreen mode

With above code, the output looks like below:

simple mat-select output

Select All Option

Let's add an option on top to handle toggle all functionality:

<mat-form-field>
  <mat-label>Toppings</mat-label>
  <mat-select [formControl]="toppings" multiple>
    <!-- mat-select-trigger remains same -->

    <!-- 📢 Notice that we are adding below option -->
    <mat-option value="select-all">Select all</mat-option>

    @for (topping of toppingList; track topping) {
        <mat-option [value]="topping">{{ topping }}</mat-option>
    }
  </mat-select>
</mat-form-field>
Enter fullscreen mode Exit fullscreen mode

Out goal is to attach a directive to select all option and achieve the toggle all functionality through directive.

<mat-option value="select-all" selectAll>Select all</mat-option>
Enter fullscreen mode Exit fullscreen mode

Directive

Let's create a directive:

import {
  Directive,
} from '@angular/core';

@Directive({
  selector: 'mat-option[selectAll]',
  standalone: true,
})
export class SelectAllDirective{}

Enter fullscreen mode Exit fullscreen mode

allValues input

First of all, we will need to tell the directive that what are all the values, so that when user toggle all, all values needs to be selected. So, we will add an input called allValues:

@Input({ required: true }) allValues: any[] = [];
Enter fullscreen mode Exit fullscreen mode

MatOption and MatSelect

Second, we will need to access the host mat-option and the parent mat-select so that we can toggle values through them:

private _matSelect = inject(MatSelect);
private _matOption = inject(MatOption);
Enter fullscreen mode Exit fullscreen mode

Before moving further, familiarize yourself with some important methods of MatSelect and MatOption:

Index Component Method Description
1 MatSelect optionSelectionChanges Combined stream of all of the child options' change events.
2 MatOption onSelectionChange Event emitted when the option is selected or deselected.
3 MatOption select(emitEvent?: boolean) Selects the option.
4 MatOption deselect(emitEvent?: boolean) Deselects the option.

Toggle all options

Now, it's time to write the implementation to toggle all options when select all is selected or deselected. We will write this implementation in ngAfterViewInit hook:

Did you know? The ngAfterViewInit method runs once after all the children in the component's template (its view) have been initialized.

@Directive({
  selector: 'mat-option[selectAll]',
  standalone: true,
})
export class SelectAllDirective implements AfterViewInit, OnDestroy {
    @Input({ required: true }) allValues: any[] = [];

    private _matSelect = inject(MatSelect);
    private _matOption = inject(MatOption);

    private _subscriptions: Subscription[] = [];

    ngAfterViewInit(): void {
        const parentSelect = this._matSelect;
        const parentFormControl = parentSelect.ngControl.control;

        // For changing other option selection based on select all
        this._subscriptions.push(
            this._matOption.onSelectionChange.subscribe((ev) => {
                if (ev.isUserInput) {
                    if (ev.source.selected) {
                        parentFormControl?.setValue(this.allValues);
                        this._matOption.select(false);
                    } else {
                        parentFormControl?.setValue([]);
                        this._matOption.deselect(false);
                    }
                }
            })
        );
    }

    ngOnDestroy(): void {
        this._subscriptions.forEach((s) => s.unsubscribe());
    }
}
Enter fullscreen mode Exit fullscreen mode

Below is the explanation of what's going on in above code:

  1. We are maintaining all _subscriptions, so that we can unsubscribe from them when component is destroyed
  2. In ngAfterViewInit
    1. We are first storing parent mat-select's AbstractControl
    2. Then, we are listening for select-all option's onSelectionChange event
    3. And if the change has been made by user's action, then
      1. If select-all is checked, then we are setting allValues in parentFormControl and we are also triggering select. Notice that we are passing false with select method, reason behind that is we don't want to trigger any further events.
      2. Else, we are setting blank array [] in parentFormControl and we are also triggering deselect.
  3. Lastly, in ngOnDestroy, we are unsubscribing from all _subscriptions

Let's look at the output:

output after implementing toggle all option

It's working as expected. But, if you select all options one-by-one, it will not mark select-all as selected, let's fix it.

Toggle select all based on options' selection

We will listen to mat-select's optionSelectionChanges, and based on options' selection, we will toggle select-all.

  ngAfterViewInit(): void {
    // rest remains same

    // For changing select all based on other option selection
    this._subscriptions.push(
      parentSelect.optionSelectionChanges.subscribe((v) => {
        if (v.isUserInput && v.source.value !== this._matOption.value) {
          if (!v.source.selected) {
            this._matOption.deselect(false);
          } else {
            if (parentFormControl?.value.length === this.allValues.length) {
              this._matOption.select(false);
            }
          }
        }
      })
    );
  }
Enter fullscreen mode Exit fullscreen mode

Let's understand what's going on here:

  1. We are listening to optionSelectionChanges event
  2. If it's user-action, option has value and it's not select-all option, then
    1. If option is not selected, then we are triggering deselect of select-all. Because, if any one option is not selected, that means select-all needs to be deselected
    2. Else, if length of selected values is same as allValues.length, then we are triggering select of select-all. You can write your own comparison logic here.

Let's look at the output:

output after implementing toggle select all based on options' selection

One more scenario remaining is when the mat-select's form-control has all values selected by default. Let's implement that.

Initial state

If all values are selected, then we will trigger select for select-all option:

  ngAfterViewInit(): void {
    // rest remains same

    // If user has kept all values selected in select's form-control from the beginning
    setTimeout(() => {
      if (parentFormControl?.value.length === this.allValues.length) {
        this._matOption.select(false);
      }
    });
  }
Enter fullscreen mode Exit fullscreen mode

That's all! With this we have covered all the scenarios.

Groups of options

You might wonder how to manage the same functionality for group of options, where we use <mat-optgroup>. Our current implementation also supports select-all for groups of options, you will just need to pass correct set of options in allValues input. Let's look at an example:

<mat-form-field>
  <mat-label>Pokemon</mat-label>
  <mat-select [formControl]="pokemonControl" multiple>
    <mat-select-trigger>
      {{pokemonControl.value?.[0] || ''}}
      @if ((pokemonControl.value?.length || 0) > 1) {
      <span class="additional-selection">
        (+{{ (pokemonControl.value?.length || 0) - 1 }}
        {{ pokemonControl.value?.length === 2 ? "other" : "others" }})
      </span>
      }
    </mat-select-trigger>

    <!-- 📢 Notice below select-all option -->
    <mat-option
      value="select-all"
      selectAll
      [allValues]="enabledPokemons"
      >Select all</mat-option
    >


    @for (group of pokemonGroups; track group) {
    <mat-optgroup [label]="group.name" [disabled]="group.disabled">
      @for (pokemon of group.pokemon; track pokemon) {
      <mat-option
        class="group-option"
        [value]="pokemon.value"
        [disabled]="group.disabled"
        >{{ pokemon.viewValue }}</mat-option
      >
      }
    </mat-optgroup>
    }
  </mat-select>
</mat-form-field>
Enter fullscreen mode Exit fullscreen mode
interface Pokemon {
  value: string;
  viewValue: string;
}

interface PokemonGroup {
  disabled?: boolean;
  name: string;
  pokemon: Pokemon[];
}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    MatFormFieldModule,
    MatSelectModule,
    FormsModule,
    ReactiveFormsModule,
    MatInputModule,
    SelectAllDirective,
  ],
  templateUrl: './app.component.html',
})
export class AppComponent {

  pokemonControl = new FormControl<string[] | undefined>([]);
  pokemonGroups: PokemonGroup[] = [
    {
      name: 'Grass',
      pokemon: [
        { value: 'bulbasaur-0', viewValue: 'Bulbasaur' },
        { value: 'oddish-1', viewValue: 'Oddish' },
        { value: 'bellsprout-2', viewValue: 'Bellsprout' },
      ],
    },
    {
      name: 'Water',
      pokemon: [
        { value: 'squirtle-3', viewValue: 'Squirtle' },
        { value: 'psyduck-4', viewValue: 'Psyduck' },
        { value: 'horsea-5', viewValue: 'Horsea' },
      ],
    },
    {
      name: 'Fire',
      disabled: true,
      pokemon: [
        { value: 'charmander-6', viewValue: 'Charmander' },
        { value: 'vulpix-7', viewValue: 'Vulpix' },
        { value: 'flareon-8', viewValue: 'Flareon' },
      ],
    },
    {
      name: 'Psychic',
      pokemon: [
        { value: 'mew-9', viewValue: 'Mew' },
        { value: 'mewtwo-10', viewValue: 'Mewtwo' },
      ],
    },
  ];

  get enabledPokemons() {
    return this.pokemonGroups
      .filter((p) => !p.disabled)
      .map((p) => p.pokemon)
      .flat()
      .map((p) => p.value);
  }
}

Enter fullscreen mode Exit fullscreen mode

Notice how we are getting all enabled pokemons through get enabledPokemons and setting it in allValues. Let's look at the output:

output for group of options

Conclusion

We learned that by using methods of MatOption and MatSelect, we can create a directive which can help in achieving the toggle all functionality.

Live Playground

💖 💪 🙅 🚩
shhdharmen
Dharmen Shah

Posted on January 27, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related