All Components

Inline Editing on Row Click

The Grid provides options for editing its data inline when the user clicks a row.

Setup

To implement the inline editing through a row click:

  1. Handle the cellClick event that contains references to both the index of the clicked row and the respective data item in its event data.

    <kendo-grid id="productsGrid"
       (cellClick)="editClick($event)"
  2. Depending on the behavior you wish to achieve, handle the click action outside the Kendo UI Grid for Angular by applying custom logic—for example, to save the currently edited item, use the following approach:

    // common constants
    const matches = (el, selector) => (el.matches || el.msMatchesSelector).call(el, selector);
    
    // component class code
    
    @ViewChild(GridComponent) private grid: GridComponent;
    
    public ngOnInit(): void {
     this.view = this.service.products();
    
     this.docClickSubscription = this.renderer.listen('document', 'click', this.onDocumentClick.bind(this));
    }
    
    public ngOnDestroy(): void {
       this.docClickSubscription();
    }
    
    private onDocumentClick(e: any): void {
       if (this.formGroup && this.formGroup.valid &&
           !matches(e.target, '#productsGrid tbody *, #productsGrid .k-grid-toolbar .k-button')) {
           this.saveCurrent();
       }
    }
    
    private saveCurrent(): void {
       if (this.formGroup) {
           this.service.save(this.formGroup.value, this.isNew);
           this.closeEditor();
       }
    }
    
    private closeEditor(): void {
       this.grid.closeRow(this.editedRowIndex);
    
       this.isNew = false;
       this.editedRowIndex = undefined;
       this.formGroup = undefined;
    }

For more details on how to implement the inline editing functionality, refer to the example on editing the Grid in Reactive Forms.

The following example demonstrates the full implementation of the approach.

import { Component, OnInit, OnDestroy, ViewChild, Renderer2 } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { ProductsService } from './products.service';
import { AddEvent, EditEvent, GridComponent } from '@progress/kendo-angular-grid';
import { groupBy, GroupDescriptor } from '@progress/kendo-data-query';

const createFormGroup = dataItem => new FormGroup({
    'Discontinued': new FormControl(dataItem.Discontinued),
    'ProductID': new FormControl(dataItem.ProductID),
    'ProductName': new FormControl(dataItem.ProductName, Validators.required),
    'UnitPrice': new FormControl(dataItem.UnitPrice),
    'UnitsInStock': new FormControl(dataItem.UnitsInStock, Validators.compose([Validators.required, Validators.pattern('^[0-9]{1,3}')]))
});

const matches = (el, selector) => (el.matches || el.msMatchesSelector).call(el, selector);

@Component({
    selector: 'my-app',
    template: `
        <kendo-grid
            id="productsGrid"
            (cellClick)="cellClickHandler($event)"
            [groupable]="true"
            [group]="groups"
            (groupChange)="groupChange($event)"
            [data]="view"
            height="500"
            (add)="addHandler($event)"
            >
            <ng-template kendoGridToolbarTemplate>
                <button kendoGridAddCommand>Add new</button>
                <button *ngIf="formGroup"
                    (click)="cancelHandler()"
                    class="k-button k-primary">Cancel</button>
            </ng-template>
            <kendo-grid-column field="ProductName" title="Product Name"></kendo-grid-column>
            <kendo-grid-column field="UnitPrice" editor="numeric" title="Price"></kendo-grid-column>
            <kendo-grid-column field="Discontinued" editor="boolean" title="Discontinued"></kendo-grid-column>
            <kendo-grid-column field="UnitsInStock" editor="numeric" title="Units In Stock"></kendo-grid-column>
        </kendo-grid>
    `
})
export class AppComponent implements OnInit, OnDestroy {
    @ViewChild(GridComponent)
    private grid: GridComponent;

    public groups: GroupDescriptor[] = [];
    public view: any[];

    public formGroup: FormGroup;

    private editedRowIndex: number;
    private docClickSubscription: any;
    private isNew: boolean;

    constructor(private service: ProductsService, private renderer: Renderer2) { }

    public ngOnInit(): void {
      this.view = this.service.products();

      this.docClickSubscription = this.renderer.listen('document', 'click', this.onDocumentClick.bind(this));
    }

    public ngOnDestroy(): void {
        this.docClickSubscription();
    }

    public addHandler(): void {
        this.closeEditor();

        this.formGroup = createFormGroup({
            'Discontinued': false,
            'ProductName': '',
            'UnitPrice': 0,
            'UnitsInStock': ''
        });
        this.isNew = true;

        this.grid.addRow(this.formGroup);
    }

    public cellClickHandler({ isEdited, dataItem, rowIndex }): void {
        if (isEdited || (this.formGroup && !this.formGroup.valid)) {
            return;
        }

        this.saveCurrent();

        this.formGroup = createFormGroup(dataItem);
        this.editedRowIndex = rowIndex;

        this.grid.editRow(rowIndex, this.formGroup);
    }

    public cancelHandler(): void {
        this.closeEditor();
    }

    public groupChange(groups: GroupDescriptor[]): void {
        this.groups = groups;
        this.view = groupBy(this.service.products(), this.groups);
    }

    private closeEditor(): void {
        this.grid.closeRow(this.editedRowIndex);

        this.isNew = false;
        this.editedRowIndex = undefined;
        this.formGroup = undefined;
    }

    private onDocumentClick(e: any): void {
        if (this.formGroup && this.formGroup.valid &&
            !matches(e.target, '#productsGrid tbody *, #productsGrid .k-grid-toolbar .k-button')) {
            this.saveCurrent();
        }
    }

    private saveCurrent(): void {
        if (this.formGroup) {
            this.service.save(this.formGroup.value, this.isNew);
            this.closeEditor();
        }
    }
}
import { Injectable } from '@angular/core';
import { products } from './products';

@Injectable()
export class ProductsService {
    private data: any[] = products;
    private counter: number = products.length;

    public products(): any[] {
        return this.data;
    }

    public remove(product: any): void {
        const index = this.data.findIndex(({ ProductID }) => ProductID === product.ProductID);
        this.data.splice(index, 1);
    }

    public save(product: any, isNew: boolean): void {
        if (isNew) {
            product.ProductID = this.counter++;
            this.data.splice(0, 0, product);
        } else {
            Object.assign(
                this.data.find(({ ProductID }) => ProductID === product.ProductID),
                product
            );
        }
    }
}
export const products = [{
    'ProductID' : 1,
    'ProductName' : "Chai",
    'SupplierID' : 1,
    'CategoryID' : 1,
    'QuantityPerUnit' : "10 boxes x 20 bags",
    'UnitPrice' : 18.0000,
    'UnitsInStock' : 39,
    'UnitsOnOrder' : 0,
    'ReorderLevel' : 10,
    'Discontinued' : false

}, {
    'ProductID' : 2,
    'ProductName' : "Chang",
    'SupplierID' : 1,
    'CategoryID' : 1,
    'QuantityPerUnit' : "24 - 12 oz bottles",
    'UnitPrice' : 19.0000,
    'UnitsInStock' : 17,
    'UnitsOnOrder' : 40,
    'ReorderLevel' : 25,
    'Discontinued' : false
}, {
    'ProductID' : 3,
    'ProductName' : "Aniseed Syrup",
    'SupplierID' : 1,
    'CategoryID' : 2,
    'QuantityPerUnit' : "12 - 550 ml bottles",
    'UnitPrice' : 10.0000,
    'UnitsInStock' : 13,
    'UnitsOnOrder' : 70,
    'ReorderLevel' : 25,
    'Discontinued' : false
}, {
    'ProductID' : 4,
    'ProductName' : "Chef Anton\'s Cajun Seasoning",
    'SupplierID' : 2,
    'CategoryID' : 2,
    'QuantityPerUnit' : "48 - 6 oz jars",
    'UnitPrice' : 22.0000,
    'UnitsInStock' : 53,
    'UnitsOnOrder' : 0,
    'ReorderLevel' : 0,
    'Discontinued' : false
}, {
    'ProductID' : 5,
    'ProductName' : "Chef Anton\'s Gumbo Mix",
    'SupplierID' : 2,
    'CategoryID' : 2,
    'QuantityPerUnit' : "36 boxes",
    'UnitPrice' : 21.3500,
    'UnitsInStock' : 0,
    'UnitsOnOrder' : 0,
    'ReorderLevel' : 0,
    'Discontinued' : true
}, {
    'ProductID' : 6,
    'ProductName' : "Grandma\'s Boysenberry Spread",
    'SupplierID' : 3,
    'CategoryID' : 2,
    'QuantityPerUnit' : "12 - 8 oz jars",
    'UnitPrice' : 25.0000,
    'UnitsInStock' : 120,
    'UnitsOnOrder' : 0,
    'ReorderLevel' : 25,
    'Discontinued' : false
}, {
    'ProductID' : 7,
    'ProductName' : "Uncle Bob\'s Organic Dried Pears",
    'SupplierID' : 3,
    'CategoryID' : 7,
    'QuantityPerUnit' : "12 - 1 lb pkgs.",
    'UnitPrice' : 30.0000,
    'UnitsInStock' : 15,
    'UnitsOnOrder' : 0,
    'ReorderLevel' : 10,
    'Discontinued' : false
}, {
    'ProductID' : 8,
    'ProductName' : "Northwoods Cranberry Sauce",
    'SupplierID' : 3,
    'CategoryID' : 2,
    'QuantityPerUnit' : "12 - 12 oz jars",
    'UnitPrice' : 40.0000,
    'UnitsInStock' : 6,
    'UnitsOnOrder' : 0,
    'ReorderLevel' : 0,
    'Discontinued' : false
}];
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

enableProdMode();

const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ReactiveFormsModule } from '@angular/forms';

import { GridModule } from '@progress/kendo-angular-grid';
import { DropDownListModule } from '@progress/kendo-angular-dropdowns';

import { AppComponent } from './app.component';
import { ProductsService } from './products.service';

@NgModule({
    declarations: [
        AppComponent
    ],
    imports: [
        BrowserModule,
        BrowserAnimationsModule,
        ReactiveFormsModule,
        GridModule,
        DropDownListModule
    ],
    providers: [
        ProductsService
    ],
    bootstrap: [AppComponent]
})
export class AppModule {}

Focusing Clicked Cells while Editing

By default, when the user opens a row for editing, the Grid focuses the first available input. To focus another cell when a row is in editing mode, programmatically focus the desired cell in the edit event handler of the Grid. To ensure that the respective inputs are rendered, wrap the operation in a setTimeout() call.

The following example demonstrates how to focus the input which corresponds to the clicked cell in the Grid.

import { Component, OnInit, Inject, ElementRef, ViewChild, Renderer2 } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { ProductsService } from './products.service';
import { AddEvent, EditEvent, GridComponent } from '@progress/kendo-angular-grid';
import { groupBy, GroupDescriptor } from '@progress/kendo-data-query';

const formGroup = dataItem => new FormGroup({
    'Discontinued': new FormControl(dataItem.Discontinued),
    'ProductID': new FormControl(dataItem.ProductID),
    'ProductName': new FormControl(dataItem.ProductName, Validators.required),
    'UnitPrice': new FormControl(dataItem.UnitPrice),
    'UnitsInStock': new FormControl(dataItem.UnitsInStock, Validators.compose([Validators.required, Validators.pattern('^[0-9]{1,3}')]))
});

const hasClass = (el, className) => new RegExp(className).test(el.className);

const isChildOf = (el, className) => {
    while (el && el.parentElement) {
        if (hasClass(el.parentElement, className)) {
            return true;
        }
        el = el.parentElement;
    }
    return false;
};

@Component({
    selector: 'my-app',
    template: `
        <kendo-grid
            (cellClick)="editClick($event)"
            [groupable]="true"
            [group]="groups"
            (groupChange)="groupChange($event)"
            [data]="view"
            height="500"
            (add)="addHandler($event)"
            >
            <ng-template kendoGridToolbarTemplate>
                <button kendoGridAddCommand>Add new</button>
                <button *ngIf="isInEditingMode"
                    (click)="cancelHandler()"
                    class="k-button k-primary">Cancel</button>
            </ng-template>
            <kendo-grid-column field="ProductName" title="Product Name"></kendo-grid-column>
            <kendo-grid-column field="UnitPrice" editor="numeric" title="Price"></kendo-grid-column>
            <kendo-grid-column field="Discontinued" editor="boolean" title="Discontinued"></kendo-grid-column>
            <kendo-grid-column field="UnitsInStock" editor="numeric" title="Units In Stock"></kendo-grid-column>
        </kendo-grid>
    `
})
export class AppComponent implements OnInit {
    public formGroup: FormGroup;
    public groups: GroupDescriptor[] = [];
    public view: any[];
    @ViewChild(GridComponent) private grid: GridComponent;
    private editedRowIndex: number;
    private isNew = false;

    public get isInEditingMode(): boolean {
        return this.editedRowIndex !== undefined || this.isNew;
    }

    public groupChange(groups: GroupDescriptor[]): void {
        this.groups = groups;
        this.view = groupBy(this.service.products(), this.groups);
    }

    constructor(private service: ProductsService, private renderer: Renderer2) { }

    public ngOnInit(): void {
      this.view = this.service.products();
      this.renderer.listen(
          "document",
          "click",
          ({ target }) => {
              if (!isChildOf(target, "k-grid")) {
                  this.saveClick();
              }
          });
    }

    public addHandler({ sender }: AddEvent): void {
        this.closeEditor(sender);

        this.formGroup = formGroup({
            'Discontinued': false,
            'ProductName': "",
            'UnitPrice': 0,
            'UnitsInStock': ""
        });

        this.isNew = true;
        sender.addRow(this.formGroup);
    }

    public editHandler({ sender, colIndex, rowIndex, dataItem }: EditEvent): void {
        if (this.formGroup && !this.formGroup.valid) {
            return;
        }

        this.saveRow();
        this.formGroup = formGroup(dataItem);
        this.editedRowIndex = rowIndex;
        sender.editRow(rowIndex, this.formGroup);
        setTimeout(() => {
          console.log(colIndex);
          document.querySelector(`.k-grid-edit-row > td:nth-child(${colIndex + 1}) input`).focus();
        });
    }

    public cancelHandler(): void {
        this.closeEditor(this.grid, this.editedRowIndex);
    }

    public editClick({ dataItem, rowIndex, columnIndex }: any): void {
        this.editHandler({
            dataItem: dataItem,
            rowIndex: rowIndex,
            colIndex: columnIndex,
            sender: this.grid
        });
    }

    public saveClick(): void {
        if (this.formGroup && !this.formGroup.valid) {
            return;
        }

        this.saveRow();
    }

    private closeEditor(grid: GridComponent, rowIndex: number = this.editedRowIndex): void {
        this.isNew = false;
        grid.closeRow(rowIndex);
        this.editedRowIndex = undefined;
        this.formGroup = undefined;
    }

    private saveRow(): void {
        if (this.isInEditingMode) {
            this.service.save(this.formGroup.value, this.isNew);
        }

        this.closeEditor(this.grid);
    }
}
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

enableProdMode();

const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ReactiveFormsModule } from '@angular/forms';

import { GridModule } from '@progress/kendo-angular-grid';
import { DropDownListModule } from '@progress/kendo-angular-dropdowns';

import { AppComponent } from './app.component';
import { ProductsService } from './products.service';

@NgModule({
    declarations: [
        AppComponent
    ],
    imports: [
        BrowserModule,
        BrowserAnimationsModule,
        ReactiveFormsModule,
        GridModule,
        DropDownListModule
    ],
    providers: [
        ProductsService
    ],
    bootstrap: [AppComponent]
})
export class AppModule {}
export const products = [{
    "ProductID" : 1,
    "ProductName" : "Chai",
    "SupplierID" : 1,
    "CategoryID" : 1,
    "QuantityPerUnit" : "10 boxes x 20 bags",
    "UnitPrice" : 18.0000,
    "UnitsInStock" : 39,
    "UnitsOnOrder" : 0,
    "ReorderLevel" : 10,
    "Discontinued" : false

}, {
    "ProductID" : 2,
    "ProductName" : "Chang",
    "SupplierID" : 1,
    "CategoryID" : 1,
    "QuantityPerUnit" : "24 - 12 oz bottles",
    "UnitPrice" : 19.0000,
    "UnitsInStock" : 17,
    "UnitsOnOrder" : 40,
    "ReorderLevel" : 25,
    "Discontinued" : false
}, {
    "ProductID" : 3,
    "ProductName" : "Aniseed Syrup",
    "SupplierID" : 1,
    "CategoryID" : 2,
    "QuantityPerUnit" : "12 - 550 ml bottles",
    "UnitPrice" : 10.0000,
    "UnitsInStock" : 13,
    "UnitsOnOrder" : 70,
    "ReorderLevel" : 25,
    "Discontinued" : false
}, {
    "ProductID" : 4,
    "ProductName" : "Chef Anton's Cajun Seasoning",
    "SupplierID" : 2,
    "CategoryID" : 2,
    "QuantityPerUnit" : "48 - 6 oz jars",
    "UnitPrice" : 22.0000,
    "UnitsInStock" : 53,
    "UnitsOnOrder" : 0,
    "ReorderLevel" : 0,
    "Discontinued" : false
}, {
    "ProductID" : 5,
    "ProductName" : "Chef Anton's Gumbo Mix",
    "SupplierID" : 2,
    "CategoryID" : 2,
    "QuantityPerUnit" : "36 boxes",
    "UnitPrice" : 21.3500,
    "UnitsInStock" : 0,
    "UnitsOnOrder" : 0,
    "ReorderLevel" : 0,
    "Discontinued" : true
}, {
    "ProductID" : 6,
    "ProductName" : "Grandma's Boysenberry Spread",
    "SupplierID" : 3,
    "CategoryID" : 2,
    "QuantityPerUnit" : "12 - 8 oz jars",
    "UnitPrice" : 25.0000,
    "UnitsInStock" : 120,
    "UnitsOnOrder" : 0,
    "ReorderLevel" : 25,
    "Discontinued" : false
}, {
    "ProductID" : 7,
    "ProductName" : "Uncle Bob's Organic Dried Pears",
    "SupplierID" : 3,
    "CategoryID" : 7,
    "QuantityPerUnit" : "12 - 1 lb pkgs.",
    "UnitPrice" : 30.0000,
    "UnitsInStock" : 15,
    "UnitsOnOrder" : 0,
    "ReorderLevel" : 10,
    "Discontinued" : false
}, {
    "ProductID" : 8,
    "ProductName" : "Northwoods Cranberry Sauce",
    "SupplierID" : 3,
    "CategoryID" : 2,
    "QuantityPerUnit" : "12 - 12 oz jars",
    "UnitPrice" : 40.0000,
    "UnitsInStock" : 6,
    "UnitsOnOrder" : 0,
    "ReorderLevel" : 0,
    "Discontinued" : false
}];
import { Injectable } from '@angular/core';
import { products } from './products';

@Injectable()
export class ProductsService {
    private data: any[] = products;
    private counter: number = products.length;

    public products(): any[] {
        return this.data;
    }

    public remove(product: any): void {
        const index = this.data.findIndex(({ ProductID }) => ProductID === product.ProductID);
        this.data.splice(index, 1);
    }

    public save(product: any, isNew: boolean): void {
        if (isNew) {
            product.ProductID = this.counter++;
            this.data.splice(0, 0, product);
        } else {
            Object.assign(
                this.data.find(({ ProductID }) => ProductID === product.ProductID),
                product
            );
        }
    }
}
In this article