Reset grid datasource and dialog edit to original value after save error?

Hello,

If saving dialog edit causes an error (constraint violation for example) the grid/edit data should revert to the original (unchanged in DB) values.  I am able to reset the grid using setRowData() however if the same row is edited again still shows the edits previously entered.  Here is the code.  Is there a better way to accomplish this?  Please let me know if you need more info.

I am using a GraphQL API and Postgres DB.

  public onRowSelected(argsRowSelectEventArgs): void {
    this.segmentData =  Object.assign({}, args.data);
    this.segmentDataOriginal = this.segmentData;
  }

  actionBegin(argsany): void {
    if (args.requestType === 'beginEdit') {
    this.segmentData = Object.assign({}, args.rowData);
    }
    if (args.requestType === 'save' && args.action === 'edit') {
      if (this.segmentEditForm.valid) {
        this.editSegmentGQL.mutate({
            id: this.segmentData.id,
            cId: this.segmentData.cId,
            name: this.segmentData.name,
            status: this.segmentData.status,
            desc: this.segmentData.desc,
            makeCalls: this.segmentData.makeCalls,
            maxTries: this.segmentData.maxTries,
            priority: this.segmentData.priority,
            onQueue: this.segmentData.onQueue
          }).subscribe(({data}) => {
            this.segmentGrid.refresh();
          },
          (error=> {
            this.segmentGrid.setRowData(args.primaryKeyValue[0], this.segmentDataOriginal);
            alert(error);
            args.cancel = true;
          });
      } else {
          args.cancel = true;
      }
    }
  }

14 Replies 1 reply marked as answer

MF Mohammed Farook J Syncfusion Team June 18, 2020 11:10 AM UTC

Hi Scott , 
 
Thanks for contacting Syncfusion support. 
 
 
We have validated the provided code and we suspect that, you want to restore the Grid value when your service getting error. By default , the setRowData() method  is update the value in only UI level and it is not reflect the Grid dataSource. The Grid can fetch data from the Grid dataSource while edit the row. So, if you have change the data in setRowData() method , the changed is not applied while editing.  We have suggest to you,  save the Grid records once your service getting success otherwise you cancel the edit state. Please find the documentation for custom data with performing CRUD action. 
 
 
 
 
Note: When the Grid action is handle through ‘dataStateChange’ event when bind the custom data(observable data) into Grid. But the ‘dataStateChange’ event is not triggered at the initial render . you need to call your remote service in ngOInit at the Grid initial render.  
 
 
 
If you want to change the Grid row value without editing , you need to call ‘updateRowValue()’ method of Grid. 
 
 
 
gridObj.updateRowValue(10248, {OrderID:10248, CustomerID:'test111', Freight:11119})   // updateRowValue(key, {rowData}), ie key denotes PrimaryKey value of the current row. rowData denotes current row data with new values) 
 
 
 
 
Note: We can’t change the primarykey value. 
 
 
Regards, 
J Mohammed Farook  



SE Scott Eaton June 26, 2020 08:28 AM UTC

I tried to refactor the code using the examples provided.  I am able to populate the grid but the dataSourceChanged event does not fire when I click 'delete' or 'add' on the toolbar. Am I missing something?  The sctionBegin, actionComplete, and rowSelected events seem to fire as expected.

Grid Component TS:
import { ComponentOnInitViewChildInject } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { GridComponent } from '@syncfusion/ej2-angular-grids';
import { Observable } from 'rxjs/Observable';
import {
  DialogEditEventArgs,
  EditSettingsModel,
  ToolbarItems,
  RowSelectEventArgs,
  SelectionSettingsModel,
  DataSourceChangedEventArgs,
  DataStateChangeEventArgs
from '@syncfusion/ej2-angular-grids';

import { FsDataService } from '../../../shared/fs-data.service';
import { TestDataService } from './test-data.service';

@Component({
  selector: 'app-test-grid',
  templateUrl: './test-grid.component.html',
  styleUrls: ['./test-grid.component.css']
})
export class TestGridComponent implements OnInit {

  public editSettingsEditSettingsModel;
  public toolbarToolbarItems[];

  @ViewChild('campGrid', {static: false}) public campGridGridComponent;

  public campaignsByOidObservable<DataStateChangeEventArgs>;
  public stateDataStateChangeEventArgs;
  public campaignDataany;
  public selectionOptionsSelectionSettingsModel;

  constructor(
    @Inject(FsDataServiceprivate fsDataServiceFsDataService,
    @Inject(TestDataServiceprivate testDataServiceTestDataService
) {
  this.campaignsByOid = testDataService;
 }

  ngOnInit() {
    this.selectionOptions = { enableToggle: false };
    this.editSettings = { allowEditing: trueallowAdding: trueallowDeleting: truemode: 'Dialog' };
    this.toolbar = ['Search''Edit''Add''Delete' ];
    this.fsDataService.userOrg.subscribe(userOrg => {
      if (userOrg) {
        const state = { oId: userOrg.id };
        this.testDataService.getCampaignsByOid(state);
      }
    });
  }

  public onRowSelected(argsRowSelectEventArgs): void {
    console.log('onRowSelected');
    console.log(args);
    this.campaignData =  Object.assign({}, args.data);
}

public dataStateChange(stateDataStateChangeEventArgs): void {
  console.log('dataStateChange');
  console.log(state);
  this.fsDataService.getCampaignsByOid(state);
}

public dataSourceChanged(stateDataSourceChangedEventArgs): void {
  console.log('dataSourceChanged');
  console.log(state);
}

public actionBegin(argsany): void {
    console.log('actionBegin');
    console.log(args);
    if (args.requestType === 'beginEdit' || args.requestType === 'add') {
      this.campaignData = Object.assign({}, args.rowData);
      }
}

actionComplete(argsDialogEditEventArgs): void {
  console.log('actionComplete');
  console.log(args);
  if (args.requestType === 'beginEdit') {
    const dialog = args.dialog;
    const name = 'name';
    // change the header of the dialog
    dialog.header = args.rowData[name];
    // Set initail Focus
    (args.form.elements.namedItem('name'as HTMLInputElement).focus();
    }
  if (args.requestType === 'add') {
      const dialog = args.dialog;
      dialog.header = 'Add New Campaign';
      (args.form.elements.namedItem('name'as HTMLInputElement).focus();
      }
}
}

Grid Component HTML:
<ejs-grid 
#campGrid
[dataSource]='campaignsByOid | async'
(dataSourceChanged)='dataSourceChanged($event)' 
(dataStateChange)='dataStateChange($event)' 
[editSettings]='editSettings' 
[selectionSettings]='selectionOptions'
[toolbar]='toolbar' 
(actionBegin)='actionBegin($event)'
(actionComplete)='actionComplete($event)'
(rowSelected)="onRowSelected($event)"
width='100%'
height='200'>
    <e-columns>
        <e-column field='name' headerText='Campaign' width200></e-column>
        <e-column field='status' headerText='Status' defaultValue'ACTIVE' width100></e-column>
        <e-column field='code' headerText='Code' width100></e-column>
        <e-column field='startDate' headerText='Start Date' [defaultValue]'dateDefault'textAlign='Right' [format]='yMd' width110></e-column>
        <e-column field='financialGoal' headerText='Goal Amount' textAlign='Right' format='C2' width100></e-column>
        <e-column field='id' headerText='ID' [isPrimaryKey]='true' width100></e-column>
    </e-columns>
</ejs-grid>
Data Service:
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { DataStateChangeEventArgsDataSourceChangedEventArgs } from '@syncfusion/ej2-grids';
import { CampaignsByOidGQL } from '../../../graphql/generated/graphql';

@Injectable({  providedIn: 'root'})
export class TestDataService extends Subject<DataStateChangeEventArgs> {
  constructor(
    private campaignsByOidGQLCampaignsByOidGQL,
  ) {
    super();
  }

  public getCampaignsByOid(stateany): void {
    this.campaignsByOidGQL.watch({
      oId: state.oId
    })
    .valueChanges
    .subscribe(({ dataloading }) => {
      if (data.campaigns) {
        super.next(data.campaigns.nodes as DataStateChangeEventArgs);
      }
    });
  }

}




SK Sujith Kumar Rajkumar Syncfusion Team June 29, 2020 09:19 AM UTC

Hi Scott, 

The Grid expects the data returned from the emitted service to be an object with result and count properties. Your reported problem occurs if the data is not returned in this format so please ensure this in your application. This is documented in the below help site, 


We have also prepared a sample based on this for your reference so that you can check the execution flow of the Grid actions, 


Let us know if you have any concerns. 

Regards, 
Sujith R 


Marked as answer

SE Scott Eaton July 15, 2020 08:57 AM UTC

This seems to be working as expected using the code below.  I thought I would post for review and a sample for anyone else is using GraphQL with Apollo.  Let me know if you see any red flags in this method.  Thank you for your help in getting this going.
SE

Data service:
  public getGridData(oId): void {
    this.campaignsByOidGQL.watch({ oId })
    .valueChanges.subscribe(({ dataloading }) => {
      if (data.campaigns) {
        const returnDataany = {};
        returnData.result = data.campaigns.nodes;
        returnData.count = data.campaigns.totalCount;
        super.next(returnData as DataStateChangeEventArgs);
      }
    }, (error=> {
      alert(error);
      console.log(error);
    });
  }

Component ts dataSourceChanged:
public dataSourceChanged(state): void {
  if (state.action === 'add') {
    this.apollo.mutate({
      mutation: AddCampaignDocument,
      variables: {
        oId: this.userOrg.id,
        name: state.data.name,
        status: state.data.status,
        code: state.data.code,
        desc: state.data.desc,
        endDate: state.data.endDate,
        financialGoal: state.data.financialGoal,
        reapproach: state.data.reapproach,
        startDate: state.data.startDate
      },
      refetchQueries: [
        {
          query: CampaignsByOidDocument,
          variables: { oId: this.userOrg.id },
        }
      ]
    }).subscribe(() => {
      state.endEdit();
    },
    (error=> {
      alert(error);
      console.log(error);
      state.endEdit();
    });


  } else if (state.requestType === 'delete') {
    this.apollo.mutate({
      mutation: DeleteCampaignDocument,
      variables: {
        id: state.data[0].id
      },
      refetchQueries: [
        {
          query: CampaignsByOidDocument,
          variables: { oId: this.userOrg.id },
        }
    ]
    }).subscribe(() => {
      state.endEdit();
    },
    (error=> {
      alert(error);
      console.log(error);
      state.endEdit();
    });


  } else if (state.action === 'edit') {
    this.apollo.mutate({
      mutation: EditCampaignDocument,
      variables: {
        id: state.data.id,
        name: state.data.name,
        status: state.data.status,
        code: state.data.code,
        desc: state.data.desc,
        endDate: state.data.endDate,
        financialGoal: state.data.financialGoal,
        reapproach: state.data.reapproach,
        startDate: state.data.startDate
      },
      refetchQueries: [
        {
          query: CampaignsByOidDocument,
          variables: { oId: this.userOrg.id },
        }
    ]
    }).subscribe(() => {
      state.endEdit();
    },
    (error=> {
      alert(error);
      console.log(error);
      state.cancelEdit();
      this.campGrid.closeEdit();
    });
  }
}



SE Scott Eaton July 15, 2020 01:31 PM UTC

Add, Edit, and Delete work fine with the changes but the search does not filter the records.  I see the dataStateChange event fires but records are not filtered.  The search works as automatically using local data but not using Observable/async pipe.  Do I have to manually code the search function when I go this route?  Do you have any examples?


Thank you,
SE


SK Sujith Kumar Rajkumar Syncfusion Team July 16, 2020 11:54 AM UTC

Hi Scott, 
 
We are glad to hear that it is working fine now. As for your other query – Add, Edit, and Delete work fine with the changes but the search does not filter the records. Do I have to manually code the search function when I go this route?, yes the dataStateChange event will be triggered while performing search operation and the search parameters will be returned in the arguments as displayed in the below image,  
  
   
  
Using this you can form the search query like demonstrated in the below code snippet, send it with the server request and return the search results back,  
  
var searchQuery = `('` + state.search[0].key + `', ` + state.search[0].operator + `)`  
  
You can also include the field names in your search query which will also be received here as shown in the above image. 
 
Let us know if you have any concerns. 
 
Regards, 
Sujith R 



SE Scott Eaton July 16, 2020 05:29 PM UTC

Hello,

Thank you for your help and information.

Am I correct that using Observables disables the built-in search/filter/sort capabilities within the grid?  Does this mean I would have to re-query the database for each operation within the grid?    It seems like a lot of unnecessary requests and coding if this is the case.  I am loading all of the needed records into the grid onInit since I typically have less than 100 records to deal with.  Using the same data as local JSON works automatically based on the data currently available in the grid.  I was directed to move to the observable/async method for CRUD operations which seems to be working as expected.

Am I missing something? Let me know if you need anything or have questions.

Thanks again,
SE


SK Sujith Kumar Rajkumar Syncfusion Team July 17, 2020 11:37 AM UTC

Hi Scott, 

Yes you are right, on using Observable binding the dataStateChange event will be triggered for every grid action for which you need to query and resolve the data based on the arguments received in the event. This is mentioned in the below help documentation link, 

Handling grid actions on observable binding documentation: https://ej2.syncfusion.com/angular/documentation/grid/observables/#handling-grid-actions 

If your requirement is to perform only CRUD actions using server and the rest to be performed by the Grid itself then we suggest you to use the RemoteSaveAdaptor. Using this adaptor all Grid Actions will be performed in client-side except the CRUD operations. The CRUD operations can be mapped to server-side using updateUrl, insertUrl, removeUrl, batchUrl, crudUrl properties. More details on the remote save adaptor can be found in the below help documentation site,    
    

We have also prepared an Angular with Core sample based on this for your reference. You can download it from the following link, 


Note: You can run this sample by, opening the project in Visual Studio, restoring the packages and node modules for it and then run the application. 

Let us know if you have any concerns. 

Regards, 
Sujith R 



SE Scott Eaton July 17, 2020 05:18 PM UTC

The link to the download was blocked by chrome.  Any ideas?



SE Scott Eaton July 17, 2020 07:13 PM UTC

Would the RemoteSaveAdaptor work with my GraphQL backend?  The URL for the API is always the same. Only the query/mutation changes.  I need to be able to run a function for each CRUD operation that sends a properly formatted gql query/mutaiton and variable map that allows me to handle API errors and keep the grid data in sync.  I am using an Apollo Angular service to send requests and process results/errors.  Below is a snip of a typical edit function.  I was hoping to find a way to get Syncfusion to work with GraphQL but it may not be a feasible combination at this time.

     public editGridItem(dataany) {
          this.editItemGQL.mutate({
            id: this.data.id,
            name: this.data.name,
            active: this.data.active,
            description: this.data.desc
          }).subscribe(() => {
            // Edit successful. Grid updated
          },
          (error=> {
            alert('Failed to edit record!');
            console.log(error);
            // Edit failed. Grid should NOT update! This is where I originally started having problems.
          });


SE Scott Eaton replied to Scott Eaton July 17, 2020 07:14 PM UTC

The link to the download was blocked by chrome.  Any ideas?


Please disregard the download error.  I was able to get it using MS Edge.

Thank you,
SE


SK Sujith Kumar Rajkumar Syncfusion Team July 20, 2020 10:38 AM UTC

Hi Scott, 
 
While using the remote save adaptor the Grid does not send the queries in the format as supported by the GraphQL. However we have already considered “GraphQL support for Essential JS 2 DataManager” as feature and added to our feature request list. It will be implemented in any of any upcoming releases. You can track the current status of this request, review the proposed resolution timeline, and contact us for any further inquiries through the following link, 
 
 
So for your case you can achieve the requirement by binding the data returned from your service as local data to the Grid. Then in the actionBegin event cancel the default CRUD operations using arguments cancel property. So now you can dynamically call the backend methods using ajax along with the corresponding data received in the actionBegin event here and the required queries to update the data in the back-end. In ajax success event you can use the Grid’s endEdit and deleteRecord methods to add/edit and delete the corresponding data respectively in the Grid. This can be handled using a flag variable which is disabled in the actionComplete and ajax failure events so that it can enter the condition on next execution. This is demonstrated in the below code snippet, 
 
[app.component.ts] 
import { DataManager, WebApiAdaptor, Query } from '@syncfusion/ej2-data'; 
import { Ajax } from '@syncfusion/ej2-base; 
  
. .. 
public flag = false; 
 
// Grid’s actionBegin event handler 
actionBegin(e) { 
                    // Initially flag needs to be false in order to enter this condition 
                    if (!this.flag) { 
                      // Add and edit operations 
                      if (e.requestType == 'save' && (e.action == 'edit' || e.action == 'add')) { 
                          var editedData = e.data; 
                          // The default edit operation is cancelled 
                          e.cancel = true; 
                          // You can send the required data to your back-end through ajax call  
                         var ajax = new Ajax({ 
                               url: 'https://ej2services.syncfusion.com/production/web-services/', 
                               type: 'POST', 
                               contentType: 'application/json; charset=utf-8', 
                               data: JSON.stringify([editedData]) 
                           }); 
                           ajax.onSuccess = (args) => { 
                            // Flag is enabled to skip this execution when grid ends add/edit 
                              this.flag = true; 
                            // The added/edited data will be saved in the Grid 
                              this.grid.endEdit(); 
                          } 
                          ajax.onFailure = (args) => { 
                              // Add/edit failed 
                           // The flag is disabled if operation is failed so that it can enter the condition on next execution 
                             this.flag = false; 
                          } 
                          ajax.send();                              
                   } 
                   if (e.requestType == 'delete') { 
                         var editedData = e.data; 
                         // The delete operation is cancelled 
                         e.cancel = true; 
                         var ajax = new Ajax({ 
                              url: 'https://ej2services.syncfusion.com/production/web-services/', 
                              type: 'POST', 
                              contentType: 'application/json; charset=utf-8', 
                              data: JSON.stringify([editedData[0][gobj.getPrimaryKeyFieldNames()[0]]]) 
                           }) 
                          ajax.onSuccess = (args) => { 
                             // Flag is enabled to skip this execution when grid deletes record 
                              this.flag = true; 
                            // The deleted data will be removed in the Grid 
                              this.grid.deleteRecord(); 
                          } 
                          ajax.onFailure = (args) => { 
                              // Delete failed 
                           // The flag is disabled if operation is failed so that it can enter the condition on next execution 
                             this.flag = false; 
                          } 
                          ajax.send();                              
                  } 
              } 
} 
 
// Grid’s actionComplete event handler 
actionComplete(e) { 
        if (e.requestType === 'save' || e.requestType === 'delete') { 
           // The flag is disabled after operation is performed so that it can enter the condition on next execution 
            this.flag = false; 
        } 
} 
  
 
Let us know if you have any concerns. 
 
Regards, 
Sujith R 



SE Scott Eaton July 22, 2020 06:32 AM UTC

Works! Below is the actionBegin method utilizing the Apollo angular service to process GraphQL mutations for the Grid in case anyone else is in need.

Thank you for all your help in getting this going!

  actionBegin(argsany): void {
  // Initially flag needs to be false in order to enter this condition
  if (!this.flag) {
    if (args.requestType === 'beginEdit' || args.requestType === 'add') {
      this.campaignData = Object.assign({}, args.rowData);
      }
    if (args.requestType === 'save' && args.action === 'edit') {
      const editedData = args.data;
      // The default edit operation is cancelled
      args.cancel = true;
      if (this.campaignEditForm.valid) {
          this.editCampaignGQL.mutate({
            id: editedData.id,
            name: editedData.name,
            status: editedData.status,
            code: editedData.code,
            desc: editedData.desc,
            contactGoal: editedData.contactGoal,
            endDate: editedData.endDate,
            financialGoal: editedData.financialGoal,
            giftCountGoal: editedData.giftCountGoal,
            lastUpdate: editedData.lastUpdate,
            reapproach: editedData.reapproach,
            startDate: editedData.startDate
          }).subscribe(() => {
            // Flag is enabled to skip this execution when grid ends add/edit
            this.flag = true;
            // The added/edited data will be saved in the Grid
            this.campGrid.endEdit();
            },
            (error=> {
              // Add/edit failed! Flag is disabled so that it can enter the condition on next execution
              this.flag = false;
              alert(error);
              console.log(error);
            });
        }
      }
    if (args.requestType === 'save' && args.action === 'add') {
      console.log(args);
      const editedData = args.data;
      // The default edit operation is cancelled
      args.cancel = true;
      if (this.campaignEditForm.valid) {
            this.addCampaignGQL.mutate({
              oId: this.userOrg.id,
              name: editedData.name,
              status: editedData.status,
              code: editedData.code,
              desc: editedData.desc,
              contactGoal: editedData.contactGoal,
              endDate: editedData.endDate,
              financialGoal: editedData.financialGoal,
              giftCountGoal: editedData.giftCountGoal,
              lastUpdate: editedData.lastUpdate,
              reapproach: editedData.reapproach,
              startDate: editedData.startDate
            }).subscribe(({ data }) => {
            // Flag is enabled to skip this execution when grid ends add/edit
            this.flag = true;
            // The added data will be saved in the Grid
            this.campGrid.addRecorddata.createCampaign.campaign0 );
          },
            (error=> {
              // Add/edit failed! Flag is disabled so that it can enter the condition on next execution
              this.flag = false;
              alert(error);
              console.log(error);
            });
          }
      }
    if (args.requestType === 'delete') {
      // console.log(args);
      const editedData = args.data[0];
      // The default edit operation is cancelled
      args.cancel = true;
      this.deleteCampaignGQL.mutate({
          id: editedData.id
        }).subscribe(() => {
          // Flag is enabled to skip this execution when grid deletes record
          this.flag = true;
          // The deleted data removed in the Grid
          this.campGrid.deleteRecord();
        },
        (error=> {
          // Delete failed! Flag is disabled so that it can enter the condition on next execution
          this.flag = false;
          alert(error);
          console.log(error);
        });
    }
  }
}



SK Sujith Kumar Rajkumar Syncfusion Team July 23, 2020 09:33 AM UTC

Hi Scott, 

We are glad to hear that. And as always we are happy to assist you with your queries. 

Please get back to us if you require any further assistance. 

Regards, 
Sujith R 


Loader.
Up arrow icon