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

7

38

My Use Case is pretty simple. A User, after editing a Cell (enableCellEdit: true), should have the data "automatically" sent to the server (on cell blur). I tried different approaches but none of them have properly worked out. I have a minimalistic grid:

// Configure ng-grid
$scope.gridOptions = {
    data: 'questions',
    enableCellSelection: true,
    selectedItems: $scope.selectedRow,
    multiSelect: false,
    columnDefs: [
        {field: 'id', displayName: 'Id'},
        {field: 'name', displayName: 'Name'},
        {field: 'answers[1].valuePercent', displayName: 'Rural', enableCellEdit: true}
    ]
};

For example, I tried to watch the data model passed to the Grid. But doing so won't return me the edited cell:

$scope.$watch('myData', function (foo) {
    // myModel.$update()
}, true);

I tried to fiddle with the "ngGridEventData" data event but it does not fire after cell edit

$scope.$on('ngGridEventData', function (e, gridId) {
    // myModel.$update()
});

Finally, I tried to observer a Cell. However, this only work for a row by the mean of the "selectedCell" property of the grid:

$scope.selectedRow = [];

$scope.gridOptions = {
    selectedItems: $scope.selectedRow,
}

$scope.$watch('selectedRow', function (foo) {
    console.log(foo)
}, true);

Is it a ng-grid plugin needed? I can't believe it is not something out of the box.

Would you have a pointer / snippet how I could solve the auto save / send to the server?

Ninefold answered 26/3, 2013 at 21:44 Comment(3)
Just spent 10 minutes searching for a better way to do this... Like really? There's no event that just has the object that changed in it? Like really? It appears the stupid ngBlur way is the best... Really...Sclerometer
How about you accept a solution, Fabien? Michael's is a good one.Jessen
Hey Fabien. Good question, I think Chris' solution is quite good. Either way, Please accept a solution.Hoar
E
30

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')

Erratic answered 2/3, 2014 at 16:37 Comment(0)
K
21

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.

Kenakenaf answered 25/8, 2013 at 5:35 Comment(5)
This works brilliantly. Instead of recording the id though, is there any problem just substituting "ng-blur" for "ng-change" in your string? It seems to work fine for me, I'm not sure if I'm missing something though.Janka
Sounds logical, but doesn't seem to work for me. My ng-blur handler never gets called. Oh wait, that's probably b/c I'm still on 1.1.4 which doesn't have ng-blur, I think that was added in 1.1.5. Agree that is simpler.Kenakenaf
I forgot to mention I already added the "simple" ngBlur directive from @Ninefold 's answer. This seems to be the best solution, it works well for me.Janka
Quite right! I missed the thrust of @Fabien's answer; that is the best approach. Upvoting it.Kenakenaf
Strange that this was not the answer the OP got in the mailing list. I wonder how to elagantly keep the user informed when all her changes are properly saved to the server.Fancywork
N
17

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() ...
}
Ninefold answered 5/4, 2013 at 11:11 Comment(1)
When I use your CellEditableTemplate, the updateEntity function is called but I get a "Error: No controller: ngModel" error and the cell is stuck as a text box. Did you have this problem?Akbar
M
6

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/

Marietta answered 6/12, 2013 at 2:6 Comment(0)
B
2

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

$scope.$on('uiGridEventEndCellEdit', function (data) {
    console.log(data.targetScope.row.entity);
}
Bendy answered 21/4, 2015 at 1:1 Comment(2)
Not sure this adds much to the highest-rated answer from quite some time ago.Teniacide
If you were using ui-grid 3.0, you'd probably use rowEdit for this purpose.Marietta
G
1

This is an improvement to the answer which has a few flaws: - it triggers a JS exception, as indicated in one of answer's comments - the data input in the cell is not retained in the grid - the updateEntity method does not illustrate how to save the input data

In order to remove the exception, create a scope attribute and add it to cellEditableTemplate:

$scope.cellValue;
...
var cellEditableTemplate = "<input style=\"width: 90%\" step=\"any\" type=\"number\" ng-class=\"'colt' + col.index\" ng-input=\"COL_FIELD\" ng-blur=\"updateEntity(col, row, cellValue)\" ng-model='cellValue'/>";

Notice that the ng-blur call to updateEntity now includes cellValue as an argument. Next, update the updateEntity blur handler to include the argument and update the grid:

$scope.updateEntity = function(column, row, cellValue) {
    console.log(row.entity);
    console.log(column.field);
    row.entity[column.field] = cellValue;

    // 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'm now able to see the changes on the screen as well as to trigger cell-based back-end updates.

Giacometti answered 1/8, 2013 at 0:2 Comment(6)
I still get the "Error: No controller ngModel" exception. Same as @bob. Could you post a working fiddle of your solution?Alternate
Thanks, I don't have an error anymore. But I actually still can't get the old value. The plunker logs to console but I don't see any output in the console at all.Alternate
Steps to see the values shown on your console using Chrome: 1) Start the plunker, 2) Run the plunker, 3) Right-click on any table cell, 4) Select Inspect Element, 5) Click on the Developer Tools Console tab, 6) Click on any table cell, 7) Type a value, 8) Click outside the table cell. Observe that the values are shown on the console. Also, running your own implementation and using the Developer Console, you should be able to breakpoint on the updateEntity function and see the arguments.Giacometti
wierd, I've used the developer tools lots before, but this time the console shows nothing. I followed your steps exactly (they are pretty standard), but I get neither any console output, nor can I hit any breakpoint. Maybe I've got an extension that is acting up.Alternate
On Angular 1.2.0, this approach gives Error: [$rootScope:inprog] $apply already in progress even if all the calls to scope.$apply are removed, replaced with scope.$eval, or substituted by $timeout. The plunkr above presently shows the same problem. (The value is being erased because the cellValue is null when the event trigger is called; I used a focus event to save it.)Gargantuan
Just having an empty ng-blur attribute defined on the input template is enough to trigger the exception.Gargantuan
L
0

As PaulL mentioned in one of the comments ui-grid now has a rowEdit feature designed to allow saving of the entire row when done editing. See http://ui-grid.info/docs/#/tutorial/205_row_editable.

Luscious answered 9/10, 2015 at 15:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.