Angular tri-state checkbox

Home / Angular tri-state checkbox

angular_small

While I’m in the process of converting my Angular 1.x directives into Angular2 components, the tri-state checkbox reared its head. This is a pretty common mechanism and is something I needed to have working before moving along to converting other components.


Similarly to the multiselect drop-down I’ve blogged about, the main premise of the Tri-state is to pass an Observable list which can be monitored and a single checkbox reacts the any of the items in the list being selected or unselected.

The component decorator function looks pretty simple. It’s a checkbox with ngModel. The only thing special is to use the “#theCheckbox” moniker so that we can get a handle to the checkbox later as a result of the way in which ‘indeterminate’ state must be set.

1
2
3
4
@Component({
    selector: 'tri-state-checkbox',
    template: `<input #theCheckbox type="checkbox" [(ngModel)]="topLevel" (change)="topLevelChange()">`
})

The class with constructor then looks like the code below.

1
2
3
4
5
6
7
8
9
10
11
12
export class Tristate implements AfterViewInit, DoChange  {
  public topLevel: bool = false;
  public _items: Array<any>;
  private _subscription: Subscription;
  @Input() items: Observable<any[]>;
  @ViewChild("theCheckbox") checkbox;
   
  constructor(private _changeDetectorRef: ChangeDetectorRef) { }
 
  ngDoCheck() {
    this.setState();
  }

The interesting thing here is that the class implements “DoChange.” This will allow us to pick up any UI changes, where the items in the observable are being modified. I had thought about creating an observable for each item in the array, since the top-level array Observable will only trigger subscriptions when the array itself is changed, but that seemed rather heavy-handed.

The “setState” method is pretty cut and dry JavaScript code. The only thing special here is that we have to access the ‘nativeElement’ set set the ‘indeterminate’ to true/false based on whether some, or none, of the items are selected. This is why the “ViewChild” reference is needed in the decorators for the Tristate class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private setState() {
  if (!this._items) return;
  var count: int = 0;
  for (var i: int = 0; i < this._items.length; i++) {
    count += this._items[i].isSelected ? 1 : 0;
  }
  this.topLevel = (count === 0) ? false : true;
  if (count > 0 && count< i) {
    console.log("Setting indeterminate.");
    this.checkbox.nativeElement.indeterminate = true;
  } else {
    console.log("Removing indeterminate.");
    this.checkbox.nativeElement.indeterminate = false;
  }
}

The component only implements a single event handler to toggle the ‘isSelected’ of the items passed on the @Input.

1
2
3
4
5
6
public topLevelChange() {
  console.log("Clicked. " + this.topLevel);
  for (var i: int = 0; i < this._items.length; i++) {
    this._items[i].isSelected = this.topLevel;
  }
}

Finally, the last thing we implement is the AfterViewInit handler to subscribe to the Observable @Input. Note that during the subscription, we trigger “detectChanges” manually or else the checkbox is not changed in the UI, if it needs to be, on initial load.

1
2
3
4
5
6
7
8
ngAfterViewInit() {
  this._subscription = this.items.subscribe(res => {
    console.log("Subscription triggered.");
    this._items = res;
    this.setState();
    this._changeDetectorRef.detectChanges();
  });
}

And that’s it. The plunk below illustrates all of the code working harmoniously.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.