Helium Grid Demo

This is the helium grid with all its bells and whistles. It's loaded with the world country data taken from This Github repository.

Things you can do:

  • Resize and hide the columns
  • Edit the cells and navigate with the keyboard
  • Lock individual rows for editing
  • Sort the data by multiple columns
  • Filter the data

Most of the cells are editable. To edit a cell you just need to click on it. To stop editing - click anywhere else. The "Languages" column has a custom edit control which shows in the modal. The changes will be applied only if you click "OK". The grid editing does not work on Internet Explorer due to a bug I'm losing my mind over ...

About the Code

An example data record and the configuration code are listed below. The code uses some Lodash functions. If you're unfamiliar with them, go ahead and check the docs.

Since we have the entire dataset in memory, the paging, sorting and filtering is done manually in JS. In other cases, you would probably want to offload that to the backend. In that case, there are 3 important points to remember:

  1. The reset() config function should make an AJAX request pass the result to the callback.
  2. The dataSize() config function should return the number of data items you have in your collection after any filtering has been applied. This can be done by returning the item count in the aforementioned AJAX call and storing it in a closure variable.
  3. The onEdit() config function should store the changes somewhere (or send them directly to the backend) since they will be lost when the grid page changes. Having item IDs is helpful here, even if you don't display them in the grid.

Example data record

The dataset is a Javascript array which is already loaded. This is how a single record looks like:

{
    "name": {
      "common": "Aruba",
      "official": "Aruba",
      "native": {
        "nld": {
          "official": "Aruba",
          "common": "Aruba"
        },
        "pap": {
          "official": "Aruba",
          "common": "Aruba"
        }
      }
    },
    "tld": [".aw"],
    "cca2": "AW",
    "ccn3": "533",
    "cca3": "ABW",
    "cioc": "ARU",
    "currency": ["AWG"],
    "callingCode": ["297"],
    "capital": "Oranjestad",
    "altSpellings": ["AW"],
    "region": "Americas",
    "subregion": "Caribbean",
    "languages": {
      "nld": "Dutch",
      "pap": "Papiamento"
    },
    "translations": {
      "deu": {"official": "Aruba", "common": "Aruba"},
      "fra": {"official": "Aruba", "common": "Aruba"},
      "hrv": {"official": "Aruba", "common": "Aruba"},
      "ita": {"official": "Aruba", "common": "Aruba"},
      "jpn": {"official": "\u30a2\u30eb\u30d0", "common": "\u30a2\u30eb\u30d0"},
      "nld": {"official": "Aruba", "common": "Aruba"},
      "por": {"official": "Aruba", "common": "Aruba"},
      "rus": {"official": "\u0410\u0440\u0443\u0431\u0430", "common": "\u0410\u0440\u0443\u0431\u0430"},
      "spa": {"official": "Aruba", "common": "Aruba"},
      "fin": {"official": "Aruba", "common": "Aruba"}
    },
    "latlng": [12.5, -69.96666666],
    "demonym": "Aruban",
    "landlocked": false,
    "borders": [],
    "area": 180
  }        
  

The code

This is the code used to configure and initialze the grid, along with an example filtering, sorting and paging implementation.

// Countries are preloaded
  var COUNTRIES = [....];
  // All the regions 
  var REGIONS = _.compact(_.uniq(_.pluck(COUNTRIES, "region")));
  // All the subregions
  var SUBREGIONS = _.compact(_.uniq(_.pluck(COUNTRIES, "subregion")));
  // All languages
  var LANGUAGES = _.map(_.reduce(_.pluck(COUNTRIES, "languages"), _.extend, {}), function(text, value){
    return { t: text, v: value };
  }).sort(function(a, b){ return a.t > b.t ? 1 : b.t > a.t ? -1 : 0 });

  // The default page size
  var PAGE_SIZE = 30;
  // The entire (filtered) data
  var DATA = COUNTRIES.slice();
  // The displayed page
  var PAGE = COUNTRIES.slice(0, PAGE_SIZE);

  // Ured for retreving values from nested objects
  // by using namespaced keys like "name.official"
  function getKey(item, name){
    var part, parts = name.split(".");
    var obj = item;
    while(obj && (part = parts.shift())){
      obj = obj[part];
    }
    return obj;
  }

  // Initialize the modal and controls for editing languages
  var modal = he('modal', document.getElementById('language-modal'), { closeAnywhere: false, closeIcon: true });
  var languages = he('scrollList', document.getElementById('language-list'), { items: LANGUAGES, multiple: true, nullable: true });
  var ok = he('button', document.getElementById('pick-language')).on('click', function(){
    modal.close(true);
  })

  // The grid config object
  var config = {
    global: {
      pagination: {
        element: document.getElementById('pagination'),
        size: PAGE_SIZE,
        links: 7
      },
      dataSize: function(){
        return DATA.length;
      },
      pageSize: function(){
        return PAGE.length;
      },
      getCell: getKey,
      onEdit: function(index, item, name, value, callback){
        // Handles namespaced keys like "name.official"
        var part, parts = name.split(".");
        var prop = parts.pop();
        var obj = item;
        while(part = parts.shift()){
          obj = obj[part];
        }
        // Objects are passed by reference so this will also
        // change the country in the original dataset.
        obj[prop] = value;
        callback();
      },
    
      // In most implementations, this function will just do an AJAX call
      // and let the backend sort this out.
      reset: function(params, callback){
        var filters  = params.filters;
        var sorting  = params.sorting;
        var pageSize = params.pagination.size;
        var page     = params.pagination.page;
    
        // Apply filtering
        DATA = _.filter(COUNTRIES, function(item){
          for(var name in filters){
            var value = getKey(item, name);
            if(value !== undefined){
              var filter = filters[name];
              for(var cmp in filter){
                // Deliberate usage of double equals
                if(filter[cmp] == null){
                  continue;
                }
                if(cmp === "~" && value.toLowerCase().indexOf(filter[cmp].toLowerCase()) === -1){
                  return false;
                }
                if(cmp === ">" && value <= filter[cmp]){
                  return false;
                }
                if(cmp === "<" && value >= filter[cmp]){
                  return false;
                }
                if(cmp === "in" && filter[cmp].length > 0 && filter[cmp].indexOf(value) === -1){
                  return false;
                }
              }
            }
          }
          return true;
        })
        // Sort the filtered data by multiple columns
        .sort(function(a, b){
          for(var i=0, ii=sorting.length; i<ii; ++i){ 
            var crit = sorting[i];
            var dir = crit.direction;
            var name = crit.name;
            var aVal = getKey(a, name);
            var bVal = getKey(b, name);
            if((dir === "asc" && aVal < bVal) || (dir === "desc" && aVal > bVal)){
              return -1;
            }
            else if((dir === "asc" && aVal > bVal) || (dir === "desc" && aVal < bVal)){
              return 1;
            }
          }
          return 0;
        });
    
        // Pages are 1-indexed
        callback(PAGE = DATA.slice((page-1)*pageSize, page*pageSize));
      }
    },
    // There's quite some repetition in the columns' definition.
    // You can map a function over an array of colum names to avoid this.
    columns: [{
      attrs: { style: { "text-align": "center" } },
      width: 60,
      name: "locked",
      title: "No Edit",
      hideable: false,
      editable: true,
      sortable: true,
      typeInfo: "boolean"
    },{
      width: 200,
      name: "name.common",
      title: "Name",
      filterable: true,
      hideable: false,
      sortable: true,
      typeInfo: "text",
      editable: function(item){ return !item.locked; },
    },{
      width: 200,
      name: "name.official",
      title: "Official Name",
      filterable: true,
      resizable: true,
      sortable: true,
      typeInfo: "text",
      editable: function(item){ return !item.locked; },
    },{
      width: 120,
      name: "capital",
      title: "Capital",
      filterable: true,
      resizable: true,
      sortable: true,
      typeInfo: "text",
      editable: function(item){ return !item.locked; },
    },{
      width: 100,
      name: "cca2",
      title: "ISO Alpha 2",
      typeInfo: "text",
      sortable: true,
      editable: function(item){ return !item.locked; },
    },{
      width: 100,
      name: "cca3",
      title: "ISO Alpha 3",
      typeInfo: "text",
      sortable: true,
      editable: function(item){ return !item.locked; },
    },{
      width: 120,
      name: "region",
      title: "Region",
      filterable: true,
      resizable: true,
      editable: true,
      sortable: true,
      typeInfo: {
        type: "enum",
        items: REGIONS
      },
      editable: function(item){ return !item.locked; },
    },{
      width: 120,
      name: "subregion",
      title: "Subregion",
      filterable: true,
      resizable: true,
      editable: true,
      sortable: true,
      typeInfo: {
        type: "enum",
        items: SUBREGIONS
      },
      editable: function(item){ return !item.locked; },
    },{
      attrs: { style: { "text-align": "right" } },
      width: 120,
      name: "area",
      title: "Area km²",
      filterable: true,
      editable: true,
      resizable: true,
      sortable: true,
      typeInfo: {
        type: "number",
        format: 2
      },
      editable: function(item){ return !item.locked; },
    },{
      width: 200,
      name: "languages",
      title: "Languages",
      editable: true,
      resizable: true,
      typeInfo: {
        type: "enum",
        items: []
      },
      template: function(item){
        return _.values(item.languages).join(", ");
      },
      editable: function(item){ 
        return !item.locked; 
      },
      editOptions: {
        type: "custom",
        onEdit: function(options, cbOk, cbCancel){
          languages.val(_.keys(options.value));
          modal.once('close', function(withOK){
            withOK ? cbOk(_.zipObject(languages.val(), languages.text())) : cbCancel();
          });
          modal.open();
          // The modal opens in a deferred function.
          // We need to focus AFTER the languages list is added to the DOM.
          setTimeout(function(){
            languages.el.focus();
          });
        }
      }
    }]
  }

  // Init the grid
  var grid = he('grid', document.getElementById('countries'), { data: PAGE, config: config });  
  

Pick Languages