26

I need to modify the functionality of the standard Angular Material toggle button component, so that clicking an active button returns the component to a state where no buttons are active. This has two steps:

  • Updating the value of the toggle group
  • Changing the 'checked' parameter of the clicked button to false

I've tried several approaches, e.g.

Template:

<mat-button-toggle-group #group="matButtonToggleGroup">
    <mat-button-toggle #no_btn value="no" (click)="update_toggle(group,no_btn)">No</mat-button-toggle>
    <mat-button-toggle #yes_btn value="yes" (click)="update_toggle(group,yes_btn)">Yes</mat-button-toggle>
</mat-button-toggle-group>

JS:

update_toggle(group,button){
    if(group.value==""){
        group.value = button.value;
    }
    else
    {
        group.value = "";
    }
button.checked=!button.checked;
}

But this doesn't update the 'checked' value of the buttons, I guess because the group value set by update_toggle() is in conflict with the user action of clicking the button.

The only approach which has worked is:

<mat-button-toggle-group #group="matButtonToggleGroup">
    <mat-button-toggle #no_btn value="no" (click)="update_toggle(group,no_btn)" (click)="group.value=='no' ? checked=false : checked=false">No</mat-button-toggle>
    <mat-button-toggle #yes_btn value="yes" (click)="update_toggle(group,yes_btn)" (click)="group.value=='yes' ? checked=false : checked=false">Yes</mat-button-toggle>
</mat-button-toggle-group>

But two click events on a single button feels very hacky, especially as the ternary in the second click is opposite to what it instinctively should be.

Any suggestions for a better approach?

I've tried:

@ViewChildren('no_btn') no_btn: ElementRef;

and then:

this.no_btn['_results'][k]['_inputElement']['nativeElement']['checked']=false;

But the result doesn't seem to be any different from passing the button reference in the function; clicking the button a second time doesn't uncheck it. Disabling does work, so I think my code is correct:

this.no_btn['_results'][k]['_inputElement']['nativeElement']['disabled']=true;
3
  • why not use @ViewChild()'s to get the instance of the checkbox and change it that way? Commented Jul 11, 2018 at 9:42
  • Have tried this - will update question. Commented Jul 11, 2018 at 10:50
  • Wouldn't that change the value of the button itself, rather than whether or not it's checked? Or do you mean the value of the group, rather than the individual button? Commented Jul 11, 2018 at 10:57

4 Answers 4

32

A simple generic solution that doesn't require resorting to events on each button or click events at all, and doesn't require ViewChildren or hard-coding value checks, is to use the button toggle in multiple mode and manage selections through the group change event. The change event gives you all you need so there is no need to access child components directly:

<mat-button-toggle-group multiple (change)="toggleChange($event)">
    <mat-button-toggle value="no">No</mat-button-toggle>
    <mat-button-toggle value="yes">Yes</mat-button-toggle>
</mat-button-toggle-group>

toggleChange(event) {
    const toggle = event.source;
    if (toggle && event.value.some(item => item == toggle.value)) {
        toggle.buttonToggleGroup.value = [toggle.value];
    }
}

Here it is on Stackblitz.

Sign up to request clarification or add additional context in comments.

7 Comments

Great solution - thanks @gtranter. If anyone finds this is not working, you may need to update Angular Material, in my case from 5.2.2 to 6.4.1.
what if you want to uncheck all of them in function that has no $event.source ?
The example supports unchecking all options when there is no event source.
Still works perfectly fine in 2023 with Angular Material v15!
This works well except the form control value is now an array instead of a single value.
|
2

Here is another solution. Note that the change event occurs before the click event.

Component

toggleChanged: boolean;

onChange() {
  this.toggleChanged = true;
}

onClick(group: MatButtonToggleGroup) {
  if (!this.toggleChanged) group.value = null;
  this.toggleChanged = false;
}

Template

<mat-button-toggle-group [(value)]="myValue" #group="matButtonToggleGroup"
    (change)="onChange()" (click)="onClick(group)">
    <mat-button-toggle *ngFor="let item of items" [value]="item.value">
        {{ item.name }}
    </mat-button-toggle>
</mat-button-toggle-group>

1 Comment

Will the change event always occur before the click event, or is it like that because of the Angular implementation? In any case, from what I was able to test, your solution works well!
1

According to source code (^6.4.1) the change event (of MatButtonToggle) fires at the mouse click always:

https://github.com/angular/material2/blob/6.4.7/src/lib/button-toggle/button-toggle.ts#L461

We can subscribe to all toggle buttons (or only those buttons that we want to make uncheked):

<mat-button-toggle-group #group="matButtonToggleGroup">
    <mat-button-toggle value="no" (change)="onChange($event, group)">No</mat-button-toggle>
    <mat-button-toggle value="yes" (change)="onChange($event, group)">Yes</mat-button-toggle>
</mat-button-toggle-group>

And we can add a simple handler for these events and a field that stores the state:

private _activeValue = "";

onChange(event, group) {
  if (this._activeValue === event.value) {
    // make unchecked
    group.value = "";
    this._activeValue = "";
  } else {
    this._activeValue = event.value;
  }
}

This example is on Stackblitz

Another implementation:

We can bind the value.

<mat-button-toggle-group [value]="_activeValue">
    <mat-button-toggle value="no" (change)="onChange($event)">No</mat-button-toggle>
    <mat-button-toggle value="yes" (change)="onChange($event)">Yes</mat-button-toggle>
</mat-button-toggle-group>

public _activeValue = "";
onChange(event, group) {
   if (this._activeValue === event.value) {
       // make unchecked
       this._activeValue = "";
    } else {
       this._activeValue = event.value;
    }
 }

Comments

0

Here is a simple directive based on @Scott VandenToorn's answer:

@Directive({
  selector: 'mat-button-toggle-group[appClickOnSelectedButton]',
})
export class ClickOnSelectedButtonDirective {
  @Output() appClickOnSelectedButton = new EventEmitter<PointerEvent>();

  private toggleChanged: boolean = false;

  @HostListener('change', ['$event'])
  onChange(): void {
    this.toggleChanged = true;
  }

  @HostListener('click', ['$event'])
  onClick(event: PointerEvent): void {
    if (!this.toggleChanged) {
      const target = event.target as HTMLElement;
      const hasClass = hasAncestorTheClass(target, 'mat-button-toggle-checked');
      if (hasClass) {
        this.appClickOnSelectedButton.emit(event);
      }
    }
    this.toggleChanged = false;
  }
}

with hasAncestorTheClass based on this SO, in case of the user clicks between two options.

Usage:

<mat-button-toggle-group
  [value]="form.value"
  (appClickOnSelectedButton)="onChange(null)"
  (change)="onChange($event.value)"
>
  <mat-button-toggle [value]="false">No</mat-button-toggle>
  <mat-button-toggle [value]="true">Yes</mat-button-toggle>
</mat-button-toggle-group>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.