Reputation: 245
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:
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
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
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
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
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
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
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
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