Reactive Forms

The TreeList provides options for editing its data by using the Reactive Angular Forms.

Basic Concepts

By default, the built-in column editors in the Kendo UI TreeList for Angular utilize the Model-driven Angular Forms directives. To manipulate a TreeList row, you need to call the addRow or the editRow method respectively and then pass the FormGroup property as one of its parameters. The FormGroup configuration is part of the Angular Forms package.

To configure the editing mode of the TreeList, you have to:

  1. Configure the type of the column editors.
  2. Configure the command column by defining the command buttons.
  3. Attach handlers for the CRUD data operations and update the data.

Data-Binding Directives vs. Manual Setup

The TreeList includes a Reactive Editing Directive that significantly reduces the amount of boiler plate code required for editing. Try it out before using the more flexible, but verbose manual setup.

The following example demonstrates how to manually set up the inline editing mode of the Kendo UI TreeList for Angular using Reactive Forms.

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { Observable } from 'rxjs';

import {
    AddEvent,
    CancelEvent,
    EditEvent,
    RemoveEvent,
    SaveEvent,
    TreeListComponent
} from '@progress/kendo-angular-treelist';

import { EmployeeEditService } from './employee-edit.service';
import { Employee } from './employee';
import { take } from 'rxjs/operators';


@Component({
  selector: 'my-app',
  template: `
    <kendo-treelist
        kendoTreeListExpandable
        [data]="rootData | async"
        idField="EmployeeId"
        [fetchChildren]="fetchChildren"
        [hasChildren]="hasChildren"
        (add)="addHandler($event)"
        (edit)="editHandler($event)"
        (remove)="removeHandler($event)"
        (save)="saveHandler($event)"
        (cancel)="cancelHandler($event)"
        [height]="533"
    >
        <ng-template kendoTreeListToolbarTemplate>
            <button kendoTreeListAddCommand type="button">Add new</button>
        </ng-template>
        <kendo-treelist-column [expandable]="true" field="FirstName" title="First Name">
        </kendo-treelist-column>
        <kendo-treelist-column field="LastName" title="Last Name">
        </kendo-treelist-column>
        <kendo-treelist-column field="Position" title="Position">
        </kendo-treelist-column>
        <kendo-treelist-column field="Extension" title="Extension" editor="numeric" format="#">
        </kendo-treelist-column>
        <kendo-treelist-command-column width="140">
            <ng-template kendoTreeListCellTemplate let-isNew="isNew" let-cellContext="cellContext">
                <!-- "Add Child" command directive, will not be visible in edit mode -->
                <button [kendoTreeListAddCommand]="cellContext"
                        icon="filter-add-expression" title="Add Child">
                </button>

                <!-- "Edit" command directive, will not be visible in edit mode -->
                <button [kendoTreeListEditCommand]="cellContext"
                        icon="edit" title="Edit" [primary]="true">
                </button>

                <!-- "Remove" command directive, will not be visible in edit mode -->
                <button [kendoTreeListRemoveCommand]="cellContext"
                        icon="delete" title="Remove">
                </button>

                <!-- "Save" command directive, will be visible only in edit mode -->
                <button [kendoTreeListSaveCommand]="cellContext"
                        [disabled]="formGroup?.invalid"
                        icon="save" title="{{ isNew ? 'Add' : 'Update' }}">
                </button>

                <!-- "Cancel" command directive, will be visible only in edit mode -->
                <button [kendoTreeListCancelCommand]="cellContext"
                        icon="cancel" title="{{ isNew ? 'Discard changes' : 'Cancel' }}">
                </button>
            </ng-template>
        </kendo-treelist-command-column>
    </kendo-treelist>
  `
})
export class AppComponent implements OnInit {
    public rootData: Observable<Employee[]>;
    public formGroup: FormGroup;
    public editedItem: Employee;

    constructor(private editService: EmployeeEditService) {}

    public ngOnInit(): void {
        this.rootData = this.editService;
        this.editService.read();
    }

    public fetchChildren = (item: Employee): Observable<Employee[]> => {
        return this.editService.fetchChildren(item.EmployeeId);
    }

    public hasChildren = (item: Employee): boolean => {
        return item.hasChildren;
    }

    public addHandler({ sender, parent }: AddEvent): void {
        // Close the current edited row, if any.
        this.closeEditor(sender);

        // Expand the parent.
        if (parent) {
            sender.expand(parent);
        }

        // Define all editable fields validators and default values
        this.formGroup = new FormGroup({
            'ReportsTo': new FormControl(parent ? parent.EmployeeId : null),
            'FirstName': new FormControl('', Validators.required),
            'LastName': new FormControl('', Validators.required),
            'Position': new FormControl(''),
            'Extension': new FormControl('', Validators.compose([Validators.required, Validators.min(0)]))
        });

        // Show the new row editor, with the `FormGroup` build above
        sender.addRow(this.formGroup, parent);
    }

    public editHandler({ sender, dataItem }: EditEvent): void {
        // Close the current edited row, if any.
        this.closeEditor(sender, dataItem);

        // Define all editable fields validators and default values
        this.formGroup = new FormGroup({
            'EmployeeId': new FormControl(dataItem.EmployeeId),
            'ReportsTo': new FormControl(dataItem.ReportsTo),
            'FirstName': new FormControl(dataItem.FirstName, Validators.required),
            'LastName': new FormControl(dataItem.LastName, Validators.required),
            'Position': new FormControl(dataItem.Position),
            'Extension': new FormControl(dataItem.Extension, Validators.compose([Validators.required, Validators.min(0)]))
        });

        this.editedItem = dataItem;

        // Put the row in edit mode, with the `FormGroup` build above
        sender.editRow(dataItem, this.formGroup);
    }

    public cancelHandler({ sender, dataItem, isNew }: CancelEvent): void {
        // Close the editor for the given row
        this.closeEditor(sender, dataItem, isNew);
    }

    public saveHandler({ sender, dataItem, parent, formGroup, isNew }: SaveEvent): void {
        // Collect the current state of the form.
        // The `formGroup` argument is the same as was provided when calling `editRow`.
        const employee: Employee = formGroup.value;

        if (!isNew) {
            // Reflect changes immediately
            Object.assign(dataItem, employee);
        } else if (parent) {
            // Update the hasChildren field on the parent node
            parent.hasChildren = true;
        }

        this.editService
            // Publish the update to the remote service.
            .save(employee, parent, isNew)
            .pipe(take(1))
            .subscribe(() => {
                if (parent) {
                    // Reload the parent node to reflect the changes.
                    sender.reload(parent);
                }
            });

        sender.closeRow(dataItem, isNew);
    }

    public removeHandler({ sender, dataItem, parent }: RemoveEvent): void {
        this.editService
            // Publish the update to the remote service.
            .remove(dataItem, parent)
            .pipe(take(1))
            .subscribe(() => {
                if (parent) {
                    // Reload the parent node to reflect the changes.
                    sender.reload(parent);
                }
            });
    }

    private closeEditor(treelist: TreeListComponent, dataItem: any = this.editedItem, isNew: boolean = false): void {
        treelist.closeRow(dataItem, isNew);
        this.editedItem = undefined;
        this.formGroup = undefined;
    }
}
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { tap, take } from 'rxjs/operators';

import { Employee } from './employee';

const CREATE_ACTION = 'Create';
const UPDATE_ACTION = 'Update';
const REMOVE_ACTION = 'Destroy';

function noop() { /* */ }

@Injectable()
export class EmployeeEditService extends BehaviorSubject<Employee[]> {
    constructor(private http: HttpClient) {
        super(null);
    }

    public read(): void {
        this.fetch('')
            .pipe(take(1))
            .subscribe(data => this.next(data));
    }

    public fetchChildren(reportsTo: number = null): Observable<Employee[]> {
        return this.fetch('', null, reportsTo);
    }

    public update(item: Employee): void {
        this.save(item, null, false)
            .pipe(take(1))
            .subscribe(noop);
    }

    public save(item: Employee, parent: Employee, isNew: boolean): Observable<Employee[]> {
        const action = isNew ? CREATE_ACTION : UPDATE_ACTION;

        return this.fetch(action, item).pipe(tap(() => {
            if (!parent && isNew) {
                this.read();
            }
        }));
    }

    public remove(item: any, parent?: Employee): Observable<Employee[]> {
        return this.fetch(REMOVE_ACTION, item).pipe(tap(() => {
            if (!parent) {
                this.read();
            }
        }));
    }

    private fetch(action: string = '', data?: any, id?: any): Observable<Employee[]> {
        let params = new HttpParams();

        if (typeof id !== 'undefined') {
            params = params.set('id', id);
        }

        if (data) {
            params = params.set('models', JSON.stringify([data]));
        }

        return this.http.jsonp<Employee[]>(
            `https://demos.telerik.com/kendo-ui/service/EmployeeDirectory/${action}?${params.toString()}`,
            'callback'
        );
    }
}
export interface Employee {
    EmployeeId: number;
    ReportsTo: number;
    FirstName: string;
    LastName: string;
    Position: string;
    Extension: string;
    hasChildren?: boolean;
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule, HttpClientJsonpModule } from '@angular/common/http';

import { TreeListModule } from '@progress/kendo-angular-treelist';

import { AppComponent } from './app.component';
import { EmployeeEditService } from './employee-edit.service';

@NgModule({
    declarations: [
        AppComponent
    ],
    imports: [
        BrowserModule,
        BrowserAnimationsModule,
        ReactiveFormsModule,
        FormsModule,
        TreeListModule,
        HttpClientModule,
        HttpClientJsonpModule
    ],
    bootstrap: [ AppComponent ],
    providers: [ EmployeeEditService ]
})
export class AppModule {}
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

enableProdMode();

const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);

Setup

To enable the editing mode of the TreeList in the Angular Reactive Forms:

  1. Configure the type of the columns editor.

    The default editor that is created by the TreeList is the text editor. To change this behavior, set the editor property.

    <!-- setting numeric editor -->
    <kendo-treelist-column field="Extension" title="Extension" editor="numeric">
    </kendo-treelist-column>
  2. Configure the command column by defining the command buttons inside the command column template.

    <kendo-treelist-command-column width="140">
       <ng-template kendoTreeListCellTemplate let-isNew="isNew" let-cellContext="cellContext">
           <!-- "Add Child" command directive, will not be visible in edit mode -->
           <button [kendoTreeListAddCommand]="cellContext"
                   icon="filter-add-expression" title="Add Child">
           </button>
    
           <!-- "Edit" command directive, will not be visible in edit mode -->
           <button [kendoTreeListEditCommand]="cellContext"
                   icon="edit" title="Edit" [primary]="true">
           </button>
    
           <!-- "Remove" command directive, will not be visible in edit mode -->
           <button [kendoTreeListRemoveCommand]="cellContext"
                   icon="delete" title="Remove">
           </button>
    
           <!-- "Save" command directive, will be visible only in edit mode -->
           <button [kendoTreeListSaveCommand]="cellContext"
                   [disabled]="formGroup?.invalid"
                   icon="save" title="{{ isNew ? 'Add' : 'Update' }}">
           </button>
    
           <!-- "Cancel" command directive, will be visible only in edit mode -->
           <button [kendoTreeListCancelCommand]="cellContext"
                   icon="cancel" title="{{ isNew ? 'Discard changes' : 'Cancel' }}">
           </button>
       </ng-template>
    </kendo-treelist-command-column>
  3. Attach handlers for the CRUD data operations.

    When a command button is clicked, the TreeList emits the corresponding event. To instruct the component what action to perform, handle the event that is emitted.

    <kendo-treelist
     [data]="view | async"
     (add)="addHandler($event)"
     (edit)="editHandler($event)"
     (remove)="removeHandler($event)"
     (save)="saveHandler($event)"
     (cancel)="cancelHandler($event)"
    >
    <!-- the rest of the configuration -->
    </kendo-treelist>

Toggling the Edit State

The TreeList enables you to toggle its edit state by using the following events:

Adding Records

The add event fires when the kendoTreeListAddCommand is clicked. Inside the event handler, you can show the new row editor by calling the addRow method and by providing a FormGroup configuration that describes the view model for that editor (similar to the editRow method).

public addHandler({ sender, parent }: AddEvent): void {
    // Close the current edited row, if any.
    this.closeEditor(sender);

    // Expand the parent.
    if (parent) {
        sender.expand(parent);
    }

    // Define all editable fields validators and default values
    this.formGroup = new FormGroup({
        'ReportsTo': new FormControl(parent ? parent.EmployeeId : null),
        'FirstName': new FormControl('', Validators.required),
        'LastName': new FormControl('', Validators.required),
        'Position': new FormControl(''),
        'Extension': new FormControl('', Validators.compose([Validators.required, Validators.min(0)]))
    });

    // Show the new row editor, with the `FormGroup` build above
    sender.addRow(this.formGroup, parent);
}

Editing Records

The edit event fires when the kendoTreeListEditCommand is clicked. Inside the event handler, you can set the row to the editing mode by calling the editRow method and by providing a FormGroup configuration that describes the view model for that editor.

public editHandler({ sender, dataItem }: EditEvent): void {
    // Close the current edited row, if any.
    this.closeEditor(sender, dataItem);

    // Define all editable fields validators and default values
    this.formGroup = new FormGroup({
        'EmployeeId': new FormControl(dataItem.EmployeeId),
        'ReportsTo': new FormControl(dataItem.ReportsTo),
        'FirstName': new FormControl(dataItem.FirstName, Validators.required),
        'LastName': new FormControl(dataItem.LastName, Validators.required),
        'Position': new FormControl(dataItem.Position),
        'Extension': new FormControl(dataItem.Extension, Validators.compose([Validators.required, Validators.min(0)]))
    });

    this.editedItem = dataItem;

    // Put the row in edit mode, with the `FormGroup` build above
    sender.editRow(dataItem, this.formGroup);
}

Removing Records

The remove event fires when the kendoTreeListRemoveCommand is clicked.

Inside the event handler, you can perform the following actions:

  • Issue a request to remove the current data item from the data source.
  • Invoke reload with the parent node as parameter to reflect the changes.
public removeHandler({ sender, dataItem, parent }: RemoveEvent): void {
    this.editService
        // Publish the update to the remote service.
        .remove(dataItem, parent)
        .pipe(take(1))
        .subscribe(() => {
            if (parent) {
                // Reload the parent node to reflect the changes.
                sender.reload(parent);
            }
        });
}

Saving Records

The save event fires when the kendoTreeListSaveCommand is clicked.

Inside the event handler, you can perform the following actions:

  • Update the value of the form in the data source.
  • Call the closeRow method to switch the current row back to the view mode.
  • Invoke reload with the parent node as parameter to reflect the changes.
public saveHandler({ sender, dataItem, parent, formGroup, isNew }: SaveEvent): void {
    // Collect the current state of the form.
    // The `formGroup` argument is the same as was provided when calling `editRow`.
    const employee: Employee = formGroup.value;

    if (!isNew) {
        // Reflect changes immediately
        Object.assign(dataItem, employee);
    }

    this.editService
        // Publish the update to the remote service.
        .save(employee, parent, isNew)
        .pipe(take(1))
        .subscribe(() => {
            if (parent) {
                // Reload the parent node to reflect the changes.
                sender.reload(parent);
            }
        });

    sender.closeRow(dataItem, isNew);
}

Cancelling Editing

The cancel event fires when the kendoTreeListCancelCommand is clicked. Inside the event handler, you can switch the row back to the view mode by calling the closeRow method.

public cancelHandler({ sender, dataItem, isNew }: CancelEvent): void {
    // Close the editor for the given row
    this.closeEditor(sender, dataItem, isNew);
}

private closeEditor(treelist: TreeListComponent, dataItem: any = this.editedItem, isNew: boolean = false): void {
    treelist.closeRow(dataItem, isNew);
    this.editedItem = undefined;
    this.formGroup = undefined;
}

In this article