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>([]);
}
<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>
With above code, the output looks like below:
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>
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>
Directive
Let's create a directive:
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
:
@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:
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:
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());
}
}
Below is the explanation of what's going on in above code:
- We are maintaining all
_subscriptions
, so that we can unsubscribe from them when component is destroyed - In
ngAfterViewInit
- We are first storing parent
mat-select
'sAbstractControl
- Then, we are listening for select-all option's
onSelectionChange
event - And if the change has been made by user's action, then
- If select-all is checked, then we are setting
allValues
inparentFormControl
and we are also triggeringselect
. Notice that we are passingfalse
withselect
method, reason behind that is we don't want to trigger any further events. - Else, we are setting blank array
[]
inparentFormControl
and we are also triggeringdeselect
.
- If select-all is checked, then we are setting
- We are first storing parent
- Lastly, in
ngOnDestroy
, we are unsubscribing from all_subscriptions
Let's look at the output:
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);
}
}
}
})
);
}
Let's understand what's going on here:
- We are listening to
optionSelectionChanges
event - If it's user-action, option has value and it's not select-all option, then
- 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 - Else, if length of selected values is same as
allValues.length
, then we are triggeringselect
ofselect-all
. You can write your own comparison logic here.
- If option is not selected, then we are triggering
Let's look at the output:
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);
}
});
}
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>
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);
}
}
Notice how we are getting all enabled pokemons through get enabledPokemons
and setting it in allValues
. Let's look at the output:
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.
Top comments (2)
Thanks for sharing !
👍😊