In a grid, reactively (real-time, immediately) compute column B based on what is typed in column A

2 Answers 599 Views
Grid
Scott
Top achievements
Rank 1
Scott asked on 10 Sep 2021, 05:21 PM

Using a Kendo grid that is in edit mode, how do you reactively compute the value of column B based on what is typed in column A?

E.g.   column B = column A x 2

I enter 1 in column A; column B immediately changes to 2.

I enter 16 in column A; column B immediately changes to 32.

2 Answers, 1 is accepted

Sort by
0
Accepted
Silviya
Telerik team
answered on 15 Sep 2021, 09:52 AM

Hi Scott,

Reactively computing the value of one field, based on the changes of another, would require some custom logic.

Please take a look at this demo I've prepared:

https://stackblitz.com/edit/angular-43qwtz-jycubz?file=app%2Fapp.component.ts

The developer would need to subscribe to the valueChanges observable for the form control, whose changes trigger the update of another dataItem field.

In the example above I am executing the custom logic in the cellClose event handler. Inside, I am listening for UnitPrice changes and then automatically update the corresponding Total Value based on that. In terms of performance optimisation we also need to unsubscribe from the valueChanges observable too.

I hope this helps.

Regards,
Silviya
Progress Telerik

Virtual Classroom, the free self-paced technical training that gets you up to speed with Telerik and Kendo UI products quickly just got a fresh new look + new and improved content including a brand new Blazor course! Check it out at https://learn.telerik.com/.

Scott
Top achievements
Rank 1
commented on 16 Sep 2021, 08:29 PM

Thanks a lot, Silviya.

I had to tweak a couple of things because there are multiple input columns involved, but it works perfectly.

Sincerely,

Scott Pearson

Scott
Top achievements
Rank 1
commented on 02 Nov 2021, 11:08 AM

Silviya,

Per your solution in the link above, I gather that the computed column MUST be read-only and also NOT part of the FormGroup.  Is this true?

What if I wanted to make another column computation based on a column that was previously computed?

See code below.

import { Observable } from 'rxjs';
import { ComponentOnInitViewEncapsulation } from '@angular/core';
import { ValidatorsFormBuilderFormGroup } from '@angular/forms';
import { GridDataResult } from '@progress/kendo-angular-grid';
import { Stateprocess } 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
      #grid
      [data]="view | async"
      [height]="533"
      [pageSize]="gridState.take"
      [skip]="gridState.skip"
      [sort]="gridState.sort"
      [pageable]="true"
      [sortable]="true"
      (dataStateChange)="onStateChange($event)"
      (cellClick)="cellClickHandler($event)"
      (cellClose)="cellCloseHandler($event)"
      (cancel)="cancelHandler($event)"
      (save)="saveHandler($event)"
      (remove)="removeHandler($event)"
      (add)="addHandler($event)"
      [navigable]="true"
    >
      <ng-template kendoGridToolbarTemplate>
        <button kendoGridAddCommand>Add new</button>
        <button
          class="k-button"
          [disabled]="!editService.hasChanges()"
          (click)="saveChanges(grid)"
        >
          Save Changes
        </button>
        <button
          class="k-button"
          [disabled]="!editService.hasChanges()"
          (click)="cancelChanges(grid)"
        >
          Cancel Changes
        </button>
      </ng-template>
      <kendo-grid-column field="ProductName" title="Product Name">
        <ng-template
          kendoGridCellTemplate
          let-dataItem
          let-column="column"
          let-rowIndex="rowIndex"
          width="200"
        >
          {{ dataItem[column.field] }}
        </ng-template>
      </kendo-grid-column>
      <kendo-grid-column
        field="UnitPrice"
        editor="numeric"
        title="Price"
        width="100"
      >
        <ng-template
          kendoGridEditTemplate
          let-dataItem="dataItem"
          let-column="column"
          let-rowIndex="rowIndex"
          let-formGroup="formGroup"
        >
          <kendo-numerictextbox
            [formControl]="formGroup.get(column.field)"
            [step]="0.1"
          >
          </kendo-numerictextbox>
        </ng-template>
        <ng-template
          kendoGridCellTemplate
          let-dataItem
          let-column="column"
          let-rowIndex="rowIndex"
        >
          {{ dataItem[column.field] }}
        </ng-template>
      </kendo-grid-column>
      <kendo-grid-column
        field="UnitsInStock"
        editor="numeric"
        title="Units In Stock"
      >
        <ng-template
          kendoGridCellTemplate
          let-dataItem
          let-column="column"
          let-rowIndex="rowIndex"
        >
          {{ dataItem[column.field] }}
        </ng-template>
      </kendo-grid-column>
      <kendo-grid-column
        field="totalValue"
        title="Total Value"
        width="100"
        [editable]="false"
      >
        <ng-template
          kendoGridCellTemplate
          let-dataItem
          let-column="column"
          let-rowIndex="rowIndex"
        >
          {{ dataItem[column.field] }}
        </ng-template>
      </kendo-grid-column>
      <kendo-grid-column
      field="modTotalValue"
      title="Mod Total Value"
      width="100"
      [editable]="false"
    >
      <ng-template
        kendoGridCellTemplate
        let-dataItem
        let-column="column"
        let-rowIndex="rowIndex"
      >
        {{ dataItem[column.field] }}
      </ng-template>
    </kendo-grid-column>
    </kendo-grid>
  `,
  encapsulation: ViewEncapsulation.None,
  styles: [
    `
      .dirty:before {
        content: '\\e003';
        font-family: WebComponentsIcons;
        font-size: 24px;
        position: absolute;
        top: -12px;
        left: -8px;
        color: red;
      }
      .k-grid .k-grid-content td {
        position: relative;
      }
    `
  ]
})
export class AppComponent implements OnInit {
  public viewObservable<GridDataResult>;
  public gridStateState = {
    sort: [],
    skip: 0,
    take: 10
  };
  private subscription;
  constructor(
    private formBuilderFormBuilder,
    public editServiceEditService
  ) {}
  public ngOnInit(): void {
    this.view = this.editService.pipe(
      map(data => process(datathis.gridState))
    );
    this.editService.read();
  }
  public onStateChange(stateState) {
    this.gridState = state;
    this.editService.read();
  }
  public cellClickHandler({
    sender,
    rowIndex,
    columnIndex,
    dataItem,
    isEdited
  }) {
    if (!isEdited) {
      sender.editCell(rowIndexcolumnIndexthis.createFormGroup(dataItem));
    }
  }
  public cellCloseHandler(argsany) {
    const { formGroupdataItem } = args;
    if (!formGroup.valid) {
      // prevent closing the edited cell if there are invalid values.
      args.preventDefault();
    } else if (formGroup.dirty) {
      this.editService.assignValues(dataItemformGroup.value);
      this.editService.update(dataItem);
    }
    // optimisation for subscription of valueChanges for the UnitPrice
    this.subscription?.unsubscribe();
  }
  public addHandler({ sender }) {
    sender.addRow(this.createFormGroup(new Product()));
  }
  public cancelHandler({ senderrowIndex }) {
    sender.closeRow(rowIndex);
  }
  public saveHandler({ senderformGrouprowIndex }) {
    if (formGroup.valid) {
      this.editService.create(formGroup.value);
      sender.closeRow(rowIndex);
    }
  }
  public removeHandler({ senderdataItem }) {
    this.editService.remove(dataItem);
    sender.cancelCell();
  }
  public saveChanges(gridany): void {
    grid.closeCell();
    grid.cancelCell();
    this.editService.saveChanges();
  }
  public cancelChanges(gridany): void {
    grid.cancelCell();
    this.editService.cancelChanges();
  }
  public createFormGroup(dataItemany): FormGroup {
    const formGroup = this.formBuilder.group({
      ProductID: dataItem.ProductID,
      ProductName: [dataItem.ProductNameValidators.required],
      UnitPrice: dataItem.UnitPrice,
      UnitsInStock: [
        dataItem.UnitsInStock,
        Validators.compose([
          Validators.required,
          Validators.pattern('^[0-9]{1,3}')
        ])
      ],
      Discontinued: dataItem.Discontinued
    });
    // Here we track the changes in UnitPrice
    // We multiply the new UnitPrice value to the UnitsInStock value
    // and as a result we update the 'Total Value' cell in the example
    this.subscription = formGroup.controls.UnitPrice.valueChanges.subscribe(
      change => (dataItem.totalValue = change * dataItem.UnitsInStock// 'change' holds the numeric value
    );
    this.subscription = formGroup.controls.totalValue.valueChanges.subscribe(
      change => (dataItem.modTotalValue = 3.14159 * dataItem.totalValue// 'change' holds the numeric value
    );
    return formGroup;
  }
}

 

0
Silviya
Telerik team
answered on 05 Nov 2021, 10:00 AM

Hi, Scott,

Thank you for the code snippet.

Indeed the StackBlitz example was with 'Total Value' column that is not editable but this is not mandatory. It is up to the developer to decide if this would be necessary or not in the specific implementation.

Usually it is readonly because otherwise there might be some cases where the end user enters as Total Value 100 - but the values for price and units stay at 10 and 5 (100 != 50). This looks inconsistent. Even if a validation is added that the Total Value has to be 50 - there is this other scenario to consider: If the TotalValue is 20, is it because the price is 4 and units 5 or is the price 2 and units 10.

For your convenience, I modified the example so now the 'Total Value' column is editable. Please take a look:

https://stackblitz.com/edit/angular-43qwtz-jycubz?file=app%2Fapp.component.ts

What is the difference: The column used to have an editable option set to false. Besides that, the TotalValue was not part of the FormGroup, as you already noted. Both those things were leading to the column being in readonly state.

As for computing a cell's value based on another computed cell - that can be done by reusing the same approach with subscribing to valueChanges - just as you have done. I only added setValue to the control because when we click the cells the form is recreated (this.createFormGroup(dataItem) is called) and in this case with 2 dynamically calculated cells - I had to modify the code to accommodate that.

I hope this proves helpful.

Regards,
Silviya
Progress Telerik

Remote troubleshooting is now easier with Telerik Fiddler Jam. Get the full context to end-users' issues in just three steps! Start your trial here - https://www.telerik.com/fiddler-jam.
Tags
Grid
Asked by
Scott
Top achievements
Rank 1
Answers by
Silviya
Telerik team
Share this question
or