How to implement toggle all option in Angular Material Select

In this quick tutorial, we will learn how to implement a toggle all option in mat-select

26th January, 2024 — 7 min read

comment Comments share Share this article code Open in StackBlitz
Photo by Jerin J
In this tutorial, we will learn how to implement toggle all functionality in `mat-select` using a directive. ## `mat-select` `` is a form control for selecting a value from a set of options, similar to the native `` element. It is designed to work inside of a [``]( element. To add options to the select, add `` elements to the ``. Each `` 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 `` 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: ```ts 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([]); } ``` ```html Toppings {{toppings.value?.[0] || ''}} @if ((toppings.value?.length || 0) > 1) { (+{{ (toppings.value?.length || 0) - 1 }} {{ toppings.value?.length === 2 ? "other" : "others" }}) } @for (topping of toppingList; track topping) { {{ topping }} } ``` 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: ```html Toppings Select all @for (topping of toppingList; track topping) { {{ topping }} } ``` Out goal is to attach a directive to select all option and achieve the toggle all functionality through directive. ```html Select all ``` ## Directive Let's create a directive: ```ts import { Directive, } from '@angular/core'; @Directive({ selector: 'mat-option[selectAll]', standalone: true, }) export class SelectAllDirective{} ``` ### `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`: ```ts @Input({ required: true }) allValues: any[] = []; ``` ### `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: ```ts private _matSelect = inject(MatSelect); private _matOption = inject(MatOption); ``` 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: ```ts @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);; } else { parentFormControl?.setValue([]); this._matOption.deselect(false); } } }) ); } ngOnDestroy(): void { this._subscriptions.forEach((s) => s.unsubscribe()); } } ``` 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. ```ts 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) {; } } } }) ); } ``` 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: ```ts 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) {; } }); } ``` 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 ``. 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: ```html Pokemon {{pokemonControl.value?.[0] || ''}} @if ((pokemonControl.value?.length || 0) > 1) { (+{{ (pokemonControl.value?.length || 0) - 1 }} {{ pokemonControl.value?.length === 2 ? "other" : "others" }}) } Select all @for (group of pokemonGroups; track group) { @for (pokemon of group.pokemon; track pokemon) { {{ pokemon.viewValue }} } } ``` ```ts 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([]); 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); } } ``` 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
Dharmen Shah
Written by Dharmen Shah

I have around 8+ years of experience in IT industry. I have got opportunity to work at different companies with different technologies, mostly focused on Front-end, like Angular, React, Next, vanilla web stack (HTML, CSS, JavaScript).

You can find me on Twitter, Linkedin and Github.

Discuss online

Share this article

Read more

View all articles

Learn more

View courses page

Support Free Content Creation

Contributions & Support

Even though the courses and articles are available at no cost, your support in my endeavor to deliver top-notch educational content would be highly valued. Your decision to contribute aids me in persistently improving the course, creating additional resources, and maintaining the accessibility of these materials for all. I'm grateful for your consideration to contribute and make a meaningful difference!

potted_plant Help me Grow
Don't miss any update

Stay up to date

Subscribe to the newsletter to stay up to date with articles, courses and much more!

Angular Material Dev

Angular Material Dev is one place stop for developers to learn about integrating Material Design in Angular applications like a pro.

Find us on X (Twitter)