Reactive Forms

The Grid 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 Grid for Angular utilize the Model-driven Angular Forms directives. To manipulate a Grid 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.

Generally, to configure the editing mode of the Grid, you have to:

  1. Click the command button so that the corresponding event gets triggered.
  2. In the corresponding event handler, define the way you want the Grid to handle the field. For example, build the FormGroup, call the editRow or the closeRow method, and so on.

After you set the event handler, the row acquires the respective behavior that is configured in it. For a detailed description on the configuration of the editing mode, refer to the following section on the setup.

The following example demonstrates how to set the inline editing mode of the Kendo UI Grid for Angular.

import { Observable } from 'rxjs';
import { Component, OnInit, Inject } from '@angular/core';
import { FormGroup, FormControl, Validators } 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';

@Component({
  selector: 'my-app',
  template: `
      <kendo-grid
          [data]="view | async"
          [height]="533"
          [pageSize]="gridState.take" [skip]="gridState.skip" [sort]="gridState.sort"
          [pageable]="true" [sortable]="true"
          (dataStateChange)="onStateChange($event)"
          (edit)="editHandler($event)" (cancel)="cancelHandler($event)"
          (save)="saveHandler($event)" (remove)="removeHandler($event)"
          (add)="addHandler($event)"
          [navigable]="true"
        >
        <ng-template kendoGridToolbarTemplate>
            <button kendoGridAddCommand>Add new</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-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 view: Observable<GridDataResult>;
    public gridState: State = {
        sort: [],
        skip: 0,
        take: 10
    };
    public formGroup: FormGroup;

    private editService: EditService;
    private editedRowIndex: number;

    constructor(@Inject(EditService) editServiceFactory: any) {
        this.editService = editServiceFactory();
    }

    public ngOnInit(): void {
        this.view = this.editService.pipe(map(data => process(data, this.gridState)));

        this.editService.read();
    }

    public onStateChange(state: State) {
        this.gridState = state;

        this.editService.read();
    }

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

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

        sender.addRow(this.formGroup);
    }

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

        this.formGroup = 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}')])),
            'Discontinued': new FormControl(dataItem.Discontinued)
        });

        this.editedRowIndex = rowIndex;

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

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

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

        this.editService.save(product, isNew);

        sender.closeRow(rowIndex);
    }

    public removeHandler({dataItem}) {
        this.editService.remove(dataItem);
    }

    private closeEditor(grid, rowIndex = this.editedRowIndex) {
        grid.closeRow(rowIndex);
        this.editedRowIndex = undefined;
        this.formGroup = undefined;
    }
}
import { Injectable } from '@angular/core';
import { Observable, BehaviorSubject } from 'rxjs';
import { HttpClient } from '@angular/common/http';

import { tap, map } 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}?${this.serializeModels(data)}`, 'callback')
            .pipe(map(res => <any[]>res));
    }

    private serializeModels(data?: any): string {
        return data ? `&models=${JSON.stringify([data])}` : '';
    }
}
export class Product {
    public ProductID: number;
    public ProductName = '';
    public Discontinued = false;
    public UnitsInStock: number;
    public UnitPrice = 0;
}
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 { HttpClient, HttpClientModule, HttpClientJsonpModule } from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms';

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

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

@NgModule({
    declarations: [
        AppComponent
    ],
    imports: [
        HttpClientModule,
        HttpClientJsonpModule,
        BrowserModule,
        BrowserAnimationsModule,
        ReactiveFormsModule,
        GridModule
    ],
    providers: [
        {
            deps: [HttpClient],
            provide: EditService,
            useFactory: (jsonp: HttpClient) => () => new EditService(jsonp)
        }
    ],
    bootstrap: [AppComponent]
})
export class AppModule {}

Setup

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

  1. Configure the type of the columns editor.

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

    <!-- setting numeric editor -->
    <kendo-grid-column field="UnitPrice" editor="numeric" title="Price">
    </kendo-grid-column>
    
    <!-- setting boolean editor -->
    <kendo-grid-column field="Discontinued" editor="boolean" title="Discontinued">
    </kendo-grid-column>
  2. Configure the command column by defining the command buttons inside the command column template.

    <kendo-grid-command-column title="command" width="220">
       <ng-template kendoGridCellTemplate let-isNew="isNew">
           <!-- edit command directive, will be visible when not in edit mode -->
           <button kendoGridEditCommand [primary]="true">Edit</button>
           <!-- remove command directive, will be visible when not in edit mode -->
           <button kendoGridRemoveCommand>Remove</button>
           <!-- save command directive, will be visible when in edit mode -->
           <button kendoGridSaveCommand>{{ isNew ? 'Add' : 'Update' }}</button>
           <!-- cancel command directive, will be visible when in edit mode -->
           <button kendoGridCancelCommand>{{ isNew ? 'Discard changes' : 'Cancel' }}</button>
       </ng-template>
    </kendo-grid-command-column>
  3. Attach handlers for the CRUD data operations.

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

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

Toggling the Edit State

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

Editing Records

The edit event fires when the kendoGridEditCommand 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.

protected editHandler({sender, rowIndex, dataItem}) {
    // define all editable fields validators and default values
    const group = 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}')])),
        'Discontinued': new FormControl(dataItem.Discontinued)
    });

    // put the row in edit mode, with the `FormGroup` build above
    sender.editRow(rowIndex, group);
}

Adding Records

The add event fires when the kendoGridAddCommand 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).

protected addHandler({sender}) {
    // define all editable fields validators and default values
    const group = new FormGroup({
        'ProductID': new FormControl(),
        'ProductName': new FormControl("", Validators.required),
        'UnitPrice': new FormControl(0),
        'UnitsInStock': new FormControl("", Validators.compose([Validators.required, Validators.pattern('^[0-9]{1,3}')])),
        'Discontinued': new FormControl(false)
    });

    // show the new row editor, with the `FormGroup` build above
    sender.addRow(group);
}

Cancelling Editing

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

protected cancelHandler({sender, rowIndex}) {
    // close the editor for the given row
    sender.closeRow(rowIndex)
}

Saving Records

The save event fires when the kendoGridSaveCommand 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.
protected saveHandler({sender, rowIndex, formGroup, isNew}) {
    // collect the current state of the form
    // `formGroup` arguments is the same as was provided when calling `editRow`
    const product: Product = formGroup.value;

    // update the data source
    this.editService.save(product, isNew);

    // close the editor, that is, revert the row back into view mode
    sender.closeRow(rowIndex);
}

Removing Records

The remove event fires when the kendoGridRemoveCommand is clicked. Inside the event handler, you can issue a request to remove the current data item from the data source.

protected removeHandler({dataItem}) {
    // remove the current dataItem from the current data source,
    // `editService` in this example
    this.editService.remove(dataItem);
}

In this article