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.