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.

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

The class with constructor then looks like the code below.

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.

  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.

  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.

  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.