Do you have a javascript MVC framework for your existing web application?  Tired of spending development cycles updating your application views every time your data provider sends something new?  Yeah, me too.  Thankfully there are a good number javascript MVC frameworks that can be used to display dynamic data AND save the display state of said data.

Recently I wanted to build such a web application that would display dynamic data key/value pairs as well as saving the user's view state of said data.  I’ve used the Extjs framework to build various web applications and I’ve been impressed with the Ext.grid.Panel’s features as well as the ease of creating ‘stateful’ components with the Extjs framework. While there isn’t anything directly out of the box that will allow you to create a dynamic stateful grid panel, the Extjs framework does give you the tools to build your own stateful grid component without modifying any of their source code.

My requirements consisted of:

  • Dynamically display json-based messages in a table (without knowing the json nested structure or key/value pairs of the data).
  • Allow for user to customize the table (sorting, grouping, columns displayed, column order, column width).
  • Save state of the user-customized table and reload state upon user return to web application.

Blog post assumptions:

  • You are somewhat familiar with Extjs.
  • You know how to setup an Extjs State Provider.
  • You are using a ‘standard’ Extjs store (I’m using an ajax proxy with my Extjs store).

Server Side Steps

Step 1: Flatten your result set.  The Dynamic Stateful Grid described in this post worked well with a flattened array of json objects.

Step 2: Define the 'dynamic columns' in your response to the client.

An Extjs grid will allow the grid columns to be defined dynamically.  To do this, the response to the Grid’s store must include a ‘metaData’ element which defines the fields and columns based on the data being served.  The ‘metaData’ element looks like this example:

{
  metaData: {
    columns: [
      {‘header’: key1, ‘dataIndex’: key1, ‘stateId’: key1},
      {‘header’: key2, ‘dataIndex’: key2, ‘stateId’: key2}
    ],
    fields: [
      {‘name’: key1, ‘type’: ‘string’},
      {‘name’: key2, ‘type’: ‘string’}
    ]
  },
  items: [
    {‘field1’: value1,  ‘field2’: value2, ‘field3’: value3},
    {‘field4’: value4,  ‘field5’: value5, ‘field6’: value6}
  ]
}

Client Side Steps

Step 1: Define a Dynamic Stateful Grid Panel (code below works with Extjs 4, 5, and 6)

When the Extjs grid sees the ‘metaData’ element in it’s store results, the ‘metachange’ event is fired.  We will need to override both the ‘metachange’ and ‘beforestatestave’ events to keep track of the columns shown/hidden, the column order, and the column widths.

Ext.define('Ext.grid.DynamicStatefulGridPanel', {
extend: 'Ext.grid.Panel',
alias: 'widget.dynamicstatefulgrid',
stateful: true,

initComponent: function() {
  var _this = this;
  this.on({
    beforestatesave: function(cmp, state, eOpts) {
      var savedState = Ext.state.Manager.getProvider().state[_this.stateId];

      // don't save a state of 0 columns shown
      if (state && state.columns.length === 0 && savedState && savedState.columns.length === 0) {
        return false;
      }

      // if the current state has 0 columns shown, replace it with the old state and 
      // return false to prevent saving an empty state
      if (state.columns.length === 0 && savedState && savedState.columns.length > 0) {
        state = savedState;
        return false;
      }

      // if a metachange event was fired previously, reset the metachangefired flag 
      // to prevent saving the incorrect state
      if (_this.store.metachangefired) {
        _this.store.metachangefired = false;
        return false;
      }

      // if new state and old state are present, update the column hidden and width attributes
      if (state && state.columns.length > 0 && savedState && savedState.columns.length > 0) {
        $.each(savedState.columns, function(index, savedColumn) {
          $.each.(state.columns, function(stateIndex, newColumn) {
            if (savedColumn['id'] === newColumn['id']) {
              if (savedColumn['hidden'] && newColumn['hidden'] != false) {
                newColumn['hidden'] = savedColumn['hidden'];
              }
              if (!newColumn['width'] && savedColumn['width']) {
                newColumn['width'] = savedColumn['width'];
              }
            }
          });
        });
      }
      _this.metachangefired = false;
    },
    beforereconfigure: function(cmp, store, columns, oldStore, oldColumns, eOpts) {
      _this.store.metachangefired = false;
    }
  });
  this.callParent(arguments);
},

bindStore: function(store) {
  var _this = this;
  //initialize the metachangefired flag on the store
  store.metachangefired = false;
  this.callParent(arguments);
  store.mon(store, 'metachange', function(store, meta) {
      store.metachangefired = true;

      // get the columns passed in on the metachange event. 
      // Note: these columns don't have the savedState applied yet
      var metaDataColumns = meta.columns;

      // initialze array to track saved state column order
      var reorderedMetaColumns = [];

      var provider = Ext.state.Manager.getProvider();
      var state = provider.state;

      // if a state is present for this grid (_this.stateId), update the columns 
      // with the saved state from the state provider
      if (state[_this.stateId]) {
        $.each(metaDataColumns, function(index, metaDataColumn) {
          $.each(state[_this.stateId]['columns'], function(stateIndex, stateColumn) {
            if (metaDataColumn['dataIndex'] === stateColumn['id']) {
              if (stateColumn['hidden']) {
                metaDataColumn['hidden'] = stateColumn['hidden'];
              }
              if (stateColumn['width']) {
                metaDataColumn['width'] = stateColumn['width'];

              }
            }
          });
        });
      }

      if (reorderedMetaColumns.length === 0) {
        reorderedMetaColumns = metaDataColumns;
      }

      // reconfigure the grid with the saved state applied to 
      // the dynamic metaData columns
      _this.reconfigure(store, reorderedMetaColumns);
    }
  }
});

Step 2: Instantiate your Stateful Grid Panel

var _this = this;
_this.store = Ext.create(‘My.ext.RecordStore’, {});
_this.grid = Ext.create(‘Ext.grid.DynamicStatefulGridPanel’, {
	store: _this.store,
	features: [{ftype: ‘grouping’}];
	columns: [],
	stateId: ‘my-dynamic-stateful-grid’
});

Step 3: Profit!

That's it!  Once your Extjs data Store is hooked up to a data source and your results are transformed into a flattened list structure, you'll be able to display whatever key/value pairs are present in the results set as well as save the user's state of columns displayed, column widths, column order, sorting and grouping.

 

Comment