LIDOR SYSTEMS

Advanced User Interface Controls and Components

How to Add CheckBox to Items in Angular TreeView

Created: 17 May 2017

In general, items in TreeView component only have a label. By modifying the item template, you can add any HTML element or Angular components and arrange them in custom layouts. For checkboxes, you can use the input element and set it to appear before the item label.

This article provides information on how to add check box to each item in the TreeView. Also explains how to update checkbox values of parent and child items automatically.

TreeView component is part of IntegralUI Web
a suite of UI Components for development of web apps

If you have any questions, don't hesitate to contact us at support@lidorsystems.com

In this example, each TreeView item has a check box, shown before its label. The demo simulates cascading changes to the checkbox. Whenever item's check box value changes, all parent and child items update their check box value.

In addition, using a ComboBox component, you can select which items are displayed in the right list, based on checkbox value: unchecked, indeterminate or checked.

How to Add a CheckBox to TreeView Items

To create a checkbox you can use the input element or for more custom look and feel a span element with checkbox icon as a background. Depending on checkbox value, a different icon is displayed. The item template that includes a checkbox looks like this:

getCheckBoxClass(item: any){
    let cbClass: string = 'trw-item-cbox';

    switch (item.checkState){
        case 'indeterminate':
            cbClass += ' trw-item-cbox-indeterminate';
            break;

        case 'checked':
            cbClass += ' trw-item-cbox-checked';
            break;
    }

    return cbClass;
}

getItemIcon(item: any){
    return item.icon ? item.icon : 'trw-cbox-icons-empty';
}

checkItem(e: any, item: any){
    if (item){
        let checkValue = this.getItemCheckValue(item);
        switch (checkValue){
            case 'unchecked':
                checkValue = 'checked';
                break;

            case 'indeterminate':
                checkValue = 'checked';
                break;

            case 'checked':
                checkValue = 'unchecked';
                break;
        }

        this.updateCheckValue(item, checkValue);

        this.updateChildItemCheckValue(item);
        this.updateParentItemCheckValue(item);
    }

    e.stopPropagation();
}

getItemCheckValue(item: any){
    if (item)
        return item.checkState == undefined ? 'unchecked' : item.checkState;

    return 'unchecked';
}

updateCheckValue(item: any, value: string){
    if (item){
        switch (value){
            case 'unchecked':
                item.checkState = 'unchecked';
                break;

            case 'indeterminate':
                item.checkState = 'indeterminate';
                break;

            case 'checked':
                item.checkState = 'checked';
                break;
        }
    }
}                            
<iui-treeview [items]="data" [controlStyle]="treeStyle" #treeview>
    <template let-item>
        <span [ngClass]="getCheckBoxClass(item)" (mousedown)="checkItem($event, item)"></span>
        <span [ngClass]="getItemIcon(item)"></span>
        <span class="trw-cbox-item-label">{{item.text}}</span>
    </template>
</iui-treeview>
                            
.trw-item-cbox
{
    background-image: url(app/integralui/resources/checkbox/checkbox-unchecked.png);
    background-repeat: no-repeat;
    display: inline-block;
    overflow: hidden;
    padding: 0 !important;
    margin: 0 2px 0 0;
    width: 16px;
    height: 16px;
    vertical-align: middle;
}
.trw-item-cbox-checked
{
    background-image: url(app/integralui/resources/checkbox/checkbox-checked.png);
}
.trw-item-cbox-indeterminate
{
    background-image: url(app/integralui/resources/checkbox/checkbox-indeterminate.png);
}
.trw-cbox-item-label
{
    display: inline-block;
    padding: 3px 0;
    vertical-align: middle;
}
                            

There are three span elements in the template:

  • first element - its background is a checkbox icon
  • second element- its background is the item icon
  • third element - represents the item label

Whenever the checkbox is clicked, its appearance is updated and checkState value of corresponding item is update. At first, this only updates the checkbox value of the item, but checkboxes of parent and child items remain unchanged. How to update these values is explained below.

Auto Update Values of CheckBoxes in Parent and Child Items

In order to update checkbox value of parent items for specified item, you need to create a loop that will go through all parents of the item. Next, for each parent calculate how many child items have its checkState in checked and indeterminate value separately.

Then, compare these values to number of child items. If the number is equal, then the parent checkbox value is checked. Otherwise if there is at least one child item with checked or indeterminate state, set the parent checkbox value to indeterminate. By default, checkbox value is unchecked.

// Update the checkbox of parent items
updateParentItemCheckValue(item: any){
    let parent = this.treeview.getItemParent(item);
    while (parent){
        let list = parent.items;

        if (list){
            let checkCount = 0;
            let indeterminateCount = 0;
            for (let i = 0; i < list.length; i++){
                let checkValue = this.getItemCheckValue(list[i]);
                if (checkValue == 'checked')
                    checkCount++;
                else if (checkValue == 'indeterminate')
                    indeterminateCount++;
            }
            
            let parentCheckValue = 'unchecked';
            if (checkCount == list.length)
                parentCheckValue = 'checked';
            else if (checkCount > 0 || indeterminateCount > 0)
                parentCheckValue = 'indeterminate';

            this.updateCheckValue(parent, parentCheckValue);
        }   
        
        parent = this.treeview.getItemParent(parent);
    }
}                            

In similar way, for child items you need to create a loop and go through all child items for specified item. This needs to be a recursive function, which will continue to go deep down the tree hierarchy until it reaches all child items.

Depending on checkbox value of top item, child items can be either checked or unchecked. In this case, you cannot have indeterminate value. This value is only applied from child to parent items.

// Update the checkbox of child items
updateChildItemCheckValue(parent: any){
    if (parent && parent.items){
        for (let i = 0; i < parent.items.length; i++){
            let checkValue = this.getItemCheckValue(parent);
            if (checkValue == 'checked')
                this.updateCheckValue(parent.items[i], 'checked');
            else
                this.updateCheckValue(parent.items[i], 'unchecked');

            this.updateChildItemCheckValue(parent.items[i]);
        }
    }
}                            

Show a List of Checked Items

Finally, we have a TreeView where each item has a checkbox, and changes are updated automatically for parent and child items. To complete this functionality, you may need to retrieve a list of checked or unchecked items.

In above demonstration, as a solution there is a ComboBox stating check box values: unchecked, indeterminate and checked. Depending on selected option, the list is populated.

showCheckList(){
    this.checkedItems.length = 0;

    let list = this.treeview.getFullList();
    for (let i = 0; i < list.length; i++){
        let checkValue: string = this.getItemCheckValue(list[i]);
        if (checkValue == this.selOption)
            this.checkedItems.push({ text: list[i].text });
    }

    this.listbox.updateLayout();
}

onComboSelectionChanged(e: any){
    switch (e.index){
      case 1:
        this.selOption = 'indeterminate';
        break;

      case 2:
        this.selOption = 'checked';
        break;

      default:
        this.selOption = 'unchecked';
        break;
    }
}                           
<div style="float:left;margin-left:30px;">
    <label>List of items depending on their check state: </label><br />
    <label>State: </label>
    <iui-combobox [items]="checkStates" [controlStyle]="comboStyle" [maxDropDownItems]="3" [integralHeight]="true" [selectedIndex]="2" (selectedIndexChanged)="onComboSelectionChanged($event)">
        <iui-item *ngFor="let item of checkStates" [text]="item.text"></iui-item>
    </iui-combobox>
    <button class="trw-cbox-cmb-button"(click)="showCheckList()">Show</button><br />
    <iui-listbox [items]="checkedItems" [controlStyle]="listStyle" #listbox>
        <iui-listitem *ngFor="let item of checkedItems" [data]="item">  
            <div class="trw-cbox-item-label">{{item.text}}</div>
        </iui-listitem>
    </iui-listbox>
</div>
                            

To populate the list (represented by the ListBox component), we are using the getFullList method, which retrieves a flat list of all items in the tree hierarchy. We are comparing the check value of each item object against the selected option in the combo box, if it matches; the item is added to the list. At the end, the layout of the list is updated.

Note Using getFullList method maintains high performance, because instead going through the tree hierarchy you are working with a linear list.

Complete Sample Code

The code used in this sample is available here:

// 
// app.module.ts
//

import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { IntegralUIModule } from './integralui/integralui.module';

import { AppComponent }   from './app.component';

@NgModule({
    imports:      [ 
          BrowserModule, 
          FormsModule, 
          IntegralUIModule
    ],
    declarations: [ 
        AppComponent, 
    ],
    bootstrap: [ AppComponent ]
})
export class AppModule { }

// 
// app.component.ts
//

import { Component, enableProdMode, ViewContainerRef, ViewChild, ViewEncapsulation } from '@angular/core';
import { IntegralUITreeView } from '../../integralui/components/integralui.treeview';
import { IntegralUIListBox } from '../../integralui/components/integralui.listbox';

@Component({
    selector: 'iui-app',
    templateUrl: 'app.template.html',
    styleUrls: ['sample-styles.css'],
    encapsulation: ViewEncapsulation.None
})
export class AppComponent {
    @ViewChild('application', {read: ViewContainerRef}) applicationRef: ViewContainerRef;
    @ViewChild('treeview') treeview: IntegralUITreeView;
    @ViewChild('listbox') listbox: IntegralUIListBox;

    // An object that holds all items in the TreeView
    public data: Array;

    // An array that holds all options in the comboo box
    private checkStates: Array;

    // An array that holds a list of all checked items
    private checkedItems: Array;

    public treeStyle: any = {
        general: {
            normal: 'trw-cbox-normal'
        }
    }

    public comboStyle: any = {
        general: {
            normal: 'trw-cbox-cmb'
        }
    }

    public listStyle: any = {
        general: {
            normal: 'trw-cbox-list'
        }
    }

    private selOption: string = 'checked';

    constructor(){
        this.data = [
            { 
                id: 1,
                text: "Books",
                icon: "trw-cbox-icons-medium library",
                items: [
                    { id: 11, pid: 1, text: "Art"  },
                    { 
                        id: 12,
                        pid: 1,
                        text: "Business",
                        icon: "trw-cbox-icons-medium people",
                        items: [
                            { id: 121, pid: 12, text: "Economics" },
                            { 
                                id: 122,
                                pid: 12,
                                checkState: 'checked',
                                text: "Investing", 
                                expanded: false,
                                icon: "trw-cbox-icons-medium economics",
                                items: [
                                    { id: 1221, pid: 122, text: "Bonds", checkState: 'checked' },
                                    { id: 1222, pid: 122, text: "Options", checkState: 'checked' },
                                    { id: 1223, pid: 122, text: "Stocks", checkState: 'checked' }
                                ]
                            },
                            { id: 123, pid: 12, text: "Management" },
                            { id: 124, pid: 12, text: "Small Business" }
                        ]
                    },
                    { id: 13, pid: 1, text: "Health", icon: "trw-cbox-icons-medium heart", checkState: 'checked' },
                    { id: 14, pid: 1, text: "Literature" },
                    { 
                        id: 15,
                        pid: 1,
                        text: "Science",
                        expanded: false,
                        items: [
                            { id: 151, pid: 15, text: "Astronomy" },
                            { id: 152, pid: 15, text: "Mathematics" },
                            { id: 153, pid: 15, text: "Evolution" },
                            { id: 154, pid: 15, text: "Nature" }
                        ]
                    }
                ]
            },
            { id: 2, text: "Computers" },
            {
                id: 3,
                checkState: 'checked',
                text: "Electronics",
                items: [
                    { id: 31, pid: 3, text: "Camera", icon: "trw-cbox-icons-medium camera", checkState: 'checked' },
                    { id: 32, pid: 3, text: "Cell Phones", checkState: 'checked' },
                    { id: 33, pid: 3, text: "Video Game Consoles", checkState: 'checked' }
                ]
            },
            { 
                id: 4,
                text: "Music", 
                expanded: false,
                icon: "trw-cbox-icons-medium album",
                items: [
                    { id: 41, pid: 4, text: "Blues" },
                    { id: 42, pid: 4, text: "Classic Rock" },
                    { id: 43, pid: 4, text: "Pop" },
                    { id: 44, pid: 4, text: "Jazz" }
                ]
            },
            { 
                id: 5,
                text: "Software",
                icon: "trw-cbox-icons-medium software",
                items: [
                    { id: 51, pid: 5, text: "Games", checkState: 'checked' },
                    { id: 52, pid: 5, text: "Operating Systems" },
                    { id: 53, pid: 5, text: "Network & Servers" },
                    { id: 54, pid: 5, text: "Security" }
                ]
            },
            { 
                id: 6,
                text: "Sports",
                expanded: false,
                icon: "trw-cbox-icons-medium sports",
                items: [
                    { id: 61, pid: 6, text: "Baseball" },
                    { id: 62, pid: 6, text: "Martial Arts", checkState: 'checked' },
                    { id: 63, pid: 6, text: "Running" },
                    { 
                        id: 64,
                        pid: 6,
                        text: "Tennis",
                        expanded: false,
                        items: [
                            { id: 641, pid: 64, text: "Accessories" },
                            { id: 642, pid: 64, text: "Balls" },
                            { id: 643, pid: 64, text: "Racquets" }
                        ]
                    }
                ]
            },
            { id: 7, text: "Video Games" },
            { id: 8, text: "Watches", icon: "trw-cbox-icons-medium clock" }
        ];

        // Options to choose from
        this.checkStates = [
          { text: "Unchecked" },
          { text: "Indeterminate" },
          { text: "Checked" }
        ];

        this.checkedItems = [];
    }

    ngAfterViewInit(){
        let list = this.treeview.getFullList();
        for (let i = 0; i < list.length; i++)
            this.updateParentItemCheckValue(list[i]);

        this.showCheckList();
    }

    // Item Template ---------------------------------------------------------------------

    getCheckBoxClass(item: any){
        let cbClass: string = 'trw-item-cbox';

        switch (item.checkState){
            case 'indeterminate':
                cbClass += ' trw-item-cbox-indeterminate';
                break;

            case 'checked':
                cbClass += ' trw-item-cbox-checked';
                break;
        }

        return cbClass;
    }

    getItemIcon(item: any){
        return item.icon ? item.icon : 'trw-cbox-icons-empty';
    }

    checkItem(e: any, item: any){
        if (item){
            let checkValue = this.getItemCheckValue(item);
            switch (checkValue){
                case 'unchecked':
                    checkValue = 'checked';
                    break;

                case 'indeterminate':
                    checkValue = 'checked';
                    break;

                case 'checked':
                    checkValue = 'unchecked';
                    break;
            }

            this.updateCheckValue(item, checkValue);

            this.updateChildItemCheckValue(item);
            this.updateParentItemCheckValue(item);
        }

        e.stopPropagation();
    }

    getItemCheckValue(item: any){
        if (item)
            return item.checkState == undefined ? 'unchecked' : item.checkState;

        return 'unchecked';
    }

    updateCheckValue(item: any, value: string){
        if (item){
            switch (value){
                case 'unchecked':
                    item.checkState = 'unchecked';
                    break;

                case 'indeterminate':
                    item.checkState = 'indeterminate';
                    break;

                case 'checked':
                    item.checkState = 'checked';
                    break;
            }
        }
    }

    // Cascading Changes to CheckBoxes ---------------------------------------------------

    // Update the checkbox of parent items
    updateParentItemCheckValue(item: any){
        let parent = this.treeview.getItemParent(item);
        while (parent){
            let list = parent.items;

            if (list){
                let checkCount = 0;
                let indeterminateCount = 0;
                for (let i = 0; i < list.length; i++){
                    let checkValue = this.getItemCheckValue(list[i]);
                    if (checkValue == 'checked')
                        checkCount++;
                    else if (checkValue == 'indeterminate')
                        indeterminateCount++;
                }
                
                let parentCheckValue = 'unchecked';
                if (checkCount == list.length)
                    parentCheckValue = 'checked';
                else if (checkCount > 0 || indeterminateCount > 0)
                    parentCheckValue = 'indeterminate';

                this.updateCheckValue(parent, parentCheckValue);
            }   
            
            parent = this.treeview.getItemParent(parent);
        }
    }
    
    // Update the checkbox of child items
    updateChildItemCheckValue(parent: any){
        if (parent && parent.items){
            for (let i = 0; i < parent.items.length; i++){
                let checkValue = this.getItemCheckValue(parent);
                if (checkValue == 'checked')
                    this.updateCheckValue(parent.items[i], 'checked');
                else
                    this.updateCheckValue(parent.items[i], 'unchecked');

                this.updateChildItemCheckValue(parent.items[i]);
            }
        }
    }

    // Check List ------------------------------------------------------------------------

    showCheckList(){
        this.checkedItems.length = 0;

        let list = this.treeview.getFullList();
        for (let i = 0; i < list.length; i++){
            let checkValue: string = this.getItemCheckValue(list[i]);
            if (checkValue == this.selOption)
                this.checkedItems.push({ text: list[i].text });
        }

        this.listbox.updateLayout();
    }

    onComboSelectionChanged(e: any){
        switch (e.index){
          case 1:
            this.selOption = 'indeterminate';
            break;

          case 2:
            this.selOption = 'checked';
            break;

          default:
            this.selOption = 'unchecked';
            break;
        }
    } 
}                            
// 
// app.template.html
//

<iui-treeview [items]="data" [controlStyle]="treeStyle" #treeview>
    <template let-item>
        <span [ngClass]="getCheckBoxClass(item)" (mousedown)="checkItem($event, item)"></span>
        <span [ngClass]="getItemIcon(item)"></span>
        <span class="trw-cbox-item-label">{{item.text}}</span>
    </template>
</iui-treeview>
<div style="float:left;margin-left:30px;">
    <label>List of items depending on their check state: </label><br />
    <label>State: </label>
    <iui-combobox [items]="checkStates" [controlStyle]="comboStyle" [maxDropDownItems]="3" [integralHeight]="true" [selectedIndex]="2" (selectedIndexChanged)="onComboSelectionChanged($event)">
        <iui-item *ngFor="let item of checkStates" [text]="item.text"></iui-item>
    </iui-combobox>
    <button class="trw-cbox-cmb-button"(click)="showCheckList()">Show</button><br />
    <iui-listbox [items]="checkedItems" [controlStyle]="listStyle" #listbox>
        <iui-listitem *ngFor="let item of checkedItems" [data]="item">  
            <div class="trw-cbox-item-label">{{item.text}}</div>
        </iui-listitem>
    </iui-listbox>
</div>
<br style="clear:both;"/>
                            
/*
* sample-styles.css
*/ 

.trw-cbox-normal
{
    float: left;
    width: 350px;
    height: 400px;
}
.trw-cbox-normal .iui-treeitem-content
{
    padding: 5px;
}
.trw-cbox-normal .iui-treeitem-expand-box
{
    margin-top: 4px !important;
}
.trw-cbox-icons-medium
{
    background-image: url(app/integralui/resources/icons-x24.png);
    background-position: 0 0;
    background-repeat: no-repeat;
    display: inline-block;
    overflow: hidden;
    padding: 0 !important;
    margin: 0 1px 0 5px;
    width: 24px;
    height: 24px;
    vertical-align: middle;
}
.trw-cbox-icons-empty
{
    display: inline-block;
    padding: 0 !important;
    height: 24px;
    vertical-align: middle;
}
.library
{
    background-position: 0 -72px;
}
.economics
{
    background-position: -24px -72px;
}
.people
{
    background-position: -120px -72px;
}
.heart
{
    background-position: -168px -72px;
}
.album
{
    background-position: -144px -48px;
}
.camera
{
    background-position: -168px -48px;
}
.software
{
    background-position: -48px -72px;
}
.clock
{
    background-position: -72px -72px;
}
.sports
{
    background-position: -96px -72px;
}
.trw-item-cbox
{
    background-image: url(app/integralui/resources/checkbox/checkbox-unchecked.png);
    background-repeat: no-repeat;
    display: inline-block;
    overflow: hidden;
    padding: 0 !important;
    margin: 0 2px 0 0;
    width: 16px;
    height: 16px;
    vertical-align: middle;
}
.trw-item-cbox-checked
{
    background-image: url(app/integralui/resources/checkbox/checkbox-checked.png);
}
.trw-item-cbox-indeterminate
{
    background-image: url(app/integralui/resources/checkbox/checkbox-indeterminate.png);
}
.trw-cbox-item-label
{
    display: inline-block;
    padding: 3px 0;
    vertical-align: middle;
}
.trw-cbox-cmb
{
    display: inline-block;
    margin-top: 10px;
    width: 200px;
}
.trw-cbox-cmb-button
{
    width: 100px;
    margin-left: 3px;
    padding: 5px;
}
.trw-cbox-list
{
    width: 350px;
    height: 340px;
}                            

Conclusion

To add a CheckBox to items in Angular TreeView component, you need to modify the item template. In addition, you can create custom functionality that auto-update checkbox value of parent and child items whenever an item is clicked. Finally, you can create a list of items that have the same checkbox value.

You can use this sample as a guideline to create your own solution, and further extend it to match your application requirements.

The TreeView component is part of IntegralUI Web.

Did you Like this Article?


Enter your e-mail address below and you will receive latest articles as well as news on upcoming events and special offers.