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.