Template editing in Angular Treegrid component

17 Nov 202224 minutes to read

Reactive forms

Reactive Forms is a model-driven approach to create and manipulate the form controls. You can use reactive form to add and update treegrid records. To use reactive forms for editing operation, you can take leverage of the template support of dialog or inline edit mode. Setting the editSettings.mode as Row/Dialog and editSettingsTemplate as template variable of NgTemplate to define the treegrid editors.

In the below sample, We have created the FormGroup with FormControls for each columns, in the actionBegin event. While saving, we have validated the formgroup and updated the treegrid with the edited data from the FormGroup object.

import { Component, OnInit } from '@angular/core';
import { sampleData } from './datasource';
import { EditSettingsModel, ToolbarItems } from '@syncfusion/ej2-angular-treegrid';
import { DialogEditEventArgs, SaveEventArgs } from '@syncfusion/ej2-angular-grids';
import { Dialog } from '@syncfusion/ej2-angular-popups';
import { FormGroup, AbstractControl, FormControl, Validators } from '@angular/forms';
import { DataUtil } from '@syncfusion/ej2-data';
import { ReactiveFormsModule ,  FormsModule} from '@angular/forms';
import { Browser } from '@syncfusion/ej2-base';

@Component({
    selector: 'app-container',
   template: `<ejs-treegrid [dataSource]='data'  [toolbar]='toolbarOptions' [treeColumnIndex]='1' height='270' [toolbar]='toolbar' [editSettings]='editSettings' childMapping='subtasks' (actionBegin)="actionBegin($event)" (actionComplete)="actionComplete($event)">
    <e-columns>
        <e-column field='taskID' headerText='Task ID' width='120' textAlign='Right' isPrimaryKey='true' ></e-column>
        <e-column field='taskName' headerText='Task Name' width='225' ></e-column>
        <e-column field='startDate' headerText='Start Date' width='150' format="yMd" ></e-column>
        <e-column field='duration' headerText='Duration' width='90'   textAlign='Right' ></e-column>
        <e-column field='progress' headerText='Progress' width='90' textAlign='Right' ></e-column>
    </e-columns>

    <ng-template #editSettingsTemplate let-data>
            <div [formGroup]="taskForm">
                <div class="form-row">
                    <div class="form-group col-md-6">
                        <div class="e-float-input e-control-wrapper" [ngClass]="{'e-error': taskID.invalid && (taskID.dirty || taskID.touched)}">
                            <input formControlName="taskID" data-msg-containerid='taskIDError' id="taskID" name="taskID" type="text" [attr.disabled]="!data.isAdd ? '' : null">
                            <span class="e-float-line"></span>
                            <label class="e-float-text e-label-top" for="taskID"> Task ID</label>
                        </div>
                        <div id="taskIDError" [style.visibility]='((taskID.invalid && (taskID.dirty || taskID.touched)) || (taskID.invalid && submitClicked))? "visible": "hidden"'>
                            <label class="e-error" for="taskID" id="taskID-info" style="display: block;">*Task ID is required</label>
                        </div>
                    </div>
                    <div class="form-group col-md-6">
                        <div class="e-float-input e-control-wrapper" [ngClass]="{'e-error': taskName.invalid && (taskName.dirty || taskName.touched)}">
                            <input formControlName="taskName" data-msg-containerid='taskNameError' id="taskName" name="taskName" type="text">
                            <span class="e-float-line"></span>
                            <label class="e-float-text e-label-top" for="taskName">Task Name</label>
                        </div>
                        <div id="taskNameError" [style.visibility]='((taskName.invalid && (taskName.dirty || taskName.touched)) || (taskName.invalid && submitClicked))? "visible": "hidden"'>
                            <label class="e-error" for="taskName" id="taskName-info" style="display: block;">*Task Name is required</label>
                        </div>
                    </div>
                </div>
                <div class="form-row">
                    <div class="form-group col-md-6">
                        <ejs-numerictextbox formControlName="duration" id="duration" placeholder="Duration" format="##" floatLabelType='Always'></ejs-numerictextbox>
                    </div>
                    <div class="form-group col-md-6">
                        <ejs-datepicker id="startDate" formControlName="startDate" placeholder="Start Date" floatLabelType='Always'></ejs-datepicker>
                        <div id="startDateError" [style.visibility]='((startDate.invalid && (startDate.dirty || startDate.touched)) || (startDate.invalid && submitClicked)) ? "visible": "hidden"'>
                            <label class="e-error" for="startDate" id="startDate-info" style="display: block;">*Start Date is required</label>
                        </div>
                    </div>
                </div>
                <div class="form-row">
                    <div class="form-group col-md-6">
                        <ejs-dropdownlist id="progress" formControlName="progress" [dataSource]='progressDistinctData' [fields]="{text: 'progress', value: 'progress' }" placeholder="Progress" popupHeight='300px' floatLabelType='Always'></ejs-dropdownlist>
                    </div>
                </div>
            </div>
        </ng-template>
</ejs-treegrid>`,
})
export class AppComponent implements OnInit {
    public data: Object[] = [];
    public editSettings: EditSettingsModel;
    public toolbarOptions: ToolbarItems[];
    public taskForm: FormGroup;
    public progressDistinctData: Object;
    public priorityDistinctData: Object;
    public submitClicked: boolean = false;

    ngOnInit(): void {
        this.data = sampleData;
        this.editSettings = { allowEditing: true, allowAdding: true, allowDeleting: true , mode: 'Dialog' , newRowPosition: 'Below'};
        this.toolbarOptions = ['Add', 'Edit', 'Delete'];
        this.progressDistinctData = DataUtil.distinct(sampleData, 'progress', true);
        this.priorityDistinctData = DataUtil.distinct(sampleData, 'priority', true );
    }

    createFormGroup(data: ITaskModel): FormGroup {
        return new FormGroup({
            taskID: new FormControl(data.taskID, Validators.required),
            startDate: new FormControl(data.startDate, this.dateValidator()),
            taskName: new FormControl(data.taskName, Validators.required),
            duration: new FormControl(data.duration),
            progress: new FormControl(data.progress),
            priority: new FormControl(data.priority),
        });
    }

    dateValidator() {
        return (control: FormControl): null | Object  => {
            return control.value && control.value.getFullYear &&
            (1900 <= control.value.getFullYear() && control.value.getFullYear() <=  2099) ? null : { OrderDate: { value : control.value}};
        };
    }

    actionBegin(args: SaveEventArgs): void {
        if (args.requestType === 'beginEdit' || args.requestType === 'add') {
            this.submitClicked = false;
            this.taskForm = this.createFormGroup(args.rowData);
        }
        if (args.requestType === 'save') {
            this.submitClicked = true;
            if (this.taskForm.valid) {
                args.data = this.taskForm.value;
            } else {
                args.cancel = true;
            }
        }
    }

    actionComplete(args: DialogEditEventArgs): void {
        if ((args.requestType === 'beginEdit' || args.requestType === 'add')) {
            // Set initial Focus
            if (args.requestType === 'beginEdit') {
                (args.form.elements.namedItem('taskName') as HTMLInputElement).focus();
            } else if (args.requestType === 'add') {
                (args.form.elements.namedItem('taskID') as HTMLInputElement).focus();
            }
        }
    }

    get taskID(): AbstractControl  { return this.taskForm.get('taskID'); }

    get taskName(): AbstractControl { return this.taskForm.get('taskName'); }

    get startDate(): AbstractControl { return this.taskForm.get('startDate'); }
}
export interface ITaskModel {
    taskID?: number;
    taskName?: string;
    startDate?: Date;
    duration?: number;
    progress?: number;
    priority?: string;
}
import { NgModule,ViewChild } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { TreeGridModule } from '@syncfusion/ej2-angular-treegrid';
import { PageService, SortService, FilterService,EditService,ToolbarService } from '@syncfusion/ej2-angular-treegrid';
import { AppComponent } from './app.component';
import {ButtonModule} from '@syncfusion/ej2-angular-buttons';
import { DropDownListAllModule } from '@syncfusion/ej2-angular-dropdowns';
import { ReactiveFormsModule ,  FormsModule} from '@angular/forms';
import { NumericTextBoxAllModule } from '@syncfusion/ej2-angular-inputs';
import { DatePickerModule, DatePickerAllModule } from '@syncfusion/ej2-angular-calendars';


/**
 * Module
 */
@NgModule({
    imports: [
        BrowserModule,
        TreeGridModule,
        ButtonModule,
        DropDownListAllModule,
        ReactiveFormsModule,
        FormsModule,
        NumericTextBoxAllModule,
        DatePickerAllModule
    ],
    declarations: [AppComponent],
    bootstrap: [AppComponent],
    providers: [PageService,
                SortService,
                FilterService,
                EditService,
                ToolbarService]
})
export class AppModule { }
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode } from '@angular/core';
import { AppModule } from './app.module';

enableProdMode();
platformBrowserDynamic().bootstrapModule(AppModule);

Template-driven forms

Template-driven forms is a template-driven approach to create and manipulate the form controls. You can use template-driven form to add and update treegrid records. To use template-driven forms for editing operation, you can take leverage of the template support of dialog or inline edit mode. Setting the editSettings.mode as Row/Dialog and editSettingsTemplate as template variable of NgTemplate to define the treegrid editors.

In some cases, you need to add the new field editors in the dialog which are not present in the column model. In that situation, the dialog template will help you to customize the default edit dialog.

You can check this video to learn about how to customize the edit dialog of TreeGrid using template driven forms.

In the following sample, treegrid enabled with dialog template editing.

import { Component, OnInit, ViewChild } from '@angular/core';
import { sampleData } from './datasource';
import { DataUtil } from '@syncfusion/ej2-data';
import { DialogEditEventArgs, SaveEventArgs } from '@syncfusion/ej2-angular-grids';
import { EditService, ToolbarService, PageService } from '@syncfusion/ej2-angular-treegrid';
import { FormGroup } from '@angular/forms';

@Component({
    selector: 'app-container',
   template: `<ejs-treegrid [dataSource]='data' height='225' childMapping='subtasks' [treeColumnIndex]='1'allowPaging='true' [pageSettings]='pageSettings' [editSettings]='editSettings' [toolbar]='toolbar' (actionBegin)="actionBegin($event)" (actionComplete)="actionComplete($event)">
        <e-columns>
            <e-column field='taskID' headerText='Task ID' width='120' textAlign='Right' isPrimaryKey='true' ></e-column>
            <e-column field='taskName' headerText='Task Name' width='225' ></e-column>
            <e-column field='startDate' headerText='Start Date' width='150' format="yMd" ></e-column>
            <e-column field='approved' headerText='Approved' type='boolean' editType='booleanedit' [displayAsCheckBox]='true' width='90' textAlign='Right' ></e-column>
        </e-columns>
        <ng-template #editSettingsTemplate let-data>
        <div ngForm #taskForm="ngForm">
            <div class="form-row">
                <div class="form-group col-md-6">
                    <div class="e-float-input e-control-wrapper" [ngClass]="{'e-error': taskID.invalid && (taskID.dirty || taskID.touched)}">
                        <input [(ngModel)]="taskData.taskID" required id="taskID" name="taskID" type="text" [attr.disabled]="!data.isAdd ? '' : null" #taskID="ngModel">
                        <span class="e-float-line"></span>
                        <label class="e-float-text e-label-top" for="taskID"> Task ID</label>
                    </div>
                    <div id="taskIDError" *ngIf='taskID.invalid && (taskID.dirty || taskID.touched)'>
                        <label class="e-error" id="taskID-info" style="display: block;">*Task ID is required</label>
                    </div>
                </div>
                <div class="form-group col-md-6">
                    <div class="e-float-input e-control-wrapper" [ngClass]="{'e-error': taskName.invalid && (taskName.dirty || taskName.touched)}">
                        <input [(ngModel)]="taskData.taskName" required id="taskName" name="taskName" type="text" #taskName="ngModel">
                        <span class="e-float-line"></span>
                        <label class="e-float-text e-label-top" for="taskName">Task Name</label>
                    </div>
                    <div id="taskNameError" *ngIf='taskName.invalid && (taskName.dirty || taskName.touched)'>
                        <label class="e-error" id="taskName-info" style="display: block;">*Task Name is required</label>
                    </div>
                </div>
            </div>
            <div class="form-row">
                <div class="form-group col-md-6">
                    <ejs-numerictextbox [(ngModel)]="taskData.progress" name="progress" id="progress" placeholder="Progress" floatLabelType='Always'></ejs-numerictextbox>
                </div>
                <div class="form-group col-md-6">
                    <ejs-datepicker id="startDate" name="startDate" required [(ngModel)]="taskData.startDate" placeholder="Start Date" floatLabelType='Always'></ejs-datepicker>
                </div>
            </div>
            <div class="form-row">
                <div class="form-group col-md-6">
                    <ejs-numerictextbox [(ngModel)]="taskData.duration" name="duration" id="duration" placeholder="Duration" floatLabelType='Always'></ejs-numerictextbox>
                </div>
                <div class="form-group col-md-3">
                <ejs-checkbox #approved label='Approved' [(ngModel)]="taskData.approved" [ngModelOptions]="{standalone: true}" [checked]="taskData.approved"></ejs-checkbox>
                </div>
            </div>
        </div>
    </ng-template>
        </ejs-treegrid>`,
})
export class AppComponent implements OnInit {
    public data: Object[] = [];
    public editSettings: Object;
    public toolbar: string[];
    public pageSettings: Object;
    public taskData: ITaskModel;
    @ViewChild('taskForm')
    public taskForm: FormGroup;
    public progressDistinctData: Object;
    public priorityDistinctData: Object;

    ngOnInit(): void {
        this.data = sampleData;
        this.editSettings = { allowEditing: true, allowAdding: true, allowDeleting: true , mode: 'Dialog' ,newRowPosition: 'Below'};
        this.toolbar = ['Add', 'Edit', 'Delete', 'Update', 'Cancel'];
        this.pageSettings = { pageCount: 5 };
        this.progressDistinctData = DataUtil.distinct(sampleData, 'progress', true);
        this.priorityDistinctData = DataUtil.distinct(sampleData, 'priority', true );
    }

    actionBegin(args: SaveEventArgs): void {
        if (args.requestType === 'beginEdit' || args.requestType === 'add') {
            this.taskData = Object.assign({}, args.rowData);
        }
        if (args.requestType === 'save') {
            if (this.taskForm.valid) {
                args.data = this.taskData;
            } else {
                args.cancel = true;
            }
        }
    }

    actionComplete(args: DialogEditEventArgs): void {
        if (args.requestType === 'beginEdit' || args.requestType === 'add') {
            // Disable the Validation Rules
            args.form.ej2_instances[0].rules = {};
            // Set initial Focus
            if (args.requestType === 'beginEdit') {
                (args.form.elements.namedItem('taskName') as HTMLInputElement).focus();
            } else if (args.requestType === 'add') {
                (args.form.elements.namedItem('taskID') as HTMLInputElement).focus();
            }

        }
    }

    public focusIn(target: HTMLElement): void {
        target.parentElement.classList.add('e-input-focus');
    }

    public focusOut(target: HTMLElement): void {
        target.parentElement.classList.remove('e-input-focus');
    }
}
export interface ITaskModel {
    taskID?: number;
    taskName?: string;
    startDate?: Date;
    duration?: number;
    progress?: number;
    priority?: string;
    approved?: boolean;
}
import { NgModule,ViewChild } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { TreeGridModule } from '@syncfusion/ej2-angular-treegrid';
import { PageService, FilterService,EditService,ToolbarService } from '@syncfusion/ej2-angular-treegrid';
import { AppComponent } from './app.component';
import {ButtonModule, CheckBoxAllModule} from '@syncfusion/ej2-angular-buttons';
import { DropDownListAllModule } from '@syncfusion/ej2-angular-dropdowns';
import { ReactiveFormsModule ,  FormsModule} from '@angular/forms';
import { NumericTextBoxAllModule } from '@syncfusion/ej2-angular-inputs';
import { DatePickerAllModule } from '@syncfusion/ej2-angular-calendars';


/**
 * Module
 */
@NgModule({
    imports: [
        BrowserModule,
        TreeGridModule,
        ButtonModule,
        DropDownListAllModule,
        ReactiveFormsModule,
        FormsModule,
        NumericTextBoxAllModule,
        CheckBoxAllModule,
        DatePickerAllModule
    ],
    declarations: [AppComponent],
    bootstrap: [AppComponent],
    providers: [PageService,
                FilterService,
                EditService,
                ToolbarService]
})
export class AppModule { }
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode } from '@angular/core';
import { AppModule } from './app.module';

enableProdMode();
platformBrowserDynamic().bootstrapModule(AppModule);

The template form editors should have name attribute.

Template context

The Essential JS2 packages has a built-in template engine. Using the template engine, you can access the row information inside the HTML element and bind the attributes, values, or elements based on this row information.

The following properties will be available at the time of template execution.

Property Name Usage
isAdd A Boolean property; it defines whether the current row should be a new record or not.

In the following code example, the taskID textbox has been disabled by using the isAdd property.

// The disabled attributes will be added based on the isAdd property.
<input formControlName="taskID" id="taskID" name="taskID" type="text" [attr.disabled]="!data.isAdd ? '' : null">

The following code example illustrates rendering the taskID textbox, when a new record is added.


<div class="form-group col-md-6" *ngIf='data.isAdd'>
    <div class="e-float-input e-control-wrapper">
        <input formControlName="taskID" id="taskID" name="taskID" type="text" [attr.disabled]="!data.isAdd ? '' : null">
        <span class="e-float-line"></span>
        <label class="e-float-text e-label-top" for="taskID">Task ID</label>
    </div>
</div>

Set focus to editor

By default, the first input element in the dialog will be focused while opening the dialog.
If the first input element is in disabled or hidden state, focus the valid input element in the actionComplete event based on requestType as beginEdit.

    actionComplete: (args: DialogEditEventArgs) => {
        // Set initial Focus
        if (args.requestType === 'beginEdit') {
            (args.form.elements.namedItem('taskName')as HTMLInputElement).focus();
        }
    }

Disable form validation

If you have interested to use angular form validation then you need to disable the default validation rules in the actionComplete event.

    actionComplete(args: DialogEditEventArgs) {
        if ((args.requestType === 'beginEdit' || args.requestType === 'add')) {
            // Disable the Validation Rules
            args.form.ej2_instances[0].rules = {};
        }
    }

Adding validation rules for custom editors

If you have used additional fields that are not present in the column model, then add the validation rules to the actionComplete event.

    actionComplete: (args: DialogEditEventArgs) => {
        if ((args.requestType === 'beginEdit' || args.requestType === 'add')) {
            // Add Validation Rules
            args.form.ej2_instances[0].addRules('progress', {max: 100});
        }
    }