Custom Editing in Reactive Forms

The Grid enables you to use templates and customize its built-in editing functionality in Reactive Forms.

Setup

The following example demonstrates how to use the EditTemplateDirective column options and the Reactive Forms to build custom DropDownList and NumericTextBox editors. It also demonstrates how to integrate custom validation messages, which utilize the built-in Kendo UI styling and the Kendo UI for Angular Popup component, and the EditTemplateDirective of the Kendo UI Grid for Angular. When you implement components as custom Grid editors, use a custom directive to expose the ElementRef of the custom editor component and link it to the anchor property of the Popup.

import { Component, OnInit, Inject } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { ProductsService } from './products.service';
import { categories } from './categories';

const createFormGroup = dataItem => new FormGroup({
    '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}')])),
    'CategoryID': new FormControl(dataItem.CategoryID, Validators.required)
});


@Component({
    selector: 'my-app',
    template: `
    <kendo-grid [data]="gridData"
        (edit)="editHandler($event)"
        (cancel)="cancelHandler($event)"
        (save)="saveHandler($event)"
        (remove)="removeHandler($event)"
        (add)="addHandler($event)"
        [height]="410"
        >
          <ng-template kendoGridToolbarTemplate>
            <button kendoGridAddCommand>Add new</button>
          </ng-template>
          <kendo-grid-column field="ProductName" title="Name" width="200">
              <ng-template kendoGridEditTemplate let-column="column" let-formGroup="formGroup" let-isNew="isNew">
                <input #input class="k-textbox" [formControl]="formGroup.get(column.field)">
                <kendo-popup
                  [anchor]="input"
                  *ngIf="formGroup.get(column.field).invalid && !(isNew && formGroup.get(column.field).untouched)"
                  popupClass="k-widget k-tooltip k-tooltip-validation k-invalid-msg"
                 >
                  <span class="k-icon k-i-warning"></span>
                  Product name is required
                </kendo-popup>
              </ng-template>
          </kendo-grid-column>
          <kendo-grid-column field="CategoryID" title="Category" width="150">
            <ng-template kendoGridCellTemplate let-dataItem>
              {{category(dataItem.CategoryID)?.CategoryName}}
            </ng-template>
            <ng-template kendoGridEditTemplate
              let-dataItem="dataItem"
              let-column="column"
              let-formGroup="formGroup">
              <kendo-dropdownlist
                #ddl="popupAnchor"
                popupAnchor
                [defaultItem]="{CategoryID: null, CategoryName: 'Test null item'}"
                [data]="categories"
                textField="CategoryName"
                valueField="CategoryID"
                [valuePrimitive]="true"
                [formControl]="formGroup.get('CategoryID')"
              >
              </kendo-dropdownlist>
              <kendo-popup
                  [anchor]="ddl.element"
                  *ngIf="formGroup.get(column.field).invalid && !(isNew && formGroup.get(column.field).untouched)"
                  popupClass="k-widget k-tooltip k-tooltip-validation k-invalid-msg"
                 >
                  <span class="k-icon k-i-warning"></span>
                  Category is required
                </kendo-popup>
            </ng-template>
          </kendo-grid-column>
          <kendo-grid-column field="UnitPrice" title="Price" format="{0:c}" width="80" editor="numeric">
          </kendo-grid-column>
          <kendo-grid-column field="UnitsInStock" title="In stock" width="80">
            <ng-template kendoGridEditTemplate let-column="column" let-formGroup="formGroup" let-isNew="isNew">
                <kendo-numerictextbox
                  #ntb="popupAnchor"
                  popupAnchor
                  [formControl]="formGroup.get(column.field)"></kendo-numerictextbox>
                <kendo-popup
                  [anchor]="ntb.element"
                  *ngIf="formGroup.get(column.field).invalid && !(isNew && formGroup.get(column.field).untouched)"
                  popupClass="k-widget k-tooltip k-tooltip-validation k-invalid-msg"
                 >
                  <span class="k-icon k-i-warning"></span>
                  UnitsInStock must be >= 0
                </kendo-popup>
              </ng-template>
          </kendo-grid-column>
          <kendo-grid-command-column title="command" width="220">
            <ng-template kendoGridCellTemplate let-isNew="isNew">
              <button kendoGridEditCommand [primary]="true">Edit</button>
              <button kendoGridRemoveCommand>Remove</button>
              <button kendoGridSaveCommand [disabled]="formGroup?.invalid">{{ isNew ? 'Add' : 'Update' }}</button>
              <button kendoGridCancelCommand>{{ isNew ? 'Discard changes' : 'Cancel' }}</button>
            </ng-template>
          </kendo-grid-command-column>
      </kendo-grid>
    `
})
export class AppComponent implements OnInit {
    public gridData: any[];
    public categories: any[] = categories;
    public formGroup: FormGroup;
    private editedRowIndex: number;

    constructor(private service: ProductsService) {
    }

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

    public category(id: number): any {
        return this.categories.find(x => x.CategoryID === id);
    }

    public addHandler({ sender }) {
        this.closeEditor(sender);

        this.formGroup = createFormGroup({
            'ProductName': '',
            'UnitPrice': 0,
            'UnitsInStock': '',
            'CategoryID': 1
        });

        sender.addRow(this.formGroup);
    }

    public editHandler({ sender, rowIndex, dataItem }) {
        this.closeEditor(sender);

        this.formGroup = createFormGroup(dataItem);

        this.editedRowIndex = rowIndex;

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

    public cancelHandler({ sender, rowIndex }) {
        this.closeEditor(sender, rowIndex);
    }

    public saveHandler({ sender, rowIndex, formGroup, isNew }): void {
        const product = formGroup.value;

        this.service.save(product, isNew);

        sender.closeRow(rowIndex);
    }

    public removeHandler({ dataItem }): void {
        this.service.remove(dataItem);
    }

    private closeEditor(grid, rowIndex = this.editedRowIndex) {
        grid.closeRow(rowIndex);
        this.editedRowIndex = undefined;
        this.formGroup = undefined;
    }
}
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 categories = [
  {
    'CategoryID': 1,
    'CategoryName': 'Beverages',
    'Description': 'Soft drinks, coffees, teas, beers, and ales'
  },
  {
    'CategoryID': 2,
    'CategoryName': 'Condiments',
    'Description': 'Sweet and savory sauces, relishes, spreads, and seasonings'
  },
  {
    'CategoryID': 6,
    'CategoryName': 'Meat/Poultry',
    'Description': 'Prepared meats'
  },
  {
    'CategoryID': 7,
    'CategoryName': 'Produce',
    'Description': 'Dried fruit and bean curd'
  },
  {
    'CategoryID': 8,
    'CategoryName': 'Seafood',
    'Description': 'Seaweed and fish'
  }
];
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 { PopupModule } from '@progress/kendo-angular-popup';
import { InputsModule } from '@progress/kendo-angular-inputs';
import { DropDownListModule } from '@progress/kendo-angular-dropdowns';

import { AppComponent } from './app.component';
import { ProductsService } from './products.service';
import { PopupAnchorDirective } from './popup.anchor-target.directive';

@NgModule({
    declarations: [
        AppComponent,
        PopupAnchorDirective
    ],
    imports: [
        BrowserModule,
        BrowserAnimationsModule,
        ReactiveFormsModule,
        GridModule,
        DropDownListModule,
        PopupModule,
        InputsModule
    ],
    providers: [
        ProductsService
    ],
    bootstrap: [AppComponent]
})
export class AppModule {}
import { Directive, ElementRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[popupAnchor]',
  exportAs: 'popupAnchor'
})
export class PopupAnchorDirective {
  constructor(public element: ElementRef) {}
}

Implementing Cascading Drop-Down Lists

You can implement cascading DropDownLists as custom editors for those columns of the Grid which contain related information. This means that when the user selects an option from the DropDownList of one of the columns, the values of the DropDownLists in the columns with dependent information will be correspondingly updated.

import { Component, OnInit, Inject, ViewChild } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { ProductsService } from './products.service';
import { categories } from './categories';

const createFormGroup = dataItem => new FormGroup({
    '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,2}')])),
    'CategoryID': new FormControl(dataItem.CategoryID)
});


@Component({
    selector: 'my-app',
    template: `
        <kendo-grid [data]="gridData"
          (dataStateChange)="onStateChange($event)"
          (edit)="editHandler($event)"
          (cancel)="cancelHandler($event)"
          (save)="saveHandler($event)"
          (remove)="removeHandler($event)"
          (add)="addHandler($event)"

          [height]="410"
          >
            <ng-template kendoGridToolbarTemplate>
              <button kendoGridAddCommand>Add new</button>
            </ng-template>
            <kendo-grid-column field="ProductName" title="Name" width="200">
              <ng-template kendoGridEditTemplate
                let-dataItem="dataItem"
                let-formGroup="formGroup">
                <kendo-dropdownlist #namesDropDown
                  [data]="names"
                  [formControl]="formGroup.get('ProductName')"
                >
                </kendo-dropdownlist>
              </ng-template>
            </kendo-grid-column>
            <kendo-grid-column field="CategoryID" title="Category" width="150">
              <ng-template kendoGridCellTemplate let-dataItem>
                {{category(dataItem.CategoryID)?.CategoryName}}
              </ng-template>
              <ng-template kendoGridEditTemplate
                let-dataItem="dataItem"
                let-formGroup="formGroup">
                <kendo-dropdownlist
                  [data]="categories"
                  (valueChange)="onCategoryChange($event)"
                  textField="CategoryName"
                  valueField="CategoryID"
                  [valuePrimitive]="true"
                  [formControl]="formGroup.get('CategoryID')"
                >
                </kendo-dropdownlist>
              </ng-template>
            </kendo-grid-column>
            <kendo-grid-column field="UnitPrice" title="Price" format="{0:c}" width="80" editor="numeric">
            </kendo-grid-column>
            <kendo-grid-column field="UnitsInStock" title="In stock" width="80" editor="numeric">
            </kendo-grid-column>
            <kendo-grid-command-column title="command" width="220">
              <ng-template kendoGridCellTemplate let-isNew="isNew">
                <button kendoGridEditCommand [primary]="true">Edit</button>
                <button kendoGridRemoveCommand>Remove</button>
                <button kendoGridSaveCommand [disabled]="formGroup?.invalid">{{ isNew ? 'Add' : 'Update' }}</button>
                <button kendoGridCancelCommand>{{ isNew ? 'Discard changes' : 'Cancel' }}</button>
              </ng-template>
            </kendo-grid-command-column>
        </kendo-grid>
    `
})
export class AppComponent implements OnInit {
    @ViewChild('namesDropDown') private namesDdl;
    private editedRowIndex: number;
    public gridData: any[];
    public categories: any[] = categories;
    public names: any[];
    public formGroup: FormGroup;

    constructor(private service: ProductsService) {
    }

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

    public category(id: number): any {
        return this.categories.find(x => x.CategoryID === id);
    }

    public getNames(categoryId: number) {
      this.names = categoryId ? this.gridData
        .filter((item) => item.CategoryID === categoryId)
        .map(item => item.ProductName) : this.gridData.map((item) => item.ProductName);
    }

    public onCategoryChange(e) {
      this.getNames(e);
      this.formGroup.controls.ProductName.setValue(undefined);
    }

    public addHandler({ sender }) {
        this.closeEditor(sender);

        this.formGroup = createFormGroup({
            'ProductName': "",
            'UnitPrice': 0,
            'UnitsInStock': "",
            'CategoryID': 1
        });

        sender.addRow(this.formGroup);
    }

    public editHandler({ sender, rowIndex, dataItem }) {
        this.closeEditor(sender);

        this.getNames(dataItem.CategoryID);

        this.formGroup = createFormGroup(dataItem);

        this.editedRowIndex = rowIndex;

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

    public cancelHandler({ sender, rowIndex }) {
        this.closeEditor(sender, rowIndex);
    }

    public saveHandler({ sender, rowIndex, formGroup, isNew }): void {
        const product = formGroup.value;

        this.service.save(product, isNew);

        sender.closeRow(rowIndex);
    }

    public removeHandler({ dataItem }): void {
        this.service.remove(dataItem);
    }

    private closeEditor(grid, rowIndex = this.editedRowIndex) {
        grid.closeRow(rowIndex);
        this.editedRowIndex = undefined;
        this.formGroup = undefined;
    }
}
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) {
        const index = this.data.findIndex(({ ProductID }) => ProductID === product.ProductID);
        this.data.splice(index, 1);
    }

    public save(product: any, isNew: boolean) {
        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 categories = [
  {
    "CategoryID": 1,
    "CategoryName": "Beverages",
    "Description": "Soft drinks, coffees, teas, beers, and ales"
  },
  {
    "CategoryID": 2,
    "CategoryName": "Condiments",
    "Description": "Sweet and savory sauces, relishes, spreads, and seasonings"
  },
  {
    "CategoryID": 6,
    "CategoryName": "Meat/Poultry",
    "Description": "Prepared meats"
  },
  {
    "CategoryID": 7,
    "CategoryName": "Produce",
    "Description": "Dried fruit and bean curd"
  },
  {
    "CategoryID": 8,
    "CategoryName": "Seafood",
    "Description": "Seaweed and fish"
  }
];

Rendering Editors in the Cell Templates

You can render editor elements directly in the cells of the Grid.

  1. Utilize the cell templates.
  2. Render the respective inputs so that all rows are ready for editing without the need to explicitly put them in editing mode.
import { Observable } from 'rxjs/Observable';
import { Component, OnInit, Inject, ViewEncapsulation } from '@angular/core';
import { FormGroup, FormControl, Validators, FormArray } from '@angular/forms';

import { GridDataResult } from '@progress/kendo-angular-grid';
import { State, process } from '@progress/kendo-data-query';

import { Product } from './model';
import { EditService } from './edit.service';
import { map } from 'rxjs/operators/map';

@Component({
    selector: 'my-app',
    template: `
      <kendo-grid
          [data]="view | async"
          [height]="400"
          [pageSize]="gridState.take" [skip]="gridState.skip" [sort]="gridState.sort"
          [pageable]="true" [sortable]="true"
          (dataStateChange)="onStateChange($event)" >
        <kendo-grid-column field="ProductName" title="Product Name" width="250">
          <ng-template kendoGridCellTemplate let-dataItem let-rowIndex="rowIndex">
            <kendo-dropdownlist [data]="names"
              [formControl]="formGroups.get('items').at(rowIndex).get('ProductName')">
              </kendo-dropdownlist>
          </ng-template>
        </kendo-grid-column>
        <kendo-grid-column field="UnitPrice" title="Price" width="200">
          <ng-template kendoGridCellTemplate let-dataItem let-rowIndex="rowIndex">
            <kendo-numerictextbox
              [formControl]="formGroups.get('items').at(rowIndex).get('UnitPrice')">
              </kendo-numerictextbox>
          </ng-template>
        </kendo-grid-column>
        <kendo-grid-column field="Discontinued" editor="boolean" width="80" title="Discontinued">
          <ng-template kendoGridCellTemplate let-dataItem let-rowIndex="rowIndex">
            <input type="checkbox"
              [formControl]="formGroups.get('items').at(rowIndex).get('Discontinued')"
               />
          </ng-template>
        </kendo-grid-column>
        <kendo-grid-column field="UnitsInStock" editor="numeric" width="200" title="Units In Stock">
          <ng-template kendoGridCellTemplate let-dataItem let-rowIndex="rowIndex">
            <input type="number"
              step="0.01"
              [formControl]="formGroups.get('items').at(rowIndex).get('UnitsInStock')"
              class="k-textbox" />
          </ng-template>
        </kendo-grid-column>
      </kendo-grid>
      {{formGroups.value | json}}
    `,
    styles: [`
    .k-textbox {
      width: 100%;
    }
    `],
    encapsulation: ViewEncapsulation.None
})
export class AppComponent implements OnInit {
  public view: Observable<GridDataResult>;
  public gridState: State = {
      sort: [],
      skip: 0,
      take: 10
  };

  // for demo purposes only - hardcoded first 10 product names for the dropdowns
  public names = [
    'Chai',
    'Chang',
    'Aniseed Syrup',
    `Chef Anton's Cajun Seasoning`,
    `Chef Anton's Gumbo Mix`,
    `Grandma's Boysenberry Spread`,
    'Northwoods Cranberry Sauce',
    'Mishi Kobe Niku',
    'Ikura'
  ];

  private data: any[];
  private dropDownData: string[] = [];
  public formGroups: FormGroup = new FormGroup({ items: new FormArray([])});

  private editedRowIndex: number;

  constructor(private editService: EditService) {}

  public ngOnInit(): void {

      this.view = this.editService.pipe(map(data => process(data, this.gridState)));

      this.view.subscribe(r => {
        r.data.forEach(i => {
            this.dropDownData.push(i.ProductName);
            const formGroup = new FormGroup({
              'ProductID': new FormControl(i.ProductID),
              'ProductName': new FormControl(i.ProductName),
              'UnitPrice': new FormControl(i.UnitPrice),
              'Discontinued': new FormControl(i.Discontinued),
              'UnitsInStock': new FormControl(i.UnitsInStock)
            });

          this.formGroups.get("items").push(formGroup);
          });
      });
      this.editService.read();
  }

  public onStateChange(state: State) {
      this.gridState = state;
      this.editService.read();
  }
}
import { Injectable } from '@angular/core';
import { Observable, BehaviorSubject } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { tap } from 'rxjs/operators';

const CREATE_ACTION = 'create';
const UPDATE_ACTION = 'update';
const REMOVE_ACTION = 'destroy';

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

    private data: any[] = [];

    public read() {
        if (this.data.length) {
            return super.next(this.data);
        }

        this.fetch()
            .pipe(
                tap(data => this.data = data)
            )
            .subscribe(data => {
                super.next(data);
            });
    }

    public save(data: any, isNew?: boolean) {
        const action = isNew ? CREATE_ACTION : UPDATE_ACTION;

        this.reset();

        this.fetch(action, data)
            .subscribe(() => this.read(), () => this.read());
    }

    public remove(data: any) {
        this.reset();

        this.fetch(REMOVE_ACTION, data)
            .subscribe(() => this.read(), () => this.read());
    }

    public resetItem(dataItem: any) {
        if (!dataItem) { return; }

        // find orignal data item
        const originalDataItem = this.data.find(item => item.ProductID === dataItem.ProductID);

        // revert changes
        Object.assign(originalDataItem, dataItem);

        super.next(this.data);
    }

    private reset() {
        this.data = [];
    }

    private fetch(action: string = "", data?: any): Observable<any[]>  {
        return this.http
            .jsonp(`https://demos.telerik.com/kendo-ui/service/Products/${action}?callback=JSONP_CALLBACK${this.serializeModels(data)}`);
    }

    private serializeModels(data?: any): string {
       return data ? `&models=${JSON.stringify([data])}` : '';
    }
}
import { AppModule } from './app.module';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

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 { HttpClientModule, HttpClientJsonpModule } from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms';

import { GridModule } from '@progress/kendo-angular-grid';
import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
import { InputsModule } from '@progress/kendo-angular-inputs';

import { AppComponent } from './app.component';
import { EditService } from './edit.service';

@NgModule({
    declarations: [
        AppComponent
    ],
    imports: [
        HttpClientModule,
        HttpClientJsonpModule,
        BrowserModule,
        ReactiveFormsModule,
        GridModule,
        InputsModule,
        DropDownsModule,
        BrowserAnimationsModule
    ],
    providers: [EditService],
    bootstrap: [AppComponent]
})
export class AppModule {}
export class Product {
    public ProductID: number;
    public ProductName = "";
    public Discountinued = false;
    public UnitsInStock: number;
    public UnitPrice = 0;
}

In this article