Monday, April 13, 2009

Dynamic Columns In Ext JS Grid

I just had another unproductive afternoon. Another thing I would expect to work out of the box that turned out to be not that easy. It was not the first time I needed dynamic number of columns in the grid but until now I was always able to find a workaround. Fortunately, today I couldn't go around and had to solve the problem once and for all.

What I needed was a column model dynamically adjusting depending on the data received from data store (i.e. JSON provided by server). Something like the following:



Where each site has a different set of dimensions (rows) and different set of auditors (columns).

It turned out that the problem was actually two-fold - first dynamically change the columns and then display the editable checkboxes instead of true/false values.

To change the columns I used metaData attribute in my JSON. Including metaData attribute causes Ext.data.JsonReader to be adjusted to the new set of fields and other config parameters and fire metachange event. Here's an example of the JSON with metaData:
{
 "results": 3,
 "auditors": [
  {"dimension_name": "Audits and Reviews", "auditor_3": false, "dimension_id"
: 10, "id": 10, "auditor_4": false, "auditor_18": false},
  {"dimension_name": "Emergency Management & Fire Safety", "auditor_3": false, "dimension_id": 8, "id": 8, "auditor_4": false, "auditor_18": false}
  .
  .
  .
 ],
 "metaData": {
  "fields": [
   {"name": "id"},
   {"type": "string", "mapping": "dimension_name", "name": "auditor[dimension]"},
   {"type": "int", "mapping": "dimension_id", "name": "auditor[dimension_id]"},
   {"type": "bool", "name": "auditor_3", "header": "Dasha Alexo"},
   {"type": "bool", "name": "auditor_18", "header": "Tester 3"},
   {"type": "bool", "name": "auditor_4", "header": "Zobak Zobakovic"}],
  "root": "auditors",
  "totalProperty": "results",
  "id": "id"
 }
}
In this case it's the number of auditors (and their names) that's changing. My data types are booleans but displaying any other data type would look pretty much the same.

Next I needed to update the column model based on the new meta data. I did that by adding metachange listener to my store
auditors_store.addListener("metachange", function(store, meta){
 var grid = Ext.getCmp('auditors-grid');

 var columns = [
  {header: 'Dimension', dataIndex: 'auditor[dimension_id]', hidden: true},
  {header: 'Dimension', dataIndex: 'auditor[dimension]', width: 200}
 ];
 
 for (var i = 3; i < meta.fields.length; i++ ) {
  var plugin = new Ext.grid.CheckColumn({
    header: meta.fields[i].header,
    dataIndex: meta.fields[i].name,
    grid: grid,
    width: 120
   });
  
  columns.push(plugin);
  plugin.init(grid);

  //use columns.push( { header: meta.fields[i].header, dataIndex: meta.fields[i].name, type: meta.fields[i].type }); for fields that don't require plugins
 }
 grid.reconfigure(auditors_store, new Ext.grid.ColumnModel(columns));
});

I had to create and initialize the plugin for each new column as I needed editable checkboxes. For normal fields just use the usual column config object { header: meta.fields[i].header, dataIndex: meta.fields[i].name, type: meta.fields[i].type }.

The actual magic happens is in the last line. Grid has an obscure method reconfigure( Ext.data.Store store, Ext.grid.ColumnModel colModel ) that allows to switch the store and column model (the columns displayed) on the fly.

My next problem was to make the checkboxes editable. To do that I had to put a small fix to the CheckColumn plugin (see the plugin) as the original plugin assumes that the grid is not rendered when initializing, however, when dynamically adding columns the grid is already rendered so I had to change the following:
 init : function(grid){
  this.grid = grid;
   this.grid.on('render', function(){
    var view = this.grid.getView();
    view.mainBody.on('mousedown', this.onMouseDown, this);
   }, this);   
 },
to the following:
 init : function(grid){
  this.grid = grid;
  if (this.grid.rendered) {
   var view = this.grid.getView();
   view.mainBody.on('mousedown', this.onMouseDown, this);
  } else {
   this.grid.on('render', function(){
    var view = this.grid.getView();
    view.mainBody.on('mousedown', this.onMouseDown, this);
   }, this);   
  }
 },
And that's all - nice and simple. If only there were more books on Ext :-(

12 comments:

  1. Hi Peter,
    thanks a lot. I was looking for this solution for days together and my quest ended here. :) Thanks a lot and keep up the great work...

    ReplyDelete
  2. does it work in ExtJs 3.0
    Nice work :)

    ReplyDelete
  3. Hello,
    How did you solve the problem, that when you change a checkbox to false, then the server gets a false instead of an empty string?

    Thanks!

    ReplyDelete
  4. Hi,
    what exactly is a problem about it? Can you show the controller code handling the saving?

    thanks

    ReplyDelete
  5. H...I have a problem in displaying the dynamic column header grid wit their values.. I dont knw where i did the mistake...
    Ext.onReady(function(){
    var proxy= new Ext.data.HttpProxy(
    {url: 'sample1.jsp'}
    );
    var reader=new Ext.data.JsonReader(
    {root:'root'
    });
    var store= new Ext.data.Store({
    proxy:proxy,
    reader: reader,
    remoteSort: true
    });
    store.load();
    Ext.data.DynamicJsonReader = function(config){
    Ext.data.DynamicJsonReader.superclass.constructor.call(this, config, []);
    };
    Ext.extend(Ext.data.DynamicJsonReader, Ext.data.JsonReader, {
    readRecords : function(o){
    alert("hi");
    this.jsonData = o;
    if(o.metaData){
    delete this.ef;
    this.meta = o.metaData;
    this.recordType = Ext.data.Record.create(o.metaData.fields);
    this.onMetaChange(this.meta, this.recordType, o);
    } else {
    var data = this.meta.root ? this.getJsonAccessor(this.meta.root)(o) : o;
    if (Ext.isArray(data) && data[0]) {
    delete this.ef;
    var fields = [];
    for (var fieldName in data[0]) {
    fields.push(fieldName);
    }
    this.meta.fields = fields;
    this.recordType = Ext.data.Record.create(fields);
    this.onMetaChange(this.meta, this.recordType, o);
    }
    }
    var s = this.meta, Record = this.recordType,
    f = Record.prototype.fields, fi = f.items, fl = f.length;
    if (!this.ef) {
    if(s.totalProperty) {
    this.getTotal = this.getJsonAccessor(s.totalProperty);
    }
    if(s.successProperty) {
    this.getSuccess = this.getJsonAccessor(s.successProperty);
    }
    this.getRoot = s.root ? this.getJsonAccessor(s.root) : function(p){return p;};
    if (s.id) {
    var g = this.getJsonAccessor(s.id);
    this.getId = function(rec) {
    var r = g(rec);
    return (r === undefined || r === "") ? null : r;
    };
    } else {
    this.getId = function(){return null;};
    }
    this.ef = [];
    for(var i = 0; i < fl; i++){
    f = fi[i];
    var map = (f.mapping !== undefined && f.mapping !== null) ? f.mapping : f.name;
    this.ef[i] = this.getJsonAccessor(map);
    }
    }
    var root = this.getRoot(o), c = root.length, totalRecords = c, success = true;
    if(s.totalProperty){
    var v = parseInt(this.getTotal(o), 10);
    if(!isNaN(v)){
    totalRecords = v;
    }
    }
    if(s.successProperty){
    var v = this.getSuccess(o);
    if(v === false || v === 'false'){
    success = false;
    }
    }
    var records = [];
    for(var i = 0; i < c; i++){
    var n = root[i];
    var values = {};
    var id = this.getId(n);
    for(var j = 0; j < fl; j++){
    f = fi[j];
    var v = this.ef[j](n);
    values[f.name] = f.convert((v !== undefined) ? v : f.defaultValue, n);
    }
    var record = new Record(values, id);
    record.json = n;
    records[i] = record;
    }
    return {
    success : success,
    records : records,
    totalRecords : totalRecords
    };
    }
    });
    Ext.grid.DynamicColumnModel = function(store, config){
    Ext.grid.DynamicColumnModel.superclass.constructor.call(this, Ext.apply({store: store, columns: []}, config));
    if (store.fields) {

    this.reconfigure();
    }
    store.on('load', this.reconfigure, this);
    };
    Ext.extend(Ext.grid.DynamicColumnModel, Ext.grid.ColumnModel, {
    reconfigure: function(){
    var cols = [];
    this.store.fields.each(function(field){
    cols.push({header: field.name, dataIndex: field.name});
    });
    this.setConfig(cols);
    }
    });
    var grid=new Ext.grid.GridPanel({
    width: 1000,
    height: 200,
    cm: new Ext.grid.DynamicColumnModel(store),
    selModel: new Ext.grid.RowSelectionModel({singleSelect:true}),
    store: store,
    viewConfig :{
    forceFit:true
    }
    });
    grid.render(document.body);
    });

    ReplyDelete
  6. my json output format from the server
    { metaData:{ "totalProperty":"total", "root":"root", "id":"id", "fields":[ { "name":"statusid", "type":"int" } { "name":"statusname", "type":"string" } { "name":"statusdesc", "type":"string" } ] }, "success":"true", "totals":3, "records":[ {"statusid":"1","statusname":"New","statusdesc":"new description"},{"statusid":"2","statusname":"Open","statusdesc":"Open description"},{"statusid":"3","statusname":"Closed","statusdesc":"Closed description"}],columns:[ { "header":"STATUSID", "dataIndex":"statusid" } , { "header":"STATUSNAME", "dataIndex":"statusname" } , { "header":"STATUSDESC", "dataIndex":"statusdesc" } ] }
    can u send me the full coding of yours!!!!!

    ReplyDelete
  7. Have you also considered explicitely firing a render event to avoid overriding the native library?

    grid.fireEvent("render", grid);

    ReplyDelete
  8. Simo thanks for that last comment, works as well! ;)

    ReplyDelete
  9. Hi Peter Bohm
    I was looking for this solution for days but i couldn't resolve by problem even by your post.

    Can i ask you to send me the full coding of Dynamic Columns In Ext JS Grid?

    ReplyDelete
  10. Why don't you just perform the logic on the server and pass the columns required to the client in the json?

    ReplyDelete
  11. It is Great! I like metadata:)

    But note that correct attribute is "metaData", not "metadata", it is very important, because of JavaScript is case-sensitive.

    Thank you for post!

    ReplyDelete