AngularJS and ng-grid - auto save data to the server after a cell was changed

Maybe this is new but ng-grid actually publishes events which can be used to implement a simple update on change.

Event Reference: https://github.com/angular-ui/ng-grid/wiki/Grid-Events

Example code (add to controller where you setup the grid):

$scope.$on('ngGridEventEndCellEdit', function(evt){
    console.log(evt.targetScope.row.entity);  // the underlying data bound to the row
    // Detect changes and send entity to server 
});

One thing to note is that the event will trigger even if no changes have been made, so you may still want to check for changes before sending to the server (for example via 'ngGridEventStartCellEdit')


I've found what I think is a much nicer solution:

  cellEditableTemplate = "<input ng-class=\"'colt' + col.index\" ng-input=\"COL_FIELD\" ng-model=\"COL_FIELD\" ng-change=\"updateEntity(row.entity)\"/>"

Using ng-change this way will cause updateEntity to be called with the entire object (row) that has been changed and you can post it back to the server. You don't need any new scope variables. A deficiency of the previous solution was that when you clicked in to start editing the field, it would always be blank instead of the original value before you began to edit.

That will cause updateEntity() to be called on each keystroke. If that is too frequent for you, you could use a timeout before posting to the server, or just use updateEntity() to record the id you want to push, and then use ng-blur to post the recorded id.


It looks I found a solution thanks to the Angular mailing list. It was pointed that AngularJS is missing the onBlur event (as well as the onFocus). However, this can be overcome by adding a "simple" directive.

angular.module('myApp.ngBlur', [])
.directive('ngBlur', function () {
    return function (scope, elem, attrs) {
        elem.bind('blur', function () {
            scope.$apply(attrs.ngBlur);
        });
    };
});

As information, there is another example of implementation related to the blur event directive here.

Then, the rest of the code in the controller looks like:

// Define the template of the cell editing with input type "number" (for my case).
// Notice the "ng-blur" directive
var cellEditableTemplate = "<input style=\"width: 90%\" step=\"any\" type=\"number\" ng-class=\"'colt' + col.index\" ng-input=\"COL_FIELD\" ng-blur=\"updateEntity(col, row)\"/>";

// Configure ng-grid
$scope.gridOptions = {
    data: 'questions',
    enableCellSelection: true,
    multiSelect: false,
    columnDefs: [
        {field: 'id', displayName: 'Id'},
        {field: 'name', displayName: 'Name'},

        // Notice the "editableCellTemplate"
        {field: 'answers[0].valuePercent', displayName: 'Rural', enableCellEdit: true, editableCellTemplate: cellEditableTemplate}
    ]
};


// Update Entity on the server side
$scope.updateEntity = function(column, row) {
    console.log(row.entity);
    console.log(column.field);

    // code for saving data to the server...
    // row.entity.$update() ... <- the simple case

    // I have nested Entity / data in the row <- the complex case
    // var answer = new Answer(question.answers[answerIndex]); // answerIndex is computed with "column.field" variable
    // answer.$update() ...
}

I've spent some time pulling together the bits of this for ng-grid 2.x. I still have a problem with having to click twice to edit a row, but I think that's a bootstrap issue, not an ngGrid issue, it doesn't happen in my sample code (which doesn't have bootstrap yet).

I've also implemented similar logic in a tutorial for ui-grid 3.0, which is still beta but will soon become the preferred version. This can be found at: http://technpol.wordpress.com/2014/08/23/upgrading-to-ng-grid-3-0-ui-grid/, and provides a much easier and cleaner api for this functionality.

For the 2.x version, to illustrate all the bits, I've created a running plunker that has an editable grid with both a dropdown and an input field, uses the ngBlur directive, and uses a $timeout to avoid duplicate saves on the update: http://plnkr.co/edit/VABAEu?p=preview

The basics of the code are:

var app = angular.module('plunker', ["ngGrid"]);

app.controller('MainCtrl', function($scope, $timeout, StatusesConstant) {
  $scope.statuses = StatusesConstant;
  $scope.cellInputEditableTemplate = '<input ng-class="\'colt\' + col.index" ng-input="COL_FIELD" ng-model="COL_FIELD" ng-blur="updateEntity(row)" />';
  $scope.cellSelectEditableTemplate = '<select ng-class="\'colt\' + col.index" ng-input="COL_FIELD" ng-model="COL_FIELD" ng-options="id as name for (id, name) in statuses" ng-blur="updateEntity(row)" />';

  $scope.list = [
    { name: 'Fred', age: 45, status: 1 },
    { name: 'Julie', age: 29, status: 2 },
    { name: 'John', age: 67, status: 1 }
  ];

  $scope.gridOptions = {
    data: 'list',
    enableRowSelection: false,
    enableCellEditOnFocus: true,
    multiSelect: false, 
    columnDefs: [
      { field: 'name', displayName: 'Name', enableCellEditOnFocus: true, 
        editableCellTemplate: $scope.cellInputEditableTemplate },
      { field: 'age', displayName: 'Age', enableCellEdit: false },
      { field: 'status', displayName: 'Status', enableCellEditOnFocus: true, 
        editableCellTemplate: $scope.cellSelectEditableTemplate,
        cellFilter: 'mapStatus'}
    ]
  };

  $scope.updateEntity = function(row) {
    if(!$scope.save) {
      $scope.save = { promise: null, pending: false, row: null };
    }
    $scope.save.row = row.rowIndex;
    if(!$scope.save.pending) {
      $scope.save.pending = true;
      $scope.save.promise = $timeout(function(){
        // $scope.list[$scope.save.row].$update();
        console.log("Here you'd save your record to the server, we're updating row: " 
                    + $scope.save.row + " to be: " 
                    + $scope.list[$scope.save.row].name + "," 
                    + $scope.list[$scope.save.row].age + ","
                    + $scope.list[$scope.save.row].status);
        $scope.save.pending = false; 
      }, 500);
    }    
  };
})

.directive('ngBlur', function () {
  return function (scope, elem, attrs) {
    elem.bind('blur', function () {
      scope.$apply(attrs.ngBlur);
    });
  };
})

.filter('mapStatus', function( StatusesConstant ) {
  return function(input) {
    if (StatusesConstant[input]) {
      return StatusesConstant[input];
    } else {
      return 'unknown';
    }
  };
})

.factory( 'StatusesConstant', function() {
  return {
    1: 'active',
    2: 'inactive'
  };
});

When you run this plunker, and the lose focus fires, you should see on the console the update trigger firing.

I also included a README.md in the plunker with some thoughts on things that gave me difficulty, reproduced here.

The functionality here is that I have a list of people, those people have names, ages and statuses. In line with what we might do in a real app, the status is a code, and we want to show the decode. Accordingly we have a status codes list (which might in a real app come from the database), and we have a filter to map the code to the decode.

What we want are two things. We'd like to be able to edit the name in an input box, and to edit the status in a dropdown.

Comments on things I've learned on this plunk.

  1. At the gridOptions level, there are both enableCellEditOnFocus and enableCellEdit. Don't enable both, you need to pick. onFocus means single click, CellEdit means double click. If you enable both then you get unexpected behaviour on the bits of your grid you didn't want to be editable

  2. At the columnDefs level, you have the same options. But this time you need to set both CellEdit and onFocus, and you need to set cellEdit to false on any cells you don't want edited - this isn't the default

  3. The documentation says that your editable cell template can be:

    <input ng-class="'colt' + col.index" ng-input="COL_FIELD" />

    actually it needs to be:

    <input ng-class="'colt' + col.index" ng-input="COL_FIELD" ng-model="COL_FIELD" />

  4. To trigger a save event when we lose focus, we've created an blur directive, the logic for which I found in stackoverflow: AngularJS and ng-grid - auto save data to the server after a cell was changed

  5. This also means changing each editable cell template to call ng-blur, which you can see at the end of the editable cell template

  6. We get two blur events when we leave the field (at least in Chrome), so we use a timer so that only one of them is processed. Ugly, but it works.

I've also created a blog post that does a more thorough walkthrough of this code: http://technpol.wordpress.com/2013/12/06/editable-nggrid-with-both-dropdowns-and-selects/


If you're using UI Grid 3.0 that event is: uiGridEventEndCellEdit

$scope.$on('uiGridEventEndCellEdit', function (data) {
    console.log(data.targetScope.row.entity);
}