Preventing Editing for Specific Fields

The TreeList enables you to restrict user input and updates for specific fields, cells, and columns.

You can prevent undesired user interfering with data, such as product ids or company names, by using any of the following approaches:

  • Set the editable option of the column to false.
  • Omit the field declaration in the FormGroup.

    private createFormGroup(item: any): FormGroup {
       const group = new FormGroup({
           'ReportsTo': new FormControl(item.ReportsTo),
           'FirstName': new FormControl(item.FirstName, Validators.required),
           'LastName': new FormControl(item.LastName, Validators.required),
           // 'Position': new FormControl(item.Position),
           'Extension': new FormControl(item.Extension, Validators.compose([Validators.required, Validators.min(0)]))
       });
    
       if (item.EmployeeId) {
           group.addControl('EmployeeId', new FormControl(item.EmployeeId));
       }
    
       return group;
    }
  • Skip the calling of the editCell method from the cellClick event handler.

    public cellClickHandler({ sender, column, columnIndex, dataItem, isEdited }: CellClickEvent): void {
       const readonly = this.readOnlyColumns.has(column.field);
       if (!isEdited && !readonly) {
           sender.editCell(dataItem, columnIndex, this.createFormGroup(dataItem));
       }
    }

The following example demonstrates the complete implementation of the cellClick event-handler approach.

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

import {
    AddEvent,
    CancelEvent,
    EditEvent,
    RemoveEvent,
    SaveEvent,
    TreeListComponent,
    CellClickEvent,
    CellCloseEvent
} 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"
        (cellClick)="cellClickHandler($event)"
        (cellClose)="cellCloseHandler($event)"
        (add)="addHandler($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="#">
            <ng-template kendoTreeListCellTemplate let-dataItem>
                <span class="k-treelist-ignore-click k-icon k-i-info"
                      title="Elements with the 'k-treelist-ignore-click' class do not trigger 'cellClick' events.">
                </span>
                {{ dataItem.Extension }}
            </ng-template>
        </kendo-treelist-column>
        <kendo-treelist-command-column width="140">
            <ng-template kendoTreeListCellTemplate
                         let-isNew="isNew"
                         let-cellContext="cellContext"
                         let-dataItem="dataItem">
                <button [kendoTreeListAddCommand]="cellContext"
                        icon="filter-add-expression" title="Add Child">
                </button>
                <button [kendoTreeListRemoveCommand]="cellContext"
                        icon="delete" title="Remove">
                </button>
                <button [kendoTreeListSaveCommand]="cellContext"
                        [disabled]="formGroup?.invalid"
                        icon="save" title="Add">
                </button>
                <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;
    public readOnlyColumns = new Set(['Position']);

    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 cellClickHandler({ sender, column, columnIndex, dataItem, isEdited }: CellClickEvent): void {
        const readonly = this.readOnlyColumns.has(column.field);
        if (!isEdited && !readonly) {
            sender.editCell(dataItem, columnIndex, this.createFormGroup(dataItem));
        }
    }

    public cellCloseHandler(e: CellCloseEvent): void {
        const { formGroup, dataItem } = e;
        if (!formGroup.valid) {
            // Prevent closing the edited cell if the form is invalid.
            e.preventDefault();
        } else if (formGroup.dirty) {
            // Reflect changes immediately
            Object.assign(dataItem, formGroup.value);

            this.editService.update(dataItem);
        }
    }

    public addHandler({ sender, parent }: AddEvent): void {
        const dataItem: any = {
            ReportsTo: parent ? parent.EmployeeId : null
        };

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

        // Show the new row editor
        sender.addRow(this.createFormGroup(dataItem), 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 = this.createFormGroup(dataItem);

        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
        sender.closeRow(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);
                }
            });

        // Close the cell editor
        sender.cancelCell();
    }

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

    private createFormGroup(item: any): FormGroup {
        const group = new FormGroup({
            'ReportsTo': new FormControl(item.ReportsTo),
            'FirstName': new FormControl(item.FirstName, Validators.required),
            'LastName': new FormControl(item.LastName, Validators.required),
            'Position': new FormControl(item.Position),
            'Extension': new FormControl(item.Extension, Validators.compose([Validators.required, Validators.min(0)]))
        });

        if (item.EmployeeId) {
            group.addControl('EmployeeId', new FormControl(item.EmployeeId));
        }

        return group;
    }
}
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);

In this article