LIDOR SYSTEMS

Advanced User Interface Controls and Components

Add and Edit Rows Dynamically in Angular TreeGrid

Created: 25 April 2017

To populate the Angular TreeGrid component with data, you can either load data on demand from local or remote data source or add new rows dynamically when required. In order to create a new row manually, you can use some of public methods available that allows you to insert a row at specific position in the grid. In following section, you will learn how to add new rows on demand and how to create and use a custom cell editor.

TreeGrid 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

The demo above presents two buttons above the grid: one button will add a new row as root, while the other button will add new row as child of currently selected row. Whenever the add button is clicked, a new row is created at specific position in the grid and an inline cell editor appears. The cells in the last column contain action buttons that confirm or cancel the change of cell values.

After row is created, you can easily edit its cell values by clicking on edit action button (represented by pencil icon). In addition, you can remove rows by clicking on remove action button (represented by delete icon).

How to Create an Inline Cell Editor

By default, each grid cell has a text value. By modifying the cell template, you can add custom HTML elements or other Angular components arranged in custom layouts, independently for each cell.

In case of text editor, the cell template should include an input element and a label. The input element will appear only when cell is in edit mode, otherwise the cell label is shown.

Note To keep the example simple, all cells have only text editor, but you can add any custom component for editing, like numeric text box, combo box, date time picker, checkbox etc.

// Selects the whole text in the text editor
selectContent(e: any){
    if (e.target){
        setTimeout(function(){
            e.target.setSelectionRange(0, e.target.value.length);
        }, 1);
    }
}


// Cancels the edit process when ESCAPE key is pressed
editorKeyDown(e: any){
    if (this.currentEditRow){
        switch (e.keyCode){
            case 27: // ESCAPE
                this.cancelEdit(this.currentEditRow.id);
                break;
        }
    }
}
                            
<iui-treegrid [columns]="columns" [rows]="rows" [showFooter]="false" [rowHeight]="26" #treegrid>
    <template let-column [iuiTemplate]="{ type: 'header' }">
        <span>{{column.headerText}}</span>
    </template>
    <template let-cell [iuiTemplate]="{ type: 'cell' }">
        <div [ngSwitch]="cell.cid">
            <div *ngSwitchCase="7" (dblclick)="onButtonsDblClick($event)" (mousedown)="onButtonsMouseDown($event)">
                <div *ngIf="cell.rid==currentEditRowID">
                    <button (click)="saveRow(cell.rid)">Save</button>
                    <button (click)="cancelEdit(cell.rid)">Cancel</button>
                </div>
                <div *ngIf="cell.rid!=currentEditRowID">
                    <div class="button-block" (click)="editRow(cell.rid)">
                        <span class="icons button-edit"></span>
                    </div>
                    <div class="button-block" (click)="removeRow(cell.rid)">
                        <span class="icons button-remove"></span>
                    </div>
                </div>
            </div>
            <div *ngSwitchDefault style="display:inline-block;">
                <input *ngIf="cell.saved==false" class="cell-input" [(ngModel)]="cell.editText" [iuiFocus]="currentEditCell==cell" (focus)="selectContent($event)" (keydown)="editorKeyDown($event)" [ngStyle]="{ width: getCellWidth(cell) + 'px' }" />
                <span *ngIf="cell.saved!=false" class="cell-text">{{cell.text}}</span>
            </div>
        </div>
    </template>
</iui-treegrid>
                            
.cell-text
{
    display: inline-block;
    margin-top: 4px;
    text-align: center;
    vertical-align: middle;
}
.cell-input
{
    margin-top: 2px;
}
                            

As template shows, the input element will appear only when cell is in edit mode (represented by a cell object field named 'saved', which when set to false marks that cell is in edit mode). When editor appears, if you press ESCAPE key the editing is cancelled.

The width of the element is adjusted to the width of the grid cell. This is done by calculating the width of the column to which cell belong, reduced by cell padding value and indent of row if the cell belongs to expanding column.

// Calculates the width of grid cell
getCellWidth(cell: any){
    let cellWidth: number = 100;

    for (let j = 0; j < this.columns.length; j++){
        if (this.columns[j].id == cell.cid){
            cellWidth = this.columns[j].width; 
            break;
        }
    }

    let cellPadding: number = 4;

    if (cell.cid == 2){
        let row = this.treegrid.findRowById(cell.rid);
        let level: number = this.getRowLevel(row);

        cellPadding = 23 + level*15;
    }

    cellWidth -= cellPadding;

    return cellWidth;
}
                            

In this way, the input element will remain with correct width, whenever a column resizes.

Grid Cells with Action Buttons

You can add action buttons in cells of the last column that when clicked will confirm or cancel the edit process. Depending on whether the treegrid is in edit or normal mode a different set of action buttons appear.

In normal mode, the cells in last column show an edit and remove button. On the other hand, while in edit mode they show confirm and cancel button. To set these two different states, the template of these cells includes both set of action buttons, using the ngIf directive determines which set is currently active.

// Confirms the changes and saves the row
saveRow(id: any){
    let row = this.treegrid.findRowById(id);
    if (row){
        this.updateEditStatus(row, true);

        for (let j = 0; j < row.cells.length; j++)
            row.cells[j].text = row.cells[j].editText;

        if (this.isNewRow)
            this.moveRowToEnd(row);
    }

    this.resetEditor();
    this.isNewRow = false;
}

// Cancels the edit process and closes the editor
cancelEdit(id: any){
    if (this.isNewRow)
        this.removeRow(id);

    this.resetEditor();
    this.isNewRow = false;
}

// Sets the row in edit mode and opens the editor
editRow(id: any){
    this.resetEditor();
        
    let row = this.treegrid.findRowById(id);
    if (row){
        this.updateEditStatus(row);

        this.currentEditRow = row;
        this.currentEditRowID = row.id;
        this.currentEditCell = row.cells[0];
    }
} 

// Removes a row from the grid
removeRow(id: any){
    let row = this.treegrid.findRowById(id);
    if (row){
        this.treegrid.removeRow(row);
        this.treegrid.updateLayout();
    }
}

// Resets the variables for editing
resetEditor(){
    this.updateEditStatus(this.currentEditRow, true);

    this.currentEditRow = null;
    this.currentEditRowID = null;
    this.currentEditCell = null;
}

// Updates the status of row, for edit or normal mode
updateEditStatus(row: any, flag?: boolean){
    let status: boolean = flag ? true : false;

    if (row)
        for (let j = 0; j < row.cells.length; j++){
            row.cells[j].saved = status;

            // If row is in edit mode, copy the text value of grid cell to the cell editor
            if (!status)
                row.cells[j].editText = row.cells[j].text;
        }
}
                            
<iui-treegrid [columns]="columns" [rows]="rows" [showFooter]="false" [rowHeight]="26" #treegrid>
    <template let-column [iuiTemplate]="{ type: 'header' }">
        <span>{{column.headerText}}</span>
    </template>
    <template let-cell [iuiTemplate]="{ type: 'cell' }">
        <div [ngSwitch]="cell.cid">
            <div *ngSwitchCase="7" (dblclick)="onButtonsDblClick($event)" (mousedown)="onButtonsMouseDown($event)">
                <div *ngIf="cell.rid==currentEditRowID">
                    <button (click)="saveRow(cell.rid)">Save</button>
                    <button (click)="cancelEdit(cell.rid)">Cancel</button>
                </div>
                <div *ngIf="cell.rid!=currentEditRowID">
                    <div class="button-block" (click)="editRow(cell.rid)">
                        <span class="icons button-edit"></span>
                    </div>
                    <div class="button-block" (click)="removeRow(cell.rid)">
                        <span class="icons button-remove"></span>
                    </div>
                </div>
            </div>
            <div *ngSwitchDefault style="display:inline-block;">
                <input *ngIf="cell.saved==false" class="cell-input" [(ngModel)]="cell.editText" [iuiFocus]="currentEditCell==cell" (focus)="selectContent($event)" (keydown)="editorKeyDown($event)" [ngStyle]="{ width: getCellWidth(cell) + 'px' }" />
                <span *ngIf="cell.saved!=false" class="cell-text">{{cell.text}}</span>
            </div>
        </div>
    </template>
</iui-treegrid>
                            
.iui-treegrid button
{
    background: #0064bb;
    border: thin solid #0048aa;
    color: white;
    margin-top: 1px;
    padding: 3px 5px;
}
.iui-treegrid button:hover
{
    background: #008000;
    border-color: #006400;
}
.button-block
{
    background: #e9e9e9;
    border: thin solid #d5d5d5;
    display: inline-block;
    padding: 3px;
}
.button-block:hover
{
    background: white;
    border-color: #bebebe;
}
.icons
{
    background-image: url(app/integralui/resources/icons.png);
    background-repeat: no-repeat;
    display: inline-block;
    overflow: hidden;
    width: 16px;
    height: 16px;
    opacity: 0.6;
    vertical-align: middle;
}
.button-edit
{
    background-position: -128px -81px;
}
.button-remove
{
    background-position: -160px -96px;
}
.button-edit:hover, .button-remove:hover
{
    opacity: 1;
} 
                            

As you can see from above code, we are using the ngSwitchCase directive to create different template for grid cells. Depending on column id value (the cell.cid field holds the identifier of column to which cell belongs), a different template is used.

Add New Rows Dynamically

To add a new row from code, you can use some of the treegrid public methods. In this case we are using the insertRowAt method. This method adds a new row at specific position either as root or as child to specified row.

In this example, above the grid there are two buttons. The 'Add Root' button adds a new row to the grid as root, while the 'Add Child' button adds a new row to the grid as child of currently selected row. If there is no row selected, both buttons will add root rows.

// Adds a new row as root
addRoot(){
    if (this.currentEditRow)
        this.cancelEdit(this.currentEditRow.id);

    this.insertRowInGrid();
}

// Adds a new row as child
addChild(){
    if (this.currentEditRow)
        this.cancelEdit(this.currentEditRow.id);

    this.insertRowInGrid(this.treegrid.selectedRow);
}

// Inserts the created row at specific position and updates the grid layout
insertRowInGrid(selRow?: any){
    let row: any = this.createNewRow();

    this.currentEditRow = row;
    this.currentEditRowID = row.id;
    this.currentEditCell = row.cells[0];

    this.treegrid.insertRowAt(row, 0, selRow);
    this.treegrid.updateLayout();
}
                            
<div class="top-panel">
    <button (click)="addRoot()"><span class="icon-add-root"></span>Add Root</button>
    <button (click)="addChild()"><span class="icon-add-child"></span>Add Child</button>
</div>
                            
/* 
    Add Rows Panel
*/
.top-panel
{
    background: #2455b0;
    padding: 3px;
    margin-bottom: 1px;
}
button
{
    background: transparent;
    border: thin solid transparent;
    color: white;
    padding: 5px 10px;
    display: inline-block;
    vertical-align: middle;
}
button:hover
{
    background-color: #153268;
    border: thin solid #0F244A;
}
.icon-add-root
{
    background: url("app/integralui/resources/icons-x24.png") no-repeat -48px -120px;
    display: inline-block;
    width: 24px;
    height: 24px;
    vertical-align: middle;
}
.icon-add-child
{
    background: url("app/integralui/resources/icons-x24.png") no-repeat -120px -168px;
    display: inline-block;
    width: 24px;
    height: 24px;
    vertical-align: middle;
}
                            

During editing phase, the new row will appear as first or child row, so that it is present in current view of the grid. When editing is completed, the new row is placed at the end of the list. Of it is a root row, it will appear as last row in the grid, and if it is a child row it will appear as last child of its parent row. This is accomplished by using the following function that changes the position of the new row:

// Moves a row at end of the list
moveRowToEnd(row: any){
    if (row){
        let list: Array = this.rows;

        let parentRow = this.treegrid.getRowParent(row);
        if (parentRow)
            list = parentRow.rows;

        if (list){
            list.splice(list.length-1, 0, list.splice(0, 1)[0]);
            this.treegrid.updateLayout();
        }
    }
}
                            

Put All Together

Finally, the complete code that adds this behavior to the TreeGrid component 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, ViewContainerRef, ViewChild, ViewEncapsulation } from '@angular/core';

@Component({
    selector: 'iui-app',
    templateUrl: 'app.template.html',
    styleUrls: ['sample-styles.css'],
    encapsulation: ViewEncapsulation.None
})
export class AppComponent {
    // Get a reference to the TreeGrid component
    @ViewChild('treegrid') treegrid: IntegralUITreeGrid;

    // An Array object that holds all column objects shown in the TreeGrid
    private columns: Array;
    // An Array object that holds all row objects shown in the TreeGrid
    private rows: Array;

    // Edit variables
    private currentEditRow: any = null;
    private currentEditRowID: any = null;
    private currentEditCell: any = null;
    private isNewRow: boolean = false;

    constructor(private commonService?: IntegralUICommonService){
        this.columns = [
            { id: 2, headerText: "Title", width: 420},
            { id: 3, headerText: "Year", headerAlignment: "center", contentAlignment: "center", width: 70 },
            { id: 5, headerText: "Released", headerAlignment: "center", contentAlignment: "center", width: 120 },
            { id: 7, contentAlignment: "center", width: 100, fixedWidth: true }
        ];

        this.rows = [
            { 
                id: 1,
                text: "Mystery",
                cells: [ { cid: 2, text: "Mystery" }, { cid: 7 }],
                rows: [
                    { 
                        id: 11,
                        pid: 1,
                        text: "Inception",
                        cells: [ { cid: 2, text: "Inception" }, { cid: 3, text: "2010" }, { cid: 5, text: "16 Jul 2010" }, { cid: 7 } ]
                    },
                    { 
                        id: 13,
                        pid: 1,
                        text: "Shutter Island",
                        cells: [ { cid: 2, text: "Shutter Island" }, { cid: 3, text: "2010" }, { cid: 5, text: "19 Feb 2010" }, { cid: 7 } ]
                    }
                ]
            }
        ];
    } 

    // Prevent dblclick and mousedown events to bubble up from clicks on active buttons
    // This stops the expand.collapse of rows when doubleclicked
    onButtonsDblClick(e: any){
        e.stopPropagation();
    }

    onButtonsMouseDown(e: any){
        e.stopPropagation();
    }

    // Returns the level of the row in tree hierearchy
    getRowLevel(row: any){  
        let level: number = 0;

        let parent: any = this.treegrid.getRowParent(row);
        while (parent){
            level++;
            parent = this.treegrid.getRowParent(parent);
        }

        return level;
    }

    // Calculates the width of grid cell
    getCellWidth(cell: any){
        let cellWidth: number = 100;

        for (let j = 0; j < this.columns.length; j++){
            if (this.columns[j].id == cell.cid){
                cellWidth = this.columns[j].width; 
                break;
            }
        }

        let cellPadding: number = 4;

        if (cell.cid == 2){
            let row = this.treegrid.findRowById(cell.rid);
            let level: number = this.getRowLevel(row);

            cellPadding = 23 + level*15;
        }

        cellWidth -= cellPadding;

        return cellWidth;
    }

    // Selects the whole text in the text editor
    selectContent(e: any){
        if (e.target){
            setTimeout(function(){
                e.target.setSelectionRange(0, e.target.value.length);
            }, 1);
        }
    }

    // Creates a new row object
    createNewRow(){
        let row: any = {
            id: this.commonService.getUniqueId(),
            cells: [
                { cid: 2, text: "", saved: false },
                { cid: 3, text: "", saved: false },
                { cid: 5, text: "", saved: false },
                { cid: 7, saved: false }
            ]
        }

        for (let j = 0; j < row.cells.length; j++)
            row.cells[j].rid = row.id;

        this.isNewRow = true;

        return row;
    }

    // Adds a new row as root
    addRoot(){
        if (this.currentEditRow)
            this.cancelEdit(this.currentEditRow.id);

        this.insertRowInGrid();
    }

    // Adds a new row as child
    addChild(){
        if (this.currentEditRow)
            this.cancelEdit(this.currentEditRow.id);

        this.insertRowInGrid(this.treegrid.selectedRow);
    }

    // Inserts the created row at specific position and updates the grid layout
    insertRowInGrid(selRow?: any){
        let row: any = this.createNewRow();
    
        this.currentEditRow = row;
        this.currentEditRowID = row.id;
        this.currentEditCell = row.cells[0];

        this.treegrid.insertRowAt(row, 0, selRow);
        this.treegrid.updateLayout();
    }

    // Confirms the changes and saves the row
    saveRow(id: any){
        let row = this.treegrid.findRowById(id);
        if (row){
            this.updateEditStatus(row, true);

            for (let j = 0; j < row.cells.length; j++)
                row.cells[j].text = row.cells[j].editText;

            if (this.isNewRow)
                this.moveRowToEnd(row);
        }

        this.resetEditor();
        this.isNewRow = false;
    }

    // Cancels the edit process and closes the editor
    cancelEdit(id: any){
        if (this.isNewRow)
            this.removeRow(id);

        this.resetEditor();
        this.isNewRow = false;
    }

    // Sets the row in edit mode and opens the editor
    editRow(id: any){
        this.resetEditor();
        
        let row = this.treegrid.findRowById(id);
        if (row){
            this.updateEditStatus(row);

            this.currentEditRow = row;
            this.currentEditRowID = row.id;
            this.currentEditCell = row.cells[0];
        }
    } 

    // Cancels the edit process when ESCAPE key is pressed
    editorKeyDown(e: any){
        if (this.currentEditRow){
            switch (e.keyCode){
                case 27: // ESCAPE
                    this.cancelEdit(this.currentEditRow.id);
                    break;
            }
        }
    }

    // Removes a row from the grid
    removeRow(id: any){
        let row = this.treegrid.findRowById(id);
        if (row){
            this.treegrid.removeRow(row);
            this.treegrid.updateLayout();
        }
    }

    // Resets the variables for editing
    resetEditor(){
        this.updateEditStatus(this.currentEditRow, true);

        this.currentEditRow = null;
        this.currentEditRowID = null;
        this.currentEditCell = null;
    }

    // Updates the status of row, for edit or normal mode
    updateEditStatus(row: any, flag?: boolean){
        let status: boolean = flag ? true : false;

        if (row)
            for (let j = 0; j < row.cells.length; j++){
                row.cells[j].saved = status;

                // If row is in edit mode, copy the text value of grid cell to the cell editor
                if (!status)
                    row.cells[j].editText = row.cells[j].text;
            }
    }

    // Moves a row at end of the list
    moveRowToEnd(row: any){
        if (row){
            let list: Array = this.rows;

            let parentRow = this.treegrid.getRowParent(row);
            if (parentRow)
                list = parentRow.rows;

            if (list){
                list.splice(list.length-1, 0, list.splice(0, 1)[0]);
                this.treegrid.updateLayout();
            }
        }
    }
}
                            
<div style="width:760px;">
    <div class="top-panel">
        <button (click)="addRoot()"><span class="icon-add-root"></span>Add Root</button>
        <button (click)="addChild()"><span class="icon-add-child"></span>Add Child</button>
    </div>
    <iui-treegrid [appRef]="applicationRef" [columns]="columns" [rows]="rows" [showFooter]="false" [rowHeight]="26" #treegrid>
        <template let-column [iuiTemplate]="{ type: 'header' }">
            <span>{{column.headerText}}</span>
        </template>
        <template let-cell [iuiTemplate]="{ type: 'cell' }">
            <div [ngSwitch]="cell.cid">
                <div *ngSwitchCase="7" (dblclick)="onButtonsDblClick($event)" (mousedown)="onButtonsMouseDown($event)">
                    <div *ngIf="cell.rid==currentEditRowID">
                        <button (click)="saveRow(cell.rid)">Save</button>
                        <button (click)="cancelEdit(cell.rid)">Cancel</button>
                    </div>
                    <div *ngIf="cell.rid!=currentEditRowID">
                        <div class="button-block" (click)="editRow(cell.rid)">
                            <span class="icons button-edit"></span>
                        </div>
                        <div class="button-block" (click)="removeRow(cell.rid)">
                            <span class="icons button-remove"></span>
                        </div>
                    </div>
                </div>
                <div *ngSwitchDefault style="display:inline-block;">
                    <input *ngIf="cell.saved==false" class="cell-input" [(ngModel)]="cell.editText" [iuiFocus]="currentEditCell==cell" (focus)="selectContent($event)" (keydown)="editorKeyDown($event)" [ngStyle]="{ width: getCellWidth(cell) + 'px' }" />
                    <span *ngIf="cell.saved!=false" class="cell-text">{{cell.text}}</span>
                </div>
            </div>
        </template>
    </iui-treegrid>
</div>
                            
/* 
    Add Rows Panel
*/
.top-panel
{
    background: #2455b0;
    padding: 3px;
    margin-bottom: 1px;
}
button
{
    background: transparent;
    border: thin solid transparent;
    color: white;
    padding: 5px 10px;
    display: inline-block;
    vertical-align: middle;
}
button:hover
{
    background-color: #153268;
    border: thin solid #0F244A;
}
.icon-add-root
{
    background: url("app/integralui/resources/icons-x24.png") no-repeat -48px -120px;
    display: inline-block;
    width: 24px;
    height: 24px;
    vertical-align: middle;
}
.icon-add-child
{
    background: url("app/integralui/resources/icons-x24.png") no-repeat -120px -168px;
    display: inline-block;
    width: 24px;
    height: 24px;
    vertical-align: middle;
}


/* 
    General TreeGrid settings
*/
.iui-treegrid
{
    width: 760px;
    height: 300px;
}
.iui-treegrid-expand-box
{
    float: left;
    margin: 3px 3px 0 0;
}

/* 
    Cell Active Buttons
*/
.iui-treegrid button
{
    background: #0064bb;
    border: thin solid #0048aa;
    color: white;
    margin-top: 1px;
    padding: 3px 5px;
}
.iui-treegrid button:hover
{
    background: #008000;
    border-color: #006400;
}
.button-block
{
    background: #e9e9e9;
    border: thin solid #d5d5d5;
    display: inline-block;
    padding: 3px;
}
.button-block:hover
{
    background: white;
    border-color: #bebebe;
}
.icons
{
    background-image: url(app/integralui/resources/icons.png);
    background-repeat: no-repeat;
    display: inline-block;
    overflow: hidden;
    width: 16px;
    height: 16px;
    opacity: 0.6;
    vertical-align: middle;
}
.button-edit
{
    background-position: -128px -81px;
}
.button-remove
{
    background-position: -160px -96px;
}
.button-edit:hover, .button-remove:hover
{
    opacity: 1;
} 

/* 
    Cell Text Editor
*/
.cell-text
{
    display: inline-block;
    margin-top: 4px;
    text-align: center;
    vertical-align: middle;
}
.cell-input
{
    margin-top: 2px;
}
                            

Conclusion

In Angular TreeGrid component, by creating custom templates and using available public methods and events, you can add new rows on demand and edit them with an inline cell editor. You can use this example as a guideline to create your own solution on how to add and edit grid rows dynamically.

The TreeGrid 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.