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 [``](https://material.angular.io/components/form-field/overview) 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:
```angular-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([]);
}
```
```angular-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](/assets/angular-material.dev/mat-select-all-simple-mat-select.gif)
## Select All Option
Let's add an option on top to handle toggle all functionality:
```angular-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.
```angular-html
Select all
```
## Directive
Let's create a directive:
```angular-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`:
```angular-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:
```angular-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:
```angular-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);
this._matOption.select(false);
} 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](/assets/angular-material.dev/mat-select-all-toggle-all-options.gif)
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.
```angular-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) {
this._matOption.select(false);
}
}
}
})
);
}
```
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](/assets/angular-material.dev/mat-select-all-toggle-select-all.gif)
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:
```angular-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) {
this._matOption.select(false);
}
});
}
```
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:
```angular-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 }}
}
}
```
```angular-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](/assets/angular-material.dev/mat-select-all-group.gif)
## 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