-->

AngularJS and ng-grid - auto save data to the serv

2019-01-10 07:10发布

问题:

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?

回答1:

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



回答2:

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.



回答3:

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() ...
}


回答4:

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/



回答5:

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

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


回答6:

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.



回答7:

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.