Editable ngGrid, with both dropdowns and selects

I’ve been working on making an editable ngGrid, allowing users to directly edit the grid rather than going to a detail page.  I’m seeing that there are two modes of operation – for simple things (maybe changing the status of a bunch of items), it’s easier to interact with the grid directly.  When you want to edit a full item, you’d do this in the detail page.

The aim is to have a grid that you can edit directly, and have it save to the server when you leave the cell you’re editing (this is a reason you wouldn’t want to edit the whole record this way – you’ll end up with lots of spurious saves if the user has lots of cells to edit).

An option if you’re starting a new project now would be to look at my tutorial on the beta ng-grid 3.0.  This isn’t ready for production use yet, but feels reasonably close, so if you’re 3 months away from production it could be an option.  It has a nicer api for editing.

I’ve created a plunker that demonstrates the code in this post, leveraging information from stackoverflow and other places.  In summary we have an index page that just includes a grid as usual:

<!DOCTYPE html>
<html ng-app="plunker">
<head>
 <meta charset="utf-8" />
 <title>AngularJS Plunker</title>
 <script>document.write('<base href="' + document.location + '" />');</script>
 <link rel="stylesheet" type="text/css" href="https://rawgithub.com/angular-ui/ng-grid/2.0.7/ng-grid.css" />
 <link rel="stylesheet" href="style.css" />
 <script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
 <script data-require="angular.js@1.2.x" src="http://code.angularjs.org/1.2.3/angular.js" data-semver="1.2.3"></script>
 <script type="text/javascript" src="https://rawgithub.com/angular-ui/ng-grid/2.0.7/build/ng-grid.debug.js"></script>
 <script src="app.js"></script>
 </head>
<body ng-controller="MainCtrl">
 <div class="gridStyle" ng-grid="gridOptions"></div>
 </body>
</html>

This includes all the scripts directly, rather than pulling them down locally like in the tutorial series, this makes it easier to run as a plunker.

We have a stylesheet that also has little in it:

/* Put your css in here */
.gridStyle {
 border: 1px solid rgb(212,212,212);
 width: 400px; 
 height: 300px
}

And finally the controller.  We’ll go through this piece by piece.  Our solution has a dropdown and an input box.  For the dropdown, we’re modeling a status field where we want to store a code on the database, but decode on-screen (and in the dropdown) to a text value.  This means we need both a filter and a dropdown.  Finally, we have to create an ngBlur directive so that we can access the field lose focus event and are able to trigger a save.

Firstly we define the module:

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

Then, we define the values for our status dropdown, and our status filter.  This has three bits.  A service that returns a constant – we’d use this in any page or controller that needs to get at our status values:

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

Then, we have a filter that draws upon that constant to map our code value to a decode.  We use this in the grid to display the decoded value rather than the code:

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

We’ll use this filter by setting a cellFilter on the status column:

cellFilter: 'mapStatus'

Finally for statuses, we declare the statuses themselves on our scope, allowing the html template to get at them:

$scope.statuses = StatusesConstant;

Then we have the blur directive, this provides us the ability to bind a function to the blur event on a field.  The directive looks as follows:

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

We use the directive within the editable cell template, in our case we want to call the “updateEntity” function, passing in the current row, this is along the lines of:

ng-blur="updateEntity(row)"

We set all the usual column defs for a grid, we need to set a series of options to tell the grid that we’d like our cells to be editable based on a single click:

      $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'}
        ]
      };

The learnings in this area are:

  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 even though the documentation suggests that it is.

Finally, the editableCellTemplate doesn’t appear as you might expect, because the example in the ngGrid documentation is incorrect and leaves off the ng-model directive.  For an input your format needs to be:

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

Once you add the ng-blur to id, you have:

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

The select template is similar:

<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)" />

Finally, we need to do some tricky work around the update method.  The blur event fires twice (sometimes three times) in quick succession in some browsers, according to this information it’s because it fires once for lose focus on the window, and again at the field level.  Either way, it’s different across browsers and unreliable, so we need a way to avoid saving twice when we get triggered twice.

We handle this by introducing a timer into the save.  We wait for half a second before saving, and if another event comes in whilst this one is pending we don’t process that extra event.  Which is a rather ugly hack, but seems to be the accepted way to deal with this.

  $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);
    }    
  };

In total, the controller therefore looks as follows:

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

Enjoy.

Advertisements

26 thoughts on “Editable ngGrid, with both dropdowns and selects

  1. Thanks for your explanation, very helpful! I managed to get as far as you did with a select in an inline editable ng-grid, but there is still one problem with your plunker: the first time you click one of the “active” or “inactive” values in the grid, the dropdown appears, but the value isn’t selected in the dropdown! This happens only the first time, because after selecting a value manually in the dropdown, it stays saved the next time you open the dropdown.
    Do you have any idea how to solve this?

  2. Hey Stephanie, glad you found it useful.

    The problem I think is that I used a hash in the dropdown. Hashes appear to always be indexed with strings, not integers, but I chose to use integers for the default values. So if we change the plunker to have “1” and “2” as the status values, then they default fine.

    See here: http://plnkr.co/edit/eYvEtvo2pvAcZRmiocUZ?p=preview

    In my real application I solved this by writing a function that mapped a hash to an array, and then used that array in the dropdown. The code for that looks like the following:


    var hashToArray = function(hash) {
    var array = [];
    for (var key in hash) {
    array.push({ id: parseInt(key, 10), value: hash[key] });
    }
    return array;
    };

    So, updating the plunker we’d have:
    http://plnkr.co/edit/zujyha15vgm6pw09ywxs?p=preview

    Good luck.

  3. Thanks Paul for your quick reply!
    In my dropdown, I used the following ng-options:

    ng-options="aard.label for aard in myAarden"

    So the value of my dropdown is a full “aard” JSON-object (while the value of your dropdown is just an integer “id”). Our server responds with a full “aard” JSON-object, so I think the dropdown should automatically select the right value. But we’re going to reconsider this and make the server respond with a simple string “code” instead of a full JSON-object, so I’ll try to change my ng-options to:


    ng-options="aard.code as aard.label for aard in myAarden"

    I’ll see if that works 🙂

  4. Stephanie, sounds reasonable.

    In case it’s useful to you, I have a few different patterns that I use (and that I’m going to eventually add to this tutorial).

    1. I have dropdowns that I populate with constants – so things like user statuses that I’d never change at run time. They use the pattern I showed in this tutorial.
    2. I have dropdowns that I populate using a code list from the server. For example, I sometimes have a dropdown that I allow people to change the values in. In that case, I’d have a fetch from my server, and I end up with a “foos” list from the server. So then I’d have ng-options=”aard.id as aard.label for aard in myAarden”.
    3. I have dropdowns that I allow nulls in, so I have a method that I wrap the call in that inserts a null option into the list. So then I’d have ng-options=”aard.id as aard.label for aard in optional(myAarden)”

    I also buried all this into a set of directives so that I don’t have to mess around with it on each individual widget – so my actual html template has directives like:

    (in this case, a dropdown that relates to something that i know is a foreign key, and therefore gets special formatting).

    This directive then buries all the error formatting etc inside it.

  5. Sounds interesting, thanks for the update!
    I managed to get my code to work by using a string as value for the dropdown instead of an entire JSON-object. I do think it’s a bug though that it doesn’t show the selected option when it’s a full JSON-object. Of course it could also be my mistake, but anyway, we’re using strings now because that’s better anyway.

  6. Pingback: ng-grid inline editing using bootstrap typeahead | Lemoncode

  7. HI,
    Thanks for your post. It was very useful. I need your help,i need to create a grid in which one row is edittable as dropdown, while in other row it should be a check box. Do i have way to render the cell of the columns and changes their type?

  8. I haven’t directly done this, but I believe you could do this by putting a conditional in the editable cell template – so it could check some value then render the right widget. It’d be pretty ugly though. 🙂

  9. This is very helpful! however, I’m having another problem: “groups”
    when I copy this code it works fine, as long as I added a “groups: [‘age’]” it stop working, seems the data bind is broken, the module data won’t be updated after the change. do you know why this is happening? thanks!

  10. Sorry Xiao, haven’t had that experience before. Does it stop working in the sense that the data doesn’t display at all, or in the sense that you can see the data but changing the data doesn’t update the server any more? Is it only the data from the dropdown that doesn’t update, or the whole row in the table?

  11. Hi ,
    Good article, I have two questions:
    1. Any particular reason for not implementing the ngGridEventEndCellEdit event?
    2. Is is possible to cancel the edit changes by pressing the ‘esc’ key?

    JP

  12. As JP, is it possible to cancel the edit changes by pressing the ‘esc’ key ?
    It’s seems an issue…
    ( angularjs 1.2.X, ng-grid 2.0.12 )

  13. I haven’t really spent time on that, but I do know that ng-grid 3.0 has better support for editing. But it’s very new as yet.

  14. I was wondering is there was a way to populate the grid with data fetched from a FORM?

    For example, a form could appear above table and as soon as we submit the form, the data saves into the grid via AJAX, or something like that!

  15. It should be possible. The normal way is for the form to appear below (known as list-detail). All you need to do is to bind the angularJS form to the currently selected row in the table, it should all automatically update. When you click “new” you’d create a new instance of the $resource, and add it to the end of your data array. And you need a save to the server. You could look at the rowEdit feature in uiGrid to get some ideas.

  16. Hey Paul this is great thanks for this i just wanted to do something similar what i am trying is that i want to edit the entire row once that row is selected based on a checkbox and i edit all the feilds or somefields in the grid and i fire a save using a button how would i do that so instead of using ngblur i want to trigger the save on click of a button

    Hoping to hear soon from you!!Thanks in advance

  17. It would be a good idea to look at alternative tables. For eg. smart table, has a content editable directive which makes the editable on click. You cloud then fire the same save event as for ng-grid(or maybe a bit different with few required changes), and implement what you want.

    That is though if you are actually willing to implement smart-table instead of ng-grid.
    For me, i started out with ng-grid but i didnt need much of its functionality and ended up using smart table instead for its simplicity!

  18. hi, i need help in creating a ui grid with dropdowns. u guys are adding dropdown when editing the row but i want it by default in the table even when not editing. is that possible?

  19. You could by changing the cell template rather than editable cell template. In general we don’t consider that to be very performant (you’ll render lots of dropdowns, which has an impact), and you can get issues with scrolling if you do that.

  20. Hi, thank your for your post, a question, how to bulk edit status, or clear all selected status by a click? .

  21. Very helpful i had this issue where I had to select the ngcell, with autocomplete, twice to get it into edit mode but setting enableCellEditOnFocus: true and enableCellEdit: false did the trick.

    Thanks once again

  22. Hi Paul! Thank you for posting this! Your explanations are thorough and easy to understand which is much appreciated 🙂 The questions from others and your response are very helpful as well. A long-time programmer, I had to step away. Recently attempting to return and learn new technologies is challenging yet stimulating. This will definitely help me!

  23. You’re very welcome. You’re probably on the same journey as me – I’d been out of coding for a while, and javascript and these frameworks were new to me. I tried to write down everything I learned along the way as I did it, so that others could find it all in one place. Glad it was useful.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s