TomasThall
TomasThall

Reputation: 245

Angular 5, Angular Material Checkbox with 3 states (checked, unchecked, indeterminate)

I'm new to Angular and Angular Material, now I'm working as a support in some project. There's a grid with filters and one checkbox, which checks if user in grid is active, inactive, or not chosen. It would be simpler with only two options (active, inactive) but well, I have to make 3 states for it:

  1. 1st click - Checked for active
  2. 2nd click - Unchecked for inactive
  3. 3rd click - Indeterminate for not chosen

Here is checkbox example from official Angular Material documentation: https://stackblitz.com/angular/rxdmnbxmkgk?file=app%2Fcheckbox-configurable-example.html

How to make it in the most simply way?

Upvotes: 15

Views: 18802

Answers (7)

nck
nck

Reputation: 2221

I wanted a solution for this without requiring another component and in a reactive form and this does seem to do the trick, although there is a duplicated animation in one transition:

component.ts

  onChangeCheckbox(checkbox: MatCheckbox): void {
    if (checkbox.indeterminate){
      checkbox.indeterminate = false;
      checkbox.checked = true;
    } 
    // by the time you click it is changed
    else if (!checkbox.indeterminate && !checkbox.checked ){
      checkbox.checked = false;
    }else if (!checkbox.indeterminate && checkbox.checked ){
      checkbox.indeterminate = true;
    }
  }

component.html

<mat-checkbox
                        #checkboxName
                        formControlName="checkboxName"
                        indeterminate="true"
                        (change)="onChangeCheckbox(checkboxName)"
                        id="edit:checkboxName"> 
                        Checkbox Name
                    </mat-checkbox>

Upvotes: 3

Zahidur Rahman
Zahidur Rahman

Reputation: 1718

@angular/material >= 9

In my scenario , it works. color: 'primary' must be provide.

 providers: [{ provide: MAT_CHECKBOX_DEFAULT_OPTIONS, useValue: { clickAction: 'noop', color: 'primary' } as MatCheckboxDefaultOptions }]

Upvotes: 0

Pianoman
Pianoman

Reputation: 395

Thanks for this solution! Using Angular 9.1.7 here.

I needed a custom set of values. This didn't work out of the box. Thanks to this (post) for enlightening me on the solution to create 2-way binding

I changed the parts as follows:

Template:

<mat-checkbox (click)="next()"
              [(ngModel)]="guiValue"
              [disabled]="disabled"
              [indeterminate]="isIndeterminate"
              [color]="isIndeterminate ? 'warn' : 'accent'"
><ng-content></ng-content>
</mat-checkbox>

Typescript:

import {Component, EventEmitter, forwardRef, Input, Output} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MatCheckboxDefaultOptions, MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox';

@Component({
  selector: 'app-tri-state-checkbox',
  templateUrl: './tri-state-checkbox.component.html',
  styleUrls: ['./tri-state-checkbox.component.css'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => TriStateCheckboxComponent),
      multi: true,
    },
    { provide: MAT_CHECKBOX_DEFAULT_OPTIONS, useValue: { clickAction: 'noop' } as MatCheckboxDefaultOptions },
  ],
})
export class TriStateCheckboxComponent implements ControlValueAccessor {
  internalValue: any;
  guiValue: any;
  disabled: boolean;
  isIndeterminate: boolean;

  @Input() states = [null, true, false];
  @Output() triStateValueChange = new EventEmitter<any>();

  @Input()
  get triStateValue(): any {
    return this.internalValue;
  }

  set triStateValue(v) {
    this.internalValue = v;
    this.writeValue();
  }


  onChange = (x: any) => {console.log(`onChange:${x}`); };
  onTouched = () => {};


  writeValue() {
    if      (this.internalValue === this.states[0]) { this.guiValue = true; }  // undetermined
    else if (this.internalValue === this.states[1]) { this.guiValue = true; }  // true
    else if (this.internalValue === this.states[2]) { this.guiValue = false; } // false
    else { console.error (`Wrong value for tri state checkbox : ${this.internalValue}`); }

    this.isIndeterminate = ( this.internalValue === this.states[0] );
  }

  setDisabledState(disabled: boolean) {
    this.disabled = disabled;
  }

  next() {
    this.determineNextValue();
    this.onTouched();
    this.onChange(this.guiValue);
  }
  determineNextValue(){
    if      (this.internalValue === this.states[0]) {this.internalValue = this.states[1]; }
    else if (this.internalValue === this.states[1]) {this.internalValue = this.states[2]; }
    else if (this.internalValue === this.states[2]) {this.internalValue = this.states[0]; }

    this.triStateValueChange.emit(this.internalValue);
  }

  registerOnChange(fn: any) {
    this.onChange = fn;
  }

  registerOnTouched(fn: any) {
    this.onTouched = fn;
  }

}

And the usage:

Template:

<app-tri-state-checkbox [states]="locTriState" [(triStateValue)]="targetVariable" ">Labeltext</app-tri-state-checkbox>

And the setup of the source (TS) using the component: (the variable targetVariable is used as a source/target using 2-way binding.

export enum TriStateValues {
  on,
  off,
  dontcare
};

let targetVariable = TriStateValues.on;
const locTriState = [ TriStateValues.dontcare, TriStateValues.on ,  TriStateValues.off];

Upvotes: 0

Julieta Corvi
Julieta Corvi

Reputation: 21

I tried to reply this Answer but I don't have enough reputation. First thank you for the answer and deep explanation. I was having trouble with the edition of the checkboxes and I figured out that if the value is false this line

this.value = value || this.tape[0];

doesn't work and the color is not updated either. I changed it for this one

this.value = value !== null ? value : this.tape[0];

I hope this comment help others.

Upvotes: 2

smnbbrv
smnbbrv

Reputation: 24541

@angular/material >= 9

Here is a ready-to-use component:

import { Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MatCheckboxDefaultOptions, MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox';

@Component({
  selector: 'app-tri-state-checkbox',
  templateUrl: './tri-state-checkbox.component.html',
  styleUrls: ['./tri-state-checkbox.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => TriStateCheckboxComponent),
      multi: true,
    },
    { provide: MAT_CHECKBOX_DEFAULT_OPTIONS, useValue: { clickAction: 'noop' } as MatCheckboxDefaultOptions },
  ],
})
export class TriStateCheckboxComponent implements ControlValueAccessor {

  @Input() tape = [null, true, false];

  value: any;

  disabled: boolean;

  private onChange: (val: boolean) => void;
  private onTouched: () => void;

  writeValue(value: any) {
    this.value = value || this.tape[0];
  }

  setDisabledState(disabled: boolean) {
    this.disabled = disabled;
  }

  next() {
    this.onChange(this.value = this.tape[(this.tape.indexOf(this.value) + 1) % this.tape.length]);
    this.onTouched();
  }

  registerOnChange(fn: any) {
    this.onChange = fn;
  }

  registerOnTouched(fn: any) {
    this.onTouched = fn;
  }

}

and template:

<mat-checkbox [ngModel]="value" (click)="next()" [disabled]="disabled" [indeterminate]="value === false" [color]="value === false ? 'warn' : 'accent'">
  <ng-content></ng-content>
</mat-checkbox>

Usage:

<app-tri-state-checkbox [(ngModel)]="done">is done</app-tri-state-checkbox>
<app-tri-state-checkbox formControlName="done">is done</app-tri-state-checkbox>

You can also override default tape to e.g. enum values:

<app-tri-state-checkbox [tape]="customTape" [(ngModel)]="done">is done</app-tri-state-checkbox>

where customTape replaces default values [null, true, false]

customTape = [Status.open, Status.complete, Status.cancelled];

@angular/material <= 8

Just change

{ provide: MAT_CHECKBOX_DEFAULT_OPTIONS, useValue: { clickAction: 'noop' } as MatCheckboxDefaultOptions },

to the deprecated since version 9 MAT_CHECKBOX_CLICK_ACTION

{ provide: MAT_CHECKBOX_CLICK_ACTION, useValue: 'noop' },

Upvotes: 29

Liviu Ilea
Liviu Ilea

Reputation: 1574

If you need a working example you can also clone the material2 project here, and then:

cd material2
npm i
npm run demo-app

Open the demo app and navigate to the checkbox component.

Upvotes: 0

rcunha
rcunha

Reputation: 16

One way to do this is to set MAT_CHECKBOX_CLICK_ACTION to 'noop' and then you'll have to set the checked values with (click). Don't forget to bind both [ngModel] and [indeterminate].

providers: [
    {provide: MAT_CHECKBOX_CLICK_ACTION, useValue: 'noop'}
]

Have a look at this: https://github.com/angular/material2/blob/master/src/lib/checkbox/checkbox.md

Upvotes: 0

Related Questions